b2in/tests/Feature/HomeDraftTest.php

261 lines
10 KiB
PHP

<?php
declare(strict_types=1);
use App\Livewire\Web\Components\Sections\HeroSlider;
use App\Models\CmsBrand;
use Database\Seeders\AudienceSwitchProjectTextSeeder;
use Database\Seeders\BrandImageSeeder;
use Database\Seeders\CmsBrandSeeder;
use FluxCms\Core\Models\CmsContent;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
uses(RefreshDatabase::class);
function heroSection(string $html): string
{
$start = strpos($html, 'id="hero"');
$end = strpos($html, '</section>', $start);
return substr($html, $start, $end - $start);
}
// ========================================
// Entwurf /entwurf (Nachtrag 19)
// ========================================
it('renders the draft homepage in the greige palette with the gold logos', function () {
$html = $this->get('/entwurf')->assertSuccessful()->getContent();
expect($html)->toMatch('/<body class="[^"]*world-interior palette-greige"/')
->toContain('b2in-interior-logo-gold-positive.svg')
->toContain('b2in-interior-logo-gold-negative.svg')
->not->toContain('b2in-interior-logo-positive.svg')
->toContain('<meta name="robots" content="noindex, nofollow">');
});
it('keeps the regular homepage on the ocher logo and without the palette class', function () {
$html = $this->get('/')->assertSuccessful()->getContent();
expect($html)->not->toContain('palette-greige')
->not->toContain('logo-gold')
->toContain('b2in-interior-logo-positive.svg');
});
it('shows the new headline with one button and no announcement bar', function () {
$html = $this->get('/entwurf')->assertSuccessful()->getContent();
$hero = heroSection($html);
expect($hero)->toContain('<em>Räume aus einer Hand.</em> Mit starken Marken und persönlichen Lösungen.')
->toContain('B2in Interior')
->toContain('Unsere Marken entdecken')
->toContain('href="/marken"')
->not->toContain('Termin anfragen');
// Ankündigungsleiste bleibt auf der regulären Startseite, fehlt im Entwurf.
expect($this->get('/')->getContent())->toContain('Rotpunkt Küchen jetzt bei B2in Interior');
expect($html)->not->toContain('Rotpunkt Küchen jetzt bei B2in Interior');
// Das Termin-Widget unten rechts bleibt.
expect($html)->toContain('appointment-toggle');
});
it('keeps the remaining homepage sections below the new hero', function () {
$html = $this->get('/entwurf')->assertSuccessful()->getContent();
$order = ['id="hero"', 'B2in by Marcel Scheibe', 'Einrichtung für Ihr Zuhause', 'Unsere <span class="text-secondary">Kernmarken', 'id="footer"'];
$last = -1;
foreach ($order as $needle) {
$pos = strpos($html, $needle);
expect($pos)->not->toBeFalse("fehlt: {$needle}")->and($pos)->toBeGreaterThan($last);
$last = $pos;
}
});
it('keeps the greige draft free of the modern class and the switch', function () {
$html = $this->get('/entwurf')->assertSuccessful()->getContent();
expect($html)->not->toContain('class="palette-modern"')
->not->toContain('data-palette-switch');
});
it('can be switched off for the live site', function () {
config(['app.home_draft' => false]);
$this->get('/entwurf')->assertNotFound();
});
// ========================================
// Slideshow
// ========================================
it('runs the brand title images as slides while no slides are maintained', function () {
$this->seed(CmsBrandSeeder::class);
$this->seed(BrandImageSeeder::class);
$slides = Livewire::test(HeroSlider::class)->get('slides');
$count = CmsBrand::query()->published()->get()->filter(fn (CmsBrand $b) => trim((string) $b->image) !== '')->count();
expect(array_slice(array_column($slides, 'label'), 0, 3))->toBe(['CABINET', 'Rotpunkt Küchen', 'Febrü'])
->and($slides)->toHaveCount($count)
->and($slides[0]['image'])->toBe('marken/cabinet/ankleide-solitaer-eiche-tano.webp');
$html = $this->get('/entwurf')->assertSuccessful()->getContent();
$hero = heroSection($html);
expect(substr_count($hero, 'data-hero-slide='))->toBe($count)
->and(substr_count($hero, 'role="tab"'))->toBe($count)
->and($hero)->toContain('marken/rotpunkt/kueche-01.webp')
->toContain('aria-roledescription="carousel"');
});
it('skips brands without a title image', function () {
$this->seed(CmsBrandSeeder::class);
CmsBrand::query()->update(['image' => '']);
CmsBrand::query()->where('slug', 'rotpunkt')->update(['image' => 'marken/rotpunkt/kueche-01.webp']);
$slides = Livewire::test(HeroSlider::class)->get('slides');
expect(array_column($slides, 'label'))->toBe(['Rotpunkt Küchen']);
});
it('says so when no brand has an image yet', function () {
$this->seed(CmsBrandSeeder::class);
CmsBrand::query()->update(['image' => '']);
expect(heroSection($this->get('/entwurf')->assertSuccessful()->getContent()))->toContain('Noch keine Bilder hinterlegt');
});
it('prefers slides maintained in the cms and honours their active flag', function () {
$this->seed(CmsBrandSeeder::class);
$this->seed(BrandImageSeeder::class);
$slides = [
['image' => 'b2in/hero-room.jpg', 'label' => 'Raumkonzepte', 'caption' => 'Alles aus einer Hand'],
['image' => 'b2in/off.jpg', 'label' => 'Aus', 'active' => false],
['image' => '', 'label' => 'Ohne Bild'],
];
CmsContent::create([
'group' => 'home',
'key' => 'hero_slider',
'type' => 'json',
'value' => ['de' => ['title' => 'Test', 'slides' => $slides], 'en' => ['title' => 'Test', 'slides' => $slides]],
'order' => 0,
]);
$result = Livewire::test(HeroSlider::class)->get('slides');
expect($result)->toHaveCount(1)
->and($result[0]['label'])->toBe('Raumkonzepte')
->and($result[0]['image'])->toBe('b2in/hero-room.jpg');
});
it('draws the green flanks as inline svgs that scale with the frame', function () {
$hero = heroSection($this->get('/entwurf')->assertSuccessful()->getContent());
expect($hero)->toContain('data-hero-flank="right"')
->toContain('data-hero-flank="left"')
->toContain('preserveAspectRatio="xMaxYMid meet"')
->toContain('stop-color: hsl(var(--secondary-light))')
->toContain('class="hero-image-frame"');
});
it('clamps the slide interval to a calm range', function () {
CmsContent::create([
'group' => 'home',
'key' => 'hero_slider',
'type' => 'json',
'value' => ['de' => ['title' => 'Test', 'interval' => 1], 'en' => ['title' => 'Test', 'interval' => 1]],
'order' => 0,
]);
expect(Livewire::test(HeroSlider::class)->get('intervalMs'))->toBe(3000);
});
// ========================================
// Farb-Schalter
// ========================================
it('links the draft from the palette switch and back', function () {
config(['app.palette_switch' => true]);
$home = $this->get('/')->assertSuccessful()->getContent();
expect($home)->toContain('Entwurf')->toContain(route('home.draft'));
$draft = $this->get('/entwurf')->assertSuccessful()->getContent();
$switch = substr($draft, strpos($draft, 'data-palette-switch'));
expect($switch)->toContain('aria-current="page"')
->toContain('?palette=classic')
->toContain('?palette=modern')
->not->toContain('classList.add(\'palette-modern\')');
});
// ========================================
// Logos und Stylesheet
// ========================================
it('ships the gold logo copies with the new fir green and without the ocher', function () {
$positive = strtoupper(file_get_contents(public_path('img/logos/b2in-interior-logo-gold-positive.svg')));
$negative = strtoupper(file_get_contents(public_path('img/logos/b2in-interior-logo-gold-negative.svg')));
expect($positive)->toContain('#AA8341')->toContain('#14342C')->not->toContain('#D39E46')->not->toContain('#202C2B');
expect($negative)->toContain('#AA8341')->toContain('#F0EEEB')->not->toContain('#D39E46');
// Die bisherigen Logos bleiben unverändert.
expect(strtoupper(file_get_contents(public_path('img/logos/b2in-interior-logo-positive.svg'))))->toContain('#D39E46');
});
it('defines the greige palette on the body and keeps gold as a hairline', function () {
$css = file_get_contents(resource_path('css/web/theme-b2in.css'));
expect($css)->toContain('.world-interior.palette-greige {')
->toContain('--background: 45 14% 93%')
->toContain('--primary: 165 44% 14%')
->toContain('--secondary: 38 45% 46%')
->toContain('.world-interior.palette-greige .footer-logo-dark')
->toContain('.hero-draft .btn-hero')
// Goldkanten als Verlaufs-Border (padding-box/border-box), auch um das Bild; Button unten kräftiger.
->toContain(') border-box;')
->toContain('.hero-draft .hero-image-frame {')
->toContain('border-bottom-width: 3px')
->not->toContain('mask-composite');
});
// ========================================
// Projekttext (Briefing Punkt 7)
// ========================================
it('uses the new project wording in the lang files', function () {
$html = $this->get('/')->assertSuccessful()->getContent();
expect($html)->toContain('Mit einem zentralen deutschen Vertragspartner und einem eingespielten Herstellernetzwerk.')
->not->toContain('Mit deutscher Vertragssicherheit');
});
it('replaces the project wording in an existing cms row and leaves the rest alone', function () {
$cards = [
['title' => 'Zuhause', 'description' => 'Bleibt so.'],
['title' => 'Projekt', 'description' => 'Vollständige Einrichtung … Mit deutscher Vertragssicherheit und einem eingespielten Herstellernetzwerk.'],
];
CmsContent::create([
'group' => 'home',
'key' => 'audience_switch',
'type' => 'json',
'value' => ['de' => ['title' => 'Für wen?', 'cards' => $cards], 'en' => ['title' => 'For whom?', 'cards' => []]],
'order' => 0,
]);
$this->seed(AudienceSwitchProjectTextSeeder::class);
$this->seed(AudienceSwitchProjectTextSeeder::class);
$row = CmsContent::query()->where('group', 'home')->where('key', 'audience_switch')->firstOrFail();
$de = $row->getTranslation('value', 'de');
expect($de['title'])->toBe('Für wen?')
->and($de['cards'][0]['description'])->toBe('Bleibt so.')
->and($de['cards'][1]['description'])->toBe('Vollständige Einrichtung … Mit einem zentralen deutschen Vertragspartner und einem eingespielten Herstellernetzwerk.');
});