- Hinweisleiste per B2IN_ANNOUNCEMENT_BAR ausgeblendet (Standard aus) - Hero: „Räume aus einer Hand.“ als neue Überschrift, zweiter Button entfernt - Projekttext mit zentralem deutschen Vertragspartner (Punkt statt Gedankenstrich) - Otterpohl als vierte Kernmarke: Text v2, Abschnitt „Montage“, Titelbild getauscht - Markenabschnitte: Absätze mit „- “ als Liste mit fetter Einleitung - Titelbilder und Galerien für CABINET, Rotpunkt, Febrü und Otterpohl (WebP) - Feedback 17.09. in den Markentexten, Demobilder entfernt, WhatsApp-Nummer - Palette „Modern“ fest ohne Schalter, Entwurf /entwurf mit Hero-Slideshow - Seeder-Liste fürs Deployment im Entwicklungsplan Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
313 lines
12 KiB
PHP
313 lines
12 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
use App\Models\CmsBrand;
|
||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||
|
||
uses(RefreshDatabase::class);
|
||
|
||
beforeEach(function () {
|
||
$this->seed(\Database\Seeders\CmsBrandSeeder::class);
|
||
});
|
||
|
||
// ========================================
|
||
// Kacheln auf der Startseite
|
||
// ========================================
|
||
|
||
it('shows the maintained brands on the homepage', function () {
|
||
$this->get('/')
|
||
->assertSuccessful()
|
||
->assertSee('CABINET')
|
||
->assertSee('Rotpunkt Küchen', false)
|
||
->assertSee('Febrü', false);
|
||
});
|
||
|
||
it('links each tile to its detail page', function () {
|
||
$this->get('/')
|
||
->assertSuccessful()
|
||
->assertSee('/marken/cabinet')
|
||
->assertSee('/marken/rotpunkt')
|
||
->assertSee('/marken/februe');
|
||
});
|
||
|
||
it('hides unpublished brands from the homepage', function () {
|
||
CmsBrand::where('slug', 'februe')->update(['is_published' => false]);
|
||
|
||
$this->get('/')
|
||
->assertSuccessful()
|
||
->assertSee('CABINET')
|
||
->assertDontSee('/marken/februe');
|
||
});
|
||
|
||
// ========================================
|
||
// Detailseite
|
||
// ========================================
|
||
|
||
it('serves a detail page for every brand', function () {
|
||
foreach (['cabinet', 'rotpunkt', 'februe'] as $slug) {
|
||
$this->get('/marken/'.$slug)->assertSuccessful();
|
||
}
|
||
});
|
||
|
||
it('renders the template sections from the concept', function () {
|
||
$this->get('/marken/cabinet')
|
||
->assertSuccessful()
|
||
->assertSee('CABINET') // Header
|
||
->assertSee('Einbauschränke nach Maß', false) // Claim
|
||
->assertSee('Showroom Bielefeld')
|
||
->assertSee('Leistungen und Besonderheiten') // Leistungen
|
||
->assertSee('Aufmaß vor Ort', false)
|
||
->assertSee('Termin anfragen'); // CTA
|
||
});
|
||
|
||
it('links to the other brands from a detail page', function () {
|
||
$this->get('/marken/cabinet')
|
||
->assertSuccessful()
|
||
->assertSee('/marken/rotpunkt')
|
||
->assertSee('/marken/februe');
|
||
});
|
||
|
||
it('returns 404 for an unknown brand', function () {
|
||
$this->get('/marken/gibt-es-nicht')->assertNotFound();
|
||
});
|
||
|
||
it('returns 404 for an unpublished brand', function () {
|
||
CmsBrand::where('slug', 'februe')->update(['is_published' => false]);
|
||
|
||
$this->get('/marken/februe')->assertNotFound();
|
||
});
|
||
|
||
it('carries a brand specific title and description', function () {
|
||
$this->get('/marken/cabinet')
|
||
->assertSuccessful()
|
||
->assertSee('CABINET – Einbauschränke nach Maß. | B2in Interior', false)
|
||
->assertDontSee('Connecting Design and Property');
|
||
});
|
||
|
||
// ========================================
|
||
// CMS-Anbindung
|
||
// ========================================
|
||
|
||
it('renders what is maintained in the CMS', function () {
|
||
$cabinet = CmsBrand::where('slug', 'cabinet')->firstOrFail();
|
||
$cabinet->setTranslation('tagline', 'de', 'Im CMS geänderter Claim');
|
||
$cabinet->save();
|
||
|
||
$this->get('/')->assertSuccessful()->assertSee('Im CMS geänderter Claim');
|
||
$this->get('/marken/cabinet')->assertSuccessful()->assertSee('Im CMS geänderter Claim');
|
||
});
|
||
|
||
it('picks up a brand that was added in the CMS, without a code change', function () {
|
||
CmsBrand::factory()->create([
|
||
'slug' => 'neue-marke',
|
||
'name' => ['de' => 'Neue Marke', 'en' => 'New Brand'],
|
||
'order' => 9,
|
||
]);
|
||
|
||
$this->get('/')->assertSuccessful()->assertSee('Neue Marke');
|
||
$this->get('/marken/neue-marke')->assertSuccessful()->assertSee('Neue Marke');
|
||
});
|
||
|
||
// ========================================
|
||
// Galerie: ein Raster, Badge je Bild (Rubrikwechsel ausgesetzt, Nachtrag 17)
|
||
// ========================================
|
||
|
||
it('shows no gallery section while no images are maintained', function () {
|
||
$this->get('/marken/cabinet')
|
||
->assertSuccessful()
|
||
->assertDontSee('id="galerie"', false)
|
||
->assertDontSee('Inspirationen und Entwürfe', false);
|
||
});
|
||
|
||
it('shows projects and visualisations in one grid with a badge each and projects first', function () {
|
||
CmsBrand::where('slug', 'cabinet')->firstOrFail()->update([
|
||
'gallery' => [
|
||
['image' => 'entwurf.webp', 'category' => 'inspiration', 'caption' => 'Entwurf Garderobe'],
|
||
['image' => 'projekt.webp', 'category' => 'project', 'caption' => 'Ankleide in Bielefeld'],
|
||
],
|
||
]);
|
||
|
||
$html = $this->get('/marken/cabinet')->assertSuccessful()->getContent();
|
||
|
||
// Gemischte Galerie: Überschrift nennt beides (Nachtrag 19).
|
||
expect($html)->toContain('Projekte und Entwürfe')
|
||
->toContain('was in einem Raum möglich ist')
|
||
->not->toContain('Inspirationen und Entwürfe')
|
||
->toContain('Realisiertes Projekt')
|
||
->toContain('Visualisierung')
|
||
->toContain('Ankleide in Bielefeld')
|
||
->toContain('Entwurf Garderobe')
|
||
->not->toContain('role="tablist"')
|
||
->not->toContain('Realisierte Projekte');
|
||
|
||
// Ohne Livewire-Snapshot vergleichen: er trägt die Galerie in Datenreihenfolge.
|
||
$visible = preg_replace('/wire:snapshot="[^"]*"/', '', $html) ?? $html;
|
||
expect(strpos($visible, 'Ankleide in Bielefeld'))->toBeLessThan((int) strpos($visible, 'Entwurf Garderobe'));
|
||
});
|
||
|
||
it('shows exactly one line below the gallery title', function () {
|
||
CmsBrand::where('slug', 'cabinet')->firstOrFail()->update([
|
||
'gallery' => [['image' => 'entwurf.webp', 'category' => 'inspiration', 'caption' => 'Entwurf']],
|
||
]);
|
||
|
||
$html = $this->get('/marken/cabinet')->assertSuccessful()->getContent();
|
||
$section = substr($html, (int) strpos($html, 'id="galerie"'));
|
||
$section = substr($section, 0, (int) strpos($section, '<div class="grid'));
|
||
|
||
expect(substr_count($section, '<p '))->toBe(1);
|
||
expect($section)->not->toContain('Entwürfe und Visualisierungen. Sie zeigen');
|
||
});
|
||
|
||
it('marks an image without a category as visualisation, never as a completed project', function () {
|
||
CmsBrand::where('slug', 'cabinet')->firstOrFail()->update([
|
||
'gallery' => [
|
||
['image' => 'ohne-kategorie.webp'],
|
||
],
|
||
]);
|
||
|
||
$this->get('/marken/cabinet')
|
||
->assertSuccessful()
|
||
->assertSee('Visualisierung')
|
||
->assertDontSee('Realisiertes Projekt');
|
||
});
|
||
|
||
it('still separates inspirations from completed projects after the rewording', function () {
|
||
CmsBrand::where('slug', 'cabinet')->firstOrFail()->update([
|
||
'gallery' => [
|
||
['image' => 'entwurf.webp', 'category' => 'inspiration'],
|
||
],
|
||
]);
|
||
|
||
$this->get('/marken/cabinet')
|
||
->assertSuccessful()
|
||
->assertSee('Visualisierung')
|
||
->assertDontSee('Realisiertes Projekt');
|
||
});
|
||
|
||
it('removes the demo images from every gallery and keeps the real ones', function () {
|
||
$this->seed(\Database\Seeders\DemoBrandGallerySeeder::class);
|
||
$this->seed(\Database\Seeders\BrandGalleryCaptionNormalizerSeeder::class);
|
||
$this->seed(\Database\Seeders\BrandImageSeeder::class);
|
||
|
||
$this->seed(\Database\Seeders\DemoBrandGalleryCleanupSeeder::class);
|
||
$this->seed(\Database\Seeders\DemoBrandGalleryCleanupSeeder::class);
|
||
|
||
expect(CmsBrand::where('slug', 'cabinet')->firstOrFail()->gallery)->toHaveCount(11)
|
||
->and(CmsBrand::where('slug', 'rotpunkt')->firstOrFail()->gallery)->toHaveCount(13)
|
||
->and(CmsBrand::where('slug', 'februe')->firstOrFail()->gallery)->toHaveCount(15)
|
||
->and(CmsBrand::where('slug', 'tischlerei')->firstOrFail()->gallery)->toHaveCount(12);
|
||
|
||
foreach (CmsBrand::all() as $brand) {
|
||
foreach ($brand->gallery ?? [] as $item) {
|
||
expect($item['image'])->toStartWith('marken/');
|
||
}
|
||
}
|
||
});
|
||
|
||
it('fills the demo galleries with both categories', function () {
|
||
$this->seed(\Database\Seeders\DemoBrandGallerySeeder::class);
|
||
|
||
$grouped = CmsBrand::groupGallery(CmsBrand::where('slug', 'cabinet')->firstOrFail()->gallery);
|
||
|
||
expect($grouped['projects'])->not->toBeEmpty()
|
||
->and($grouped['inspirations'])->not->toBeEmpty();
|
||
|
||
// Der Vermerk „BEISPIEL“ ist auf Kundenwunsch entfallen. Die Unterscheidung muss deshalb
|
||
// an der Kategorie haengen – jedes Demobild braucht eine ausdrueckliche.
|
||
foreach (CmsBrand::all() as $brand) {
|
||
foreach ($brand->gallery ?? [] as $item) {
|
||
expect($item['caption'])->not->toContain('BEISPIEL')
|
||
->and($item['category'])->toBeIn(CmsBrand::GALLERY_CATEGORIES);
|
||
}
|
||
}
|
||
});
|
||
|
||
it('strips the old example prefix without touching image, category or order', function () {
|
||
CmsBrand::where('slug', 'cabinet')->firstOrFail()->update([
|
||
'gallery' => [
|
||
['image' => 'a.webp', 'category' => 'project', 'caption' => 'BEISPIEL – Ankleide, Bielefeld'],
|
||
['image' => 'b.webp', 'category' => 'inspiration', 'caption' => 'Entwurf Garderobe'],
|
||
],
|
||
]);
|
||
|
||
$this->seed(\Database\Seeders\BrandGalleryCaptionNormalizerSeeder::class);
|
||
|
||
$gallery = CmsBrand::where('slug', 'cabinet')->firstOrFail()->gallery;
|
||
|
||
expect($gallery)->toHaveCount(2)
|
||
->and($gallery[0]['caption'])->toBe('Ankleide, Bielefeld')
|
||
->and($gallery[0]['image'])->toBe('a.webp')
|
||
->and($gallery[0]['category'])->toBe('project')
|
||
->and($gallery[1]['caption'])->toBe('Entwurf Garderobe');
|
||
});
|
||
|
||
// ========================================
|
||
// Markenlogos
|
||
// ========================================
|
||
|
||
it('shows the logo instead of the wordmark at the top of a brand page', function () {
|
||
$html = $this->get('/marken/rotpunkt')->assertSuccessful()->getContent();
|
||
|
||
// Das Logo sitzt im <h1>, damit die Ueberschrift erhalten bleibt.
|
||
expect($html)->toMatch('/<h1>\s*<img[^>]*marken\/rotpunkt\.svg/')
|
||
->and($html)->toContain('alt="Rotpunkt Küchen"');
|
||
});
|
||
|
||
it('falls back to the wordmark for a brand without a logo', function () {
|
||
CmsBrand::where('slug', 'rotpunkt')->firstOrFail()->update(['logo' => '']);
|
||
|
||
$this->get('/marken/rotpunkt')
|
||
->assertSuccessful()
|
||
->assertSee('<h1 class="text-section-title">Rotpunkt Küchen</h1>', false);
|
||
});
|
||
|
||
it('shows the logo on every tile that leads to a brand page', function (string $path) {
|
||
$html = $this->get($path)->assertSuccessful()->getContent();
|
||
|
||
foreach (['cabinet.svg', 'rotpunkt.svg', 'februe.png'] as $file) {
|
||
expect($html)->toContain('marken/'.$file);
|
||
}
|
||
})->with([
|
||
'Startseite' => ['/'],
|
||
'Markenuebersicht' => ['/marken'],
|
||
]);
|
||
|
||
it('shows the logo on the other-brands tiles of a detail page', function () {
|
||
$html = $this->get('/marken/rotpunkt')->assertSuccessful()->getContent();
|
||
|
||
// Auf der Rotpunkt-Seite verweisen die Kacheln auf CABINET und Februe.
|
||
expect($html)->toContain('marken/cabinet.svg')
|
||
->and($html)->toContain('marken/februe.png');
|
||
});
|
||
|
||
it('ships the logo files the seeder points at', function () {
|
||
foreach (['cabinet.svg', 'rotpunkt.svg', 'februe.png'] as $file) {
|
||
expect(public_path('img/assets/marken/'.$file))->toBeFile();
|
||
}
|
||
});
|
||
|
||
it('ships the logos cropped to their content, not as delivered', function (string $file) {
|
||
// Die Zulieferungen haben viel leeren Rand in der Datei – CABINET nutzte nur 29 Prozent der
|
||
// Hoehe. In einer Logo-Box wirkt so ein Logo winzig, egal wie gross die Box ist. Die
|
||
// beschnittenen Fassungen erkennt man am Ursprung: der ist nach dem Beschnitt nicht mehr 0 0.
|
||
$viewBox = (string) simplexml_load_file(public_path('img/assets/marken/'.$file))['viewBox'];
|
||
|
||
expect($viewBox)->not->toStartWith('0 0 ');
|
||
})->with(['cabinet.svg', 'rotpunkt.svg']);
|
||
|
||
it('keeps a logo that was uploaded in the admin', function () {
|
||
CmsBrand::where('slug', 'cabinet')->firstOrFail()->update(['logo' => 'eigenes-logo.svg']);
|
||
|
||
$this->seed(\Database\Seeders\BrandLogoSeeder::class);
|
||
|
||
expect(CmsBrand::where('slug', 'cabinet')->firstOrFail()->logo)->toBe('eigenes-logo.svg');
|
||
});
|
||
|
||
it('adds the logo to a brand that has none yet', function () {
|
||
CmsBrand::where('slug', 'cabinet')->firstOrFail()->update(['logo' => '']);
|
||
|
||
$this->seed(\Database\Seeders\BrandLogoSeeder::class);
|
||
|
||
expect(CmsBrand::where('slug', 'cabinet')->firstOrFail()->logo)->toBe('marken/cabinet.svg');
|
||
});
|