10-04-2026

This commit is contained in:
Kevin Adametz 2026-04-10 17:18:17 +02:00
parent 4d6b4930b2
commit 4bb89aad8c
836 changed files with 52961 additions and 5950 deletions

View file

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
it('about page loads successfully', function () {
$this->get('/about')
->assertSuccessful()
->assertSee('Über B2in');
});
it('about page shows image break section', function () {
$this->get('/about')
->assertSuccessful()
->assertSee('best-of-two-worlds.jpg');
});

View file

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
use App\Livewire\Admin\Cms\MediaPicker;
use FluxCms\Core\Models\CmsMedia;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
uses(RefreshDatabase::class);
test('media picker zeigt leerzustand ohne ausgewähltes medium', function () {
Livewire::test(MediaPicker::class, ['value' => null])
->assertSuccessful()
->assertSee('Kein Medium ausgewählt');
});
test('media picker zeigt ausgewähltes medium ohne livewire property fehler', function () {
$media = CmsMedia::query()->create([
'filename' => 'test-image.jpg',
'path' => 'media/test-image.jpg',
'type' => 'image',
'mime_type' => 'image/jpeg',
'disk' => 'public',
]);
Livewire::test(MediaPicker::class, ['value' => $media->id])
->assertSuccessful()
->assertSee('test-image.jpg')
->assertDontSee('Kein Medium ausgewählt');
});

View file

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
use App\Models\User;
use Livewire\Volt\Volt;
beforeEach(function () {
$portalDomain = config('domains.domain_portal');
url()->forceRootUrl('https://'.$portalDomain);
});
test('authenticated user can render admin documentation volt page', function () {
$user = User::factory()->create();
Volt::actingAs($user)
->test('admin.documentation')
->assertSuccessful();
});

View file

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
it('announcement bar is visible on homepage', function () {
$this->get('/')
->assertSuccessful()
->assertSee('Azizi Creek Views 4')
->assertSee('NEW LAUNCH');
});
it('announcement bar is visible on immobilien page', function () {
$this->get('/immobilien')
->assertSuccessful()
->assertSee('Off-Market-Projekt');
});
it('announcement bar links to project expose', function () {
$this->get('/')
->assertSuccessful()
->assertSee('/immobilien/azizi-creek-views-4');
});

View file

@ -0,0 +1,154 @@
<?php
use App\Livewire\Admin\Cms\CabinetInfoTablet;
use App\Models\CabinetTabletSetting;
use App\Models\User;
use Livewire\Livewire;
beforeEach(function () {
// Set the portal domain so route() resolves domain-scoped routes
$portalDomain = config('domains.domain_portal');
Livewire::withoutLazyLoading();
url()->forceRootUrl('https://'.$portalDomain);
});
test('cabinet info tablet page requires authentication', function () {
$response = $this->get(route('admin.cms.cabinet-tablet'));
$response->assertRedirect('/login');
});
test('cabinet info tablet page renders for authenticated users', function () {
$user = User::factory()->create();
$this->actingAs($user);
$response = $this->get(route('admin.cms.cabinet-tablet'));
$response->assertSuccessful();
$response->assertSeeLivewire(CabinetInfoTablet::class);
});
test('component mounts with existing settings', function () {
CabinetTabletSetting::factory()->create([
'store_status' => 'notice',
'notice_headline' => 'Sonderverkauf',
'contact_phone' => '0521 12345',
'hours_monday_open' => '10:00',
'hours_monday_close' => '18:00',
]);
$user = User::factory()->create();
Livewire::actingAs($user)
->test(CabinetInfoTablet::class)
->assertSet('storeStatus', 'notice')
->assertSet('noticeHeadline', 'Sonderverkauf')
->assertSet('contactPhone', '0521 12345');
});
test('save persists settings to database', function () {
$user = User::factory()->create();
Livewire::actingAs($user)
->test(CabinetInfoTablet::class)
->set('storeStatus', 'notice')
->set('noticeHeadline', 'Winterpause')
->set('noticeSubtext', 'Bis 02.01. geschlossen')
->set('contactPhone', '0521 99999')
->set('contactEmail', 'test@example.com')
->call('save')
->assertHasNoErrors();
$settings = CabinetTabletSetting::current();
expect($settings->store_status)->toBe('notice');
expect($settings->notice_headline)->toBe('Winterpause');
expect($settings->contact_phone)->toBe('0521 99999');
});
test('save validates store status', function () {
$user = User::factory()->create();
Livewire::actingAs($user)
->test(CabinetInfoTablet::class)
->set('storeStatus', 'open') // 'open' is not a valid mode anymore
->call('save')
->assertHasErrors(['storeStatus']);
Livewire::actingAs($user)
->test(CabinetInfoTablet::class)
->set('storeStatus', 'warning')
->call('save')
->assertHasNoErrors(['storeStatus']);
});
test('save validates override time format', function () {
$user = User::factory()->create();
Livewire::actingAs($user)
->test(CabinetInfoTablet::class)
->set('overrideOpenToday', 'abc')
->call('save')
->assertHasErrors(['overrideOpenToday']);
});
test('save validates email format', function () {
$user = User::factory()->create();
Livewire::actingAs($user)
->test(CabinetInfoTablet::class)
->set('contactEmail', 'not-an-email')
->call('save')
->assertHasErrors(['contactEmail']);
});
test('save validates headline max length', function () {
$user = User::factory()->create();
Livewire::actingAs($user)
->test(CabinetInfoTablet::class)
->set('noticeHeadline', str_repeat('A', 41))
->call('save')
->assertHasErrors(['noticeHeadline']);
});
test('save allows empty opening hours for closed days', function () {
CabinetTabletSetting::factory()->create([
'hours_monday_open' => '10:00',
'hours_monday_close' => '18:00',
]);
$user = User::factory()->create();
Livewire::actingAs($user)
->test(CabinetInfoTablet::class)
->set('hoursMondayOpen', '')
->set('hoursMondayClose', '')
->call('save')
->assertHasNoErrors();
$settings = CabinetTabletSetting::current();
expect($settings->hours_monday_open)->toBeNull();
expect($settings->hours_monday_close)->toBeNull();
});
test('clearOverrides resets override fields', function () {
CabinetTabletSetting::factory()->create([
'override_open_today' => '09:00',
'override_close_today' => '20:00',
]);
$user = User::factory()->create();
Livewire::actingAs($user)
->test(CabinetInfoTablet::class)
->assertSet('overrideOpenToday', '09:00')
->assertSet('overrideCloseToday', '20:00')
->call('clearOverrides')
->assertSet('overrideOpenToday', '')
->assertSet('overrideCloseToday', '');
$settings = CabinetTabletSetting::current();
expect($settings->override_open_today)->toBeNull();
expect($settings->override_close_today)->toBeNull();
});

View file

@ -0,0 +1,113 @@
<?php
use App\Livewire\Cabinet\QuickStatus;
use App\Models\CabinetTabletSetting;
use Livewire\Livewire;
const VALID_KEY = 'test-secret-key-123';
beforeEach(function () {
config(['domains.cabinet_status_key' => VALID_KEY]);
});
test('page is accessible without login', function () {
$this->get('/info/status?k='.VALID_KEY)
->assertSuccessful()
->assertSeeLivewire(QuickStatus::class);
});
test('page shows forbidden when key is missing', function () {
Livewire::test(QuickStatus::class, ['k' => ''])
->assertSet('authorized', false)
->assertSee('Kein Zugriff');
});
test('page shows forbidden when key is wrong', function () {
Livewire::test(QuickStatus::class, ['k' => 'wrong-key'])
->assertSet('authorized', false)
->assertSee('Kein Zugriff');
});
test('page loads current status when key is correct', function () {
CabinetTabletSetting::factory()->create([
'store_status' => 'notice',
'notice_headline' => 'Testhinweis',
]);
Livewire::test(QuickStatus::class, ['k' => VALID_KEY])
->assertSet('authorized', true)
->assertSet('storeStatus', 'notice')
->assertSet('noticeHeadline', 'Testhinweis');
});
test('selectStatus changes storeStatus', function () {
Livewire::test(QuickStatus::class, ['k' => VALID_KEY])
->call('selectStatus', 'closed')
->assertSet('storeStatus', 'closed');
});
test('selectStatus ignores unknown values', function () {
Livewire::test(QuickStatus::class, ['k' => VALID_KEY])
->call('selectStatus', 'invalid')
->assertSet('storeStatus', 'auto');
});
test('save persists status and headline to database', function () {
Livewire::test(QuickStatus::class, ['k' => VALID_KEY])
->set('storeStatus', 'notice')
->set('noticeHeadline', 'Sonderöffnung')
->set('noticeSubtext', 'Heute nur bis 14 Uhr')
->call('save')
->assertHasNoErrors()
->assertSet('saved', true);
$settings = CabinetTabletSetting::current();
expect($settings->store_status)->toBe('notice');
expect($settings->notice_headline)->toBe('Sonderöffnung');
expect($settings->notice_subtext)->toBe('Heute nur bis 14 Uhr');
});
test('save clears headline when switching to auto', function () {
CabinetTabletSetting::factory()->create([
'store_status' => 'notice',
'notice_headline' => 'Alter Text',
]);
Livewire::test(QuickStatus::class, ['k' => VALID_KEY])
->set('storeStatus', 'auto')
->call('save')
->assertHasNoErrors();
expect(CabinetTabletSetting::current()->store_status)->toBe('auto');
expect(CabinetTabletSetting::current()->notice_headline)->toBeNull();
});
test('save validates headline max length', function () {
Livewire::test(QuickStatus::class, ['k' => VALID_KEY])
->set('storeStatus', 'notice')
->set('noticeHeadline', str_repeat('A', 41))
->call('save')
->assertHasErrors(['noticeHeadline']);
});
test('save validates subtext max length', function () {
Livewire::test(QuickStatus::class, ['k' => VALID_KEY])
->set('storeStatus', 'notice')
->set('noticeSubtext', str_repeat('A', 81))
->call('save')
->assertHasErrors(['noticeSubtext']);
});
test('unauthorized user cannot save', function () {
$component = Livewire::test(QuickStatus::class, ['k' => 'wrong'])
->set('storeStatus', 'closed')
->call('save');
expect(CabinetTabletSetting::count())->toBe(0);
});
test('all four status options are available', function () {
$component = Livewire::test(QuickStatus::class, ['k' => VALID_KEY]);
expect($component->get('statusOptions'))->toHaveKeys(['auto', 'closed', 'notice', 'warning']);
});

View file

@ -0,0 +1,100 @@
<?php
use App\Models\CabinetTabletSetting;
use Illuminate\Support\Carbon;
test('status endpoint returns full JSON response', function () {
CabinetTabletSetting::factory()->create([
'store_status' => 'auto',
'hours_thursday_open' => '10:00',
'hours_thursday_close' => '18:00',
'contact_phone' => '0521 98620100',
'contact_email' => 'info@cabinet-bielefeld.de',
'next_appointment_date' => '2026-03-01',
'next_appointment_time' => '14:00',
]);
// Thursday 12:00 within opening hours
Carbon::setTestNow(Carbon::create(2026, 3, 5, 12, 0, 0, 'Europe/Berlin'));
$response = $this->getJson('/api/cabinet-tablet/status');
$response->assertSuccessful()
->assertJsonStructure([
'store_status',
'today_close',
'next_open',
'notice_headline',
'notice_subtext',
'override_open_today',
'override_close_today',
'next_appointment' => ['date', 'time'],
'hours' => ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'],
'contact' => ['phone', 'email'],
'updated_at',
])
->assertJsonPath('store_status', 'open')
->assertJsonPath('today_close', '18:00')
->assertJsonPath('hours.monday', '10:00 18:00')
->assertJsonPath('hours.sunday', 'Geschlossen')
->assertJsonPath('contact.phone', '0521 98620100')
->assertJsonPath('next_appointment.date', '2026-03-01')
->assertJsonPath('next_appointment.time', '14:00');
Carbon::setTestNow();
});
test('status endpoint returns closed with next_open outside opening hours', function () {
CabinetTabletSetting::factory()->create([
'store_status' => 'auto',
'hours_thursday_open' => '10:00',
'hours_thursday_close' => '18:00',
'hours_friday_open' => '10:00',
'hours_friday_close' => '18:00',
]);
// Thursday 20:00 after closing
Carbon::setTestNow(Carbon::create(2026, 3, 5, 20, 0, 0, 'Europe/Berlin'));
$response = $this->getJson('/api/cabinet-tablet/status');
$response->assertSuccessful()
->assertJsonPath('store_status', 'closed')
->assertJsonPath('next_open.label', 'Morgen')
->assertJsonPath('next_open.time', '10:00');
Carbon::setTestNow();
});
test('status endpoint returns notice when notice mode is set', function () {
CabinetTabletSetting::factory()->create([
'store_status' => 'notice',
'notice_headline' => 'Urlaub',
'notice_subtext' => 'Wir sind vom 10.20. März im Urlaub.',
]);
$response = $this->getJson('/api/cabinet-tablet/status');
$response->assertSuccessful()
->assertJsonPath('store_status', 'notice')
->assertJsonPath('notice_headline', 'Urlaub');
});
test('check endpoint returns updated_at and store_status but not full data', function () {
CabinetTabletSetting::factory()->create(['store_status' => 'auto']);
$response = $this->getJson('/api/cabinet-tablet/check');
$response->assertSuccessful()
->assertJsonStructure(['updated_at', 'store_status'])
->assertJsonMissingPath('hours');
});
test('status endpoint creates settings row if none exists', function () {
expect(CabinetTabletSetting::count())->toBe(0);
$response = $this->getJson('/api/cabinet-tablet/status');
$response->assertSuccessful();
expect(CabinetTabletSetting::count())->toBe(1);
});

View file

@ -0,0 +1,410 @@
<?php
declare(strict_types=1);
use App\Models\CmsArticle;
use App\Models\CmsProject;
use App\Models\User;
use FluxCms\Core\Models\CmsContent;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Volt\Volt;
uses(RefreshDatabase::class);
beforeEach(function () {
$portalDomain = config('domains.domain_portal');
url()->forceRootUrl('https://'.$portalDomain);
});
// ========================================
// Authentication
// ========================================
test('cms dashboard requires authentication', function () {
$this->get(route('cms.dashboard'))->assertRedirect('/login');
});
test('cms content index requires authentication', function () {
$this->get(route('cms.content.index'))->assertRedirect('/login');
});
test('cms projects index requires authentication', function () {
$this->get(route('cms.projects.index'))->assertRedirect('/login');
});
test('cms media index requires authentication', function () {
$this->get(route('cms.media.index'))->assertRedirect('/login');
});
// ========================================
// Dashboard
// ========================================
test('cms dashboard renders for authenticated users', function () {
$user = User::factory()->create();
$this->actingAs($user)
->get(route('cms.dashboard'))
->assertSuccessful()
->assertSee('So funktioniert das CMS')
->assertSee('Medienbibliothek');
});
test('cms dashboard shows correct stats', function () {
$user = User::factory()->create();
CmsProject::factory()->published()->count(2)->create();
CmsProject::factory()->unpublished()->create();
Volt::actingAs($user)
->test('admin.cms.dashboard')
->assertSee('3')
->assertSee('2 veröffentlicht');
});
// ========================================
// Content Index
// ========================================
test('cms content index renders for authenticated users', function () {
$user = User::factory()->create();
$this->actingAs($user)
->get(route('cms.content.index'))
->assertSuccessful();
});
test('cms content index shows groups', function () {
$user = User::factory()->create();
CmsContent::create([
'group' => 'home',
'key' => 'hero',
'type' => 'json',
'value' => ['de' => ['title' => 'Test']],
]);
CmsContent::create([
'group' => 'home',
'key' => 'about',
'type' => 'text',
'value' => ['de' => 'About text'],
]);
Volt::actingAs($user)
->test('admin.cms.content-index')
->assertSee('home')
->assertSee('2');
});
test('cms content index can select group and show flattened fields', function () {
$user = User::factory()->create();
CmsContent::create([
'group' => 'home',
'key' => 'hero',
'type' => 'json',
'value' => ['de' => ['title' => 'Hero Title', 'subtitle' => 'Hero Sub']],
]);
Volt::actingAs($user)
->test('admin.cms.content-index')
->call('selectGroup', 'home')
->assertSee('hero')
->assertSee('title')
->assertSee('subtitle')
->assertSee('Hero Title')
->assertSee('Hero Sub');
});
test('cms content index can edit a flattened field', function () {
$user = User::factory()->create();
$content = CmsContent::create([
'group' => 'home',
'key' => 'hero',
'type' => 'json',
'value' => ['de' => ['title' => 'Old Title', 'subtitle' => 'Sub']],
]);
Volt::actingAs($user)
->test('admin.cms.content-index')
->call('selectGroup', 'home')
->call('startFieldEdit', $content->id, 'title')
->assertSet('editingId', $content->id)
->assertSet('editingField', 'title')
->assertSet('editValue', 'Old Title')
->set('editValue', 'New Title')
->call('saveEdit')
->assertSet('editingId', null);
$content->refresh();
$value = $content->getTranslation('value', 'de');
expect($value['title'])->toBe('New Title')
->and($value['subtitle'])->toBe('Sub');
});
test('cms content index opens json modal for array subfields', function () {
$user = User::factory()->create();
$content = CmsContent::create([
'group' => 'home',
'key' => 'hero',
'type' => 'json',
'value' => ['de' => ['title' => 'Title', 'stats' => [['label' => 'Projects', 'value' => '50+']]]],
]);
Volt::actingAs($user)
->test('admin.cms.content-index')
->call('selectGroup', 'home')
->call('startFieldEdit', $content->id, 'stats')
->assertSet('showJsonModal', true)
->assertSet('jsonEditingKey', 'hero.stats');
});
// ========================================
// Projects Index
// ========================================
test('cms projects index renders for authenticated users', function () {
$user = User::factory()->create();
$this->actingAs($user)
->get(route('cms.projects.index'))
->assertSuccessful();
});
test('cms projects index lists projects', function () {
$user = User::factory()->create();
CmsProject::factory()->published()->create(['slug' => 'test-project', 'title' => ['de' => 'Test Projekt']]);
Volt::actingAs($user)
->test('admin.cms.projects-index')
->assertSee('Test Projekt');
});
test('cms projects can open create form', function () {
$user = User::factory()->create();
Volt::actingAs($user)
->test('admin.cms.projects-index')
->call('openCreate')
->assertSet('showForm', true)
->assertSet('editingId', null);
});
test('cms projects can save a new project', function () {
$user = User::factory()->create();
Volt::actingAs($user)
->test('admin.cms.projects-index')
->call('openCreate')
->set('slug', 'new-project')
->set('projectTitle', 'New Project')
->set('is_published', true)
->set('order', 1)
->call('save')
->assertSet('showForm', false)
->assertHasNoErrors();
expect(CmsProject::where('slug', 'new-project')->exists())->toBeTrue();
});
test('cms projects can save with all detail fields', function () {
$user = User::factory()->create();
Volt::actingAs($user)
->test('admin.cms.projects-index')
->call('openCreate')
->set('slug', 'full-project')
->set('projectTitle', 'Full Project')
->set('location', 'Dubai')
->set('status', 'NEW LAUNCH')
->set('price_from_aed', 1125000)
->set('highlights', ['Highlight A', 'Highlight B'])
->set('quick_facts', [['icon' => 'home-modern', 'label' => 'Typen', 'value' => '1BR']])
->set('investTitle', 'Starkes Investment')
->set('investText', 'Beschreibung')
->set('investViews', ['Road View'])
->set('galleryItems', ['img1.jpg', 'img2.jpg'])
->set('locTitle', 'Location: Al Jaddaf')
->set('locMapUrl', 'https://maps.google.com')
->set('locPoints', ['Punkt 1', 'Punkt 2'])
->set('contactTitle', 'Kontakt Titel')
->set('contactSubtitle', 'Ihr Ansprechpartner')
->set('contactOptions', [['key' => '', 'value' => 'Bitte wählen'], ['key' => '1br', 'value' => '1BR Apt']])
->call('save')
->assertSet('showForm', false);
$project = CmsProject::where('slug', 'full-project')->first();
expect($project)->not->toBeNull()
->and($project->getTranslation('highlights', 'de'))->toHaveCount(2)
->and($project->quick_facts)->toHaveCount(1)
->and($project->gallery)->toHaveCount(2)
->and($project->getTranslation('investment_case', 'de')['title'])->toBe('Starkes Investment')
->and($project->getTranslation('location_info', 'de')['points'])->toHaveCount(2)
->and($project->getTranslation('contact', 'de')['options'])->toHaveKey('1br');
});
test('cms projects can edit existing project', function () {
$user = User::factory()->create();
$project = CmsProject::factory()->published()->create(['slug' => 'edit-me', 'title' => ['de' => 'Edit Me']]);
Volt::actingAs($user)
->test('admin.cms.projects-index')
->call('openEdit', $project->id)
->assertSet('showForm', true)
->assertSet('editingId', $project->id)
->assertSet('slug', 'edit-me');
});
test('cms projects can duplicate a project', function () {
$user = User::factory()->create();
$project = CmsProject::factory()->published()->create([
'slug' => 'original-project',
'title' => ['de' => 'Original Projekt'],
]);
Volt::actingAs($user)
->test('admin.cms.projects-index')
->call('duplicateProject', $project->id)
->assertSet('showForm', true)
->assertSet('editingId', null)
->assertSet('projectTitle', 'Original Projekt');
expect($this->slug ?? '')->not->toBe('original-project');
});
test('cms projects can delete a project', function () {
$user = User::factory()->create();
$project = CmsProject::factory()->create(['slug' => 'delete-me']);
Volt::actingAs($user)
->test('admin.cms.projects-index')
->call('deleteProject', $project->id);
expect(CmsProject::where('slug', 'delete-me')->exists())->toBeFalse();
});
// ========================================
// Media Index
// ========================================
test('cms media index renders for authenticated users', function () {
$user = User::factory()->create();
$this->actingAs($user)
->get(route('cms.media.index'))
->assertSuccessful();
});
// ========================================
// Articles (Magazin)
// ========================================
test('cms articles index requires authentication', function () {
$this->get(route('cms.articles.index'))->assertRedirect('/login');
});
test('cms articles index renders for authenticated users', function () {
$user = User::factory()->create();
$this->actingAs($user)
->get(route('cms.articles.index'))
->assertSuccessful()
->assertSee('Magazin Beiträge');
});
test('cms articles index shows existing articles', function () {
$user = User::factory()->create();
CmsArticle::factory()->create([
'slug' => 'test-magazin-artikel',
'title' => ['de' => 'Ein Test Artikel'],
'category' => 'Dubai Investment',
'author' => ['name' => 'Marcel Scheibe', 'bio' => 'CEO', 'avatar' => 'a.jpg'],
]);
$this->actingAs($user)
->get(route('cms.articles.index'))
->assertSee('Ein Test Artikel')
->assertSee('Dubai Investment')
->assertSee('Marcel Scheibe');
});
test('cms articles index can create new article', function () {
$user = User::factory()->create();
$this->actingAs($user);
Volt::test('admin.cms.articles-index')
->call('openCreate')
->assertSet('showForm', true)
->assertSet('editingId', null);
});
test('cms articles index can save a new article', function () {
$user = User::factory()->create();
$this->actingAs($user);
Volt::test('admin.cms.articles-index')
->call('openCreate')
->set('slug', 'neuer-test-artikel')
->set('articleTitle', 'Neuer Testartikel')
->set('subtitle', 'Ein Untertitel')
->set('category', 'Test Kategorie')
->set('date_label', 'März 23, 2026')
->set('read_time', '5 min read')
->set('authorName', 'Test Autor')
->set('intro', 'Einleitungstext hier.')
->set('sections', [['title' => 'Abschnitt 1', 'content' => 'Inhalt 1']])
->call('save')
->assertSet('showForm', false);
$article = CmsArticle::where('slug', 'neuer-test-artikel')->first();
expect($article)->not->toBeNull()
->and($article->category)->toBe('Test Kategorie')
->and($article->getTranslation('title', 'de'))->toBe('Neuer Testartikel');
$content = $article->getTranslation('content', 'de');
expect($content['intro'])->toBe('Einleitungstext hier.')
->and($content['sections'])->toHaveCount(1);
});
test('cms articles index can edit existing article', function () {
$user = User::factory()->create();
$this->actingAs($user);
$article = CmsArticle::factory()->create([
'slug' => 'edit-test',
'title' => ['de' => 'Original Titel'],
]);
Volt::test('admin.cms.articles-index')
->call('openEdit', $article->id)
->assertSet('editingId', $article->id)
->assertSet('slug', 'edit-test')
->assertSet('showForm', true);
});
test('cms articles index can delete article', function () {
$user = User::factory()->create();
$this->actingAs($user);
$article = CmsArticle::factory()->create();
Volt::test('admin.cms.articles-index')
->call('deleteArticle', $article->id);
expect(CmsArticle::find($article->id))->toBeNull();
});
test('cms articles index can toggle published status', function () {
$user = User::factory()->create();
$this->actingAs($user);
$article = CmsArticle::factory()->published()->create();
Volt::test('admin.cms.articles-index')
->call('togglePublished', $article->id);
expect($article->refresh()->is_published)->toBeFalse();
});

View file

@ -0,0 +1,112 @@
<?php
declare(strict_types=1);
use App\Models\CmsArticle;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
test('cms article can be created with factory', function () {
$article = CmsArticle::factory()->create();
expect($article)->toBeInstanceOf(CmsArticle::class)
->and($article->slug)->not->toBeEmpty()
->and($article->title)->not->toBeEmpty();
});
test('published scope returns only published articles', function () {
CmsArticle::factory()->published()->count(2)->create();
CmsArticle::factory()->unpublished()->create();
expect(CmsArticle::published()->count())->toBe(2);
});
test('ordered scope sorts by order then created_at desc', function () {
$first = CmsArticle::factory()->create(['order' => 1]);
$second = CmsArticle::factory()->create(['order' => 2]);
$third = CmsArticle::factory()->create(['order' => 3]);
$sorted = CmsArticle::ordered()->pluck('id')->toArray();
expect($sorted)->toBe([$first->id, $second->id, $third->id]);
});
test('toFrontendArray returns compatible structure', function () {
$article = CmsArticle::factory()->create([
'slug' => 'test-article',
'title' => 'Test Article',
'subtitle' => 'Test Subtitle',
'image' => 'b2in/magazin-1.jpg',
'category' => 'Dubai Investment',
'date_label' => 'März 10, 2026',
'read_time' => '6 min read',
'author' => ['name' => 'Marcel', 'bio' => 'CEO', 'avatar' => 'avatar.jpg'],
'content' => ['de' => ['intro' => 'Intro text', 'sections' => [['title' => 'S1', 'content' => 'C1']]]],
]);
$array = $article->toFrontendArray();
expect($array)
->toHaveKeys(['id', 'slug', 'title', 'subtitle', 'image', 'category', 'date', 'readTime', 'author', 'content'])
->and($array['slug'])->toBe('test-article')
->and($array['date'])->toBe('März 10, 2026')
->and($array['readTime'])->toBe('6 min read')
->and($array['author']['name'])->toBe('Marcel')
->and($array['content']['sections'])->toHaveCount(1);
});
test('slug is unique', function () {
CmsArticle::factory()->create(['slug' => 'unique-slug']);
expect(fn () => CmsArticle::factory()->create(['slug' => 'unique-slug']))
->toThrow(\Illuminate\Database\QueryException::class);
});
test('author cast returns array', function () {
$article = CmsArticle::factory()->create([
'author' => ['name' => 'Test Author', 'bio' => 'Bio text', 'avatar' => 'a.jpg'],
]);
$article->refresh();
expect($article->author)
->toBeArray()
->toHaveKey('name', 'Test Author')
->toHaveKey('bio', 'Bio text');
});
test('translatable fields work correctly', function () {
$article = CmsArticle::factory()->create([
'title' => ['de' => 'Deutscher Titel', 'en' => 'English Title'],
'subtitle' => ['de' => 'Deutsch', 'en' => 'English'],
]);
expect($article->getTranslation('title', 'de'))->toBe('Deutscher Titel')
->and($article->getTranslation('title', 'en'))->toBe('English Title');
});
test('seeder creates articles from config data', function () {
$this->seed(\Database\Seeders\CmsArticleSeeder::class);
expect(CmsArticle::count())->toBe(5);
$article = CmsArticle::where('slug', 'escrow-system-dubai-investoren')->first();
expect($article)->not->toBeNull()
->and($article->category)->toBe('Dubai Investment')
->and($article->read_time)->toBe('6 min read')
->and($article->author['name'])->toBe('Marcel Scheibe')
->and($article->is_published)->toBeTrue();
$content = $article->getTranslation('content', 'de');
expect($content)->toHaveKey('intro')
->and($content['sections'])->toHaveCount(3);
});
test('seeder is idempotent', function () {
$this->seed(\Database\Seeders\CmsArticleSeeder::class);
$this->seed(\Database\Seeders\CmsArticleSeeder::class);
expect(CmsArticle::count())->toBe(5);
});

View file

@ -0,0 +1,135 @@
<?php
declare(strict_types=1);
use FluxCms\Core\Models\CmsContent;
use FluxCms\Core\Services\CmsContentService;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
test('seeder creates content entries for all mapped sections', function () {
$this->seed(\Database\Seeders\CmsContentSeeder::class);
expect(CmsContent::count())->toBeGreaterThan(0);
$expectedGroups = ['global', 'home', 'immobilien', 'netzwerk', 'faq', 'contact', 'about', 'magazin'];
foreach ($expectedGroups as $group) {
expect(CmsContent::where('group', $group)->exists())
->toBeTrue("Expected group '{$group}' to exist in CmsContent");
}
});
test('seeder creates all home sections', function () {
$this->seed(\Database\Seeders\CmsContentSeeder::class);
$homeKeys = CmsContent::where('group', 'home')->pluck('key')->toArray();
expect($homeKeys)
->toContain('hero')
->toContain('founder_bar')
->toContain('ecosystem_core')
->toContain('vision_section');
});
test('seeder stores hero content as translatable json', function () {
$this->seed(\Database\Seeders\CmsContentSeeder::class);
$hero = CmsContent::where('group', 'home')->where('key', 'hero')->first();
expect($hero)->not->toBeNull()
->and($hero->type)->toBe('json');
$valueDe = $hero->getTranslation('value', 'de');
$valueEn = $hero->getTranslation('value', 'en');
expect($valueDe)->toBeArray()
->toHaveKeys(['title', 'subtitle', 'image']);
expect($valueEn)->toBeArray()
->toHaveKeys(['title', 'subtitle', 'image']);
});
test('cms helper returns correct content after seeding', function () {
$this->seed(\Database\Seeders\CmsContentSeeder::class);
$hero = cms('home.hero');
expect($hero)->toBeArray()
->and($hero['title'])->toContain('B2in');
});
test('cms content service getGroup returns all section keys', function () {
$this->seed(\Database\Seeders\CmsContentSeeder::class);
$service = app(CmsContentService::class);
$homeGroup = $service->getGroup('home');
expect($homeGroup)->toBeArray()
->toHaveKey('hero')
->toHaveKey('founder_bar')
->toHaveKey('ecosystem_core');
expect($homeGroup['hero'])->toBeArray()
->toHaveKey('title');
});
test('seeder is idempotent', function () {
$this->seed(\Database\Seeders\CmsContentSeeder::class);
$countFirst = CmsContent::count();
$this->seed(\Database\Seeders\CmsContentSeeder::class);
$countSecond = CmsContent::count();
expect($countSecond)->toBe($countFirst);
});
test('immobilien sections are seeded correctly', function () {
$this->seed(\Database\Seeders\CmsContentSeeder::class);
$immoKeys = CmsContent::where('group', 'immobilien')->pluck('key')->toArray();
expect($immoKeys)
->toContain('hero_v2')
->toContain('warum_dubai')
->toContain('kaufprozess')
->toContain('bruecke')
->toContain('mindset')
->toContain('moebel_vorteil');
});
test('faq content contains questions array', function () {
$this->seed(\Database\Seeders\CmsContentSeeder::class);
$faq = cms('faq.faq');
expect($faq)->toBeArray()
->toHaveKey('questions')
->and($faq['questions'])->toBeArray()
->not->toBeEmpty();
});
test('cms_theme_section returns CMS content after seeding', function () {
$this->seed(\Database\Seeders\CmsContentSeeder::class);
$hero = cms_theme_section('hero');
expect($hero)->toBeArray()
->and($hero['title'])->toContain('B2in');
});
test('portfolio is stored under immobilien group for CMS', function () {
$this->seed(\Database\Seeders\CmsContentSeeder::class);
$portfolio = CmsContent::where('group', 'immobilien')->where('key', 'portfolio')->first();
expect($portfolio)->not->toBeNull()
->and($portfolio->type)->toBe('json');
$valueDe = $portfolio->getTranslation('value', 'de');
expect($valueDe)->toBeArray()
->toHaveKey('projects')
->and($valueDe['projects'])->toBeArray();
});

View file

@ -0,0 +1,115 @@
<?php
declare(strict_types=1);
use App\Models\CmsProject;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
test('cms project can be created with factory', function () {
$project = CmsProject::factory()->create();
expect($project)->toBeInstanceOf(CmsProject::class)
->and($project->slug)->not->toBeEmpty()
->and($project->title)->not->toBeEmpty();
});
test('published scope returns only published projects', function () {
CmsProject::factory()->published()->count(2)->create();
CmsProject::factory()->unpublished()->create();
expect(CmsProject::published()->count())->toBe(2);
});
test('ordered scope sorts by order then launch_date desc', function () {
$first = CmsProject::factory()->create([
'order' => 0,
'launch_date' => '2026-06-01',
]);
$second = CmsProject::factory()->create([
'order' => 0,
'launch_date' => '2026-01-01',
]);
$third = CmsProject::factory()->create([
'order' => 1,
'launch_date' => '2026-12-01',
]);
$sorted = CmsProject::ordered()->pluck('id')->toArray();
expect($sorted)->toBe([$first->id, $second->id, $third->id]);
});
test('getFormattedPrice returns formatted AED with EUR and USD', function () {
$project = CmsProject::factory()->create(['price_from_aed' => 1_125_000]);
$formatted = $project->getFormattedPrice();
expect($formatted)
->toContain('AED')
->toContain('EUR')
->toContain('USD')
->toContain('1.125.000');
});
test('getFormattedPrice returns empty string when no price', function () {
$project = CmsProject::factory()->create(['price_from_aed' => null]);
expect($project->getFormattedPrice())->toBe('');
});
test('toFrontendArray returns compatible structure', function () {
$project = CmsProject::factory()->create([
'slug' => 'test-project',
'title' => 'Test Project',
'location' => 'Dubai',
'status' => 'NEW LAUNCH',
'launch_date' => '2026-03-03',
'price_from_aed' => 1_000_000,
'highlights' => ['Highlight 1', 'Highlight 2'],
'quick_facts' => [['icon' => 'home', 'label' => 'Type', 'value' => '1BR']],
'gallery' => ['img1.jpg', 'img2.jpg'],
]);
$array = $project->toFrontendArray();
expect($array)
->toHaveKeys(['slug', 'title', 'location', 'status', 'launch_date', 'price_from', 'image', 'highlights', 'quick_facts', 'investment_case', 'gallery', 'location_info', 'contact', 'investor_trust', 'furniture_benefit'])
->and($array['slug'])->toBe('test-project')
->and($array['launch_date'])->toBe('03.03.2026')
->and($array['highlights'])->toHaveCount(2)
->and($array['gallery'])->toHaveCount(2)
->and($array['price_from'])->toContain('AED');
});
test('slug is unique', function () {
CmsProject::factory()->create(['slug' => 'unique-slug']);
expect(fn () => CmsProject::factory()->create(['slug' => 'unique-slug']))
->toThrow(\Illuminate\Database\QueryException::class);
});
test('seeder creates project from config data', function () {
$this->seed(\Database\Seeders\CmsProjectSeeder::class);
$project = CmsProject::where('slug', 'azizi-creek-views-4')->first();
expect($project)->not->toBeNull()
->and($project->title)->toBe('Azizi Developments: Creek Views 4')
->and($project->price_from_aed)->toBe(1_125_000)
->and($project->is_published)->toBeTrue()
->and($project->gallery)->toHaveCount(6)
->and($project->quick_facts)->toHaveCount(4)
->and($project->investor_trust)->toBeArray()
->and($project->investor_trust['columns'] ?? [])->toHaveCount(3)
->and($project->furniture_benefit)->toBeArray()
->and($project->furniture_benefit['button_link'] ?? '')->toBe('/netzwerk');
});
test('seeder is idempotent', function () {
$this->seed(\Database\Seeders\CmsProjectSeeder::class);
$this->seed(\Database\Seeders\CmsProjectSeeder::class);
expect(CmsProject::count())->toBe(1);
});

View file

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
use FluxCms\Core\Services\HeroiconOutlineList;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
test('heroicon outline list returns a sorted string array', function () {
$names = HeroiconOutlineList::names();
expect($names)->toBeArray();
foreach ($names as $name) {
expect($name)->toBeString()->not->toBeEmpty();
}
$sorted = $names;
sort($sorted);
expect($names)->toEqual($sorted);
});
test('heroicon outline forgetCached allows list to be rebuilt', function () {
HeroiconOutlineList::forgetCached();
$names = HeroiconOutlineList::names();
expect($names)->toBeArray();
});

View file

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
use FluxCms\Core\Models\CmsContent;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
test('netzwerk page lang blocks exist for de', function () {
$cabinet = trans('b2in.themes.b2in.netzwerk_cabinet_partner', [], 'de');
$hint = trans('b2in.themes.b2in.netzwerk_immobilien_hint', [], 'de');
expect($cabinet)->toBeArray()
->toHaveKey('badge')
->and($cabinet['paragraphs'])->toBeArray();
expect($hint)->toBeArray()
->toHaveKey('title')
->toHaveKey('button_text');
});
test('netzwerk page cms seeder creates rows only once', function () {
$this->seed(\Database\Seeders\NetzwerkPageCmsSeeder::class);
expect(CmsContent::where('group', 'netzwerk')->where('key', 'cabinet_partner')->exists())->toBeTrue()
->and(CmsContent::where('group', 'netzwerk')->where('key', 'immobilien_hint')->exists())->toBeTrue();
$count = CmsContent::count();
$this->seed(\Database\Seeders\NetzwerkPageCmsSeeder::class);
expect(CmsContent::count())->toBe($count);
});
test('cms_theme_section returns cabinet content from database after netzwerk page seed', function () {
$this->seed(\Database\Seeders\NetzwerkPageCmsSeeder::class);
app()->setLocale('de');
$cabinet = cms_theme_section('netzwerk_cabinet_partner');
expect($cabinet)->toBeArray()
->and($cabinet['badge'])->toBe('Premiumpartner');
});

View file

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
use FluxCms\Core\Models\CmsContent;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
test('cms legal seeder legt vier rechtliche seiten an', function () {
$this->seed(\Database\Seeders\CmsLegalSeeder::class);
expect(CmsContent::query()->where('group', 'legal')->count())->toBe(4);
foreach (['impressum', 'privacy', 'terms', 'cookie_policy'] as $key) {
$row = CmsContent::query()->where('group', 'legal')->where('key', $key)->first();
expect($row)->not->toBeNull()
->and($row->type)->toBe('json');
$de = $row->getTranslation('value', 'de');
expect($de)->toBeArray()
->toHaveKeys(['title', 'meta_title', 'subtitle', 'back_link', 'content'])
->and($de['title'])->not->toBeEmpty();
}
});
test('legal_page liefert cms daten wenn vorhanden', function () {
$this->seed(\Database\Seeders\CmsLegalSeeder::class);
$legal = legal_page('impressum');
expect($legal)->toBeArray()
->toHaveKey('content')
->and($legal['title'])->not->toBeEmpty();
});

View file

@ -0,0 +1,145 @@
<?php
declare(strict_types=1);
use Acme\ContactForm\ContactFormService;
use App\Livewire\Web\Components\Sections\ImmobilienContactForm;
use App\Livewire\Web\Components\Ui\ContactForm;
use Illuminate\Support\Facades\Mail;
use Livewire\Livewire;
it('contact page loads with contact form component', function () {
$this->get('/contact')
->assertSuccessful()
->assertSeeLivewire(ContactForm::class);
});
it('contact form renders with privacy checkbox', function () {
Livewire::test(ContactForm::class)
->assertSee('Datenschutzerklärung');
});
it('contact form validates required fields', function () {
Livewire::test(ContactForm::class)
->call('submit')
->assertHasErrors(['firstName', 'lastName', 'email', 'subject', 'message', 'privacy']);
});
it('contact form requires privacy acceptance', function () {
Livewire::test(ContactForm::class)
->set('firstName', 'Max')
->set('lastName', 'Mustermann')
->set('email', 'max@example.com')
->set('subject', 'Allgemein')
->set('message', 'Testnachricht')
->set('privacy', false)
->call('submit')
->assertHasErrors(['privacy']);
});
it('contact form sends email on valid submission', function () {
Mail::fake();
$service = $this->mock(ContactFormService::class);
$service->shouldReceive('handle')->once();
Livewire::test(ContactForm::class)
->set('firstName', 'Max')
->set('lastName', 'Mustermann')
->set('email', 'max@example.com')
->set('subject', 'Allgemein')
->set('message', 'Testnachricht für das Kontaktformular')
->set('privacy', true)
->call('submit')
->assertHasNoErrors()
->assertSet('success', true);
});
it('contact form resets after successful submission', function () {
Mail::fake();
$service = $this->mock(ContactFormService::class);
$service->shouldReceive('handle')->once();
Livewire::test(ContactForm::class)
->set('firstName', 'Max')
->set('lastName', 'Mustermann')
->set('email', 'max@example.com')
->set('subject', 'Allgemein')
->set('message', 'Testnachricht')
->set('privacy', true)
->call('submit')
->assertSet('firstName', '')
->assertSet('lastName', '')
->assertSet('email', '')
->assertSet('message', '')
->assertSet('privacy', false);
});
it('contact form blocks honeypot spam', function () {
Livewire::test(ContactForm::class)
->set('firstName', 'Max')
->set('lastName', 'Mustermann')
->set('email', 'max@example.com')
->set('subject', 'Allgemein')
->set('message', 'Testnachricht')
->set('privacy', true)
->set('website', 'https://spam.com')
->call('submit')
->assertHasErrors(['website']);
});
it('immobilien contact form renders with privacy checkbox', function () {
Livewire::test(ImmobilienContactForm::class, [
'projectSlug' => 'test-projekt',
'projectTitle' => 'Testprojekt Dubai',
'interestOptions' => ['kauf' => 'Kauf', 'miete' => 'Miete'],
])
->assertSee('Datenschutzerklärung')
->assertSee('Exposé');
});
it('immobilien contact form validates required fields', function () {
Livewire::test(ImmobilienContactForm::class)
->call('submit')
->assertHasErrors(['firstName', 'lastName', 'email', 'privacy']);
});
it('immobilien contact form requires privacy acceptance', function () {
Livewire::test(ImmobilienContactForm::class)
->set('firstName', 'Max')
->set('lastName', 'Mustermann')
->set('email', 'max@example.com')
->set('privacy', false)
->call('submit')
->assertHasErrors(['privacy']);
});
it('immobilien contact form sends inquiry on valid submission', function () {
Mail::fake();
$service = $this->mock(ContactFormService::class);
$service->shouldReceive('handle')->once();
Livewire::test(ImmobilienContactForm::class, [
'projectSlug' => 'azizi-creek-views-4',
'projectTitle' => 'Creek Views 4',
'interestOptions' => ['kauf' => 'Kauf'],
])
->set('firstName', 'Max')
->set('lastName', 'Mustermann')
->set('email', 'max@example.com')
->set('phone', '+49 123 456789')
->set('interest', 'kauf')
->set('message', 'Ich interessiere mich für diese Immobilie.')
->set('privacy', true)
->call('submit')
->assertHasNoErrors()
->assertSet('success', true);
});
it('immobilien show page renders livewire contact form component', function () {
$this->get('/immobilien/azizi-creek-views-4')
->assertSuccessful()
->assertSeeLivewire(ImmobilienContactForm::class);
});

View file

@ -0,0 +1,148 @@
<?php
use App\Livewire\Admin\Cms\DisplayList;
use App\Models\Display;
use App\Models\DisplayVersion;
use App\Models\User;
use Livewire\Livewire;
beforeEach(function () {
$portalDomain = config('domains.domain_portal');
Livewire::withoutLazyLoading();
url()->forceRootUrl('https://'.$portalDomain);
});
test('display list requires authentication', function () {
$response = $this->get(route('admin.cms.displays'));
$response->assertRedirect('/login');
});
test('display list renders for authenticated users', function () {
$user = User::factory()->create();
$this->actingAs($user);
$response = $this->get(route('admin.cms.displays'));
$response->assertSuccessful();
$response->assertSeeLivewire(DisplayList::class);
});
test('can create a display', function () {
$user = User::factory()->create();
Livewire::actingAs($user)
->test(DisplayList::class)
->call('openModal')
->set('displayName', 'Display 1 - Eingang')
->set('displayLocation', 'Schaufenster links')
->call('save');
expect(Display::where('name', 'Display 1 - Eingang')->exists())->toBeTrue();
});
test('can assign versions to a display', function () {
$user = User::factory()->create();
$version1 = DisplayVersion::factory()->create();
$version2 = DisplayVersion::factory()->create();
$display = Display::factory()->create();
Livewire::actingAs($user)
->test(DisplayList::class)
->call('openModal', $display->id)
->call('addVersion', $version1->id)
->call('addVersion', $version2->id)
->call('save');
$display->refresh();
expect($display->versions)->toHaveCount(2);
expect($display->versions->first()->id)->toBe($version1->id);
expect($display->versions->last()->id)->toBe($version2->id);
});
test('can reorder versions in playlist', function () {
$user = User::factory()->create();
$version1 = DisplayVersion::factory()->create();
$version2 = DisplayVersion::factory()->create();
$display = Display::factory()->create();
Livewire::actingAs($user)
->test(DisplayList::class)
->call('openModal', $display->id)
->call('addVersion', $version1->id)
->call('addVersion', $version2->id)
->call('moveVersion', 1, 'up')
->call('save');
$display->refresh();
expect($display->versions->first()->id)->toBe($version2->id);
expect($display->versions->last()->id)->toBe($version1->id);
});
test('can remove version from playlist', function () {
$user = User::factory()->create();
$version1 = DisplayVersion::factory()->create();
$version2 = DisplayVersion::factory()->create();
$display = Display::factory()->create();
$display->versions()->attach([
$version1->id => ['sort_order' => 0],
$version2->id => ['sort_order' => 1],
]);
Livewire::actingAs($user)
->test(DisplayList::class)
->call('openModal', $display->id)
->call('removeVersion', 0)
->call('save');
$display->refresh();
expect($display->versions)->toHaveCount(1);
expect($display->versions->first()->id)->toBe($version2->id);
});
test('can delete a display', function () {
$user = User::factory()->create();
$display = Display::factory()->create();
Livewire::actingAs($user)
->test(DisplayList::class)
->call('deleteDisplay', $display->id);
expect(Display::find($display->id))->toBeNull();
});
test('can toggle display active status', function () {
$user = User::factory()->create();
$display = Display::factory()->create(['is_active' => true]);
Livewire::actingAs($user)
->test(DisplayList::class)
->call('toggleActive', $display->id);
expect($display->fresh()->is_active)->toBeFalse();
});
test('validates required fields when creating display', function () {
$user = User::factory()->create();
Livewire::actingAs($user)
->test(DisplayList::class)
->call('openModal')
->set('displayName', '')
->call('save')
->assertHasErrors(['displayName']);
});
test('does not add duplicate version to playlist', function () {
$user = User::factory()->create();
$version = DisplayVersion::factory()->create();
$component = Livewire::actingAs($user)
->test(DisplayList::class)
->call('openModal')
->call('addVersion', $version->id)
->call('addVersion', $version->id);
expect($component->get('selectedVersionIds'))->toHaveCount(1);
});

View file

@ -0,0 +1,202 @@
<?php
declare(strict_types=1);
use App\Models\DisplayMedia;
use App\Models\User;
use App\Services\DisplayMediaService;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Spatie\Permission\Models\Role;
beforeEach(function () {
Storage::fake('public');
});
// ========================================
// MODEL TESTS
// ========================================
it('creates an uploaded image media', function () {
$media = DisplayMedia::factory()->create();
expect($media->isUpload())->toBeTrue()
->and($media->isExternal())->toBeFalse()
->and($media->isImage())->toBeTrue()
->and($media->isVideo())->toBeFalse()
->and($media->source_type)->toBe('upload');
});
it('creates an external video media', function () {
$media = DisplayMedia::factory()->externalVideo()->create();
expect($media->isExternal())->toBeTrue()
->and($media->isUpload())->toBeFalse()
->and($media->isVideo())->toBeTrue()
->and($media->external_url)->toStartWith('https://');
});
it('returns correct URL for uploaded media', function () {
$media = DisplayMedia::factory()->create(['path' => 'display-media/2026/03/test.webp']);
expect($media->getUrl())->toContain('display-media/2026/03/test.webp');
});
it('returns external URL for external media', function () {
$url = 'https://drive.google.com/file/d/abc123/view';
$media = DisplayMedia::factory()->external()->create(['external_url' => $url]);
expect($media->getUrl())->toBe($url);
});
it('returns human readable file size', function () {
$media = DisplayMedia::factory()->create(['file_size' => 1536000]);
expect($media->getHumanFileSize())->toBe('1.5 MB');
$external = DisplayMedia::factory()->external()->create();
expect($external->getHumanFileSize())->toBe('Extern');
});
it('returns display name from title or filename', function () {
$withTitle = DisplayMedia::factory()->create(['title' => 'Mein Video', 'filename' => 'file.mp4']);
expect($withTitle->getDisplayName())->toBe('Mein Video');
$withoutTitle = DisplayMedia::factory()->create(['title' => null, 'filename' => 'file.mp4']);
expect($withoutTitle->getDisplayName())->toBe('file.mp4');
});
// ========================================
// SCOPE TESTS
// ========================================
it('filters by image scope', function () {
DisplayMedia::factory()->create(['type' => 'image']);
DisplayMedia::factory()->video()->create();
expect(DisplayMedia::images()->count())->toBe(1);
});
it('filters by video scope', function () {
DisplayMedia::factory()->create(['type' => 'image']);
DisplayMedia::factory()->video()->create();
expect(DisplayMedia::videos()->count())->toBe(1);
});
it('filters by upload scope', function () {
DisplayMedia::factory()->create();
DisplayMedia::factory()->external()->create();
expect(DisplayMedia::uploads()->count())->toBe(1);
});
it('filters by external scope', function () {
DisplayMedia::factory()->create();
DisplayMedia::factory()->external()->create();
expect(DisplayMedia::externals()->count())->toBe(1);
});
it('filters by collection scope', function () {
DisplayMedia::factory()->create(['collection' => 'immobilien']);
DisplayMedia::factory()->create(['collection' => 'moebel']);
expect(DisplayMedia::inCollection('immobilien')->count())->toBe(1);
});
it('filters by active scope', function () {
DisplayMedia::factory()->create(['is_active' => true]);
DisplayMedia::factory()->create(['is_active' => false]);
expect(DisplayMedia::active()->count())->toBe(1);
});
// ========================================
// SERVICE TESTS
// ========================================
it('stores an uploaded file', function () {
$service = app(DisplayMediaService::class);
$file = UploadedFile::fake()->image('test-photo.jpg', 800, 600);
$media = $service->storeUpload($file, 'immobilien');
expect($media)->toBeInstanceOf(DisplayMedia::class)
->and($media->filename)->toBe('test-photo.jpg')
->and($media->source_type)->toBe('upload')
->and($media->type)->toBe('image')
->and($media->collection)->toBe('immobilien')
->and($media->path)->not->toBeNull();
Storage::disk('public')->assertExists($media->path);
});
it('stores a video upload', function () {
$service = app(DisplayMediaService::class);
$file = UploadedFile::fake()->create('showroom.mp4', 10000, 'video/mp4');
$media = $service->storeUpload($file);
expect($media->type)->toBe('video')
->and($media->mime_type)->toBe('video/mp4');
});
it('creates media from external URL', function () {
$service = app(DisplayMediaService::class);
$media = $service->createFromUrl(
url: 'https://drive.google.com/file/d/abc123/view',
type: 'video',
title: 'Showroom Tour 4K',
collection: 'brand',
);
expect($media->source_type)->toBe('external')
->and($media->external_url)->toBe('https://drive.google.com/file/d/abc123/view')
->and($media->title)->toBe('Showroom Tour 4K')
->and($media->type)->toBe('video')
->and($media->collection)->toBe('brand')
->and($media->file_size)->toBe(0);
});
it('deletes uploaded media and its file', function () {
$service = app(DisplayMediaService::class);
$file = UploadedFile::fake()->image('delete-me.jpg');
$media = $service->storeUpload($file);
$path = $media->path;
Storage::disk('public')->assertExists($path);
$service->delete($media);
Storage::disk('public')->assertMissing($path);
expect(DisplayMedia::find($media->id))->toBeNull();
});
it('deletes external media record', function () {
$service = app(DisplayMediaService::class);
$media = $service->createFromUrl('https://example.com/video.mp4', 'video');
$service->delete($media);
expect(DisplayMedia::find($media->id))->toBeNull();
});
// ========================================
// ROUTE TESTS
// ========================================
it('requires authentication for display media library', function () {
$this->get(route('admin.cms.display-media'))
->assertRedirect();
});
it('loads display media library for authenticated admin', function () {
Role::findOrCreate('Super-Admin', 'web');
$user = User::factory()->create();
$user->assignRole('Super-Admin');
$this->actingAs($user)
->get(route('admin.cms.display-media'))
->assertSuccessful();
});

View file

@ -0,0 +1,168 @@
<?php
use App\Models\Display;
use App\Models\DisplayVersion;
use App\Models\DisplayVersionItem;
beforeEach(function () {
$portalDomain = config('domains.domain_portal');
url()->forceRootUrl('https://'.$portalDomain);
});
test('returns playlist with video-display config', function () {
$version = DisplayVersion::factory()->create(['type' => 'video-display']);
DisplayVersionItem::factory()->create([
'display_version_id' => $version->id,
'item_type' => 'video',
'content' => ['filename' => 'test.mp4', 'title' => 'Test', 'position' => 30],
]);
DisplayVersionItem::factory()->create([
'display_version_id' => $version->id,
'item_type' => 'footer',
'content' => ['headline' => 'Hello', 'subline' => 'World', 'url' => null],
]);
$display = Display::factory()->create();
$display->versions()->attach($version->id, ['sort_order' => 0]);
$response = $this->getJson("/api/display/{$display->id}/config");
$response->assertSuccessful();
$response->assertJsonStructure(['playlist', 'updated_at']);
$response->assertJsonPath('playlist.0.type', 'video-display');
$response->assertJsonPath('playlist.0.videoPlaylist.0.src', 'assets/test.mp4');
$response->assertJsonPath('playlist.0.footerContent.0.headline', 'Hello');
});
test('returns playlist with b2in config', function () {
$version = DisplayVersion::factory()->create([
'type' => 'b2in',
'settings' => ['theme' => 'dark', 'footer_name' => 'Marcel'],
]);
DisplayVersionItem::factory()->create([
'display_version_id' => $version->id,
'item_type' => 'media',
'content' => [
'category' => 'immobilien',
'media_type' => 'image',
'media_url' => '../assets/test.jpg',
'headline' => 'Headline',
'subline' => 'Subline',
'duration_seconds' => 10,
],
]);
$display = Display::factory()->create();
$display->versions()->attach($version->id, ['sort_order' => 0]);
$response = $this->getJson("/api/display/{$display->id}/config");
$response->assertSuccessful();
$response->assertJsonPath('playlist.0.type', 'b2in');
$response->assertJsonPath('playlist.0.settings.theme', 'dark');
$response->assertJsonPath('playlist.0.items.0.category', 'immobilien');
});
test('returns playlist with offers config', function () {
$version = DisplayVersion::factory()->create([
'type' => 'offers',
'settings' => ['loop' => true],
]);
DisplayVersionItem::factory()->create([
'display_version_id' => $version->id,
'item_type' => 'slide',
'content' => [
'type' => 'product-hero',
'duration' => 10000,
'image_url' => '../assets/goya1.jpg',
'badge_text' => 'Einzelstück',
'eyebrow' => 'Hersteller: Sudbrock',
'title' => 'GOYA Sideboard',
'subline' => '',
'price' => '489 €',
'original_price' => 'statt 4.744 €',
'tag_text' => '',
'bullets' => [],
'disclaimer' => '',
'qr_url' => 'https://cabinet-bielefeld.de',
'qr_title' => 'Reservieren',
'contact' => "0521 98620100\nTel. oder WhatsApp",
'show_brand_text' => false,
'brand_tagline' => '',
],
]);
$display = Display::factory()->create();
$display->versions()->attach($version->id, ['sort_order' => 0]);
$response = $this->getJson("/api/display/{$display->id}/config");
$response->assertSuccessful();
$response->assertJsonPath('playlist.0.type', 'offers');
$response->assertJsonPath('playlist.0.slides.0.type', 'product-hero');
$response->assertJsonPath('playlist.0.slides.0.title', 'GOYA Sideboard');
$response->assertJsonPath('playlist.0.slides.0.price', '489 €');
$response->assertJsonPath('playlist.0.slides.0.qr_url', 'https://cabinet-bielefeld.de');
});
test('returns playlist with multiple versions in order', function () {
$videoVersion = DisplayVersion::factory()->create(['type' => 'video-display', 'name' => 'Videos']);
DisplayVersionItem::factory()->create([
'display_version_id' => $videoVersion->id,
'item_type' => 'video',
'content' => ['filename' => 'test.mp4', 'title' => 'Test', 'position' => 25],
]);
$b2inVersion = DisplayVersion::factory()->create(['type' => 'b2in', 'name' => 'B2in']);
DisplayVersionItem::factory()->create([
'display_version_id' => $b2inVersion->id,
'item_type' => 'media',
'content' => ['category' => 'immobilien', 'media_type' => 'image', 'media_url' => 'test.jpg', 'headline' => 'H', 'subline' => 'S', 'duration_seconds' => 5],
]);
$display = Display::factory()->create();
$display->versions()->attach([
$videoVersion->id => ['sort_order' => 0],
$b2inVersion->id => ['sort_order' => 1],
]);
$response = $this->getJson("/api/display/{$display->id}/config");
$response->assertSuccessful();
$response->assertJsonCount(2, 'playlist');
$response->assertJsonPath('playlist.0.type', 'video-display');
$response->assertJsonPath('playlist.1.type', 'b2in');
});
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]);
$response = $this->getJson("/api/display/{$display->id}/config");
$response->assertNotFound();
});
test('returns 404 for display without versions', function () {
$display = Display::factory()->create();
$response = $this->getJson("/api/display/{$display->id}/config");
$response->assertNotFound();
});
test('check endpoint returns only updated_at', function () {
$version = DisplayVersion::factory()->create();
$display = Display::factory()->create();
$display->versions()->attach($version->id, ['sort_order' => 0]);
$response = $this->getJson("/api/display/{$display->id}/check");
$response->assertSuccessful();
$response->assertJsonStructure(['updated_at']);
});
test('existing display config api still works', function () {
$response = $this->getJson('/api/display/config');
$response->assertSuccessful();
$response->assertJsonStructure(['videoPlaylist', 'footerContent']);
});

View file

@ -0,0 +1,315 @@
<?php
use App\Enums\DisplayVersionType;
use App\Livewire\Admin\Cms\DisplayVersionEditor;
use App\Livewire\Admin\Cms\DisplayVersionList;
use App\Models\DisplayVersion;
use App\Models\DisplayVersionItem;
use App\Models\User;
use Livewire\Livewire;
beforeEach(function () {
$portalDomain = config('domains.domain_portal');
Livewire::withoutLazyLoading();
url()->forceRootUrl('https://'.$portalDomain);
});
// ========================================
// DisplayVersionList
// ========================================
test('display version list requires authentication', function () {
$response = $this->get(route('admin.cms.display-versions'));
$response->assertRedirect('/login');
});
test('display version list renders for authenticated users', function () {
$user = User::factory()->create();
$this->actingAs($user);
$response = $this->get(route('admin.cms.display-versions'));
$response->assertSuccessful();
$response->assertSeeLivewire(DisplayVersionList::class);
});
test('can create a display version', function () {
$user = User::factory()->create();
Livewire::actingAs($user)
->test(DisplayVersionList::class)
->set('newName', 'Test Version')
->set('newType', 'b2in')
->call('createVersion');
expect(DisplayVersion::where('name', 'Test Version')->exists())->toBeTrue();
$version = DisplayVersion::where('name', 'Test Version')->first();
expect($version->type)->toBe(DisplayVersionType::B2in);
expect($version->settings)->toBeArray();
expect($version->settings)->toHaveKey('theme');
});
test('create version validates required fields', function () {
$user = User::factory()->create();
Livewire::actingAs($user)
->test(DisplayVersionList::class)
->set('newName', '')
->set('newType', '')
->call('createVersion')
->assertHasErrors(['newName', 'newType']);
});
test('can delete a display version', function () {
$user = User::factory()->create();
$version = DisplayVersion::factory()->create();
Livewire::actingAs($user)
->test(DisplayVersionList::class)
->call('deleteVersion', $version->id);
expect(DisplayVersion::find($version->id))->toBeNull();
});
test('can toggle display version active status', function () {
$user = User::factory()->create();
$version = DisplayVersion::factory()->create(['is_active' => true]);
Livewire::actingAs($user)
->test(DisplayVersionList::class)
->call('toggleActive', $version->id);
expect($version->fresh()->is_active)->toBeFalse();
});
// ========================================
// DisplayVersionEditor
// ========================================
test('display version editor renders with correct version data', function () {
$user = User::factory()->create();
$version = DisplayVersion::factory()->create(['name' => 'My Version']);
$this->actingAs($user);
$response = $this->get(route('admin.cms.display-version-edit', $version));
$response->assertSuccessful();
$response->assertSeeLivewire(DisplayVersionEditor::class);
});
test('can add a video item to video-display version', function () {
$user = User::factory()->create();
$version = DisplayVersion::factory()->create(['type' => 'video-display']);
Livewire::actingAs($user)
->test(DisplayVersionEditor::class, ['displayVersion' => $version])
->call('openItemModal', null, 'video')
->set('videoFilename', 'test.mp4')
->set('videoTitle', 'Test Video')
->set('videoPosition', 50)
->call('saveItem');
$item = DisplayVersionItem::where('display_version_id', $version->id)->first();
expect($item)->not->toBeNull();
expect($item->item_type)->toBe('video');
expect($item->content['filename'])->toBe('test.mp4');
expect($item->content['position'])->toBe(50);
});
test('can add a media item to b2in version', function () {
$user = User::factory()->create();
$version = DisplayVersion::factory()->create(['type' => 'b2in']);
Livewire::actingAs($user)
->test(DisplayVersionEditor::class, ['displayVersion' => $version])
->call('openItemModal', null, 'media')
->set('mediaType', 'video')
->set('mediaCategory', 'immobilien')
->set('mediaUrl', '../assets/test.mp4')
->set('mediaHeadline', 'Test Headline')
->set('mediaSubline', 'Test Subline')
->call('saveItem');
$item = DisplayVersionItem::where('display_version_id', $version->id)->first();
expect($item)->not->toBeNull();
expect($item->item_type)->toBe('media');
expect($item->content['media_type'])->toBe('video');
expect($item->content['headline'])->toBe('Test Headline');
});
test('can edit an existing item', 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],
]);
Livewire::actingAs($user)
->test(DisplayVersionEditor::class, ['displayVersion' => $version])
->call('openItemModal', $item->id)
->set('videoFilename', 'new.mp4')
->set('videoTitle', 'New Title')
->call('saveItem');
$item->refresh();
expect($item->content['filename'])->toBe('new.mp4');
expect($item->content['title'])->toBe('New Title');
});
test('can delete an item', function () {
$user = User::factory()->create();
$version = DisplayVersion::factory()->create();
$item = DisplayVersionItem::factory()->create(['display_version_id' => $version->id]);
Livewire::actingAs($user)
->test(DisplayVersionEditor::class, ['displayVersion' => $version])
->call('deleteItem', $item->id);
expect(DisplayVersionItem::find($item->id))->toBeNull();
});
test('can toggle item active status', function () {
$user = User::factory()->create();
$version = DisplayVersion::factory()->create();
$item = DisplayVersionItem::factory()->create([
'display_version_id' => $version->id,
'is_active' => true,
]);
Livewire::actingAs($user)
->test(DisplayVersionEditor::class, ['displayVersion' => $version])
->call('toggleItemStatus', $item->id);
expect($item->fresh()->is_active)->toBeFalse();
});
test('can reorder items', function () {
$user = User::factory()->create();
$version = DisplayVersion::factory()->create();
$item1 = DisplayVersionItem::factory()->create([
'display_version_id' => $version->id,
'item_type' => 'video',
'sort_order' => 0,
]);
$item2 = DisplayVersionItem::factory()->create([
'display_version_id' => $version->id,
'item_type' => 'video',
'sort_order' => 1,
]);
Livewire::actingAs($user)
->test(DisplayVersionEditor::class, ['displayVersion' => $version])
->call('moveItem', $item2->id, 'up');
expect($item1->fresh()->sort_order)->toBe(1);
expect($item2->fresh()->sort_order)->toBe(0);
});
test('can add a slide item to offers version', function () {
$user = User::factory()->create();
$version = DisplayVersion::factory()->create(['type' => 'offers']);
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('slideBadge', 'Einzelstück')
->set('slideEyebrow', 'Hersteller: Sudbrock')
->set('slideTitle', 'GOYA Sideboard')
->set('slidePrice', '489 €')
->set('slideOriginalPrice', 'statt 4.744 €')
->set('slideQrUrl', 'https://cabinet-bielefeld.de')
->set('slideQrTitle', 'Reservieren')
->set('slideContact', '0521 98620100')
->call('saveItem');
$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['title'])->toBe('GOYA Sideboard');
expect($item->content['price'])->toBe('489 €');
expect($item->content['image_url'])->toBe('../assets/goya1.jpg');
expect($item->content['qr_url'])->toBe('https://cabinet-bielefeld.de');
});
test('can add a slide with bullets to offers version', function () {
$user = User::factory()->create();
$version = DisplayVersion::factory()->create(['type' => 'offers']);
Livewire::actingAs($user)
->test(DisplayVersionEditor::class, ['displayVersion' => $version])
->call('openItemModal', null, 'slide')
->set('slideType', 'product-details')
->set('slideTitle', 'GOYA Sideboard')
->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');
});
test('can edit a slide item with new fields', 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',
'duration' => 10000,
'image_url' => '../assets/old.jpg',
'badge_text' => 'Old Badge',
'eyebrow' => 'Old',
'title' => 'Old Title',
'subline' => '',
'price' => '100 €',
'original_price' => '',
'tag_text' => '',
'bullets' => [],
'disclaimer' => '',
'qr_url' => 'https://example.com',
'qr_title' => 'QR',
'contact' => '',
'show_brand_text' => false,
'brand_tagline' => '',
],
]);
Livewire::actingAs($user)
->test(DisplayVersionEditor::class, ['displayVersion' => $version])
->call('openItemModal', $item->id)
->set('slideTitle', 'New Title')
->set('slidePrice', '299 €')
->call('saveItem');
$item->refresh();
expect($item->content['title'])->toBe('New Title');
expect($item->content['price'])->toBe('299 €');
expect($item->content['image_url'])->toBe('../assets/old.jpg');
});
test('can save version settings', function () {
$user = User::factory()->create();
$version = DisplayVersion::factory()->create([
'type' => 'b2in',
'settings' => ['theme' => 'dark'],
]);
Livewire::actingAs($user)
->test(DisplayVersionEditor::class, ['displayVersion' => $version])
->call('openSettingsModal')
->set('settings.theme', 'light')
->call('saveSettings');
expect($version->fresh()->settings['theme'])->toBe('light');
});

View file

@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
it('immobilien index page loads successfully', function () {
$this->get('/immobilien')
->assertSuccessful()
->assertSee('Creek Views 4');
});
it('immobilien page shows new hero section', function () {
$this->get('/immobilien')
->assertSuccessful()
->assertSee('globale')
->assertSee('Dynamik');
});
it('immobilien page shows warum dubai section', function () {
$this->get('/immobilien')
->assertSuccessful()
->assertSee('Investment in Dubai')
->assertSee('0 % Steuern')
->assertSee('Golden Visa');
});
it('immobilien page shows kaufprozess section', function () {
$this->get('/immobilien')
->assertSuccessful()
->assertSee('Kaufprozess')
->assertSee('Reservierung')
->assertSee('Finaler Kaufvertrag');
});
it('immobilien page shows bruecke section', function () {
$this->get('/immobilien')
->assertSuccessful()
->assertSee('Meine Aufgabe')
->assertSee('Ihr B2in-Vorteil');
});
it('immobilien page shows mindset check section', function () {
$this->get('/immobilien')
->assertSuccessful()
->assertSee('Investor')
->assertSee('Der Schritt ist kleiner');
});
it('immobilien show page loads for valid slug', function () {
$this->get('/immobilien/azizi-creek-views-4')
->assertSuccessful()
->assertSee('Creek Views 4')
->assertSee('Al Jaddaf, Dubai')
->assertSee('Galerie')
->assertSee('Starkes Investment');
});
it('immobilien show page returns 404 for invalid slug', function () {
$this->get('/immobilien/nonexistent-project')
->assertNotFound();
});
it('ecosystem redirects to netzwerk via partner', function () {
$this->get('/ecosystem')
->assertRedirect('/partner');
});

View file

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
it('interior redirects to netzwerk', function () {
$this->get('/interior')
->assertRedirect('/netzwerk');
});
it('partner redirects to netzwerk', function () {
$this->get('/partner')
->assertRedirect('/netzwerk');
});
it('netzwerk page loads successfully', function () {
$this->get('/netzwerk')
->assertSuccessful()
->assertSee('B2in');
});
it('netzwerk page shows teaser cards', function () {
$this->get('/netzwerk')
->assertSuccessful()
->assertSee('Einrichtungsnetzwerk')
->assertSee('In Entwicklung');
});
it('netzwerk page shows immobilien cross-link', function () {
$this->get('/netzwerk')
->assertSuccessful()
->assertSee('Immobilien-Projekten');
});

View file

@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
// ========================================
// Live-Seiten: HTTP Status
// ========================================
it('homepage loads', fn () => $this->get('/')->assertSuccessful());
it('immobilien page loads', fn () => $this->get('/immobilien')->assertSuccessful());
it('immobilien expose loads', fn () => $this->get('/immobilien/azizi-creek-views-4')->assertSuccessful());
it('netzwerk page loads', fn () => $this->get('/netzwerk')->assertSuccessful());
it('magazin list loads', fn () => $this->get('/magazin')->assertSuccessful());
it('magazin article 1 loads', fn () => $this->get('/magazin/1')->assertSuccessful());
it('magazin article 2 loads', fn () => $this->get('/magazin/2')->assertSuccessful());
it('magazin article 3 loads', fn () => $this->get('/magazin/3')->assertSuccessful());
it('magazin article 4 loads', fn () => $this->get('/magazin/4')->assertSuccessful());
it('magazin article 5 loads', fn () => $this->get('/magazin/5')->assertSuccessful());
it('about page loads', fn () => $this->get('/about')->assertSuccessful());
it('contact page loads', fn () => $this->get('/contact')->assertSuccessful());
it('faq page loads', fn () => $this->get('/faq')->assertSuccessful());
// ========================================
// Redirects
// ========================================
it('interior redirects to netzwerk', fn () => $this->get('/interior')->assertRedirect('/netzwerk'));
it('partner redirects to netzwerk', fn () => $this->get('/partner')->assertRedirect('/netzwerk'));
it('ecosystem redirects to partner', fn () => $this->get('/ecosystem')->assertRedirect('/partner'));
// ========================================
// Dev/Archiv-Seiten
// ========================================
it('dev sitemap loads', fn () => $this->get('/dev/sitemap')->assertSuccessful());
it('dev immobilien v1 loads', fn () => $this->get('/dev/immobilien-v1')->assertSuccessful());
it('dev interior v1 loads', fn () => $this->get('/dev/interior-v1')->assertSuccessful());
it('dev partner v1 loads', fn () => $this->get('/dev/partner-v1')->assertSuccessful());
// ========================================
// 404 für ungültige Routen
// ========================================
it('invalid immobilien slug returns 404', fn () => $this->get('/immobilien/nonexistent')->assertNotFound());
// ========================================
// Announcement Bar auf allen Seiten
// ========================================
it('announcement bar visible on homepage', fn () => $this->get('/')->assertSee('Azizi Creek Views 4'));
it('announcement bar visible on about', fn () => $this->get('/about')->assertSee('Azizi Creek Views 4'));
// ========================================
// Kritische Inhalte vorhanden
// ========================================
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');
});
it('netzwerk has teaser cards', fn () => $this->get('/netzwerk')->assertSee('Einrichtungsnetzwerk')->assertSee('In Entwicklung'));
it('magazin has all five articles', function () {
$this->get('/magazin')
->assertSee('Escrow-System')
->assertSee('Al Jaddaf')
->assertSee('Turnkey')
->assertSee('Supply-Chain')
->assertSee('Local for Local');
});

View file

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
it('magazin list page loads successfully', function () {
$this->get('/magazin')
->assertSuccessful()
->assertSee('Magazin');
});
it('magazin list shows all five articles', function () {
$this->get('/magazin')
->assertSuccessful()
->assertSee('Escrow-System')
->assertSee('Al Jaddaf')
->assertSee('Turnkey-Investments')
->assertSee('Supply-Chain-Management')
->assertSee('Local for Local');
});
it('magazin detail page loads for article 1', function () {
$this->get('/magazin/1')
->assertSuccessful()
->assertSee('Escrow-System')
->assertSee('Dubai Land Department');
});
it('magazin detail page loads for article 5', function () {
$this->get('/magazin/5')
->assertSuccessful()
->assertSee('Local for Local')
->assertSee('Support your Locals');
});

View file

@ -0,0 +1,208 @@
<?php
use App\Models\CabinetTabletSetting;
use Illuminate\Support\Carbon;
test('current() creates a singleton settings row', function () {
expect(CabinetTabletSetting::count())->toBe(0);
$settings = CabinetTabletSetting::current();
expect($settings)->toBeInstanceOf(CabinetTabletSetting::class);
expect($settings->id)->toBe(1);
expect(CabinetTabletSetting::count())->toBe(1);
});
test('current() returns the same row on subsequent calls', function () {
$first = CabinetTabletSetting::current();
$second = CabinetTabletSetting::current();
expect($first->id)->toBe($second->id);
expect(CabinetTabletSetting::count())->toBe(1);
});
test('getHoursArray returns all seven days as display strings', function () {
$settings = CabinetTabletSetting::factory()->create();
$hours = $settings->getHoursArray();
expect($hours)->toHaveKeys(['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']);
expect($hours['monday'])->toBe('10:00 18:00');
expect($hours['sunday'])->toBe('Geschlossen');
});
test('getHoursArray shows Geschlossen for days with null times', function () {
$settings = CabinetTabletSetting::factory()->create([
'hours_wednesday_open' => null,
'hours_wednesday_close' => null,
]);
expect($settings->getHoursArray()['wednesday'])->toBe('Geschlossen');
});
test('clearOverrides nulls both override fields', function () {
$settings = CabinetTabletSetting::factory()->create([
'override_open_today' => '09:00',
'override_close_today' => '20:00',
]);
$settings->clearOverrides();
$settings->refresh();
expect($settings->override_open_today)->toBeNull();
expect($settings->override_close_today)->toBeNull();
});
test('next_appointment_date is cast to date', function () {
$settings = CabinetTabletSetting::factory()->create([
'next_appointment_date' => '2026-03-15',
]);
expect($settings->next_appointment_date)->toBeInstanceOf(Carbon::class);
expect($settings->next_appointment_date->format('Y-m-d'))->toBe('2026-03-15');
});
test('computeStatus returns open when within opening hours', function () {
$settings = CabinetTabletSetting::factory()->create([
'store_status' => 'auto',
'hours_thursday_open' => '10:00',
'hours_thursday_close' => '18:00',
]);
// Thursday 12:00 Berlin time
Carbon::setTestNow(Carbon::create(2026, 3, 5, 12, 0, 0, 'Europe/Berlin'));
$result = $settings->computeStatus();
expect($result['status'])->toBe('open');
expect($result['today_close'])->toBe('18:00');
expect($result['next_open'])->toBeNull();
Carbon::setTestNow();
});
test('computeStatus returns closed before opening time', function () {
$settings = CabinetTabletSetting::factory()->create([
'store_status' => 'auto',
'hours_thursday_open' => '10:00',
'hours_thursday_close' => '18:00',
]);
// Thursday 08:00 Berlin time
Carbon::setTestNow(Carbon::create(2026, 3, 5, 8, 0, 0, 'Europe/Berlin'));
$result = $settings->computeStatus();
expect($result['status'])->toBe('closed');
expect($result['next_open']['label'])->toBe('Heute');
expect($result['next_open']['time'])->toBe('10:00');
Carbon::setTestNow();
});
test('computeStatus returns closed after closing time with next day', function () {
$settings = CabinetTabletSetting::factory()->create([
'store_status' => 'auto',
'hours_thursday_open' => '10:00',
'hours_thursday_close' => '18:00',
'hours_friday_open' => '10:00',
'hours_friday_close' => '18:00',
]);
// Thursday 20:00 Berlin time
Carbon::setTestNow(Carbon::create(2026, 3, 5, 20, 0, 0, 'Europe/Berlin'));
$result = $settings->computeStatus();
expect($result['status'])->toBe('closed');
expect($result['next_open']['label'])->toBe('Morgen');
expect($result['next_open']['time'])->toBe('10:00');
Carbon::setTestNow();
});
test('computeStatus returns notice when store_status is notice', function () {
$settings = CabinetTabletSetting::factory()->create([
'store_status' => 'notice',
'notice_headline' => 'Wir haben Urlaub',
]);
$result = $settings->computeStatus();
expect($result['status'])->toBe('notice');
expect($result['next_open'])->toBeNull();
});
test('computeStatus returns warning when store_status is warning', function () {
$settings = CabinetTabletSetting::factory()->create([
'store_status' => 'warning',
'notice_headline' => 'Notfall',
]);
$result = $settings->computeStatus();
expect($result['status'])->toBe('warning');
expect($result['next_open'])->toBeNull();
});
test('computeStatus returns closed when store_status is manually closed', function () {
$settings = CabinetTabletSetting::factory()->create([
'store_status' => 'closed',
'hours_friday_open' => '10:00',
'hours_friday_close' => '18:00',
]);
// Thursday 12:00 (normally would be open, but manually closed)
Carbon::setTestNow(Carbon::create(2026, 3, 5, 12, 0, 0, 'Europe/Berlin'));
$result = $settings->computeStatus();
expect($result['status'])->toBe('closed');
expect($result['next_open']['label'])->toBe('Morgen');
Carbon::setTestNow();
});
test('computeStatus uses override times when set', function () {
$settings = CabinetTabletSetting::factory()->create([
'store_status' => 'auto',
'hours_thursday_open' => '10:00',
'hours_thursday_close' => '18:00',
'override_open_today' => '12:00',
'override_close_today' => '16:00',
]);
// Thursday 11:00 after regular open, but before override open
Carbon::setTestNow(Carbon::create(2026, 3, 5, 11, 0, 0, 'Europe/Berlin'));
$result = $settings->computeStatus();
expect($result['status'])->toBe('closed');
expect($result['next_open']['time'])->toBe('12:00');
Carbon::setTestNow();
});
test('computeStatus skips closed days when finding next open time', function () {
$settings = CabinetTabletSetting::factory()->create([
'store_status' => 'auto',
'hours_thursday_open' => '10:00',
'hours_thursday_close' => '18:00',
'hours_friday_open' => null,
'hours_friday_close' => null,
'hours_saturday_open' => '10:00',
'hours_saturday_close' => '14:00',
'hours_sunday_open' => null,
'hours_sunday_close' => null,
]);
// Thursday 20:00 after closing, Friday is closed
Carbon::setTestNow(Carbon::create(2026, 3, 5, 20, 0, 0, 'Europe/Berlin'));
$result = $settings->computeStatus();
expect($result['next_open']['label'])->toBe('Samstag');
expect($result['next_open']['time'])->toBe('10:00');
Carbon::setTestNow();
});

View file

@ -13,8 +13,8 @@ beforeEach(function () {
Role::create(['name' => 'Admin']);
Role::create(['name' => 'Retailer']);
Permission::create(['name' => 'curate products']);
Role::findByName('Admin')->givePermissionTo('curate products');
$permission = Permission::firstOrCreate(['name' => 'curate products']);
Role::findByName('Admin')->givePermissionTo($permission);
});
test('admin can view any partner', function () {

View file

@ -0,0 +1,316 @@
<?php
declare(strict_types=1);
use App\Models\Media;
use App\Models\Partner;
use App\Models\User;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Livewire\Volt\Volt;
use Spatie\Permission\Models\Role;
beforeEach(function () {
app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions();
Role::firstOrCreate(['name' => 'Retailer']);
Role::firstOrCreate(['name' => 'Manufacturer']);
Storage::fake('public');
});
// ── Zugriff ──────────────────────────────────────────────────────────────────
test('retailer can access my-data page', function () {
$partner = Partner::factory()->setupCompleted()->create();
$user = User::factory()->create(['partner_id' => $partner->id]);
$user->assignRole('Retailer');
$this->actingAs($user)
->get(route('partner.my-data'))
->assertSuccessful();
});
test('manufacturer can access my-data page', function () {
$partner = Partner::factory()->setupCompleted()->create(['type' => 'manufacturer']);
$user = User::factory()->create(['partner_id' => $partner->id]);
$user->assignRole('Manufacturer');
$this->actingAs($user)
->get(route('partner.my-data'))
->assertSuccessful();
});
test('user without partner is redirected from my-data', function () {
Role::firstOrCreate(['name' => 'Admin']);
$user = User::factory()->create(['partner_id' => null]);
$user->assignRole('Admin');
$this->actingAs($user)
->get(route('partner.my-data'))
->assertRedirect(route('dashboard'));
});
// ── Händler: Story + Profil-Felder ───────────────────────────────────────────
test('retailer can save story text and founded year', function () {
$partner = Partner::factory()->setupCompleted()->create(['type' => 'retailer']);
$user = User::factory()->create(['partner_id' => $partner->id]);
$user->assignRole('Retailer');
Volt::actingAs($user)->test('partner.my-data')
->set('companyName', 'Möbelhaus Mustermann')
->set('salutation', 'Herr')
->set('firstName', 'Max')
->set('lastName', 'Mustermann')
->set('street', 'Musterstraße')
->set('houseNumber', '1')
->set('zip', '12345')
->set('city', 'Musterstadt')
->set('country', 'Deutschland')
->set('deliveryRadius', 50)
->set('assemblyRadius', 30)
->set('storyText', 'Seit 1992 sind wir Ihr Einrichtungshaus im Herzen der Stadt.')
->set('foundedYear', 1992)
->set('specialtiesInput', 'Polstermöbel, Outdoor, Küchen')
->call('saveData')
->assertHasNoErrors();
$partner->refresh();
expect($partner->story_text)->toBe('Seit 1992 sind wir Ihr Einrichtungshaus im Herzen der Stadt.');
expect($partner->founded_year)->toBe(1992);
expect($partner->specialties)->toContain('Polstermöbel');
expect($partner->specialties)->toContain('Outdoor');
expect($partner->specialties)->toContain('Küchen');
});
test('retailer can save opening hours', function () {
$partner = Partner::factory()->setupCompleted()->create(['type' => 'retailer']);
$user = User::factory()->create(['partner_id' => $partner->id]);
$user->assignRole('Retailer');
Volt::actingAs($user)->test('partner.my-data')
->set('companyName', $partner->company_name)
->set('salutation', 'Herr')
->set('firstName', 'Max')
->set('lastName', 'Mustermann')
->set('street', 'Str.')
->set('houseNumber', '1')
->set('zip', '12345')
->set('city', 'Stadt')
->set('country', 'Deutschland')
->set('deliveryRadius', 50)
->set('assemblyRadius', 30)
->set('openingHours.monday.open', '08:00')
->set('openingHours.monday.close', '20:00')
->set('openingHours.sunday.closed', true)
->call('saveData')
->assertHasNoErrors();
$partner->refresh();
expect($partner->opening_hours['monday']['open'])->toBe('08:00');
expect($partner->opening_hours['monday']['close'])->toBe('20:00');
expect($partner->opening_hours['sunday']['closed'])->toBeTrue();
});
// ── Händler: Validierung ──────────────────────────────────────────────────────
test('retailer story text cannot exceed 2000 characters', function () {
$partner = Partner::factory()->setupCompleted()->create(['type' => 'retailer']);
$user = User::factory()->create(['partner_id' => $partner->id]);
$user->assignRole('Retailer');
Volt::actingAs($user)->test('partner.my-data')
->set('storyText', str_repeat('a', 2001))
->call('saveData')
->assertHasErrors(['storyText' => 'max']);
});
test('retailer founded year must be a valid year', function () {
$partner = Partner::factory()->setupCompleted()->create(['type' => 'retailer']);
$user = User::factory()->create(['partner_id' => $partner->id]);
$user->assignRole('Retailer');
Volt::actingAs($user)->test('partner.my-data')
->set('foundedYear', 1700)
->call('saveData')
->assertHasErrors(['foundedYear']);
});
test('retailer founded year cannot be in the future', function () {
$partner = Partner::factory()->setupCompleted()->create(['type' => 'retailer']);
$user = User::factory()->create(['partner_id' => $partner->id]);
$user->assignRole('Retailer');
Volt::actingAs($user)->test('partner.my-data')
->set('foundedYear', now()->year + 1)
->call('saveData')
->assertHasErrors(['foundedYear']);
});
// ── Händler: Foto-Upload ─────────────────────────────────────────────────────
test('retailer can upload team photos', function () {
$partner = Partner::factory()->setupCompleted()->create(['type' => 'retailer']);
$user = User::factory()->create(['partner_id' => $partner->id]);
$user->assignRole('Retailer');
$photo = UploadedFile::fake()->image('team.jpg', 400, 400);
Volt::actingAs($user)->test('partner.my-data')
->set('companyName', $partner->company_name)
->set('salutation', 'Herr')
->set('firstName', 'Max')
->set('lastName', 'Muster')
->set('street', 'Str.')
->set('houseNumber', '1')
->set('zip', '12345')
->set('city', 'Stadt')
->set('country', 'Deutschland')
->set('deliveryRadius', 50)
->set('assemblyRadius', 30)
->set('newTeamPhotos', [$photo])
->call('saveData')
->assertHasNoErrors();
expect(
Media::where('model_type', Partner::class)
->where('model_id', $partner->id)
->where('type', 'team_photo')
->exists()
)->toBeTrue();
});
test('retailer can upload showroom photos', function () {
$partner = Partner::factory()->setupCompleted()->create(['type' => 'retailer']);
$user = User::factory()->create(['partner_id' => $partner->id]);
$user->assignRole('Retailer');
$photo = UploadedFile::fake()->image('showroom.jpg', 800, 600);
Volt::actingAs($user)->test('partner.my-data')
->set('companyName', $partner->company_name)
->set('salutation', 'Herr')
->set('firstName', 'Max')
->set('lastName', 'Muster')
->set('street', 'Str.')
->set('houseNumber', '1')
->set('zip', '12345')
->set('city', 'Stadt')
->set('country', 'Deutschland')
->set('deliveryRadius', 50)
->set('assemblyRadius', 30)
->set('newShowroomPhotos', [$photo])
->call('saveData')
->assertHasNoErrors();
expect(
Media::where('model_type', Partner::class)
->where('model_id', $partner->id)
->where('type', 'showroom')
->exists()
)->toBeTrue();
});
test('retailer can delete a team photo', function () {
$partner = Partner::factory()->setupCompleted()->create(['type' => 'retailer']);
$user = User::factory()->create(['partner_id' => $partner->id]);
$user->assignRole('Retailer');
Storage::disk('public')->put('partners/'.$partner->id.'/team_photo/test.jpg', 'fake');
$media = $partner->media()->create([
'file_path' => 'partners/'.$partner->id.'/team_photo/test.jpg',
'type' => 'team_photo',
'alt_text' => 'Team',
'order_column' => 1,
]);
Volt::actingAs($user)->test('partner.my-data')
->call('removeExistingPhoto', $media->id, 'team_photo')
->assertHasNoErrors();
expect(Media::find($media->id))->toBeNull();
});
// ── Hersteller: Story + Marke ─────────────────────────────────────────────────
test('manufacturer can save story text and specialties', function () {
$partner = Partner::factory()->setupCompleted()->create(['type' => 'manufacturer']);
$user = User::factory()->create(['partner_id' => $partner->id]);
$user->assignRole('Manufacturer');
Volt::actingAs($user)->test('partner.my-data')
->set('companyName', 'Muster Möbelwerke GmbH')
->set('salutation', 'Herr')
->set('firstName', 'Hans')
->set('lastName', 'Muster')
->set('street', 'Industriestr.')
->set('houseNumber', '5')
->set('zip', '33602')
->set('city', 'Bielefeld')
->set('country', 'Deutschland')
->set('brandName', 'Muster Collection')
->set('storyText', 'Seit 1975 produzieren wir hochwertige Möbel.')
->set('foundedYear', 1975)
->set('specialtiesInput', 'Polstermöbel, Massivholz')
->call('saveData')
->assertHasNoErrors();
$partner->refresh();
expect($partner->story_text)->toBe('Seit 1975 produzieren wir hochwertige Möbel.');
expect($partner->founded_year)->toBe(1975);
expect($partner->specialties)->toContain('Polstermöbel');
});
test('manufacturer can upload brand images', function () {
$partner = Partner::factory()->setupCompleted()->create(['type' => 'manufacturer']);
$user = User::factory()->create(['partner_id' => $partner->id]);
$user->assignRole('Manufacturer');
$image = UploadedFile::fake()->image('brand.jpg', 800, 600);
Volt::actingAs($user)->test('partner.my-data')
->set('companyName', $partner->company_name)
->set('salutation', 'Herr')
->set('firstName', 'Hans')
->set('lastName', 'Muster')
->set('street', 'Str.')
->set('houseNumber', '1')
->set('zip', '12345')
->set('city', 'Stadt')
->set('country', 'Deutschland')
->set('brandName', 'Muster Marke')
->set('newBrandImages', [$image])
->call('saveData')
->assertHasNoErrors();
expect(
Media::where('model_type', Partner::class)
->where('model_id', $partner->id)
->where('type', 'brand_image')
->exists()
)->toBeTrue();
});
// ── Öffentliches Profil ───────────────────────────────────────────────────────
test('public profile shows story text when set', function () {
$partner = Partner::factory()->setupCompleted()->create([
'story_text' => 'Unsere Geschichte seit 1985.',
]);
$user = User::factory()->create(['partner_id' => $partner->id]);
$user->assignRole('Retailer');
Volt::actingAs($user)->test('partner.profile', ['partnerId' => $partner->id])
->assertSee('Unsere Geschichte seit 1985.');
});
test('public profile shows specialties when set', function () {
$partner = Partner::factory()->setupCompleted()->create([
'specialties' => ['Sofas', 'Esstische'],
]);
$user = User::factory()->create(['partner_id' => $partner->id]);
$user->assignRole('Retailer');
Volt::actingAs($user)->test('partner.profile', ['partnerId' => $partner->id])
->assertSee('Sofas')
->assertSee('Esstische');
});

View file

@ -16,8 +16,8 @@ beforeEach(function () {
app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions();
Role::create(['name' => 'Admin']);
Role::create(['name' => 'Retailer']);
Permission::create(['name' => 'curate products']);
Role::findByName('Admin')->givePermissionTo('curate products');
$permission = Permission::firstOrCreate(['name' => 'curate products']);
Role::findByName('Admin')->givePermissionTo($permission);
});
function makeAdminUser(): User

View file

@ -16,8 +16,8 @@ beforeEach(function () {
Role::create(['name' => 'Retailer']);
Role::create(['name' => 'Manufacturer']);
Role::create(['name' => 'Customer']);
Permission::create(['name' => 'curate products']);
Role::findByName('Admin')->givePermissionTo('curate products');
$permission = Permission::firstOrCreate(['name' => 'curate products']);
Role::findByName('Admin')->givePermissionTo($permission);
});
test('admin can view any product', function () {

View file

@ -0,0 +1,27 @@
<?php
use App\Models\CabinetTabletSetting;
test('cabinet:reset-overrides command clears override times', function () {
CabinetTabletSetting::factory()->create([
'override_open_today' => '09:00',
'override_close_today' => '20:00',
]);
$this->artisan('cabinet:reset-overrides')
->assertSuccessful();
$settings = CabinetTabletSetting::current();
expect($settings->override_open_today)->toBeNull();
expect($settings->override_close_today)->toBeNull();
});
test('cabinet:reset-overrides works when no overrides are set', function () {
CabinetTabletSetting::factory()->create([
'override_open_today' => null,
'override_close_today' => null,
]);
$this->artisan('cabinet:reset-overrides')
->assertSuccessful();
});

View file

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
it('dev sitemap page loads successfully', function () {
$this->get('/dev/sitemap')
->assertSuccessful()
->assertSee('Dev Sitemap')
->assertSee('Live-Seiten')
->assertSee('Archiv');
});
it('dev immobilien v1 archive page loads successfully', function () {
$this->get('/dev/immobilien-v1')
->assertSuccessful();
});
it('dev interior v1 archive page loads successfully', function () {
$this->get('/dev/interior-v1')
->assertSuccessful();
});
it('dev partner v1 archive page loads successfully', function () {
$this->get('/dev/partner-v1')
->assertSuccessful();
});

View file

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
test('theme_image_url returns empty string for null or blank', function () {
expect(theme_image_url(null))->toBe('')
->and(theme_image_url(''))->toBe('')
->and(theme_image_url(' '))->toBe('');
});
test('theme_image_url returns legacy path for slash-separated theme paths', function () {
$url = theme_image_url('b2in/example.jpg');
expect($url)->toContain('img/assets/b2in/example.jpg');
});
test('theme_image_url passes through absolute http urls', function () {
$url = theme_image_url('https://example.org/x.webp');
expect($url)->toBe('https://example.org/x.webp');
});

View file

@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
use App\Services\CmsFluxEditorHtmlTransformer;
test('toEditor wandelt span text-secondary in mark um', function () {
$html = '<p>Hallo <span class="text-secondary">Akzent</span>!</p>';
$out = CmsFluxEditorHtmlTransformer::toEditor($html);
expect($out)->toContain('<mark>');
expect($out)->toContain('Akzent');
expect($out)->not->toContain('text-secondary');
});
test('fromEditor wandelt mark in span text-secondary um', function () {
$html = '<p>Hallo <mark>Akzent</mark>!</p>';
$out = CmsFluxEditorHtmlTransformer::fromEditor($html);
expect($out)->toContain('class="text-secondary"');
expect($out)->toContain('Akzent');
expect($out)->not->toContain('<mark>');
});
test('Roundtrip erhält verschachtelte Formatierung', function () {
$original = '<p><span class="text-secondary"><strong>Fett</strong> und normal</span></p>';
$editor = CmsFluxEditorHtmlTransformer::toEditor($original);
$saved = CmsFluxEditorHtmlTransformer::fromEditor($editor);
expect($saved)->toContain('text-secondary');
expect($saved)->toContain('<strong>Fett</strong>');
});
test('JSON-Rich-Text-Felder werden für den Editor umgewandelt', function () {
$items = [
[
'title' => 'x',
'description' => '<p><span class="text-secondary">Hinweis</span></p>',
],
];
$out = CmsFluxEditorHtmlTransformer::toEditorJsonItems($items, false);
expect($out[0]['description'])->toContain('<mark>');
});
test('JSON-Rich-Text-Felder werden beim Speichern zurückgewandelt', function () {
$items = [
[
'description' => '<p><mark>Hinweis</mark></p>',
],
];
$out = CmsFluxEditorHtmlTransformer::fromEditorJsonItems($items, false);
expect($out[0]['description'])->toContain('text-secondary');
});