b2in/tests/Feature/BrandPageSectionsTest.php
Kevin Adametz f971f416db Akkordeons weicher aufklappen, starken Hover entfernen
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>
2026-09-03 10:00:39 +00:00

225 lines
8.6 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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();
});