- 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>
245 lines
9.5 KiB
PHP
245 lines
9.5 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
use App\Livewire\Web\Components\Ui\AppointmentWidget;
|
||
use App\Models\CmsBrand;
|
||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||
use Livewire\Livewire;
|
||
|
||
uses(RefreshDatabase::class);
|
||
|
||
beforeEach(function () {
|
||
$this->seed(\Database\Seeders\CmsBrandSeeder::class);
|
||
$this->seed(\Database\Seeders\BrandPageTextSeeder::class);
|
||
});
|
||
|
||
// ========================================
|
||
// Abschnitte aus der Zulieferung
|
||
// ========================================
|
||
|
||
it('renders the delivered sections on the rotpunkt page', function () {
|
||
$this->get('/marken/rotpunkt')
|
||
->assertSuccessful()
|
||
->assertSee('Aus Ostwestfalen. Für Ihr Zuhause.', false)
|
||
->assertSee('Gestaltungsfreiheit bis ins Detail')
|
||
->assertSee('Nachhaltigkeit mit konkreter Substanz')
|
||
->assertSee('Mehr als Küchen', false)
|
||
->assertSee('Planung plus Handwerk', false);
|
||
});
|
||
|
||
it('renders the cabinet sections written to the same pattern', function () {
|
||
$this->get('/marken/cabinet')
|
||
->assertSuccessful()
|
||
->assertSee('Millimeter statt Standardmaß', false)
|
||
->assertSee('Ein Bild und ein grobes Maß reichen für den Anfang', false)
|
||
->assertSee('Warum CABINET bei uns?')
|
||
->assertSee('Ihr Raum. Persönlich geplant.', false);
|
||
});
|
||
|
||
it('renders the delivered sections on the februe page', function () {
|
||
$this->get('/marken/februe')
|
||
->assertSuccessful()
|
||
->assertSee('Aus Herford. Für moderne Arbeitswelten.', false)
|
||
->assertSee('Nicht drei Marken. Ein durchgängiger Raum.', false)
|
||
->assertSee('Ergonomie, die im Alltag funktioniert');
|
||
});
|
||
|
||
it('turns every marked deepening into a collapsible area', function () {
|
||
$html = $this->get('/marken/rotpunkt')->assertSuccessful()->getContent();
|
||
|
||
// Rotpunkt bringt vier Vertiefungen mit.
|
||
expect(substr_count($html, 'aria-controls="detail-'))->toBe(4)
|
||
->and($html)->toContain('FENIX und authentische Holzoptiken')
|
||
->and($html)->toContain('NEW DIMENSIONS');
|
||
});
|
||
|
||
it('renders every deepening collapsed and animates its height', function () {
|
||
$html = $this->get('/marken/rotpunkt')->assertSuccessful()->getContent();
|
||
|
||
// Der zugeklappte Zustand muss im statischen class-Attribut stehen, sonst blitzt der Inhalt
|
||
// vor dem Start von Alpine aufgeklappt auf.
|
||
expect(substr_count($html, 'grid grid-rows-[0fr] opacity-0 transition-all'))->toBe(4)
|
||
// grid-template-rows animiert die tatsaechliche Hoehe – die alte Bewegung blendete den
|
||
// Inhalt nur ein und sprang dabei auf die volle Hoehe.
|
||
->and($html)->not->toContain('x-transition:enter-start="opacity-0 -translate-y-2"')
|
||
->and($html)->toContain("'grid-rows-[1fr] opacity-100': open");
|
||
});
|
||
|
||
it('uses no full-surface hover on the accordion buttons', function () {
|
||
foreach (['/marken/rotpunkt', '/faq'] as $path) {
|
||
$html = $this->get($path)->assertSuccessful()->getContent();
|
||
|
||
expect($html)->not->toContain('hover:bg-muted/30');
|
||
}
|
||
});
|
||
|
||
it('merges the two sustainability sections as the corrected version requires', function () {
|
||
$html = $this->get('/marken/rotpunkt')->assertSuccessful()->getContent();
|
||
|
||
expect($html)->not->toContain('Greenline hält dem Vergleich stand')
|
||
->and($html)->toContain('ohne Verkaufsnebel')
|
||
// Der doppelte Satz steht nur noch einmal auf der Seite.
|
||
->and(substr_count($html, 'Nachhaltigkeit braucht keine großen Versprechen'))->toBe(1);
|
||
});
|
||
|
||
it('keeps the blauer engel claim tied to the listed product groups', function () {
|
||
$this->get('/marken/februe')
|
||
->assertSuccessful()
|
||
->assertSee('Ausgewählte Febrü-Tischsysteme und Stauraummöbel tragen den Blauen Engel.', false);
|
||
});
|
||
|
||
it('shows the pull quote from marcel on the februe page', function () {
|
||
$this->get('/marken/februe')
|
||
->assertSuccessful()
|
||
->assertSee('Mein Arzt sagt, ich solle weniger sitzen.', false)
|
||
->assertSee('Marcel Scheibe, Geschäftsführer von B2in', false);
|
||
});
|
||
|
||
it('shows the closing claims of a section', function () {
|
||
$this->get('/marken/februe')
|
||
->assertSuccessful()
|
||
->assertSee('Made in Herford. Geplant in Bielefeld.')
|
||
->assertSee('Ob im Kleinen oder im Großen', false);
|
||
});
|
||
|
||
it('never publishes the internal source notes', function () {
|
||
foreach (['rotpunkt', 'februe'] as $slug) {
|
||
$html = $this->get('/marken/'.$slug)->assertSuccessful()->getContent();
|
||
|
||
expect($html)->not->toContain('QUELLENHINWEISE')
|
||
->and($html)->not->toContain('NICHT VERÖFFENTLICHEN')
|
||
->and($html)->not->toContain('UMSETZUNGSHINWEIS');
|
||
}
|
||
});
|
||
|
||
// ========================================
|
||
// Abschlussblock
|
||
// ========================================
|
||
|
||
it('uses the brand specific closing block instead of the generic one', function () {
|
||
$this->get('/marken/rotpunkt')
|
||
->assertSuccessful()
|
||
->assertSee('Ihre Küche. Persönlich geplant.', false)
|
||
->assertSee('Rathausstraße 11 in Bielefeld', false)
|
||
->assertDontSee('Interesse an Rotpunkt Küchen?', false);
|
||
});
|
||
|
||
it('falls back to the generic closing block for a brand without one', function () {
|
||
// Beide Sprachen leeren: bei nur geleertem Deutsch greift die Uebersetzungs-Fallbackkette.
|
||
CmsBrand::where('slug', 'cabinet')->firstOrFail()->update(['cta_title' => [], 'cta_text' => []]);
|
||
|
||
$this->get('/marken/cabinet')
|
||
->assertSuccessful()
|
||
->assertSee('Interesse an CABINET?');
|
||
});
|
||
|
||
it('closes the page with a gradient band across the full width', function () {
|
||
$html = $this->get('/marken/rotpunkt')->assertSuccessful()->getContent();
|
||
|
||
expect($html)->toContain('cta-band')
|
||
// Die weichen Kreise setzten sichtbare Flecken in die Ecken statt eines Verlaufs.
|
||
->and($html)->not->toContain('blur-3xl');
|
||
});
|
||
|
||
it('exempts the closing band from the glass-flow section background', function () {
|
||
// `.variante-glass-flow section` faerbt jede Sektion hell ein und ist spezifischer als
|
||
// `.cta-band` – ohne diese Ausnahme steht der Abschluss hellgrau mit weisser Schrift da.
|
||
// `/marken/{slug}` haengt in genau diesem Wrapper.
|
||
expect(file_get_contents(resource_path('views/web/marken-show.blade.php')))
|
||
->toContain('variante-glass-flow');
|
||
|
||
expect(file_get_contents(resource_path('css/web/shared-styles.css')))
|
||
->toContain('section.cta-band');
|
||
});
|
||
|
||
// ========================================
|
||
// Beschriftung des Terminanfrage-Widgets
|
||
// ========================================
|
||
|
||
it('labels the widget for the brand of the page', function (string $slug, string $label) {
|
||
$this->get('/marken/'.$slug)
|
||
->assertSuccessful()
|
||
->assertSee($label, false);
|
||
})->with([
|
||
'rotpunkt' => ['rotpunkt', 'Persönliche Küchenberatung vereinbaren'],
|
||
'februe' => ['februe', 'Persönliche Arbeitsplatzberatung vereinbaren'],
|
||
'cabinet' => ['cabinet', 'Persönliche Schrankplanung vereinbaren'],
|
||
]);
|
||
|
||
it('keeps the general label outside the brand pages', function () {
|
||
$this->get('/')
|
||
->assertSuccessful()
|
||
->assertSee('Termin anfragen')
|
||
->assertDontSee('Persönliche Küchenberatung', false);
|
||
});
|
||
|
||
it('adds no second cta button, only the label changes', function () {
|
||
// Die Zulieferung verlangt ausdruecklich keinen zweiten Button.
|
||
$html = $this->get('/marken/rotpunkt')->assertSuccessful()->getContent();
|
||
|
||
expect(substr_count($html, 'Persönliche Küchenberatung vereinbaren'))->toBe(1);
|
||
});
|
||
|
||
it('leaves the label untouched for a brand without its own', function () {
|
||
$label = Livewire::test(AppointmentWidget::class)->get('content')['button_label'] ?? '';
|
||
|
||
expect($label)->toBe('Termin anfragen');
|
||
});
|
||
|
||
// ========================================
|
||
// Datenform
|
||
// ========================================
|
||
|
||
it('normalises sections into a fixed shape', function () {
|
||
$sections = CmsBrand::normalizeSections([
|
||
['title' => ' Titel ', 'paragraphs' => [' Erster ', '', 'Zweiter']],
|
||
['title' => '', 'paragraphs' => []],
|
||
'kein Array',
|
||
]);
|
||
|
||
expect($sections)->toHaveCount(1)
|
||
->and($sections[0]['title'])->toBe('Titel')
|
||
->and($sections[0]['paragraphs'])->toBe(['Erster', 'Zweiter'])
|
||
->and($sections[0]['detail'])->toBeNull();
|
||
});
|
||
|
||
it('keeps a deepening only when it has content', function () {
|
||
$sections = CmsBrand::normalizeSections([
|
||
['title' => 'A', 'detail' => ['title' => '', 'paragraphs' => []]],
|
||
['title' => 'B', 'detail' => ['title' => 'Vertiefung', 'paragraphs' => ['Text']]],
|
||
]);
|
||
|
||
expect($sections[0]['detail'])->toBeNull()
|
||
->and($sections[1]['detail']['title'])->toBe('Vertiefung');
|
||
});
|
||
|
||
it('accepts a text block instead of a paragraph list', function () {
|
||
$sections = CmsBrand::normalizeSections([
|
||
['title' => 'A', 'paragraphs' => "Erster Absatz.\n\nZweiter Absatz."],
|
||
]);
|
||
|
||
expect($sections[0]['paragraphs'])->toBe(['Erster Absatz.', 'Zweiter Absatz.']);
|
||
});
|
||
|
||
// ========================================
|
||
// Redaktionsschutz des Seeders
|
||
// ========================================
|
||
|
||
it('leaves images, gallery and services untouched', function () {
|
||
$brand = CmsBrand::where('slug', 'rotpunkt')->firstOrFail();
|
||
$brand->update([
|
||
'image' => 'gepflegt.webp',
|
||
'gallery' => [['image' => 'projekt.webp', 'category' => 'project', 'caption' => 'Küche']],
|
||
'order' => 42,
|
||
]);
|
||
|
||
$this->seed(\Database\Seeders\BrandPageTextSeeder::class);
|
||
|
||
$brand->refresh();
|
||
|
||
expect($brand->image)->toBe('gepflegt.webp')
|
||
->and($brand->gallery)->toHaveCount(1)
|
||
->and($brand->order)->toBe(42)
|
||
->and($brand->services)->not->toBeEmpty();
|
||
});
|