b2in/tests/Feature/Cms/CmsAdminTest.php
Kevin Adametz 90aa079cf7 CMS: Standorte an der Marke, Seitenbaum mit Vorschau, Referenzen-Modul
Nachtrag 11: Herkunfts-Sektion liest Ort, Entfernung und Rolle aus der
Markenverwaltung (drei neue Felder an cms_brands, BrandOriginSeeder),
Kartenpositionen im Code, „Marken“ in der Sidebar.

Nachtrag 12: Inhalte-Editor zeigt die Seiten der Website mit ihren
Sektionen in Reihenfolge (config/cms_pages.php, CmsPageTree), Felder in
Klartext, fehlende Sektionen aus der Sprachdatei anlegbar, Vorschau der
echten Seite im iframe mit Sprung zur Sektion; Anker an allen lebenden
Sektionen nachgezogen.

Nachtrag 13: Referenzen als eigenes Modul (CmsReference, Portal-Seite,
/referenzen mit Fallback auf die Markengalerien); Immobilien-Projekte
aus Sidebar und CMS-Übersicht genommen, Route bleibt.

Dazu .gitignore für _b2in_interior_shop/ und docs/.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-15 14:58:45 +00:00

536 lines
17 KiB
PHP

<?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();
\App\Models\CmsReference::factory()->published()->count(2)->create();
\App\Models\CmsReference::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();
});
// ========================================
// Inhalte-Editor: Seitenbaum, Beschriftungen, Vorschau (Nachtrag 12)
// ========================================
test('cms content index shows the pages of the website instead of raw groups', function () {
$user = User::factory()->create();
CmsContent::create([
'group' => 'home',
'key' => 'hero',
'type' => 'json',
'value' => ['de' => ['title' => 'Test']],
]);
Volt::actingAs($user)
->test('admin.cms.content-index')
->assertSee('Startseite')
->assertSee('Aus der Region gefertigt')
->assertSee('Auf jeder Seite')
->assertSee('Rechtliches')
->assertDontSee('Nicht mehr verwendet');
});
test('cms content index selects one section and labels its fields in plain language', function () {
$user = User::factory()->create();
$hero = CmsContent::create([
'group' => 'home',
'key' => 'hero',
'type' => 'json',
'value' => ['de' => ['title' => 'Hero Title', 'button_text' => 'Los']],
]);
CmsContent::create([
'group' => 'home',
'key' => 'sponsoring',
'type' => 'json',
'value' => ['de' => ['title' => 'Sponsoring Title']],
]);
$component = Volt::actingAs($user)
->test('admin.cms.content-index')
->call('selectSection', 'home', 'hero')
->assertSet('selectedGroup', 'home')
->assertSet('selectedKey', 'hero')
->assertSee('Überschrift')
->assertSee('Button-Beschriftung')
->assertSee('Hero Title')
->assertDontSee('Sponsoring Title');
expect($component->instance()->previewUrl)->toEndWith('/?cms-preview=0#hero');
$component->call('startFieldEdit', $hero->id, 'title')
->set('editValue', 'Neu')
->call('saveEdit')
->assertSet('previewVersion', 1);
});
test('cms content index lists unregistered rows under "not used any more"', function () {
$user = User::factory()->create();
CmsContent::create([
'group' => 'netzwerk',
'key' => 'teasers',
'type' => 'json',
'value' => ['de' => ['title' => 'Alt']],
]);
Volt::actingAs($user)
->test('admin.cms.content-index')
->assertSee('Nicht mehr verwendet')
->assertSee('netzwerk / teasers')
->call('selectSection', 'netzwerk', 'teasers')
->assertSee('Alt');
expect(Volt::actingAs($user)->test('admin.cms.content-index')
->call('selectSection', 'netzwerk', 'teasers')
->instance()->previewUrl)->toBeNull();
});
test('cms content index creates a missing section from the lang template', function () {
$user = User::factory()->create();
expect(CmsContent::where('group', 'global')->where('key', 'appointment_widget')->exists())->toBeFalse();
Volt::actingAs($user)
->test('admin.cms.content-index')
->call('createSection', 'global', 'appointment_widget')
->assertSet('selectedGroup', 'global')
->assertSet('selectedKey', 'appointment_widget');
$row = CmsContent::where('group', 'global')->where('key', 'appointment_widget')->first();
expect($row)->not->toBeNull();
expect($row->getTranslation('value', 'de'))->toBe(cms_theme_section('appointment_widget'));
});
test('cms content index refuses to create a section without a lang template', function () {
$user = User::factory()->create();
Volt::actingAs($user)
->test('admin.cms.content-index')
->call('createSection', 'global', 'contact')
->assertSet('selectedKey', null);
expect(CmsContent::where('group', 'global')->where('key', 'contact')->exists())->toBeFalse();
});
test('cms content index can hide the preview', function () {
$user = User::factory()->create();
CmsContent::create([
'group' => 'home',
'key' => 'hero',
'type' => 'json',
'value' => ['de' => ['title' => 'Test']],
]);
Volt::actingAs($user)
->test('admin.cms.content-index')
->call('selectSection', 'home', 'hero')
->assertSee('Vorschau der Website')
->call('togglePreview')
->assertSet('showPreview', false)
->assertDontSee('Vorschau der Website');
});