Startseite und Heroes - Hero-Slideshow (11 Motive inkl. Febrü) mit Infokarte, Laufzeit-Linie, größeren Klickflächen, Goldrahmen und mobiler Karte unter dem Bild; Zeile über der Überschrift, neue Vertrauenspunkte - Hero-Stil auf Marken, Über uns und Kontakt übertragen, Breadcrumb entfernt - Gründer-Leiste, Vision-Box auf Ocker, Showroom-Display in Box mit „Größer ansehen“/Vollbild - Akzentfarbe #a8741f, größere Serif-Überschriften in der Interior-Welt - Marken: Bildpause mit Zitat; Vereine aktualisiert und verlinkt Inhalte - Lektorat aller angezeigten Texte, /about-Korrekturen, Rechtstexte (DDG, TDDDG, Hosting Hetzner) - Ablauf Maßanfertigung überall einheitlich, Magazin-Beitrag neu, alte Beiträge offline, deutsches Datum/Lesezeit, FAQ Projektausstattung, Otterpohl - Referenzen-Seite per CMS-Schalter ausblendbar CMS und Technik - Tote Sektionen, Felder, Seiten und Komponenten entfernt und archiviert (dev/2026-08-28/archiv-*) - Entwurf /entwurf, Farb-Schalter und /welcome entfernt; Test-Routen nur noch lokal - Seitenbaum-Beschreibungen geprüft, fehlende CMS-Felder ergänzt, Markenliste aus „Kernmarken“ - Admin-Bildvorschauen repariert, verwendete Bilder in die Mediathek übernommen - Seeder für das Deployment siehe dev/2026-08-28/entwicklungsplan.md Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
238 lines
12 KiB
PHP
238 lines
12 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
use App\Models\CmsBrand;
|
||
use Database\Seeders\AudienceSwitchProjectTextSeeder;
|
||
use Database\Seeders\CmsBrandSeeder;
|
||
use Database\Seeders\HomeHeroTextSeeder;
|
||
use Database\Seeders\OtterpohlBrandSeeder;
|
||
use FluxCms\Core\Models\CmsContent;
|
||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||
|
||
uses(RefreshDatabase::class);
|
||
|
||
/**
|
||
* Korrekturen aus der Abstimmung vom 01.10.2026: Hinweisleiste aus, neue Hero-Überschrift mit
|
||
* einem Button, Projekttext mit Punkt, Otterpohl mit neuem Titelbild und Abschnitt „Montage“.
|
||
*/
|
||
function storeHomeHero(array $de, array $en = []): CmsContent
|
||
{
|
||
return CmsContent::create([
|
||
'group' => 'home',
|
||
'key' => 'hero',
|
||
'type' => 'json',
|
||
'value' => array_filter(['de' => $de, 'en' => $en]),
|
||
'order' => 0,
|
||
]);
|
||
}
|
||
|
||
// ========================================
|
||
// Hinweisleiste
|
||
// ========================================
|
||
|
||
it('hides the announcement bar by default on both worlds', function (string $path) {
|
||
$this->get($path)->assertSuccessful()->assertDontSee('id="topbar"', false);
|
||
})->with(['/', '/immobilien', '/marken']);
|
||
|
||
it('brings the announcement bar back with the flag', function () {
|
||
config(['app.announcement_bar' => true]);
|
||
|
||
$this->get('/')->assertSuccessful()->assertSee('id="topbar"', false);
|
||
});
|
||
|
||
// ========================================
|
||
// Hero
|
||
// ========================================
|
||
|
||
it('ships the new headline without a second button in both languages', function (string $locale, string $title) {
|
||
app()->setLocale($locale);
|
||
$hero = cms_theme_section('hero');
|
||
|
||
expect($hero['title'])->toBe($title)->and($hero)->not->toHaveKey('cta2_text');
|
||
})->with([
|
||
'de' => ['de', '<span class="text-secondary">Räume aus einer Hand.</span><br>Mit starken Marken und persönlichen Lösungen.'],
|
||
'en' => ['en', '<span class="text-secondary">Spaces from a single source.</span><br>With strong brands and personal solutions.'],
|
||
]);
|
||
|
||
it('replaces the old CMS headline and drops the second button', function () {
|
||
storeHomeHero(
|
||
['title' => '<span class="text-secondary">B2in Interior</span> – Premium-Einrichtung von starken Marken. Für Ihr Zuhause.', 'subtitle' => 'Untertitel', 'image' => 'b2in/hero-room.jpg', 'image_alt' => '', 'card_title' => '', 'card_text' => '', 'cta1_text' => 'Unsere Marken entdecken', 'cta1_link' => '/marken', 'cta2_text' => 'Termin anfragen', 'cta2_link' => '/contact'],
|
||
['title' => '<span class="text-secondary">B2in Interior</span> – Premium furnishing from strong brands.', 'cta2_text' => 'Request an appointment', 'cta2_link' => '/contact'],
|
||
);
|
||
|
||
$this->seed(HomeHeroTextSeeder::class);
|
||
|
||
$value = CmsContent::where('group', 'home')->where('key', 'hero')->first()->getTranslations('value');
|
||
|
||
expect($value['de']['title'])->toBe(HomeHeroTextSeeder::TITLES['de']['new'])
|
||
->and($value['en']['title'])->toBe(HomeHeroTextSeeder::TITLES['en']['new'])
|
||
->and($value['de'])->not->toHaveKey('cta2_text')
|
||
->and($value['en'])->not->toHaveKey('cta2_link')
|
||
->and($value['de']['cta1_text'])->toBe('Unsere Marken entdecken');
|
||
|
||
$hero = substr($html = $this->get('/')->assertSuccessful()->getContent(), strpos($html, 'id="hero"'), 6000);
|
||
expect($hero)->toContain('Räume aus einer Hand.')->not->toContain('Termin anfragen');
|
||
});
|
||
|
||
it('adds the eyebrow, the new subtitle and the B2in label on the first slide (07.10.)', function () {
|
||
storeHomeHero(
|
||
['title' => 'Titel', 'subtitle' => 'Von der einzelnen Maßanfertigung bis zur vollständigen Ausstattung – mit CABINET …', 'cta1_text' => 'Marken', 'cta1_link' => '/marken', 'card_title' => '', 'card_text' => '',
|
||
'slides' => [
|
||
['image' => HomeHeroTextSeeder::FIRST_SLIDE_IMAGE, 'label' => 'Rotpunkt Küchen', 'caption' => 'Haus'],
|
||
['image' => 'b2in/slideshow/rotpunkt-kueche-mk06.webp', 'label' => 'Rotpunkt Küchen', 'caption' => 'Küche'],
|
||
]],
|
||
['title' => 'Title', 'subtitle' => 'From a single bespoke piece to a complete fit-out – with CABINET …', 'eyebrow' => 'Own eyebrow'],
|
||
);
|
||
|
||
$this->seed(HomeHeroTextSeeder::class);
|
||
$this->seed(HomeHeroTextSeeder::class);
|
||
|
||
$value = CmsContent::where('group', 'home')->where('key', 'hero')->first()->getTranslations('value');
|
||
|
||
expect($value['de']['eyebrow'])->toBe('B2in Interior')
|
||
->and($value['en']['eyebrow'])->toBe('Own eyebrow')
|
||
->and($value['de']['subtitle'])->toBe(HomeHeroTextSeeder::SUBTITLES['de']['new'])
|
||
->and($value['en']['subtitle'])->toBe(HomeHeroTextSeeder::SUBTITLES['en']['new'])
|
||
->and(array_column($value['de']['slides'], 'label'))->toBe(['B2in Interior', 'Rotpunkt Küchen']);
|
||
|
||
$hero = substr($html = $this->get('/')->assertSuccessful()->getContent(), strpos($html, 'id="hero"'), 8000);
|
||
expect($hero)->toContain('data-hero-eyebrow')
|
||
->toContain('Wir vereinen CABINET, Rotpunkt Küchen, Febrü und ein Netzwerk regionaler Experten');
|
||
});
|
||
|
||
it('ships the eyebrow, the new subtitle and the B2in label in the language files', function () {
|
||
$hero = __('b2in.themes.b2in.hero', [], 'de');
|
||
|
||
expect($hero['eyebrow'])->toBe('B2in Interior')
|
||
->and($hero['subtitle'])->toBe(HomeHeroTextSeeder::SUBTITLES['de']['new'])
|
||
->and($hero['slides'][0]['label'])->toBe('B2in Interior')
|
||
->and(__('b2in.themes.b2in.hero', [], 'en')['subtitle'])->toBe(HomeHeroTextSeeder::SUBTITLES['en']['new']);
|
||
});
|
||
|
||
it('keeps a headline that was already rewritten in the CMS', function () {
|
||
storeHomeHero(['title' => 'Eigene Überschrift', 'cta2_text' => 'Termin anfragen']);
|
||
|
||
$this->seed(HomeHeroTextSeeder::class);
|
||
$this->seed(HomeHeroTextSeeder::class);
|
||
|
||
$de = CmsContent::where('group', 'home')->where('key', 'hero')->first()->getTranslation('value', 'de');
|
||
|
||
expect($de['title'])->toBe('Eigene Überschrift')->and($de)->not->toHaveKey('cta2_text');
|
||
});
|
||
|
||
it('does nothing when the hero is not stored in the CMS', function () {
|
||
$this->seed(HomeHeroTextSeeder::class);
|
||
|
||
expect(CmsContent::where('group', 'home')->where('key', 'hero')->exists())->toBeFalse();
|
||
});
|
||
|
||
it('still renders a second hero button when one is maintained', function () {
|
||
storeHomeHero(['title' => 'Titel', 'subtitle' => '', 'image' => 'b2in/hero-room.jpg', 'image_alt' => '', 'cta1_text' => 'Marken', 'cta1_link' => '/marken', 'cta2_text' => 'Showroom besuchen', 'cta2_link' => '/contact', 'card_title' => '', 'card_text' => '']);
|
||
|
||
$this->get('/')->assertSuccessful()->assertSee('Showroom besuchen');
|
||
});
|
||
|
||
// ========================================
|
||
// Projekttext
|
||
// ========================================
|
||
|
||
it('uses the new project text with a full stop', function () {
|
||
$this->get('/')->assertSuccessful()
|
||
->assertSee('Vollständige Einrichtung für Objekte, Büros und Projektentwicklungen. Koordiniert aus einer Hand, von der Küche bis zur letzten Blume. Mit einem zentralen deutschen Vertragspartner und einem eingespielten Herstellernetzwerk.', false);
|
||
});
|
||
|
||
it('turns the dash into a full stop in a stored project text', function () {
|
||
CmsContent::create([
|
||
'group' => 'home',
|
||
'key' => 'audience_switch',
|
||
'type' => 'json',
|
||
'value' => ['de' => ['cards' => [['description' => 'Vollständige Einrichtung für Objekte, Büros und Projektentwicklungen – koordiniert aus einer Hand, von der Küche bis zur letzten Blume. Mit deutscher Vertragssicherheit und einem eingespielten Herstellernetzwerk.']]]],
|
||
'order' => 0,
|
||
]);
|
||
|
||
$this->seed(AudienceSwitchProjectTextSeeder::class);
|
||
|
||
$de = CmsContent::where('group', 'home')->where('key', 'audience_switch')->first()->getTranslation('value', 'de');
|
||
|
||
expect($de['cards'][0]['description'])->toBe('Vollständige Einrichtung für Objekte, Büros und Projektentwicklungen. Koordiniert aus einer Hand, von der Küche bis zur letzten Blume. Mit einem zentralen deutschen Vertragspartner und einem eingespielten Herstellernetzwerk.');
|
||
});
|
||
|
||
// ========================================
|
||
// Otterpohl
|
||
// ========================================
|
||
|
||
it('swaps the Otterpohl title image with the staircase from the gallery', function () {
|
||
$this->seed(CmsBrandSeeder::class);
|
||
$brand = CmsBrand::where('slug', 'tischlerei')->firstOrFail();
|
||
$brand->update([
|
||
'image' => OtterpohlBrandSeeder::FORMER_TITLE_IMAGE,
|
||
'gallery' => [
|
||
['image' => OtterpohlBrandSeeder::TITLE_IMAGE_IN_GALLERY, 'category' => CmsBrand::GALLERY_PROJECT, 'caption' => 'Treppe'],
|
||
['image' => 'marken/otterpohl/werkstatt-02.webp', 'category' => CmsBrand::GALLERY_COMPANY, 'caption' => 'CNC'],
|
||
],
|
||
]);
|
||
|
||
$this->seed(OtterpohlBrandSeeder::class);
|
||
$this->seed(OtterpohlBrandSeeder::class);
|
||
|
||
$brand->refresh();
|
||
|
||
expect($brand->image)->toBe(OtterpohlBrandSeeder::TITLE_IMAGE)
|
||
->and(array_column($brand->gallery, 'image'))->toBe([OtterpohlBrandSeeder::FORMER_TITLE_IMAGE, 'marken/otterpohl/werkstatt-02.webp'])
|
||
->and($brand->gallery[0]['category'])->toBe(CmsBrand::GALLERY_COMPANY)
|
||
->and(file_exists(public_path('img/assets/'.OtterpohlBrandSeeder::TITLE_IMAGE)))->toBeTrue();
|
||
});
|
||
|
||
it('keeps a title image chosen in the backend', function () {
|
||
$this->seed(CmsBrandSeeder::class);
|
||
CmsBrand::where('slug', 'tischlerei')->update(['image' => 'marken/otterpohl/eigenes.webp']);
|
||
|
||
$this->seed(OtterpohlBrandSeeder::class);
|
||
|
||
expect(CmsBrand::where('slug', 'tischlerei')->value('image'))->toBe('marken/otterpohl/eigenes.webp');
|
||
});
|
||
|
||
it('shows the Montage section with its four points as a list', function () {
|
||
$this->seed(CmsBrandSeeder::class);
|
||
|
||
$html = $this->get('/marken/tischlerei')->assertSuccessful()->getContent();
|
||
|
||
expect($html)->toContain('Eine perfekte Montage ist keine Glückssache, sondern das Ergebnis starker Logistik.')
|
||
->toContain('<strong class="font-medium text-foreground">Sichere Prüfung & Terminierung:</strong>')
|
||
->toContain('<strong class="font-medium text-foreground">Perfekt vorbereitete Lieferung:</strong>')
|
||
->toContain('<strong class="font-medium text-foreground">Flexibler Renovierungsschutz:</strong>')
|
||
->toContain('<strong class="font-medium text-foreground">Erweiterter Komfort:</strong>')
|
||
->toContain('Wir lagern Ihre Möbel fachgerecht und sicher ein.')
|
||
->not->toContain('- Sichere Prüfung');
|
||
});
|
||
|
||
it('groups dash paragraphs into lists with an optional bold lead', function () {
|
||
$blocks = CmsBrand::paragraphBlocks(['Einleitung:', '- Eins: Text eins', '- Ohne Einleitung', 'Zwischentext', '– Zwei: Text: mit Doppelpunkt']);
|
||
|
||
expect($blocks)->toBe([
|
||
['type' => 'paragraph', 'text' => 'Einleitung:'],
|
||
['type' => 'list', 'items' => [['lead' => 'Eins', 'text' => 'Text eins'], ['lead' => '', 'text' => 'Ohne Einleitung']]],
|
||
['type' => 'paragraph', 'text' => 'Zwischentext'],
|
||
['type' => 'list', 'items' => [['lead' => 'Zwei', 'text' => 'Text: mit Doppelpunkt']]],
|
||
]);
|
||
});
|
||
|
||
it('sets the founder bar off from the interior heroes', function (string $path) {
|
||
$html = $this->get($path)->assertSuccessful()->getContent();
|
||
expect($html)->toMatch('/<section [^>]*id="gruender"[^>]*class="py-8 bg-accent /');
|
||
|
||
// bg-accent im Glass-Flow-Bereich = linear-gradient(135deg, accent/.5, accent-dark/.5)
|
||
expect(file_get_contents(resource_path('css/web/shared-styles.css')))
|
||
->toContain("& section.bg-accent {\n background: linear-gradient(\n 135deg,\n hsl(var(--accent) /0.5) 0%,\n hsl(var(--accent-dark) / 0.5) 100%");
|
||
})->with(['home' => '/', 'brands' => '/marken', 'about' => '/about']);
|
||
|
||
it('places the founder bar right below the brands hero', function () {
|
||
$html = $this->get('/marken')->assertSuccessful()->getContent();
|
||
|
||
$hero = strpos($html, 'id="seitenkopf"');
|
||
$founder = strpos($html, 'id="gruender"');
|
||
|
||
expect($founder)->toBeGreaterThan($hero)
|
||
->and(substr_count(substr($html, $hero, $founder - $hero), '<section'))->toBe(1);
|
||
});
|