b2in/tests/Feature/HomeCorrectionsTest.php
Kevin Adametz fcb10be120 Interior-Feinschliff: Hero-Slideshow, Lektorat, Rechtstexte, Aufräumen und Korrekturen 08.10.
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>
2026-10-08 14:00:15 +00:00

238 lines
12 KiB
PHP
Raw Permalink 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\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 &amp; 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);
});