Display CMS Optimierungen 29-05-2026
- Mediathek: Video-Vorschaubilder statt Icons (FFmpeg-Thumbnails + Backfill-Command), Kategorie "Sonstiges" - B2in Media-Picker zeigt alle Medientypen, Typ wird automatisch erkannt; Thumbnail-Preview vor allen Medien-URL-Feldern - B2in Marke/Footer: Footer ein/aus, Logo+Claim frei positionierbar (Ecken) mit Constraints, separate Anzeige-Schalter - Angebote-Modul dynamisch: kein Slide-Typ mehr, einheitliches Detail-Layout mit ein-/ausblendbaren Bloecken, Logo/Brand pro Slide, Streichpreis-Option - Player: leere Module stoppen Endlosschleife, dynamische Layout-Anpassung bei verstecktem Footer/Header - Fix: Script-Ladereihenfolge (Livewire vor Flux), entfernte stale public/flux/flux.js, Modal-Crash beim Aktualisieren behoben Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
9262132325
commit
6c6d683b9a
42 changed files with 2267 additions and 13905 deletions
|
|
@ -197,7 +197,7 @@ test('can create a draft playlist from live modules', function () {
|
|||
test('can discard a draft playlist', function () {
|
||||
$user = User::factory()->create();
|
||||
$version = DisplayVersion::factory()->create();
|
||||
$display = Display::factory()->create();
|
||||
$display = Display::factory()->create(['preview_token' => 'token-discard-123456789012345678901234']);
|
||||
createDisplayListPlaylist($display, DisplayPlaylist::STATUS_DRAFT, [$version->id]);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
|
|
@ -205,6 +205,57 @@ test('can discard a draft playlist', function () {
|
|||
->call('discardDraft', $display->id);
|
||||
|
||||
expect($display->fresh()->draftPlaylist)->toBeNull();
|
||||
expect($display->fresh()->preview_token)->toBeNull();
|
||||
});
|
||||
|
||||
test('publishing a draft clears the preview token', function () {
|
||||
$user = User::factory()->create();
|
||||
$liveVersion = DisplayVersion::factory()->create();
|
||||
$draftVersion = DisplayVersion::factory()->create();
|
||||
$display = Display::factory()->create(['preview_token' => 'token-publish-12345678901234567890123456']);
|
||||
createDisplayListPlaylist($display, DisplayPlaylist::STATUS_PUBLISHED, [$liveVersion->id]);
|
||||
createDisplayListPlaylist($display, DisplayPlaylist::STATUS_DRAFT, [$draftVersion->id]);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(DisplayList::class)
|
||||
->call('publishDraft', $display->id);
|
||||
|
||||
expect($display->fresh()->preview_token)->toBeNull();
|
||||
});
|
||||
|
||||
test('can rotate the preview token of a draft', function () {
|
||||
$user = User::factory()->create();
|
||||
$version = DisplayVersion::factory()->create();
|
||||
$display = Display::factory()->create(['preview_token' => 'token-old-1234567890123456789012345678901']);
|
||||
createDisplayListPlaylist($display, DisplayPlaylist::STATUS_DRAFT, [$version->id]);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(DisplayList::class)
|
||||
->call('rotatePreviewToken', $display->id);
|
||||
|
||||
$newToken = $display->fresh()->preview_token;
|
||||
|
||||
expect($newToken)->not->toBeNull()
|
||||
->and($newToken)->not->toBe('token-old-1234567890123456789012345678901');
|
||||
});
|
||||
|
||||
test('can add multiple modules to a playlist at once', function () {
|
||||
$user = User::factory()->create();
|
||||
$version1 = DisplayVersion::factory()->create();
|
||||
$version2 = DisplayVersion::factory()->create();
|
||||
$version3 = DisplayVersion::factory()->create();
|
||||
$display = Display::factory()->create();
|
||||
createDisplayListPlaylist($display, DisplayPlaylist::STATUS_PUBLISHED, []);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(DisplayList::class)
|
||||
->call('openModal', $display->id, DisplayPlaylist::STATUS_PUBLISHED)
|
||||
->set('versionsToAdd', [$version1->id, $version3->id])
|
||||
->call('addSelectedVersions')
|
||||
->assertSet('selectedVersionIds', [$version1->id, $version3->id])
|
||||
->assertSet('versionsToAdd', []);
|
||||
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
|
||||
test('can publish a draft playlist over the live playlist', function () {
|
||||
|
|
@ -313,8 +364,8 @@ test('module select only shows modules that can still be added', function () {
|
|||
Livewire::actingAs($user)
|
||||
->test(DisplayList::class)
|
||||
->call('openModal', $display->id, DisplayPlaylist::STATUS_PUBLISHED)
|
||||
->assertDontSeeHtml('<option value="'.$selectedVersion->id.'">Schon gewählt')
|
||||
->assertSeeHtml('<option value="'.$availableVersion->id.'">Noch verfügbar');
|
||||
->assertDontSee('Schon gewählt (')
|
||||
->assertSee('Noch verfügbar (');
|
||||
});
|
||||
|
||||
test('module select is replaced by a hint when all modules are already selected', function () {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,31 @@ beforeEach(function () {
|
|||
Storage::fake('public');
|
||||
});
|
||||
|
||||
/**
|
||||
* Create a real, tiny mp4 on disk using ffmpeg and return the path.
|
||||
* Returns null when ffmpeg is unavailable so callers can skip.
|
||||
*/
|
||||
function makeRealTestVideo(): ?string
|
||||
{
|
||||
$ffmpeg = trim((string) shell_exec('command -v ffmpeg'));
|
||||
|
||||
if ($ffmpeg === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$path = tempnam(sys_get_temp_dir(), 'vid_').'.mp4';
|
||||
|
||||
$command = sprintf(
|
||||
'%s -y -f lavfi -i testsrc=duration=2:size=320x240:rate=10 -pix_fmt yuv420p %s 2>/dev/null',
|
||||
escapeshellarg($ffmpeg),
|
||||
escapeshellarg($path)
|
||||
);
|
||||
|
||||
shell_exec($command);
|
||||
|
||||
return is_file($path) && filesize($path) > 0 ? $path : null;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// MODEL TESTS
|
||||
// ========================================
|
||||
|
|
@ -66,6 +91,20 @@ it('returns display name from title or filename', function () {
|
|||
expect($withoutTitle->getDisplayName())->toBe('file.mp4');
|
||||
});
|
||||
|
||||
it('returns the stored poster as thumbnail url for a video', function () {
|
||||
$media = DisplayMedia::factory()->video()->create([
|
||||
'thumbnail_path' => 'display-media/2026/05/tour-poster.jpg',
|
||||
]);
|
||||
|
||||
expect($media->getThumbnailUrl())->toContain('display-media/2026/05/tour-poster.jpg');
|
||||
});
|
||||
|
||||
it('has no thumbnail url for a video without a poster', function () {
|
||||
$media = DisplayMedia::factory()->video()->create(['thumbnail_path' => null]);
|
||||
|
||||
expect($media->getThumbnailUrl())->toBeNull();
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// SCOPE TESTS
|
||||
// ========================================
|
||||
|
|
@ -105,6 +144,41 @@ it('filters by collection scope', function () {
|
|||
expect(DisplayMedia::inCollection('immobilien')->count())->toBe(1);
|
||||
});
|
||||
|
||||
it('keeps preceding filters when combined with the search scope', function () {
|
||||
DisplayMedia::factory()->create([
|
||||
'type' => 'image',
|
||||
'filename' => 'haus.webp',
|
||||
'title' => 'Schaufenster',
|
||||
]);
|
||||
DisplayMedia::factory()->video()->create([
|
||||
'filename' => 'tour.mp4',
|
||||
'title' => 'Haus Tour',
|
||||
]);
|
||||
|
||||
$results = DisplayMedia::images()->search('haus')->get();
|
||||
|
||||
expect($results)->toHaveCount(1)
|
||||
->and($results->first()->filename)->toBe('haus.webp');
|
||||
});
|
||||
|
||||
it('search scope respects the active filter on the media picker', function () {
|
||||
DisplayMedia::factory()->create([
|
||||
'filename' => 'aktiv.webp',
|
||||
'title' => 'Goya',
|
||||
'is_active' => true,
|
||||
]);
|
||||
DisplayMedia::factory()->create([
|
||||
'filename' => 'inaktiv.webp',
|
||||
'title' => 'Goya',
|
||||
'is_active' => false,
|
||||
]);
|
||||
|
||||
$results = DisplayMedia::active()->search('goya')->get();
|
||||
|
||||
expect($results)->toHaveCount(1)
|
||||
->and($results->first()->filename)->toBe('aktiv.webp');
|
||||
});
|
||||
|
||||
it('filters by active scope', function () {
|
||||
DisplayMedia::factory()->create(['is_active' => true]);
|
||||
DisplayMedia::factory()->create(['is_active' => false]);
|
||||
|
|
@ -142,6 +216,33 @@ it('stores a video upload', function () {
|
|||
->and($media->mime_type)->toBe('video/mp4');
|
||||
});
|
||||
|
||||
it('generates a poster thumbnail when a real video is uploaded', function () {
|
||||
$videoPath = makeRealTestVideo();
|
||||
|
||||
if ($videoPath === null) {
|
||||
$this->markTestSkipped('ffmpeg is not available.');
|
||||
}
|
||||
|
||||
$service = app(DisplayMediaService::class);
|
||||
$file = new UploadedFile($videoPath, 'showroom.mp4', 'video/mp4', null, true);
|
||||
|
||||
$media = $service->storeUpload($file);
|
||||
|
||||
expect($media->thumbnail_path)->not->toBeNull();
|
||||
expect(Storage::disk('public')->exists($media->thumbnail_path))->toBeTrue();
|
||||
|
||||
@unlink($videoPath);
|
||||
});
|
||||
|
||||
it('does not set a thumbnail when the video cannot be decoded', function () {
|
||||
$service = app(DisplayMediaService::class);
|
||||
$file = UploadedFile::fake()->create('broken.mp4', 500, 'video/mp4');
|
||||
|
||||
$media = $service->storeUpload($file);
|
||||
|
||||
expect($media->thumbnail_path)->toBeNull();
|
||||
});
|
||||
|
||||
it('stores an svg upload as image media', function () {
|
||||
$service = app(DisplayMediaService::class);
|
||||
$file = UploadedFile::fake()->createWithContent(
|
||||
|
|
@ -158,6 +259,44 @@ it('stores an svg upload as image media', function () {
|
|||
expect(Storage::disk('public')->exists($media->path))->toBeTrue();
|
||||
});
|
||||
|
||||
it('resets pagination to page one when a filter changes', function () {
|
||||
Volt::test('admin.cms.display-media-library')
|
||||
->call('gotoPage', 3)
|
||||
->set('filterType', 'video')
|
||||
->assertSet('paginators.page', 1);
|
||||
});
|
||||
|
||||
it('renders external image media with an inline thumbnail', function () {
|
||||
$media = DisplayMedia::factory()->external()->create([
|
||||
'type' => 'image',
|
||||
'external_url' => 'https://example.com/foto.jpg',
|
||||
]);
|
||||
|
||||
Volt::test('admin.cms.display-media-library')
|
||||
->assertSeeHtml('src="https://example.com/foto.jpg"');
|
||||
});
|
||||
|
||||
it('renders a stored poster image for a video with a thumbnail', function () {
|
||||
DisplayMedia::factory()->video()->create([
|
||||
'path' => 'display-media/2026/05/tour.mp4',
|
||||
'thumbnail_path' => 'display-media/2026/05/tour-poster.jpg',
|
||||
]);
|
||||
|
||||
Volt::test('admin.cms.display-media-library')
|
||||
->assertSeeHtml('tour-poster.jpg');
|
||||
});
|
||||
|
||||
it('renders an inline video frame for an uploaded video without a poster', function () {
|
||||
DisplayMedia::factory()->video()->create([
|
||||
'path' => 'display-media/2026/05/clip.mp4',
|
||||
'thumbnail_path' => null,
|
||||
'mime_type' => 'video/mp4',
|
||||
]);
|
||||
|
||||
Volt::test('admin.cms.display-media-library')
|
||||
->assertSeeHtml('clip.mp4#t=1');
|
||||
});
|
||||
|
||||
it('accepts svg uploads in the display media library', function () {
|
||||
$file = UploadedFile::fake()->createWithContent(
|
||||
'brand.svg',
|
||||
|
|
@ -237,6 +376,42 @@ it('deletes external media record', function () {
|
|||
expect(DisplayMedia::find($media->id))->toBeNull();
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// COMMAND TESTS
|
||||
// ========================================
|
||||
|
||||
it('reports when there are no videos to process', function () {
|
||||
$this->artisan('display-media:generate-video-thumbnails')
|
||||
->expectsOutputToContain('Keine Videos zum Verarbeiten gefunden.')
|
||||
->assertSuccessful();
|
||||
});
|
||||
|
||||
it('backfills posters for existing uploaded videos', function () {
|
||||
$videoPath = makeRealTestVideo();
|
||||
|
||||
if ($videoPath === null) {
|
||||
$this->markTestSkipped('ffmpeg is not available.');
|
||||
}
|
||||
|
||||
Storage::disk('public')->putFileAs(
|
||||
'display-media/2026/05',
|
||||
new UploadedFile($videoPath, 'clip.mp4', 'video/mp4', null, true),
|
||||
'clip.mp4'
|
||||
);
|
||||
|
||||
$media = DisplayMedia::factory()->video()->create([
|
||||
'source_type' => 'upload',
|
||||
'path' => 'display-media/2026/05/clip.mp4',
|
||||
'thumbnail_path' => null,
|
||||
]);
|
||||
|
||||
$this->artisan('display-media:generate-video-thumbnails')->assertSuccessful();
|
||||
|
||||
expect($media->fresh()->thumbnail_path)->not->toBeNull();
|
||||
|
||||
@unlink($videoPath);
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// ROUTE TESTS
|
||||
// ========================================
|
||||
|
|
|
|||
|
|
@ -115,9 +115,45 @@ test('returns playlist with b2in config', function () {
|
|||
$response->assertSuccessful();
|
||||
$response->assertJsonPath('playlist.0.type', 'b2in');
|
||||
$response->assertJsonPath('playlist.0.settings.theme', 'dark');
|
||||
$response->assertJsonPath('playlist.0.settings.show_footer', true);
|
||||
$response->assertJsonPath('playlist.0.settings.logo_position', 'top-left');
|
||||
$response->assertJsonPath('playlist.0.settings.claim_position', 'top-right');
|
||||
$response->assertJsonPath('playlist.0.items.0.category', 'immobilien');
|
||||
});
|
||||
|
||||
test('b2in config passes through custom brand positions and footer toggle', function () {
|
||||
$version = DisplayVersion::factory()->create([
|
||||
'type' => 'b2in',
|
||||
'settings' => [
|
||||
'show_footer' => false,
|
||||
'logo_position' => 'bottom-right',
|
||||
'claim_position' => 'top-left',
|
||||
],
|
||||
]);
|
||||
DisplayVersionItem::factory()->create([
|
||||
'display_version_id' => $version->id,
|
||||
'item_type' => 'media',
|
||||
'content' => [
|
||||
'category' => 'sonstiges',
|
||||
'media_type' => 'image',
|
||||
'media_url' => '../assets/test.jpg',
|
||||
'headline' => 'H',
|
||||
'subline' => 'S',
|
||||
'duration_seconds' => 10,
|
||||
],
|
||||
]);
|
||||
$display = Display::factory()->create();
|
||||
publishDisplayModules($display, [$version->id]);
|
||||
|
||||
$response = $this->getJson("/api/display/{$display->id}/config");
|
||||
|
||||
$response->assertSuccessful();
|
||||
$response->assertJsonPath('playlist.0.settings.show_footer', false);
|
||||
$response->assertJsonPath('playlist.0.settings.logo_position', 'bottom-right');
|
||||
$response->assertJsonPath('playlist.0.settings.claim_position', 'top-left');
|
||||
$response->assertJsonPath('playlist.0.items.0.category', 'sonstiges');
|
||||
});
|
||||
|
||||
test('returns playlist with offers config', function () {
|
||||
$version = DisplayVersion::factory()->create([
|
||||
'type' => 'offers',
|
||||
|
|
@ -320,38 +356,32 @@ test('module preview returns a single module config', function () {
|
|||
$response->assertJsonPath('playlist.0.items.0.category', 'moebel');
|
||||
});
|
||||
|
||||
test('module preview exposes configurable player chrome settings', function () {
|
||||
test('offers slides carry their own logo and brand chrome', function () {
|
||||
$version = DisplayVersion::factory()->create([
|
||||
'type' => 'offers',
|
||||
'name' => 'Custom Chrome',
|
||||
'settings' => [
|
||||
'logo_url' => '/storage/display-media/logo.svg',
|
||||
'brand_text' => 'Musterstadt',
|
||||
'footer_claim' => 'Beratung im Schauraum',
|
||||
'footer_url' => 'cabinet-bielefeld.de',
|
||||
'qr_default_title' => 'Mehr Infos',
|
||||
'qr_subtitle' => 'Jetzt scannen',
|
||||
],
|
||||
]);
|
||||
DisplayVersionItem::factory()->create([
|
||||
'display_version_id' => $version->id,
|
||||
'item_type' => 'slide',
|
||||
'content' => [
|
||||
'type' => 'intro',
|
||||
'type' => 'detail',
|
||||
'title' => 'Intro',
|
||||
'image_url' => '../assets/intro.jpg',
|
||||
'show_logo' => true,
|
||||
'logo_url' => '/storage/display-media/logo.svg',
|
||||
'brand_text' => 'Musterstadt',
|
||||
'brand_tagline' => 'Beratung im Schauraum',
|
||||
],
|
||||
]);
|
||||
|
||||
$response = $this->getJson("/api/display/module/{$version->id}/preview");
|
||||
|
||||
$response->assertSuccessful();
|
||||
$response->assertJsonPath('playlist.0.settings.logo_url', '/storage/display-media/logo.svg');
|
||||
$response->assertJsonPath('playlist.0.settings.brand_text', 'Musterstadt');
|
||||
$response->assertJsonPath('playlist.0.settings.footer_claim', 'Beratung im Schauraum');
|
||||
$response->assertJsonPath('playlist.0.settings.footer_url', 'cabinet-bielefeld.de');
|
||||
$response->assertJsonPath('playlist.0.settings.qr_default_title', 'Mehr Infos');
|
||||
$response->assertJsonPath('playlist.0.settings.qr_subtitle', 'Jetzt scannen');
|
||||
$response->assertJsonPath('playlist.0.slides.0.show_logo', true);
|
||||
$response->assertJsonPath('playlist.0.slides.0.logo_url', '/storage/display-media/logo.svg');
|
||||
$response->assertJsonPath('playlist.0.slides.0.brand_text', 'Musterstadt');
|
||||
$response->assertJsonPath('playlist.0.slides.0.brand_tagline', 'Beratung im Schauraum');
|
||||
});
|
||||
|
||||
test('module item preview returns only the selected slide', function () {
|
||||
|
|
@ -416,11 +446,28 @@ test('display player keeps previews in a strict 9 by 16 viewport', function () {
|
|||
->toContain('Aktive Live-Displays')
|
||||
->toContain('renderOverview(data.displays || [])')
|
||||
->toContain('translate(${offsetX}px, ${offsetY}px) scale(${scale})')
|
||||
->toContain('this.settings.logo_url')
|
||||
->toContain('this.settings.footer_claim')
|
||||
->toContain('this.settings.footer_url')
|
||||
->toContain('this.settings.header_logo_url')
|
||||
->toContain('this.settings.qr_label');
|
||||
->toContain('this.settings.qr_label')
|
||||
->toContain('this.settings.show_footer !== false')
|
||||
->toContain('this.settings.show_logo !== false')
|
||||
->toContain('this.settings.show_claim !== false')
|
||||
->toContain('this.settings.logo_position')
|
||||
->toContain('this.settings.claim_position')
|
||||
->toContain('b2in-brand-logo pos-')
|
||||
->toContain('b2in-scrim')
|
||||
->toContain('.b2in-layer.no-footer .b2in-text');
|
||||
});
|
||||
|
||||
test('display player stops looping when the playlist has no playable content', function () {
|
||||
$player = file_get_contents(public_path('_cabinet/display/index.html'));
|
||||
|
||||
expect($player)
|
||||
->toContain('versionHasContent(version)')
|
||||
->toContain('showEmptyPlaylist()')
|
||||
->toContain('this.emptyVersionStreak >= this.playlist.length')
|
||||
->toContain('Noch keine Inhalte vorhanden');
|
||||
});
|
||||
|
||||
test('preview player pages are reachable for valid display and module previews', function () {
|
||||
|
|
@ -438,6 +485,20 @@ test('preview player pages are reachable for valid display and module previews',
|
|||
->assertSuccessful();
|
||||
});
|
||||
|
||||
test('preview player is served with a revalidation cache header', function () {
|
||||
$display = Display::factory()->create(['preview_token' => 'fresh-token']);
|
||||
$version = DisplayVersion::factory()->create();
|
||||
|
||||
foreach ([
|
||||
'/preview/fresh-token',
|
||||
"/preview/module/{$version->id}",
|
||||
] as $url) {
|
||||
$response = $this->get($url);
|
||||
$response->assertSuccessful();
|
||||
expect($response->headers->get('Cache-Control'))->toContain('no-cache');
|
||||
}
|
||||
});
|
||||
|
||||
test('existing display config api still works', function () {
|
||||
$response = $this->getJson('/api/display/config');
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@
|
|||
use App\Enums\DisplayVersionType;
|
||||
use App\Livewire\Admin\Cms\DisplayVersionEditor;
|
||||
use App\Livewire\Admin\Cms\DisplayVersionList;
|
||||
use App\Models\Display;
|
||||
use App\Models\DisplayMedia;
|
||||
use App\Models\DisplayPlaylist;
|
||||
use App\Models\DisplayPlaylistItem;
|
||||
use App\Models\DisplayVersion;
|
||||
use App\Models\DisplayVersionItem;
|
||||
use App\Models\User;
|
||||
|
|
@ -91,6 +95,29 @@ test('can delete a display version', function () {
|
|||
expect(DisplayVersion::find($version->id))->toBeNull();
|
||||
});
|
||||
|
||||
test('cannot delete a display version that is used by a playlist', function () {
|
||||
$user = User::factory()->create();
|
||||
$version = DisplayVersion::factory()->create(['name' => 'Genutztes Modul']);
|
||||
$display = Display::factory()->create();
|
||||
$playlist = DisplayPlaylist::factory()->create([
|
||||
'display_id' => $display->id,
|
||||
'status' => DisplayPlaylist::STATUS_PUBLISHED,
|
||||
'published_at' => now(),
|
||||
]);
|
||||
DisplayPlaylistItem::factory()->create([
|
||||
'display_playlist_id' => $playlist->id,
|
||||
'display_version_id' => $version->id,
|
||||
'sort_order' => 0,
|
||||
]);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(DisplayVersionList::class)
|
||||
->call('deleteVersion', $version->id)
|
||||
->assertSee('kann nicht gelöscht werden');
|
||||
|
||||
expect(DisplayVersion::find($version->id))->not->toBeNull();
|
||||
});
|
||||
|
||||
test('can toggle display version active status', function () {
|
||||
$user = User::factory()->create();
|
||||
$version = DisplayVersion::factory()->create(['is_active' => true]);
|
||||
|
|
@ -152,6 +179,81 @@ test('item edit modal renders module iframe preview', function () {
|
|||
->assertSee('Schließen');
|
||||
});
|
||||
|
||||
test('item modal url inputs avoid wire:model.blur to prevent the teleported-modal cleanup crash', function () {
|
||||
$user = User::factory()->create();
|
||||
$version = DisplayVersion::factory()->create(['type' => 'b2in']);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(DisplayVersionEditor::class, ['displayVersion' => $version])
|
||||
->call('openItemModal', null, 'media')
|
||||
->assertDontSee('wire:model.blur', false)
|
||||
->assertSee('wire:model.live.debounce.500ms="mediaUrl"', false);
|
||||
});
|
||||
|
||||
test('video item modal labels the title field as an internal name not shown on screen', function () {
|
||||
$user = User::factory()->create();
|
||||
$version = DisplayVersion::factory()->create(['type' => 'video-display']);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(DisplayVersionEditor::class, ['displayVersion' => $version])
|
||||
->call('openItemModal', null, 'video')
|
||||
->assertSee('Name')
|
||||
->assertSee('wird nicht im Video eingeblendet')
|
||||
->assertDontSee('Titel (optional)');
|
||||
});
|
||||
|
||||
test('video playlist renders a media thumbnail for stored videos', function () {
|
||||
$user = User::factory()->create();
|
||||
$version = DisplayVersion::factory()->create(['type' => 'video-display']);
|
||||
DisplayVersionItem::factory()->create([
|
||||
'display_version_id' => $version->id,
|
||||
'item_type' => 'video',
|
||||
'content' => ['filename' => '/storage/display-media/clip.mp4', 'title' => 'Clip', 'position' => 25],
|
||||
]);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(DisplayVersionEditor::class, ['displayVersion' => $version])
|
||||
->assertSee('/storage/display-media/clip.mp4#t=1', false);
|
||||
});
|
||||
|
||||
test('new item modal shows a placeholder instead of the module preview', function () {
|
||||
$user = User::factory()->create();
|
||||
$version = DisplayVersion::factory()->create(['type' => 'video-display']);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(DisplayVersionEditor::class, ['displayVersion' => $version])
|
||||
->call('openItemModal', null, 'video')
|
||||
->assertSee('Die Vorschau erscheint, sobald der Inhalt gespeichert wurde.')
|
||||
->assertSee('src="about:blank"', false)
|
||||
->assertDontSee('/preview/module/'.$version->id.'/item/', false);
|
||||
});
|
||||
|
||||
test('item modal preview iframe keeps a stable wire:key to avoid morph crashes', function () {
|
||||
$user = User::factory()->create();
|
||||
$version = DisplayVersion::factory()->create(['type' => 'video-display']);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(DisplayVersionEditor::class, ['displayVersion' => $version])
|
||||
->call('openItemModal', null, 'video')
|
||||
->assertSee('wire:key="item-modal-preview"', false);
|
||||
});
|
||||
|
||||
test('saved item modal shows the single-item preview iframe', function () {
|
||||
$user = User::factory()->create();
|
||||
$version = DisplayVersion::factory()->create(['type' => 'video-display']);
|
||||
$item = DisplayVersionItem::factory()->create([
|
||||
'display_version_id' => $version->id,
|
||||
'item_type' => 'video',
|
||||
'content' => ['filename' => 'clip.mp4', 'title' => 'Clip', 'position' => 25],
|
||||
]);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(DisplayVersionEditor::class, ['displayVersion' => $version])
|
||||
->call('openItemModal', $item->id)
|
||||
->assertSee('wire:key="item-modal-preview"', false)
|
||||
->assertSee('/preview/module/'.$version->id.'/item/'.$item->id, false);
|
||||
});
|
||||
|
||||
test('can add a video item to video-display version', function () {
|
||||
$user = User::factory()->create();
|
||||
$version = DisplayVersion::factory()->create(['type' => 'video-display']);
|
||||
|
|
@ -213,6 +315,26 @@ test('can edit an existing item', function () {
|
|||
expect($item->content['title'])->toBe('New Title');
|
||||
});
|
||||
|
||||
test('editing an inactive item keeps it inactive', function () {
|
||||
$user = User::factory()->create();
|
||||
$version = DisplayVersion::factory()->create(['type' => 'video-display']);
|
||||
$item = DisplayVersionItem::factory()->create([
|
||||
'display_version_id' => $version->id,
|
||||
'item_type' => 'video',
|
||||
'content' => ['filename' => 'old.mp4', 'title' => 'Old', 'position' => 25],
|
||||
'is_active' => false,
|
||||
]);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(DisplayVersionEditor::class, ['displayVersion' => $version])
|
||||
->call('openItemModal', $item->id)
|
||||
->assertSet('videoIsActive', false)
|
||||
->set('videoTitle', 'New Title')
|
||||
->call('saveItem');
|
||||
|
||||
expect($item->fresh()->is_active)->toBeFalse();
|
||||
});
|
||||
|
||||
test('updating an item keeps modal open and refreshes iframe preview', function () {
|
||||
$user = User::factory()->create();
|
||||
$version = DisplayVersion::factory()->create(['type' => 'offers']);
|
||||
|
|
@ -299,12 +421,13 @@ test('can add a slide item to offers version', function () {
|
|||
Livewire::actingAs($user)
|
||||
->test(DisplayVersionEditor::class, ['displayVersion' => $version])
|
||||
->call('openItemModal', null, 'slide')
|
||||
->set('slideType', 'product-hero')
|
||||
->set('slideDuration', 10000)
|
||||
->set('slideImageUrl', '../assets/goya1.jpg')
|
||||
->set('slideShowBadge', true)
|
||||
->set('slideBadge', 'Einzelstück')
|
||||
->set('slideEyebrow', 'Hersteller: Sudbrock')
|
||||
->set('slideTitle', 'GOYA Sideboard')
|
||||
->set('slideShowPrice', true)
|
||||
->set('slidePrice', '489 €')
|
||||
->set('slideOriginalPrice', 'statt 4.744 €')
|
||||
->set('slideQrUrl', 'https://cabinet-bielefeld.de')
|
||||
|
|
@ -315,9 +438,10 @@ test('can add a slide item to offers version', function () {
|
|||
$item = DisplayVersionItem::where('display_version_id', $version->id)->first();
|
||||
expect($item)->not->toBeNull();
|
||||
expect($item->item_type)->toBe('slide');
|
||||
expect($item->content['type'])->toBe('product-hero');
|
||||
expect($item->content['type'])->toBe('detail');
|
||||
expect($item->content['title'])->toBe('GOYA Sideboard');
|
||||
expect($item->content['price'])->toBe('489 €');
|
||||
expect($item->content['show_price'])->toBeTrue();
|
||||
expect($item->content['image_url'])->toBe('../assets/goya1.jpg');
|
||||
expect($item->content['qr_url'])->toBe('https://cabinet-bielefeld.de');
|
||||
});
|
||||
|
|
@ -329,14 +453,99 @@ test('can add a slide with bullets to offers version', function () {
|
|||
Livewire::actingAs($user)
|
||||
->test(DisplayVersionEditor::class, ['displayVersion' => $version])
|
||||
->call('openItemModal', null, 'slide')
|
||||
->set('slideType', 'product-details')
|
||||
->set('slideTitle', 'GOYA Sideboard')
|
||||
->set('slideShowBullets', true)
|
||||
->set('slideBullets', ['Einzelstück', 'Abholung möglich', 'Lieferung optional'])
|
||||
->call('saveItem');
|
||||
|
||||
$item = DisplayVersionItem::where('display_version_id', $version->id)->first();
|
||||
expect($item->content['bullets'])->toHaveCount(3);
|
||||
expect($item->content['bullets'][0])->toBe('Einzelstück');
|
||||
expect($item->content['show_bullets'])->toBeTrue();
|
||||
});
|
||||
|
||||
test('slide editor persists the strike-through original price option', function () {
|
||||
$user = User::factory()->create();
|
||||
$version = DisplayVersion::factory()->create(['type' => 'offers']);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(DisplayVersionEditor::class, ['displayVersion' => $version])
|
||||
->call('openItemModal', null, 'slide')
|
||||
->set('slideTitle', 'Streich Slide')
|
||||
->set('slideShowPrice', true)
|
||||
->set('slidePrice', '199 €')
|
||||
->set('slideOriginalPrice', 'statt 399 €')
|
||||
->set('slideStrikeOriginalPrice', true)
|
||||
->call('saveItem');
|
||||
|
||||
$item = DisplayVersionItem::where('display_version_id', $version->id)->first();
|
||||
expect($item->content['strike_original_price'])->toBeTrue();
|
||||
expect($item->content['original_price'])->toBe('statt 399 €');
|
||||
});
|
||||
|
||||
test('slide editor no longer exposes a slide type selector', function () {
|
||||
$user = User::factory()->create();
|
||||
$version = DisplayVersion::factory()->create(['type' => 'offers']);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(DisplayVersionEditor::class, ['displayVersion' => $version])
|
||||
->call('openItemModal', null, 'slide')
|
||||
->assertDontSee('Slide-Typ')
|
||||
->assertSee('Bild & Badge')
|
||||
->assertSee('Badge anzeigen')
|
||||
->assertSee('Aufzählung anzeigen')
|
||||
->assertSee('QR-Code anzeigen');
|
||||
});
|
||||
|
||||
test('hidden slide blocks are persisted via their show flags', function () {
|
||||
$user = User::factory()->create();
|
||||
$version = DisplayVersion::factory()->create(['type' => 'offers']);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(DisplayVersionEditor::class, ['displayVersion' => $version])
|
||||
->call('openItemModal', null, 'slide')
|
||||
->set('slideTitle', 'Nur Titel')
|
||||
->set('slideShowBadge', false)
|
||||
->set('slideShowEyebrow', false)
|
||||
->set('slideShowBullets', false)
|
||||
->set('slideShowPrice', false)
|
||||
->set('slideShowQr', false)
|
||||
->set('slideShowContact', false)
|
||||
->call('saveItem');
|
||||
|
||||
$item = DisplayVersionItem::where('display_version_id', $version->id)->first();
|
||||
expect($item->content['show_badge'])->toBeFalse();
|
||||
expect($item->content['show_eyebrow'])->toBeFalse();
|
||||
expect($item->content['show_bullets'])->toBeFalse();
|
||||
expect($item->content['show_price'])->toBeFalse();
|
||||
expect($item->content['show_qr'])->toBeFalse();
|
||||
expect($item->content['show_contact'])->toBeFalse();
|
||||
});
|
||||
|
||||
test('legacy slides without show flags keep rendering their populated blocks', function () {
|
||||
$version = DisplayVersion::factory()->create(['type' => 'offers']);
|
||||
DisplayVersionItem::factory()->create([
|
||||
'display_version_id' => $version->id,
|
||||
'item_type' => 'slide',
|
||||
'content' => [
|
||||
'type' => 'product-hero',
|
||||
'title' => 'Legacy Slide',
|
||||
'badge_text' => 'Einzelstück',
|
||||
'price' => '489 €',
|
||||
'bullets' => ['Punkt A'],
|
||||
],
|
||||
]);
|
||||
|
||||
$config = app(\App\Services\DisplayPlaylistConfigBuilder::class)
|
||||
->fromModules(DisplayVersion::whereKey($version->id)->with('items')->get());
|
||||
|
||||
$slide = $config['playlist'][0]['slides'][0];
|
||||
|
||||
expect($slide['show_badge'])->toBeTrue();
|
||||
expect($slide['show_price'])->toBeTrue();
|
||||
expect($slide['show_bullets'])->toBeTrue();
|
||||
expect($slide['show_subline'])->toBeFalse();
|
||||
expect($slide['show_disclaimer'])->toBeFalse();
|
||||
});
|
||||
|
||||
test('can edit a slide item with new fields', function () {
|
||||
|
|
@ -399,22 +608,174 @@ test('can save version settings', function () {
|
|||
expect($version->fresh()->settings['footer_prefix'])->toBe('powered by');
|
||||
});
|
||||
|
||||
test('module settings expose player chrome fields in the editor', function () {
|
||||
test('b2in defaults include brand position and footer toggle', function () {
|
||||
$defaults = \App\Support\DisplayModuleSettings::defaults(DisplayVersionType::B2in);
|
||||
|
||||
expect($defaults)->toHaveKey('logo_position', 'top-left')
|
||||
->and($defaults)->toHaveKey('claim_position', 'top-right')
|
||||
->and($defaults)->toHaveKey('show_footer', true)
|
||||
->and($defaults)->toHaveKey('show_logo', true)
|
||||
->and($defaults)->toHaveKey('show_claim', true);
|
||||
});
|
||||
|
||||
test('selecting a video medium auto-sets the media type to video', function () {
|
||||
$user = User::factory()->create();
|
||||
$version = DisplayVersion::factory()->create(['type' => 'b2in']);
|
||||
$media = DisplayMedia::factory()->video()->create();
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(DisplayVersionEditor::class, ['displayVersion' => $version])
|
||||
->call('openItemModal', null, 'media')
|
||||
->set('mediaType', 'image')
|
||||
->call('onDisplayMediaSelected', 'mediaUrl', $media->id, $media->getUrl())
|
||||
->assertSet('mediaType', 'video');
|
||||
});
|
||||
|
||||
test('selecting an image medium auto-sets the media type to image', function () {
|
||||
$user = User::factory()->create();
|
||||
$version = DisplayVersion::factory()->create(['type' => 'b2in']);
|
||||
$media = DisplayMedia::factory()->create(['type' => 'image']);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(DisplayVersionEditor::class, ['displayVersion' => $version])
|
||||
->call('openItemModal', null, 'media')
|
||||
->set('mediaType', 'video')
|
||||
->call('onDisplayMediaSelected', 'mediaUrl', $media->id, $media->getUrl())
|
||||
->assertSet('mediaType', 'image');
|
||||
});
|
||||
|
||||
test('media item modal offers the sonstiges category and an automatic type hint', function () {
|
||||
$user = User::factory()->create();
|
||||
$version = DisplayVersion::factory()->create(['type' => 'b2in']);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(DisplayVersionEditor::class, ['displayVersion' => $version])
|
||||
->call('openItemModal', null, 'media')
|
||||
->assertSee('Sonstiges')
|
||||
->assertSee('wird automatisch aus dem gewählten Medium erkannt');
|
||||
});
|
||||
|
||||
test('b2in settings editor shows the brand section and footer toggle', function () {
|
||||
$user = User::factory()->create();
|
||||
$version = DisplayVersion::factory()->create(['type' => 'b2in']);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(DisplayVersionEditor::class, ['displayVersion' => $version])
|
||||
->assertSee('Marke')
|
||||
->assertSee('Logo-Position')
|
||||
->assertSee('Claim-Position')
|
||||
->assertSee('Logo anzeigen')
|
||||
->assertSee('Claim anzeigen')
|
||||
->assertSee('Footer anzeigen');
|
||||
});
|
||||
|
||||
test('saving b2in settings persists logo and claim visibility', function () {
|
||||
$user = User::factory()->create();
|
||||
$version = DisplayVersion::factory()->create(['type' => 'b2in']);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(DisplayVersionEditor::class, ['displayVersion' => $version])
|
||||
->call('openSettingsModal')
|
||||
->set('settings.show_logo', false)
|
||||
->set('settings.show_claim', false)
|
||||
->call('saveSettings');
|
||||
|
||||
$settings = $version->fresh()->settings;
|
||||
expect($settings['show_logo'])->toBeFalse()
|
||||
->and($settings['show_claim'])->toBeFalse();
|
||||
});
|
||||
|
||||
test('enabling the footer pulls bottom brand positions back to the top', function () {
|
||||
$user = User::factory()->create();
|
||||
$version = DisplayVersion::factory()->create(['type' => 'b2in']);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(DisplayVersionEditor::class, ['displayVersion' => $version])
|
||||
->set('settings.show_footer', false)
|
||||
->set('settings.logo_position', 'bottom-left')
|
||||
->set('settings.claim_position', 'bottom-right')
|
||||
->set('settings.show_footer', true)
|
||||
->assertSet('settings.logo_position', 'top-left')
|
||||
->assertSet('settings.claim_position', 'top-right');
|
||||
});
|
||||
|
||||
test('bottom brand positions are kept when the footer is hidden', function () {
|
||||
$user = User::factory()->create();
|
||||
$version = DisplayVersion::factory()->create(['type' => 'b2in']);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(DisplayVersionEditor::class, ['displayVersion' => $version])
|
||||
->set('settings.show_footer', false)
|
||||
->set('settings.logo_position', 'bottom-left')
|
||||
->assertSet('settings.logo_position', 'bottom-left');
|
||||
});
|
||||
|
||||
test('claim never shares the same corner as the logo', function () {
|
||||
$user = User::factory()->create();
|
||||
$version = DisplayVersion::factory()->create(['type' => 'b2in']);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(DisplayVersionEditor::class, ['displayVersion' => $version])
|
||||
->set('settings.show_footer', false)
|
||||
->set('settings.logo_position', 'bottom-left')
|
||||
->set('settings.claim_position', 'bottom-left')
|
||||
->assertSet('settings.claim_position', fn ($value) => $value !== 'bottom-left');
|
||||
});
|
||||
|
||||
test('saving b2in settings persists brand positions and footer toggle', function () {
|
||||
$user = User::factory()->create();
|
||||
$version = DisplayVersion::factory()->create(['type' => 'b2in']);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(DisplayVersionEditor::class, ['displayVersion' => $version])
|
||||
->call('openSettingsModal')
|
||||
->set('settings.show_footer', false)
|
||||
->set('settings.logo_position', 'bottom-right')
|
||||
->set('settings.claim_position', 'top-left')
|
||||
->call('saveSettings');
|
||||
|
||||
$settings = $version->fresh()->settings;
|
||||
expect($settings['show_footer'])->toBeFalse()
|
||||
->and($settings['logo_position'])->toBe('bottom-right')
|
||||
->and($settings['claim_position'])->toBe('top-left');
|
||||
});
|
||||
|
||||
test('offers meta settings only keep loop and transition', function () {
|
||||
$user = User::factory()->create();
|
||||
$version = DisplayVersion::factory()->create(['type' => 'offers']);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(DisplayVersionEditor::class, ['displayVersion' => $version])
|
||||
->assertSee('Meta-Einstellungen für dieses Modul')
|
||||
->assertSee('Diese Werte gelten für die gesamte Media-Playlist bzw. alle Slides dieses Moduls.')
|
||||
->call('openSettingsModal')
|
||||
->assertSee('Branding')
|
||||
->assertSee('Logo URL')
|
||||
->assertDontSee('Logo Alt-Text')
|
||||
->assertSee('Brand-Text')
|
||||
->assertSee('Footer & QR für alle Slides')
|
||||
->assertSee('Footer-Claim')
|
||||
->assertSee('Web/QR-URL')
|
||||
->assertSee('Standard QR-Titel')
|
||||
->assertSee('QR-Unterzeile');
|
||||
->assertSee('Endlosschleife')
|
||||
->assertSee('Transition')
|
||||
->assertSee('Transition-Dauer (ms)')
|
||||
->assertDontSee('Branding')
|
||||
->assertDontSee('Brand-Text')
|
||||
->assertDontSee('Footer & QR für alle Slides')
|
||||
->assertDontSee('Footer-Claim');
|
||||
});
|
||||
|
||||
test('offer slide editor manages logo and brand text per slide', function () {
|
||||
$user = User::factory()->create();
|
||||
$version = DisplayVersion::factory()->create(['type' => 'offers']);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(DisplayVersionEditor::class, ['displayVersion' => $version])
|
||||
->call('openItemModal', null, 'slide')
|
||||
->assertSee('Logo & Marke')
|
||||
->assertSee('Logo & Marken-Text anzeigen')
|
||||
->set('slideTitle', 'Logo Slide')
|
||||
->set('slideShowLogo', true)
|
||||
->set('slideLogoUrl', '/storage/display-media/logo.svg')
|
||||
->set('slideBrandText', 'Musterstadt')
|
||||
->set('slideBrandTagline', 'Beratung • Lieferung')
|
||||
->call('saveItem');
|
||||
|
||||
$item = DisplayVersionItem::where('display_version_id', $version->id)->first();
|
||||
expect($item->content['show_logo'])->toBeTrue();
|
||||
expect($item->content['logo_url'])->toBe('/storage/display-media/logo.svg');
|
||||
expect($item->content['brand_text'])->toBe('Musterstadt');
|
||||
expect($item->content['brand_tagline'])->toBe('Beratung • Lieferung');
|
||||
});
|
||||
|
|
|
|||
41
tests/Feature/LayoutScriptOrderTest.php
Normal file
41
tests/Feature/LayoutScriptOrderTest.php
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
|
||||
beforeEach(function () {
|
||||
URL::forceRootUrl('https://'.config('domains.domain_portal'));
|
||||
});
|
||||
|
||||
test('admin layout loads livewire before flux so flux can register its alpine components', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$content = $this->actingAs($user)
|
||||
->get(route('admin.cms.display-modules'))
|
||||
->assertSuccessful()
|
||||
->getContent();
|
||||
|
||||
$livewirePosition = strpos($content, 'livewire.js?id=');
|
||||
$fluxPosition = strpos($content, '/flux/flux');
|
||||
|
||||
expect($livewirePosition)->not->toBeFalse('Livewire script tag is missing from the layout.');
|
||||
expect($fluxPosition)->not->toBeFalse('Flux script tag is missing from the layout.');
|
||||
expect($livewirePosition)->toBeLessThan(
|
||||
$fluxPosition,
|
||||
'Livewire must be loaded before Flux; otherwise window.Alpine is undefined when flux.js registers fluxModal/fluxSelectSearchClearable.'
|
||||
);
|
||||
});
|
||||
|
||||
test('admin layout loads the livewire script only once', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$content = $this->actingAs($user)
|
||||
->get(route('admin.cms.display-modules'))
|
||||
->assertSuccessful()
|
||||
->getContent();
|
||||
|
||||
expect(substr_count($content, 'livewire.js?id='))->toBe(
|
||||
1,
|
||||
'Livewire must only be injected once; a duplicate script tag creates multiple Alpine instances.'
|
||||
);
|
||||
});
|
||||
|
|
@ -57,11 +57,11 @@ it('announcement bar visible on about', fn () => $this->get('/about')->assertSee
|
|||
it('homepage has navigation', fn () => $this->get('/')->assertSee('Immobilien')->assertSee('Netzwerk')->assertSee('Magazin'));
|
||||
it('immobilien has all five sections', function () {
|
||||
$this->get('/immobilien')
|
||||
->assertSee('Dynamik')
|
||||
->assertSee('Investment in Dubai')
|
||||
->assertSee('Kaufprozess')
|
||||
->assertSee('Meine Aufgabe')
|
||||
->assertSee('Investor');
|
||||
->assertSee('Warum Dubai für Investoren relevant bleibt')
|
||||
->assertSee('Der Kaufprozess mit B2in')
|
||||
->assertSee('Passt Dubai zu Ihrer Investmentstrategie?')
|
||||
->assertSee('Einrichtung mitdenken')
|
||||
->assertSee('Investoren');
|
||||
});
|
||||
it('netzwerk has teaser cards', fn () => $this->get('/netzwerk')->assertSee('Einrichtungsnetzwerk')->assertSee('In Entwicklung'));
|
||||
it('magazin has all five articles', function () {
|
||||
|
|
|
|||
19
tests/Feature/TestDisplaySeederTest.php
Normal file
19
tests/Feature/TestDisplaySeederTest.php
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\Display;
|
||||
use Database\Seeders\TestDisplaySeeder;
|
||||
|
||||
it('seeds exactly one test display', function () {
|
||||
$this->seed(TestDisplaySeeder::class);
|
||||
|
||||
expect(Display::query()->where('is_test', true)->count())->toBe(1);
|
||||
});
|
||||
|
||||
it('is idempotent and does not create a second test display', function () {
|
||||
$this->seed(TestDisplaySeeder::class);
|
||||
$this->seed(TestDisplaySeeder::class);
|
||||
|
||||
expect(Display::query()->where('is_test', true)->count())->toBe(1);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue