fix(gallery): fill tall portrait cards to full block width with object-cover crop
- ArtworkCard: add w-full to nova-card-media, use absolute inset-0 on img so object-cover fills the max-height capped box instead of collapsing the width - MasonryGallery.css: add width:100% to media container, position img absolutely so top/bottom is cropped rather than leaving dark gaps - Add React MasonryGallery + ArtworkCard components and entry point - Add recommendation system: UserRecoProfile model/DTO/migration, SuggestedCreatorsController, SuggestedTagsController, Recommendation services, config/recommendations.php - SimilarArtworksController, DiscoverController, HomepageService updates - Update routes (api + web) and discover/for-you views - Refresh favicon assets, update vite.config.js
This commit is contained in:
81
tests/Feature/Recommendations/UserPreferenceBuilderTest.php
Normal file
81
tests/Feature/Recommendations/UserPreferenceBuilderTest.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\DTOs\UserRecoProfileDTO;
|
||||
use App\Models\Artwork;
|
||||
use App\Models\User;
|
||||
use App\Models\UserRecoProfile;
|
||||
use App\Services\Recommendation\UserPreferenceBuilder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// UserRecoProfileDTO
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
it('DTO serialises and round-trips correctly', function () {
|
||||
$dto = new UserRecoProfileDTO(
|
||||
topTagSlugs: ['space', 'nature'],
|
||||
topCategorySlugs: ['wallpapers'],
|
||||
strongCreatorIds: [1, 2, 3],
|
||||
tagWeights: ['space' => 0.6, 'nature' => 0.4],
|
||||
categoryWeights: ['wallpapers' => 1.0],
|
||||
);
|
||||
|
||||
$arr = $dto->toArray();
|
||||
$restored = UserRecoProfileDTO::fromArray($arr);
|
||||
|
||||
expect($restored->topTagSlugs)->toBe(['space', 'nature'])
|
||||
->and($restored->topCategorySlugs)->toBe(['wallpapers'])
|
||||
->and($restored->strongCreatorIds)->toBe([1, 2, 3])
|
||||
->and($restored->tagWeight('space'))->toBe(0.6)
|
||||
->and($restored->followsCreator(2))->toBeTrue()
|
||||
->and($restored->followsCreator(99))->toBeFalse();
|
||||
});
|
||||
|
||||
it('DTO hasSignals returns false for empty profile', function () {
|
||||
$empty = new UserRecoProfileDTO();
|
||||
expect($empty->hasSignals())->toBeFalse();
|
||||
});
|
||||
|
||||
it('DTO hasSignals returns true when tags are present', function () {
|
||||
$dto = new UserRecoProfileDTO(topTagSlugs: ['space']);
|
||||
expect($dto->hasSignals())->toBeTrue();
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// UserPreferenceBuilder
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
it('UserPreferenceBuilder returns empty DTO for user with no activity', function () {
|
||||
$user = User::factory()->create();
|
||||
$builder = app(UserPreferenceBuilder::class);
|
||||
|
||||
$dto = $builder->build($user);
|
||||
|
||||
expect($dto)->toBeInstanceOf(UserRecoProfileDTO::class)
|
||||
->and($dto->topTagSlugs)->toBe([])
|
||||
->and($dto->strongCreatorIds)->toBe([]);
|
||||
});
|
||||
|
||||
it('UserPreferenceBuilder persists profile row on first build', function () {
|
||||
$user = User::factory()->create();
|
||||
$builder = app(UserPreferenceBuilder::class);
|
||||
|
||||
$builder->buildFresh($user);
|
||||
|
||||
expect(UserRecoProfile::find($user->id))->not->toBeNull();
|
||||
});
|
||||
|
||||
it('UserPreferenceBuilder produces stable output on repeated calls', function () {
|
||||
$user = User::factory()->create();
|
||||
$builder = app(UserPreferenceBuilder::class);
|
||||
|
||||
$first = $builder->buildFresh($user)->toArray();
|
||||
$second = $builder->buildFresh($user)->toArray();
|
||||
|
||||
expect($first)->toBe($second);
|
||||
});
|
||||
Reference in New Issue
Block a user