12-05-2026 admin, Panel Displays
This commit is contained in:
parent
0762e3beac
commit
6a65354f4c
43 changed files with 3273 additions and 410 deletions
|
|
@ -2,16 +2,39 @@
|
|||
|
||||
use App\Livewire\Admin\Cms\DisplayList;
|
||||
use App\Models\Display;
|
||||
use App\Models\DisplayPlaylist;
|
||||
use App\Models\DisplayPlaylistItem;
|
||||
use App\Models\DisplayVersion;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Livewire\Livewire;
|
||||
|
||||
beforeEach(function () {
|
||||
$portalDomain = config('domains.domain_portal');
|
||||
Livewire::withoutLazyLoading();
|
||||
url()->forceRootUrl('https://'.$portalDomain);
|
||||
URL::forceRootUrl('https://'.$portalDomain);
|
||||
});
|
||||
|
||||
function createDisplayListPlaylist(Display $display, string $status, array $versionIds): DisplayPlaylist
|
||||
{
|
||||
$playlist = DisplayPlaylist::factory()
|
||||
->state(['status' => $status])
|
||||
->create([
|
||||
'display_id' => $display->id,
|
||||
'published_at' => $status === DisplayPlaylist::STATUS_PUBLISHED ? now() : null,
|
||||
]);
|
||||
|
||||
foreach (array_values($versionIds) as $sortOrder => $versionId) {
|
||||
DisplayPlaylistItem::factory()->create([
|
||||
'display_playlist_id' => $playlist->id,
|
||||
'display_version_id' => $versionId,
|
||||
'sort_order' => $sortOrder,
|
||||
]);
|
||||
}
|
||||
|
||||
return $playlist;
|
||||
}
|
||||
|
||||
test('display list requires authentication', function () {
|
||||
$response = $this->get(route('admin.cms.displays'));
|
||||
|
||||
|
|
@ -59,6 +82,7 @@ test('can assign versions to a display', function () {
|
|||
expect($display->versions)->toHaveCount(2);
|
||||
expect($display->versions->first()->id)->toBe($version1->id);
|
||||
expect($display->versions->last()->id)->toBe($version2->id);
|
||||
expect($display->livePlaylist->modules->pluck('id')->all())->toBe([$version1->id, $version2->id]);
|
||||
});
|
||||
|
||||
test('can reorder versions in playlist', function () {
|
||||
|
|
@ -78,6 +102,7 @@ test('can reorder versions in playlist', function () {
|
|||
$display->refresh();
|
||||
expect($display->versions->first()->id)->toBe($version2->id);
|
||||
expect($display->versions->last()->id)->toBe($version1->id);
|
||||
expect($display->livePlaylist->modules->pluck('id')->all())->toBe([$version2->id, $version1->id]);
|
||||
});
|
||||
|
||||
test('can remove version from playlist', function () {
|
||||
|
|
@ -99,6 +124,7 @@ test('can remove version from playlist', function () {
|
|||
$display->refresh();
|
||||
expect($display->versions)->toHaveCount(1);
|
||||
expect($display->versions->first()->id)->toBe($version2->id);
|
||||
expect($display->livePlaylist->modules->pluck('id')->all())->toBe([$version2->id]);
|
||||
});
|
||||
|
||||
test('can delete a display', function () {
|
||||
|
|
@ -146,3 +172,191 @@ test('does not add duplicate version to playlist', function () {
|
|||
|
||||
expect($component->get('selectedVersionIds'))->toHaveCount(1);
|
||||
});
|
||||
|
||||
test('plus button adds the first available module when select has no explicit value', function () {
|
||||
$user = User::factory()->create();
|
||||
$firstVersion = DisplayVersion::factory()->create(['name' => 'Angebote Schauraum']);
|
||||
DisplayVersion::factory()->create(['name' => 'Schaufenster Video']);
|
||||
|
||||
$component = Livewire::actingAs($user)
|
||||
->test(DisplayList::class)
|
||||
->call('openModal')
|
||||
->call('addVersion');
|
||||
|
||||
expect($component->get('selectedVersionIds'))->toBe([$firstVersion->id]);
|
||||
});
|
||||
|
||||
test('can create a draft playlist from live modules', function () {
|
||||
$user = User::factory()->create();
|
||||
$version1 = DisplayVersion::factory()->create();
|
||||
$version2 = DisplayVersion::factory()->create();
|
||||
$display = Display::factory()->create(['preview_token' => null]);
|
||||
createDisplayListPlaylist($display, DisplayPlaylist::STATUS_PUBLISHED, [$version1->id, $version2->id]);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(DisplayList::class)
|
||||
->call('createDraft', $display->id);
|
||||
|
||||
$display->refresh();
|
||||
|
||||
expect($display->preview_token)->not->toBeNull();
|
||||
expect($display->draftPlaylist)->not->toBeNull();
|
||||
expect($display->draftPlaylist->modules->pluck('id')->all())->toBe([$version1->id, $version2->id]);
|
||||
});
|
||||
|
||||
test('can discard a draft playlist', function () {
|
||||
$user = User::factory()->create();
|
||||
$version = DisplayVersion::factory()->create();
|
||||
$display = Display::factory()->create();
|
||||
createDisplayListPlaylist($display, DisplayPlaylist::STATUS_DRAFT, [$version->id]);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(DisplayList::class)
|
||||
->call('discardDraft', $display->id);
|
||||
|
||||
expect($display->fresh()->draftPlaylist)->toBeNull();
|
||||
});
|
||||
|
||||
test('can publish a draft playlist over the live playlist', function () {
|
||||
$user = User::factory()->create();
|
||||
$liveVersion = DisplayVersion::factory()->create(['name' => 'Live Modul']);
|
||||
$draftVersion = DisplayVersion::factory()->create(['name' => 'Draft Modul']);
|
||||
$display = Display::factory()->create();
|
||||
createDisplayListPlaylist($display, DisplayPlaylist::STATUS_PUBLISHED, [$liveVersion->id]);
|
||||
createDisplayListPlaylist($display, DisplayPlaylist::STATUS_DRAFT, [$draftVersion->id]);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(DisplayList::class)
|
||||
->call('publishDraft', $display->id);
|
||||
|
||||
$display->refresh();
|
||||
|
||||
expect($display->draftPlaylist)->toBeNull();
|
||||
expect($display->livePlaylist->modules->pluck('id')->all())->toBe([$draftVersion->id]);
|
||||
expect($display->versions->pluck('id')->all())->toBe([$draftVersion->id]);
|
||||
});
|
||||
|
||||
test('can edit live playlist without changing draft playlist', function () {
|
||||
$user = User::factory()->create();
|
||||
$liveVersion = DisplayVersion::factory()->create();
|
||||
$draftVersion = DisplayVersion::factory()->create();
|
||||
$newLiveVersion = DisplayVersion::factory()->create();
|
||||
$display = Display::factory()->create();
|
||||
createDisplayListPlaylist($display, DisplayPlaylist::STATUS_PUBLISHED, [$liveVersion->id]);
|
||||
createDisplayListPlaylist($display, DisplayPlaylist::STATUS_DRAFT, [$draftVersion->id]);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(DisplayList::class)
|
||||
->call('openModal', $display->id, DisplayPlaylist::STATUS_PUBLISHED)
|
||||
->call('addVersion', $newLiveVersion->id)
|
||||
->call('save');
|
||||
|
||||
$display->refresh();
|
||||
|
||||
expect($display->livePlaylist->modules->pluck('id')->all())->toBe([$liveVersion->id, $newLiveVersion->id]);
|
||||
expect($display->draftPlaylist->modules->pluck('id')->all())->toBe([$draftVersion->id]);
|
||||
});
|
||||
|
||||
test('can edit draft playlist without changing live playlist', function () {
|
||||
$user = User::factory()->create();
|
||||
$liveVersion = DisplayVersion::factory()->create();
|
||||
$draftVersion = DisplayVersion::factory()->create();
|
||||
$newDraftVersion = DisplayVersion::factory()->create();
|
||||
$display = Display::factory()->create(['preview_token' => null]);
|
||||
createDisplayListPlaylist($display, DisplayPlaylist::STATUS_PUBLISHED, [$liveVersion->id]);
|
||||
createDisplayListPlaylist($display, DisplayPlaylist::STATUS_DRAFT, [$draftVersion->id]);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(DisplayList::class)
|
||||
->call('openModal', $display->id, DisplayPlaylist::STATUS_DRAFT)
|
||||
->call('addVersion', $newDraftVersion->id)
|
||||
->call('save');
|
||||
|
||||
$display->refresh();
|
||||
|
||||
expect($display->preview_token)->not->toBeNull();
|
||||
expect($display->livePlaylist->modules->pluck('id')->all())->toBe([$liveVersion->id]);
|
||||
expect($display->draftPlaylist->modules->pluck('id')->all())->toBe([$draftVersion->id, $newDraftVersion->id]);
|
||||
expect($display->versions->pluck('id')->all())->toBe([]);
|
||||
});
|
||||
|
||||
test('draft editor renders iframe preview url', function () {
|
||||
$user = User::factory()->create();
|
||||
$draftVersion = DisplayVersion::factory()->create();
|
||||
$display = Display::factory()->create(['preview_token' => null]);
|
||||
createDisplayListPlaylist($display, DisplayPlaylist::STATUS_DRAFT, [$draftVersion->id]);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(DisplayList::class)
|
||||
->call('openModal', $display->id, DisplayPlaylist::STATUS_DRAFT)
|
||||
->assertSee('Live-Vorschau')
|
||||
->assertSee('/preview/'.$display->fresh()->preview_token, false)
|
||||
->assertSeeHtml('<iframe');
|
||||
});
|
||||
|
||||
test('draft editor persists module changes immediately for preview reloads', function () {
|
||||
$user = User::factory()->create();
|
||||
$draftVersion = DisplayVersion::factory()->create();
|
||||
$newDraftVersion = DisplayVersion::factory()->create();
|
||||
$display = Display::factory()->create();
|
||||
createDisplayListPlaylist($display, DisplayPlaylist::STATUS_DRAFT, [$draftVersion->id]);
|
||||
|
||||
$component = Livewire::actingAs($user)
|
||||
->test(DisplayList::class)
|
||||
->call('openModal', $display->id, DisplayPlaylist::STATUS_DRAFT);
|
||||
|
||||
$initialRefreshCounter = $component->get('previewFrameRefreshCounter');
|
||||
|
||||
$component->call('addVersion', $newDraftVersion->id);
|
||||
|
||||
$display->refresh();
|
||||
|
||||
expect($display->draftPlaylist->modules->pluck('id')->all())->toBe([$draftVersion->id, $newDraftVersion->id]);
|
||||
expect($component->get('previewFrameRefreshCounter'))->toBeGreaterThan($initialRefreshCounter);
|
||||
});
|
||||
|
||||
test('module select only shows modules that can still be added', function () {
|
||||
$user = User::factory()->create();
|
||||
$selectedVersion = DisplayVersion::factory()->create(['name' => 'Schon gewählt']);
|
||||
$availableVersion = DisplayVersion::factory()->create(['name' => 'Noch verfügbar']);
|
||||
$display = Display::factory()->create();
|
||||
createDisplayListPlaylist($display, DisplayPlaylist::STATUS_PUBLISHED, [$selectedVersion->id]);
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
test('module select is replaced by a hint when all modules are already selected', function () {
|
||||
$user = User::factory()->create();
|
||||
$selectedVersion = DisplayVersion::factory()->create(['name' => 'Einziges Modul']);
|
||||
$display = Display::factory()->create();
|
||||
createDisplayListPlaylist($display, DisplayPlaylist::STATUS_PUBLISHED, [$selectedVersion->id]);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(DisplayList::class)
|
||||
->call('openModal', $display->id, DisplayPlaylist::STATUS_PUBLISHED)
|
||||
->assertSee('Alle verfügbaren Module sind bereits hinzugefügt.')
|
||||
->assertDontSeeHtml('<option value="'.$selectedVersion->id.'">Einziges Modul');
|
||||
});
|
||||
|
||||
test('renders live and draft playlist columns', function () {
|
||||
$user = User::factory()->create();
|
||||
$liveVersion = DisplayVersion::factory()->create(['name' => 'Live Modul']);
|
||||
$draftVersion = DisplayVersion::factory()->create(['name' => 'Draft Modul']);
|
||||
$display = Display::factory()->test()->create(['name' => 'Test-Display']);
|
||||
createDisplayListPlaylist($display, DisplayPlaylist::STATUS_PUBLISHED, [$liveVersion->id]);
|
||||
createDisplayListPlaylist($display, DisplayPlaylist::STATUS_DRAFT, [$draftVersion->id]);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(DisplayList::class)
|
||||
->assertSee('Live Modul')
|
||||
->assertSee('Draft Modul')
|
||||
->assertSee('Live bearbeiten')
|
||||
->assertSee('Live-URL zum Kopieren')
|
||||
->assertSee(url('/_cabinet/display/index.html').'?id='.$display->id, false)
|
||||
->assertSee('Entwurf bearbeiten')
|
||||
->assertSee('Test-Display');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -142,6 +142,41 @@ it('stores a video upload', function () {
|
|||
->and($media->mime_type)->toBe('video/mp4');
|
||||
});
|
||||
|
||||
it('stores an svg upload as image media', function () {
|
||||
$service = app(DisplayMediaService::class);
|
||||
$file = UploadedFile::fake()->createWithContent(
|
||||
'logo.svg',
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10"><rect width="10" height="10"/></svg>'
|
||||
);
|
||||
|
||||
$media = $service->storeUpload($file);
|
||||
|
||||
expect($media->filename)->toBe('logo.svg')
|
||||
->and($media->type)->toBe('image')
|
||||
->and($media->mime_type)->toBe('image/svg+xml');
|
||||
|
||||
expect(Storage::disk('public')->exists($media->path))->toBeTrue();
|
||||
});
|
||||
|
||||
it('accepts svg uploads in the display media library', function () {
|
||||
$file = UploadedFile::fake()->createWithContent(
|
||||
'brand.svg',
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10"><circle cx="5" cy="5" r="5"/></svg>'
|
||||
);
|
||||
|
||||
Volt::test('admin.cms.display-media-library')
|
||||
->set('uploads', [$file])
|
||||
->call('handleUploads')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$media = DisplayMedia::query()->first();
|
||||
|
||||
expect($media)->not->toBeNull()
|
||||
->and($media->filename)->toBe('brand.svg')
|
||||
->and($media->type)->toBe('image')
|
||||
->and($media->mime_type)->toBe('image/svg+xml');
|
||||
});
|
||||
|
||||
it('accepts display media videos up to 200 mb', function () {
|
||||
$file = UploadedFile::fake()->create('showroom.mp4', 204800, 'video/mp4');
|
||||
|
||||
|
|
|
|||
152
tests/Feature/DisplayPlaylistMigrationTest.php
Normal file
152
tests/Feature/DisplayPlaylistMigrationTest.php
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Display;
|
||||
use App\Models\DisplayPlaylist;
|
||||
use App\Models\DisplayPlaylistItem;
|
||||
use App\Models\DisplayVersion;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
it('creates the display_playlists table with unique status per display', function () {
|
||||
expect(Schema::hasTable('display_playlists'))->toBeTrue();
|
||||
expect(Schema::hasColumns('display_playlists', [
|
||||
'id', 'display_id', 'status', 'published_at', 'published_by', 'notes',
|
||||
'created_at', 'updated_at',
|
||||
]))->toBeTrue();
|
||||
});
|
||||
|
||||
it('creates the display_playlist_items table', function () {
|
||||
expect(Schema::hasTable('display_playlist_items'))->toBeTrue();
|
||||
expect(Schema::hasColumns('display_playlist_items', [
|
||||
'id', 'display_playlist_id', 'display_version_id', 'sort_order',
|
||||
'created_at', 'updated_at',
|
||||
]))->toBeTrue();
|
||||
});
|
||||
|
||||
it('adds is_test and preview_token to displays', function () {
|
||||
expect(Schema::hasColumns('displays', ['is_test', 'preview_token']))->toBeTrue();
|
||||
});
|
||||
|
||||
it('migrates existing pivot entries into a published playlist with same ordering', function () {
|
||||
$display = Display::factory()->create();
|
||||
$moduleA = DisplayVersion::factory()->create(['name' => 'Modul A']);
|
||||
$moduleB = DisplayVersion::factory()->create(['name' => 'Modul B']);
|
||||
|
||||
DB::table('display_playlists')->where('display_id', $display->id)->delete();
|
||||
|
||||
DB::table('display_display_version')->insert([
|
||||
['display_id' => $display->id, 'display_version_id' => $moduleA->id, 'sort_order' => 0],
|
||||
['display_id' => $display->id, 'display_version_id' => $moduleB->id, 'sort_order' => 1],
|
||||
]);
|
||||
|
||||
$migration = require database_path('migrations/2026_05_11_113330_migrate_pivot_to_display_playlists.php');
|
||||
$migration->up();
|
||||
|
||||
$playlist = DisplayPlaylist::query()
|
||||
->where('display_id', $display->id)
|
||||
->where('status', DisplayPlaylist::STATUS_PUBLISHED)
|
||||
->first();
|
||||
|
||||
expect($playlist)->not->toBeNull();
|
||||
expect($playlist->published_at)->not->toBeNull();
|
||||
|
||||
$orderedIds = $playlist->items()->pluck('display_version_id')->all();
|
||||
expect($orderedIds)->toBe([$moduleA->id, $moduleB->id]);
|
||||
});
|
||||
|
||||
it('is idempotent and does not duplicate published playlists on re-run', function () {
|
||||
$display = Display::factory()->create();
|
||||
$module = DisplayVersion::factory()->create();
|
||||
|
||||
DB::table('display_playlists')->where('display_id', $display->id)->delete();
|
||||
|
||||
DB::table('display_display_version')->insert([
|
||||
['display_id' => $display->id, 'display_version_id' => $module->id, 'sort_order' => 0],
|
||||
]);
|
||||
|
||||
$migration = require database_path('migrations/2026_05_11_113330_migrate_pivot_to_display_playlists.php');
|
||||
$migration->up();
|
||||
$migration->up();
|
||||
|
||||
$count = DisplayPlaylist::query()
|
||||
->where('display_id', $display->id)
|
||||
->where('status', DisplayPlaylist::STATUS_PUBLISHED)
|
||||
->count();
|
||||
|
||||
expect($count)->toBe(1);
|
||||
});
|
||||
|
||||
it('does not break the legacy versions() relation', function () {
|
||||
$display = Display::factory()->create();
|
||||
$module = DisplayVersion::factory()->create();
|
||||
|
||||
DB::table('display_display_version')->insert([
|
||||
['display_id' => $display->id, 'display_version_id' => $module->id, 'sort_order' => 0],
|
||||
]);
|
||||
|
||||
expect($display->fresh()->versions)->toHaveCount(1);
|
||||
});
|
||||
|
||||
it('exposes a live playlist relation and a draft playlist relation on display', function () {
|
||||
$display = Display::factory()->create();
|
||||
|
||||
$live = DisplayPlaylist::factory()->published()->create(['display_id' => $display->id]);
|
||||
$draft = DisplayPlaylist::factory()->draft()->create(['display_id' => $display->id]);
|
||||
|
||||
expect($display->livePlaylist->is($live))->toBeTrue();
|
||||
expect($display->draftPlaylist->is($draft))->toBeTrue();
|
||||
});
|
||||
|
||||
it('prevents two playlists with the same status for one display', function () {
|
||||
$display = Display::factory()->create();
|
||||
DisplayPlaylist::factory()->published()->create(['display_id' => $display->id]);
|
||||
|
||||
DisplayPlaylist::factory()->published()->create(['display_id' => $display->id]);
|
||||
})->throws(\Illuminate\Database\QueryException::class);
|
||||
|
||||
it('orders playlist items by sort_order on the modules relation', function () {
|
||||
$display = Display::factory()->create();
|
||||
$playlist = DisplayPlaylist::factory()->published()->create(['display_id' => $display->id]);
|
||||
$modules = DisplayVersion::factory()->count(3)->create();
|
||||
|
||||
DisplayPlaylistItem::factory()->create([
|
||||
'display_playlist_id' => $playlist->id,
|
||||
'display_version_id' => $modules[2]->id,
|
||||
'sort_order' => 2,
|
||||
]);
|
||||
DisplayPlaylistItem::factory()->create([
|
||||
'display_playlist_id' => $playlist->id,
|
||||
'display_version_id' => $modules[0]->id,
|
||||
'sort_order' => 0,
|
||||
]);
|
||||
DisplayPlaylistItem::factory()->create([
|
||||
'display_playlist_id' => $playlist->id,
|
||||
'display_version_id' => $modules[1]->id,
|
||||
'sort_order' => 1,
|
||||
]);
|
||||
|
||||
expect($playlist->modules->pluck('id')->all())->toBe([
|
||||
$modules[0]->id,
|
||||
$modules[1]->id,
|
||||
$modules[2]->id,
|
||||
]);
|
||||
});
|
||||
|
||||
it('generates a preview token on demand and persists it', function () {
|
||||
$display = Display::factory()->create(['preview_token' => null]);
|
||||
|
||||
$token = $display->ensurePreviewToken();
|
||||
|
||||
expect($token)->toBeString();
|
||||
expect(strlen($token))->toBeGreaterThanOrEqual(40);
|
||||
expect($display->fresh()->preview_token)->toBe($token);
|
||||
|
||||
expect($display->ensurePreviewToken())->toBe($token);
|
||||
});
|
||||
|
||||
it('marks a display as test display via factory state', function () {
|
||||
$display = Display::factory()->test()->create();
|
||||
|
||||
expect($display->is_test)->toBeTrue();
|
||||
expect($display->name)->toBe('Test-Display');
|
||||
});
|
||||
|
|
@ -1,14 +1,51 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Display;
|
||||
use App\Models\DisplayPlaylist;
|
||||
use App\Models\DisplayPlaylistItem;
|
||||
use App\Models\DisplayVersion;
|
||||
use App\Models\DisplayVersionItem;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
|
||||
beforeEach(function () {
|
||||
$portalDomain = config('domains.domain_portal');
|
||||
url()->forceRootUrl('https://'.$portalDomain);
|
||||
URL::forceRootUrl('https://'.$portalDomain);
|
||||
});
|
||||
|
||||
function publishDisplayModules(Display $display, array $versionIds): DisplayPlaylist
|
||||
{
|
||||
$playlist = DisplayPlaylist::factory()->published()->create([
|
||||
'display_id' => $display->id,
|
||||
]);
|
||||
|
||||
foreach (array_values($versionIds) as $sortOrder => $versionId) {
|
||||
DisplayPlaylistItem::factory()->create([
|
||||
'display_playlist_id' => $playlist->id,
|
||||
'display_version_id' => $versionId,
|
||||
'sort_order' => $sortOrder,
|
||||
]);
|
||||
}
|
||||
|
||||
return $playlist;
|
||||
}
|
||||
|
||||
function draftDisplayModules(Display $display, array $versionIds): DisplayPlaylist
|
||||
{
|
||||
$playlist = DisplayPlaylist::factory()->draft()->create([
|
||||
'display_id' => $display->id,
|
||||
]);
|
||||
|
||||
foreach (array_values($versionIds) as $sortOrder => $versionId) {
|
||||
DisplayPlaylistItem::factory()->create([
|
||||
'display_playlist_id' => $playlist->id,
|
||||
'display_version_id' => $versionId,
|
||||
'sort_order' => $sortOrder,
|
||||
]);
|
||||
}
|
||||
|
||||
return $playlist;
|
||||
}
|
||||
|
||||
test('returns playlist with video-display config', function () {
|
||||
$version = DisplayVersion::factory()->create(['type' => 'video-display']);
|
||||
DisplayVersionItem::factory()->create([
|
||||
|
|
@ -22,7 +59,7 @@ test('returns playlist with video-display config', function () {
|
|||
'content' => ['headline' => 'Hello', 'subline' => 'World', 'url' => null],
|
||||
]);
|
||||
$display = Display::factory()->create();
|
||||
$display->versions()->attach($version->id, ['sort_order' => 0]);
|
||||
publishDisplayModules($display, [$version->id]);
|
||||
|
||||
$response = $this->getJson("/api/display/{$display->id}/config");
|
||||
|
||||
|
|
@ -33,6 +70,26 @@ test('returns playlist with video-display config', function () {
|
|||
$response->assertJsonPath('playlist.0.footerContent.0.headline', 'Hello');
|
||||
});
|
||||
|
||||
test('returns display media video urls without legacy asset prefix', function () {
|
||||
$version = DisplayVersion::factory()->create(['type' => 'video-display']);
|
||||
DisplayVersionItem::factory()->create([
|
||||
'display_version_id' => $version->id,
|
||||
'item_type' => 'video',
|
||||
'content' => [
|
||||
'filename' => '/storage/display-media/2026/05/video.mp4',
|
||||
'title' => 'Uploaded Video',
|
||||
'position' => 25,
|
||||
],
|
||||
]);
|
||||
$display = Display::factory()->create();
|
||||
publishDisplayModules($display, [$version->id]);
|
||||
|
||||
$response = $this->getJson("/api/display/{$display->id}/config");
|
||||
|
||||
$response->assertSuccessful();
|
||||
$response->assertJsonPath('playlist.0.videoPlaylist.0.src', '/storage/display-media/2026/05/video.mp4');
|
||||
});
|
||||
|
||||
test('returns playlist with b2in config', function () {
|
||||
$version = DisplayVersion::factory()->create([
|
||||
'type' => 'b2in',
|
||||
|
|
@ -51,7 +108,7 @@ test('returns playlist with b2in config', function () {
|
|||
],
|
||||
]);
|
||||
$display = Display::factory()->create();
|
||||
$display->versions()->attach($version->id, ['sort_order' => 0]);
|
||||
publishDisplayModules($display, [$version->id]);
|
||||
|
||||
$response = $this->getJson("/api/display/{$display->id}/config");
|
||||
|
||||
|
|
@ -90,7 +147,7 @@ test('returns playlist with offers config', function () {
|
|||
],
|
||||
]);
|
||||
$display = Display::factory()->create();
|
||||
$display->versions()->attach($version->id, ['sort_order' => 0]);
|
||||
publishDisplayModules($display, [$version->id]);
|
||||
|
||||
$response = $this->getJson("/api/display/{$display->id}/config");
|
||||
|
||||
|
|
@ -118,10 +175,7 @@ test('returns playlist with multiple versions in order', function () {
|
|||
]);
|
||||
|
||||
$display = Display::factory()->create();
|
||||
$display->versions()->attach([
|
||||
$videoVersion->id => ['sort_order' => 0],
|
||||
$b2inVersion->id => ['sort_order' => 1],
|
||||
]);
|
||||
publishDisplayModules($display, [$videoVersion->id, $b2inVersion->id]);
|
||||
|
||||
$response = $this->getJson("/api/display/{$display->id}/config");
|
||||
|
||||
|
|
@ -134,7 +188,7 @@ test('returns playlist with multiple versions in order', function () {
|
|||
test('returns 404 for inactive display', function () {
|
||||
$version = DisplayVersion::factory()->create();
|
||||
$display = Display::factory()->create(['is_active' => false]);
|
||||
$display->versions()->attach($version->id, ['sort_order' => 0]);
|
||||
publishDisplayModules($display, [$version->id]);
|
||||
|
||||
$response = $this->getJson("/api/display/{$display->id}/config");
|
||||
|
||||
|
|
@ -152,7 +206,7 @@ test('returns 404 for display without versions', function () {
|
|||
test('check endpoint returns only updated_at', function () {
|
||||
$version = DisplayVersion::factory()->create();
|
||||
$display = Display::factory()->create();
|
||||
$display->versions()->attach($version->id, ['sort_order' => 0]);
|
||||
publishDisplayModules($display, [$version->id]);
|
||||
|
||||
$response = $this->getJson("/api/display/{$display->id}/check");
|
||||
|
||||
|
|
@ -160,6 +214,206 @@ test('check endpoint returns only updated_at', function () {
|
|||
$response->assertJsonStructure(['updated_at']);
|
||||
});
|
||||
|
||||
test('display config ignores legacy pivot and reads published playlist', function () {
|
||||
$legacyVersion = DisplayVersion::factory()->create(['type' => 'video-display', 'name' => 'Legacy']);
|
||||
DisplayVersionItem::factory()->create([
|
||||
'display_version_id' => $legacyVersion->id,
|
||||
'item_type' => 'video',
|
||||
'content' => ['filename' => 'legacy.mp4', 'title' => 'Legacy', 'position' => 25],
|
||||
]);
|
||||
|
||||
$publishedVersion = DisplayVersion::factory()->create(['type' => 'video-display', 'name' => 'Published']);
|
||||
DisplayVersionItem::factory()->create([
|
||||
'display_version_id' => $publishedVersion->id,
|
||||
'item_type' => 'video',
|
||||
'content' => ['filename' => 'published.mp4', 'title' => 'Published', 'position' => 25],
|
||||
]);
|
||||
|
||||
$display = Display::factory()->create();
|
||||
$display->versions()->attach($legacyVersion->id, ['sort_order' => 0]);
|
||||
publishDisplayModules($display, [$publishedVersion->id]);
|
||||
|
||||
$response = $this->getJson("/api/display/{$display->id}/config");
|
||||
|
||||
$response->assertSuccessful();
|
||||
$response->assertJsonPath('playlist.0.version_name', 'Published');
|
||||
$response->assertJsonPath('playlist.0.videoPlaylist.0.src', 'assets/published.mp4');
|
||||
});
|
||||
|
||||
test('draft preview returns playlist by preview token', function () {
|
||||
$liveVersion = DisplayVersion::factory()->create(['type' => 'video-display', 'name' => 'Live']);
|
||||
DisplayVersionItem::factory()->create([
|
||||
'display_version_id' => $liveVersion->id,
|
||||
'item_type' => 'video',
|
||||
'content' => ['filename' => 'live.mp4', 'title' => 'Live', 'position' => 25],
|
||||
]);
|
||||
|
||||
$draftVersion = DisplayVersion::factory()->create(['type' => 'video-display', 'name' => 'Draft']);
|
||||
DisplayVersionItem::factory()->create([
|
||||
'display_version_id' => $draftVersion->id,
|
||||
'item_type' => 'video',
|
||||
'content' => ['filename' => 'draft.mp4', 'title' => 'Draft', 'position' => 25],
|
||||
]);
|
||||
|
||||
$display = Display::factory()->create(['preview_token' => 'preview-token-123']);
|
||||
publishDisplayModules($display, [$liveVersion->id]);
|
||||
draftDisplayModules($display, [$draftVersion->id]);
|
||||
|
||||
$response = $this->getJson('/api/display/preview/preview-token-123');
|
||||
|
||||
$response->assertSuccessful();
|
||||
$response->assertJsonPath('playlist.0.version_name', 'Draft');
|
||||
$response->assertJsonPath('playlist.0.videoPlaylist.0.src', 'assets/draft.mp4');
|
||||
});
|
||||
|
||||
test('draft preview requires configured draft playlist', function () {
|
||||
$display = Display::factory()->create(['preview_token' => 'empty-preview-token']);
|
||||
|
||||
$response = $this->getJson('/api/display/preview/empty-preview-token');
|
||||
|
||||
$response->assertNotFound();
|
||||
});
|
||||
|
||||
test('module preview returns a single module config', function () {
|
||||
$version = DisplayVersion::factory()->create([
|
||||
'type' => 'b2in',
|
||||
'name' => 'Preview Module',
|
||||
'settings' => ['theme' => 'light'],
|
||||
]);
|
||||
DisplayVersionItem::factory()->create([
|
||||
'display_version_id' => $version->id,
|
||||
'item_type' => 'media',
|
||||
'content' => [
|
||||
'category' => 'moebel',
|
||||
'media_type' => 'image',
|
||||
'media_url' => '../assets/module.jpg',
|
||||
'headline' => 'Modul',
|
||||
'subline' => 'Vorschau',
|
||||
'duration_seconds' => 8,
|
||||
],
|
||||
]);
|
||||
|
||||
$response = $this->getJson("/api/display/module/{$version->id}/preview");
|
||||
|
||||
$response->assertSuccessful();
|
||||
$response->assertJsonCount(1, 'playlist');
|
||||
$response->assertJsonPath('playlist.0.version_name', 'Preview Module');
|
||||
$response->assertJsonPath('playlist.0.items.0.category', 'moebel');
|
||||
});
|
||||
|
||||
test('module preview exposes configurable player chrome settings', 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',
|
||||
'title' => 'Intro',
|
||||
'image_url' => '../assets/intro.jpg',
|
||||
],
|
||||
]);
|
||||
|
||||
$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');
|
||||
});
|
||||
|
||||
test('module item preview returns only the selected slide', function () {
|
||||
$version = DisplayVersion::factory()->create([
|
||||
'type' => 'offers',
|
||||
'name' => 'Offers Module',
|
||||
]);
|
||||
$firstSlide = DisplayVersionItem::factory()->create([
|
||||
'display_version_id' => $version->id,
|
||||
'item_type' => 'slide',
|
||||
'content' => [
|
||||
'type' => 'product-hero',
|
||||
'title' => 'First Slide',
|
||||
'image_url' => '../assets/first.jpg',
|
||||
],
|
||||
'sort_order' => 0,
|
||||
]);
|
||||
$secondSlide = DisplayVersionItem::factory()->create([
|
||||
'display_version_id' => $version->id,
|
||||
'item_type' => 'slide',
|
||||
'content' => [
|
||||
'type' => 'product-details',
|
||||
'title' => 'Second Slide',
|
||||
'image_url' => '../assets/second.jpg',
|
||||
],
|
||||
'sort_order' => 1,
|
||||
]);
|
||||
|
||||
$response = $this->getJson("/api/display/module/{$version->id}/item/{$secondSlide->id}/preview");
|
||||
|
||||
$response->assertSuccessful();
|
||||
$response->assertJsonCount(1, 'playlist');
|
||||
$response->assertJsonCount(1, 'playlist.0.slides');
|
||||
$response->assertJsonPath('playlist.0.slides.0.title', 'Second Slide');
|
||||
$response->assertJsonMissingPath('playlist.0.slides.1');
|
||||
|
||||
expect($firstSlide->fresh()->content['title'])->toBe('First Slide');
|
||||
});
|
||||
|
||||
test('module item preview requires the item to belong to the module', function () {
|
||||
$version = DisplayVersion::factory()->create(['type' => 'offers']);
|
||||
$otherVersion = DisplayVersion::factory()->create(['type' => 'offers']);
|
||||
$item = DisplayVersionItem::factory()->create([
|
||||
'display_version_id' => $otherVersion->id,
|
||||
'item_type' => 'slide',
|
||||
]);
|
||||
|
||||
$this->getJson("/api/display/module/{$version->id}/item/{$item->id}/preview")
|
||||
->assertNotFound();
|
||||
});
|
||||
|
||||
test('display player keeps previews in a strict 9 by 16 viewport', function () {
|
||||
$player = file_get_contents(public_path('_cabinet/display/index.html'));
|
||||
|
||||
expect($player)
|
||||
->toContain('width: min(100vw, calc(100vh * 9 / 16));')
|
||||
->toContain('height: min(100vh, calc(100vw * 16 / 9));')
|
||||
->toContain('container-type: size;')
|
||||
->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');
|
||||
});
|
||||
|
||||
test('preview player pages are reachable for valid display and module previews', function () {
|
||||
$display = Display::factory()->create(['preview_token' => 'page-preview-token']);
|
||||
$version = DisplayVersion::factory()->create();
|
||||
$item = DisplayVersionItem::factory()->create(['display_version_id' => $version->id]);
|
||||
|
||||
$this->get('/preview/page-preview-token')
|
||||
->assertSuccessful();
|
||||
|
||||
$this->get("/preview/module/{$version->id}")
|
||||
->assertSuccessful();
|
||||
|
||||
$this->get("/preview/module/{$version->id}/item/{$item->id}")
|
||||
->assertSuccessful();
|
||||
});
|
||||
|
||||
test('existing display config api still works', function () {
|
||||
$response = $this->getJson('/api/display/config');
|
||||
|
||||
|
|
|
|||
|
|
@ -6,12 +6,13 @@ use App\Livewire\Admin\Cms\DisplayVersionList;
|
|||
use App\Models\DisplayVersion;
|
||||
use App\Models\DisplayVersionItem;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Livewire\Livewire;
|
||||
|
||||
beforeEach(function () {
|
||||
$portalDomain = config('domains.domain_portal');
|
||||
Livewire::withoutLazyLoading();
|
||||
url()->forceRootUrl('https://'.$portalDomain);
|
||||
URL::forceRootUrl('https://'.$portalDomain);
|
||||
});
|
||||
|
||||
// ========================================
|
||||
|
|
@ -19,7 +20,7 @@ beforeEach(function () {
|
|||
// ========================================
|
||||
|
||||
test('display version list requires authentication', function () {
|
||||
$response = $this->get(route('admin.cms.display-versions'));
|
||||
$response = $this->get(route('admin.cms.display-modules'));
|
||||
|
||||
$response->assertRedirect('/login');
|
||||
});
|
||||
|
|
@ -29,7 +30,7 @@ test('display version list renders for authenticated users', function () {
|
|||
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = $this->get(route('admin.cms.display-versions'));
|
||||
$response = $this->get(route('admin.cms.display-modules'));
|
||||
|
||||
$response->assertSuccessful();
|
||||
$response->assertSeeLivewire(DisplayVersionList::class);
|
||||
|
|
@ -50,6 +51,8 @@ test('can create a display version', function () {
|
|||
expect($version->type)->toBe(DisplayVersionType::B2in);
|
||||
expect($version->settings)->toBeArray();
|
||||
expect($version->settings)->toHaveKey('theme');
|
||||
expect($version->settings)->toHaveKey('header_logo_url');
|
||||
expect($version->settings)->toHaveKey('footer_url');
|
||||
});
|
||||
|
||||
test('create version validates required fields', function () {
|
||||
|
|
@ -95,12 +98,59 @@ test('display version editor renders with correct version data', function () {
|
|||
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = $this->get(route('admin.cms.display-version-edit', $version));
|
||||
$response = $this->get(route('admin.cms.display-module-edit', $version));
|
||||
|
||||
$response->assertSuccessful();
|
||||
$response->assertSeeLivewire(DisplayVersionEditor::class);
|
||||
});
|
||||
|
||||
test('old display version routes redirect to module routes', function () {
|
||||
$user = User::factory()->create();
|
||||
$version = DisplayVersion::factory()->create();
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
$this->get(route('admin.cms.display-versions'))
|
||||
->assertRedirect(route('admin.cms.display-modules'));
|
||||
|
||||
$this->get(route('admin.cms.display-version-edit', $version))
|
||||
->assertRedirect(route('admin.cms.display-module-edit', $version));
|
||||
});
|
||||
|
||||
test('display module editor renders module preview', function () {
|
||||
$user = User::factory()->create();
|
||||
$version = DisplayVersion::factory()->create(['name' => 'Preview Modul']);
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = $this->get(route('admin.cms.display-module-edit', $version));
|
||||
|
||||
$response->assertSuccessful();
|
||||
$response->assertSee('Modul-Vorschau');
|
||||
$response->assertSee('/preview/module/'.$version->id, false);
|
||||
});
|
||||
|
||||
test('item edit modal renders module iframe preview', function () {
|
||||
$user = User::factory()->create();
|
||||
$version = DisplayVersion::factory()->create(['type' => 'offers']);
|
||||
$item = DisplayVersionItem::factory()->create([
|
||||
'display_version_id' => $version->id,
|
||||
'item_type' => 'slide',
|
||||
'content' => [
|
||||
'type' => 'product-hero',
|
||||
'title' => 'Preview Slide',
|
||||
'image_url' => '../assets/preview.jpg',
|
||||
],
|
||||
]);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(DisplayVersionEditor::class, ['displayVersion' => $version])
|
||||
->call('openItemModal', $item->id)
|
||||
->assertSee('Einzel-Vorschau im Bearbeiten-Dialog')
|
||||
->assertSee('/preview/module/'.$version->id.'/item/'.$item->id, false)
|
||||
->assertSee('Schließen');
|
||||
});
|
||||
|
||||
test('can add a video item to video-display version', function () {
|
||||
$user = User::factory()->create();
|
||||
$version = DisplayVersion::factory()->create(['type' => 'video-display']);
|
||||
|
|
@ -162,6 +212,36 @@ test('can edit an existing item', function () {
|
|||
expect($item->content['title'])->toBe('New Title');
|
||||
});
|
||||
|
||||
test('updating an item keeps modal open and refreshes iframe preview', function () {
|
||||
$user = User::factory()->create();
|
||||
$version = DisplayVersion::factory()->create(['type' => 'offers']);
|
||||
$item = DisplayVersionItem::factory()->create([
|
||||
'display_version_id' => $version->id,
|
||||
'item_type' => 'slide',
|
||||
'content' => [
|
||||
'type' => 'product-hero',
|
||||
'title' => 'Old Title',
|
||||
'image_url' => '../assets/old.jpg',
|
||||
],
|
||||
]);
|
||||
|
||||
$component = Livewire::actingAs($user)
|
||||
->test(DisplayVersionEditor::class, ['displayVersion' => $version])
|
||||
->call('openItemModal', $item->id);
|
||||
|
||||
$initialRefreshCounter = $component->get('previewFrameRefreshCounter');
|
||||
|
||||
$component
|
||||
->set('slideTitle', 'New Title')
|
||||
->call('saveItem')
|
||||
->assertSet('showItemModal', true)
|
||||
->assertSet('itemId', $item->id)
|
||||
->assertSee('Einzel-Vorschau im Bearbeiten-Dialog');
|
||||
|
||||
expect($item->fresh()->content['title'])->toBe('New Title');
|
||||
expect($component->get('previewFrameRefreshCounter'))->toBeGreaterThan($initialRefreshCounter);
|
||||
});
|
||||
|
||||
test('can delete an item', function () {
|
||||
$user = User::factory()->create();
|
||||
$version = DisplayVersion::factory()->create();
|
||||
|
|
@ -309,7 +389,31 @@ test('can save version settings', function () {
|
|||
->test(DisplayVersionEditor::class, ['displayVersion' => $version])
|
||||
->call('openSettingsModal')
|
||||
->set('settings.theme', 'light')
|
||||
->set('settings.header_claim', 'Custom Claim')
|
||||
->set('settings.footer_prefix', 'powered by')
|
||||
->call('saveSettings');
|
||||
|
||||
expect($version->fresh()->settings['theme'])->toBe('light');
|
||||
expect($version->fresh()->settings['header_claim'])->toBe('Custom Claim');
|
||||
expect($version->fresh()->settings['footer_prefix'])->toBe('powered by');
|
||||
});
|
||||
|
||||
test('module settings expose player chrome fields in the editor', 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');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -54,6 +54,8 @@ it('immobilien show page loads for valid slug', function () {
|
|||
->assertSee('Creek Views 4')
|
||||
->assertSee('Al Jaddaf, Dubai')
|
||||
->assertSee('Investment-Einordnung')
|
||||
->assertSee('href="#projekt-anfrage"', false)
|
||||
->assertSee('id="projekt-anfrage"', false)
|
||||
->assertSee('Verfügbarkeit anfragen')
|
||||
->assertDontSee('Projektquelle');
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue