Files
SkinbaseNova/tests/Feature/Recommendations/UserPreferenceBuilderTest.php
Gregor Klevze 67ef79766c 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
2026-02-27 13:34:08 +01:00

82 lines
3.3 KiB
PHP

<?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);
});