10-04-2026
This commit is contained in:
parent
4d6b4930b2
commit
4bb89aad8c
836 changed files with 52961 additions and 5950 deletions
15
tests/Feature/AboutPageTest.php
Normal file
15
tests/Feature/AboutPageTest.php
Normal 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');
|
||||
});
|
||||
31
tests/Feature/Admin/Cms/MediaPickerTest.php
Normal file
31
tests/Feature/Admin/Cms/MediaPickerTest.php
Normal 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');
|
||||
});
|
||||
19
tests/Feature/AdminDocumentationPageTest.php
Normal file
19
tests/Feature/AdminDocumentationPageTest.php
Normal 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();
|
||||
});
|
||||
22
tests/Feature/AnnouncementBarTest.php
Normal file
22
tests/Feature/AnnouncementBarTest.php
Normal 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');
|
||||
});
|
||||
154
tests/Feature/CabinetInfoTabletTest.php
Normal file
154
tests/Feature/CabinetInfoTabletTest.php
Normal 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();
|
||||
});
|
||||
113
tests/Feature/CabinetQuickStatusTest.php
Normal file
113
tests/Feature/CabinetQuickStatusTest.php
Normal 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']);
|
||||
});
|
||||
100
tests/Feature/CabinetTabletApiTest.php
Normal file
100
tests/Feature/CabinetTabletApiTest.php
Normal 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);
|
||||
});
|
||||
410
tests/Feature/Cms/CmsAdminTest.php
Normal file
410
tests/Feature/Cms/CmsAdminTest.php
Normal 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();
|
||||
});
|
||||
112
tests/Feature/Cms/CmsArticleTest.php
Normal file
112
tests/Feature/Cms/CmsArticleTest.php
Normal 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);
|
||||
});
|
||||
135
tests/Feature/Cms/CmsContentSeederTest.php
Normal file
135
tests/Feature/Cms/CmsContentSeederTest.php
Normal 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();
|
||||
});
|
||||
115
tests/Feature/Cms/CmsProjectTest.php
Normal file
115
tests/Feature/Cms/CmsProjectTest.php
Normal 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);
|
||||
});
|
||||
30
tests/Feature/Cms/HeroiconOutlineListTest.php
Normal file
30
tests/Feature/Cms/HeroiconOutlineListTest.php
Normal 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();
|
||||
});
|
||||
45
tests/Feature/Cms/NetzwerkPageCmsSeederTest.php
Normal file
45
tests/Feature/Cms/NetzwerkPageCmsSeederTest.php
Normal 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');
|
||||
});
|
||||
35
tests/Feature/CmsLegalSeederTest.php
Normal file
35
tests/Feature/CmsLegalSeederTest.php
Normal 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();
|
||||
});
|
||||
145
tests/Feature/ContactFormTest.php
Normal file
145
tests/Feature/ContactFormTest.php
Normal 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);
|
||||
});
|
||||
148
tests/Feature/DisplayListTest.php
Normal file
148
tests/Feature/DisplayListTest.php
Normal 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);
|
||||
});
|
||||
202
tests/Feature/DisplayMediaTest.php
Normal file
202
tests/Feature/DisplayMediaTest.php
Normal 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();
|
||||
});
|
||||
168
tests/Feature/DisplayVersionApiTest.php
Normal file
168
tests/Feature/DisplayVersionApiTest.php
Normal 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']);
|
||||
});
|
||||
315
tests/Feature/DisplayVersionTest.php
Normal file
315
tests/Feature/DisplayVersionTest.php
Normal 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');
|
||||
});
|
||||
65
tests/Feature/ImmobilienShowTest.php
Normal file
65
tests/Feature/ImmobilienShowTest.php
Normal 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');
|
||||
});
|
||||
32
tests/Feature/InteriorPageTest.php
Normal file
32
tests/Feature/InteriorPageTest.php
Normal 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');
|
||||
});
|
||||
74
tests/Feature/LiveSiteReviewTest.php
Normal file
74
tests/Feature/LiveSiteReviewTest.php
Normal 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');
|
||||
});
|
||||
33
tests/Feature/MagazinPageTest.php
Normal file
33
tests/Feature/MagazinPageTest.php
Normal 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');
|
||||
});
|
||||
208
tests/Feature/Models/CabinetTabletSettingTest.php
Normal file
208
tests/Feature/Models/CabinetTabletSettingTest.php
Normal 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();
|
||||
});
|
||||
|
|
@ -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 () {
|
||||
|
|
|
|||
316
tests/Feature/PartnerSelfServiceProfileTest.php
Normal file
316
tests/Feature/PartnerSelfServiceProfileTest.php
Normal 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');
|
||||
});
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 () {
|
||||
|
|
|
|||
27
tests/Feature/ResetCabinetTabletOverridesTest.php
Normal file
27
tests/Feature/ResetCabinetTabletOverridesTest.php
Normal 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();
|
||||
});
|
||||
26
tests/Feature/SoftLaunchDevPagesTest.php
Normal file
26
tests/Feature/SoftLaunchDevPagesTest.php
Normal 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();
|
||||
});
|
||||
25
tests/Feature/ThemeImageUrlTest.php
Normal file
25
tests/Feature/ThemeImageUrlTest.php
Normal 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');
|
||||
});
|
||||
55
tests/Unit/CmsFluxEditorHtmlTransformerTest.php
Normal file
55
tests/Unit/CmsFluxEditorHtmlTransformerTest.php
Normal 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');
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue