Die Klappelemente sprangen auf und zu: x-show gibt dem Element sofort die volle Hoehe und blendet nur den Inhalt ein. Ersetzt durch eine Animation von grid-template-rows 0fr → 1fr ueber 500 ms mit cubic-bezier(0.33, 1, 0.68, 1), die die tatsaechliche Hoehe bewegt – ohne das Alpine-Collapse-Plugin, das in diesem Projekt nicht registriert ist. - Der zugeklappte Zustand steht auch statisch im class-Attribut, damit vor dem Start von Alpine nichts aufgeklappt aufblitzt. - aria-hidden haengt am Zustand, weil der Inhalt jetzt dauerhaft im DOM steht. - Hover: die Flaechenaufhellung (hover:bg-muted/30) ist raus, geblieben ist ein feiner Farbwechsel der Ueberschrift. Im FAQ entfaellt zusaetzlich der Hintergrundwechsel am Icon-Kreis. - Der Pfeil dreht in derselben Dauer und Kurve wie das Aufklappen. Betrifft beide Akkordeons – die Vertiefungen der Markenseiten und das FAQ. Tests: 861 gruen (3189 Assertions), Pint sauber, npm run build:b2in gebaut. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
225 lines
8.6 KiB
PHP
225 lines
8.6 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('Küchenstudio 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('Aufmaß vor Ort statt Schätzung am Telefon', 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?');
|
||
});
|
||
|
||
// ========================================
|
||
// 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();
|
||
});
|