Interior-Farbwelt, Welten-Trennung, Tischlerei und Herkunfts-Sektion
Arbeitsplan 14.09.2026, Punkte 1.1 bis 1.4 (Nachträge 6 bis 10 im Entwicklungsplan): - Farbwelt Interior (Tanne, Creme, Sand, Ocker, Clay) als Standard, Immobilien-Welt behält das bisherige Blau über body.world-immobilien; Logo je Welt, Favicons neu, Welten-Wechsler in Kopf- und Fußzeile. - Saubere Trennung der Welten: eigene Navigation, Ankündigungsleiste, Magazinliste, Über-uns-Seite, FAQ, Kontaktseite und Abschluss-CTA für Immobilien; Beiträge tragen eine Welt (Migration + Admin-Feld). Abschluss-CTA Ton in Ton statt Grün-zu-Ocker. - Kontakt: Betreffe in Gruppen Interior / Immobilien / B2in, Marken darin. Terminanfrage-Buttons öffnen das Widget mit vorgewähltem Anliegen; Select-Felder einheitlich gestaltet. - Tischlerei als vierte Kernmarke (Platzhalter), Kachelraster 2×2. - Herkunfts-Sektion „Aus der Region gefertigt“ mit schematischer Standortkarte und Ablauf CABINET (Beratung Bielefeld, Fertigung Köln, Aufbau mit Tischlerei). Deployment: migrate, WorldSplitCmsSeeder, CmsBrandSeeder, OriginSectionCmsSeeder (Abschnitt 8 im Entwicklungsplan). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
1242d4bce8
commit
7953a8c9a1
137 changed files with 3840 additions and 589 deletions
|
|
@ -2,6 +2,9 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Livewire\Web\Components\Ui\AnnouncementBar;
|
||||
use Livewire\Livewire;
|
||||
|
||||
it('announcement bar is visible on homepage', function () {
|
||||
$this->get('/')
|
||||
->assertSuccessful()
|
||||
|
|
@ -12,7 +15,50 @@ it('announcement bar is visible on homepage', function () {
|
|||
it('announcement bar is visible on immobilien page', function () {
|
||||
$this->get('/immobilien')
|
||||
->assertSuccessful()
|
||||
->assertSee('B2in Interior');
|
||||
->assertSee('id="topbar"', false);
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// Eine Meldung je Welt
|
||||
// ========================================
|
||||
|
||||
it('carries the real estate topic in the real estate world and the interior topic elsewhere', function () {
|
||||
$this->get('/immobilien')
|
||||
->assertSuccessful()
|
||||
->assertSee('Azizi Creek Views 4')
|
||||
->assertSee('/immobilien/azizi-creek-views-4')
|
||||
->assertDontSee('Rotpunkt Küchen jetzt bei B2in Interior', false);
|
||||
|
||||
$this->get('/')
|
||||
->assertSuccessful()
|
||||
->assertSee('Rotpunkt Küchen jetzt bei B2in Interior', false)
|
||||
->assertDontSee('Azizi Creek Views 4');
|
||||
});
|
||||
|
||||
it('shows the real estate topic on the real estate magazine and about pages too', function (string $path) {
|
||||
$this->get($path)->assertSuccessful()->assertSee('Azizi Creek Views 4');
|
||||
})->with(['/immobilien/magazin', '/immobilien/ueber-b2in', '/immobilien/faq']);
|
||||
|
||||
it('uses a separate dismiss key per world so closing one bar keeps the other visible', function () {
|
||||
$interior = Livewire::test(AnnouncementBar::class)->get('dismissKey');
|
||||
|
||||
b2in_world_set('immobilien');
|
||||
$immobilien = Livewire::test(AnnouncementBar::class)->get('dismissKey');
|
||||
|
||||
expect($immobilien)->not->toBe($interior);
|
||||
});
|
||||
|
||||
it('takes the real estate bar from the CMS when one is stored', function () {
|
||||
\FluxCms\Core\Models\CmsContent::create([
|
||||
'group' => 'global',
|
||||
'key' => 'announcement_bar_immobilien',
|
||||
'type' => 'json',
|
||||
'value' => ['de' => ['enabled' => true, 'id' => 'cms', 'text' => 'Neues Projekt aus dem CMS', 'link_text' => 'Mehr', 'link_url' => '/immobilien']],
|
||||
'order' => 0,
|
||||
]);
|
||||
|
||||
$this->get('/immobilien')->assertSuccessful()->assertSee('Neues Projekt aus dem CMS');
|
||||
$this->get('/')->assertSuccessful()->assertDontSee('Neues Projekt aus dem CMS');
|
||||
});
|
||||
|
||||
it('announcement bar carries an interior topic, not a dubai launch', function () {
|
||||
|
|
|
|||
87
tests/Feature/AppointmentTriggerTest.php
Normal file
87
tests/Feature/AppointmentTriggerTest.php
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
// ========================================
|
||||
// <x-appointment-link>: Kontakt-Links oeffnen das Widget
|
||||
// ========================================
|
||||
|
||||
it('marks links to the contact pages as widget triggers', function (string $href) {
|
||||
$html = Blade::render('<x-appointment-link :href="$href" class="btn">Termin</x-appointment-link>', ['href' => $href]);
|
||||
|
||||
expect($html)->toContain('data-open-appointment')
|
||||
->toContain('href="'.$href.'"')
|
||||
->toContain('class="btn"');
|
||||
})->with([
|
||||
'interior' => '/contact',
|
||||
'immobilien' => '/immobilien/kontakt',
|
||||
'absolute' => 'https://b2in.test/contact',
|
||||
]);
|
||||
|
||||
it('leaves links to other pages as plain links', function () {
|
||||
$html = Blade::render('<x-appointment-link href="/marken" class="btn">Marken</x-appointment-link>');
|
||||
|
||||
expect($html)->not->toContain('data-open-appointment')
|
||||
->toContain('href="/marken"');
|
||||
});
|
||||
|
||||
it('hands over the topic that should be preselected', function () {
|
||||
$html = Blade::render('<x-appointment-link href="/contact" topic="cabinet">Termin</x-appointment-link>');
|
||||
|
||||
expect($html)->toContain('data-appointment-topic="cabinet"');
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// Sektionen auf der Seite
|
||||
// ========================================
|
||||
|
||||
it('opens the widget from the appointment buttons of the homepage', function () {
|
||||
$html = $this->get('/')->assertSuccessful()->getContent();
|
||||
|
||||
// Hero (zweiter Button), Weiche (beide Karten) und Abschluss-CTA
|
||||
expect(substr_count($html, 'data-open-appointment'))->toBeGreaterThanOrEqual(4);
|
||||
});
|
||||
|
||||
it('preselects the brand when the widget is opened from a brand page', function () {
|
||||
$html = $this->get('/marken/cabinet')->assertSuccessful()->getContent();
|
||||
|
||||
expect($html)->toContain('data-appointment-topic="cabinet"');
|
||||
});
|
||||
|
||||
it('opens the widget from the real estate calls to action too', function () {
|
||||
$html = $this->get('/immobilien')->assertSuccessful()->getContent();
|
||||
|
||||
expect($html)->toContain('data-open-appointment');
|
||||
});
|
||||
|
||||
it('keeps the contact button in the header as a link to the contact page', function () {
|
||||
$html = $this->get('/')->assertSuccessful()->getContent();
|
||||
$header = substr($html, (int) strpos($html, '<header'), (int) strpos($html, '</header>') - (int) strpos($html, '<header'));
|
||||
|
||||
expect($header)->toContain('href="'.route('contact').'"')
|
||||
->not->toContain('data-open-appointment');
|
||||
});
|
||||
|
||||
it('listens for the open event in the widget and intercepts the links in the frontend script', function () {
|
||||
expect($this->get('/')->assertSuccessful()->getContent())->toContain('open-appointment.window');
|
||||
expect(file_get_contents(resource_path('js/app.js')))->toContain('a[data-open-appointment]')
|
||||
->toContain("new CustomEvent('open-appointment'");
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// Formularfelder
|
||||
// ========================================
|
||||
|
||||
it('styles the selects of the widget and the contact page with the shared field classes', function () {
|
||||
expect($this->get('/')->assertSuccessful()->getContent())->toMatch('/<select[^>]*id="appointment-topic"[^>]*class="form-field form-select"/');
|
||||
expect($this->get('/contact')->assertSuccessful()->getContent())->toMatch('/<select[^>]*id="subject"[^>]*class="form-field form-select"/');
|
||||
|
||||
$css = file_get_contents(resource_path('css/web/shared-styles.css'));
|
||||
|
||||
expect($css)->toContain('.form-field')->toContain('.form-select');
|
||||
});
|
||||
|
|
@ -59,11 +59,12 @@ it('marks maintained brands as having a detail page', function () {
|
|||
}
|
||||
});
|
||||
|
||||
it('seeds the three core brands without overwriting maintained ones', function () {
|
||||
it('seeds the four core brands without overwriting maintained ones', function () {
|
||||
$this->seed(\Database\Seeders\CmsBrandSeeder::class);
|
||||
|
||||
expect(CmsBrand::count())->toBe(3);
|
||||
expect(CmsBrand::pluck('slug')->all())->toContain('cabinet', 'rotpunkt', 'februe');
|
||||
// Drei Kernmarken plus die Tischlerei als Platzhalter (Arbeitsplan 14.09., Punkt 1.2).
|
||||
expect(CmsBrand::count())->toBe(4);
|
||||
expect(CmsBrand::pluck('slug')->all())->toContain('cabinet', 'rotpunkt', 'februe', 'tischlerei');
|
||||
|
||||
$cabinet = CmsBrand::where('slug', 'cabinet')->firstOrFail();
|
||||
$cabinet->setTranslation('tagline', 'de', 'Von Hand gepflegt');
|
||||
|
|
@ -71,7 +72,7 @@ it('seeds the three core brands without overwriting maintained ones', function (
|
|||
|
||||
$this->seed(\Database\Seeders\CmsBrandSeeder::class);
|
||||
|
||||
expect(CmsBrand::count())->toBe(3);
|
||||
expect(CmsBrand::count())->toBe(4);
|
||||
expect($cabinet->fresh()->getTranslation('tagline', 'de'))->toBe('Von Hand gepflegt');
|
||||
});
|
||||
|
||||
|
|
|
|||
163
tests/Feature/ColorWorldsTest.php
Normal file
163
tests/Feature/ColorWorldsTest.php
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Helpers\ThemeHelper;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
// ========================================
|
||||
// Farbwelten: Interior (Standard) und Immobilien
|
||||
// ========================================
|
||||
|
||||
it('renders the homepage in the interior world with the interior logo', function () {
|
||||
$html = $this->get('/')->assertSuccessful()->getContent();
|
||||
|
||||
expect($html)->toMatch('/<body class="[^"]*world-interior"/')
|
||||
->not->toMatch('/<body class="[^"]*world-immobilien"/')
|
||||
->toContain('b2in-interior-logo-positive.svg')
|
||||
->not->toContain('img/logos/b2in-logo-positive.svg');
|
||||
});
|
||||
|
||||
it('renders the real estate pages in the immobilien world with the plain logo', function (string $path) {
|
||||
$html = $this->get($path)->assertSuccessful()->getContent();
|
||||
|
||||
expect($html)->toMatch('/<body class="[^"]*world-immobilien"/')
|
||||
->toContain('img/logos/b2in-logo-positive.svg')
|
||||
->toContain('img/logos/b2in-logo-negative.svg')
|
||||
->not->toContain('b2in-interior-logo');
|
||||
})->with([
|
||||
'overview' => '/immobilien',
|
||||
'detail' => '/immobilien/azizi-creek-views-4',
|
||||
]);
|
||||
|
||||
it('keeps the other pages in the interior world', function (string $path) {
|
||||
$html = $this->get($path)->assertSuccessful()->getContent();
|
||||
|
||||
expect($html)->toMatch('/<body class="[^"]*world-interior"/');
|
||||
})->with([
|
||||
'brands' => '/marken',
|
||||
'about' => '/about',
|
||||
'contact' => '/contact',
|
||||
]);
|
||||
|
||||
// ========================================
|
||||
// Logo je Welt
|
||||
// ========================================
|
||||
|
||||
it('resolves the logo per world', function () {
|
||||
config(['app.theme' => 'b2in']);
|
||||
|
||||
expect(ThemeHelper::getLogoPath('positive', 'interior'))->toBe('img/logos/b2in-interior-logo-positive.svg');
|
||||
expect(ThemeHelper::getLogoPath('negative', 'interior'))->toBe('img/logos/b2in-interior-logo-negative.svg');
|
||||
expect(ThemeHelper::getLogoPath('positive', 'immobilien'))->toBe('img/logos/b2in-logo-positive.svg');
|
||||
expect(ThemeHelper::getLogoPath('negative', 'immobilien'))->toBe('img/logos/b2in-logo-negative.svg');
|
||||
});
|
||||
|
||||
it('ignores the world for the other themes', function () {
|
||||
config(['app.theme' => 'b2a']);
|
||||
|
||||
expect(ThemeHelper::getLogoPath('positive', 'immobilien'))->toBe('img/logos/b2a-logo-positiv.svg');
|
||||
});
|
||||
|
||||
it('ships both logo families in both variants as svg and png', function (string $family) {
|
||||
foreach (['positive', 'negative'] as $variant) {
|
||||
expect(file_exists(public_path("img/logos/{$family}-{$variant}.svg")))->toBeTrue();
|
||||
expect(file_exists(public_path("img/logos/{$family}-{$variant}.png")))->toBeTrue();
|
||||
}
|
||||
})->with(['b2in-interior-logo', 'b2in-logo']);
|
||||
|
||||
it('colors the interior logo with the new palette', function () {
|
||||
$positive = strtolower(file_get_contents(public_path('img/logos/b2in-interior-logo-positive.svg')));
|
||||
$negative = strtolower(file_get_contents(public_path('img/logos/b2in-interior-logo-negative.svg')));
|
||||
|
||||
expect($positive)->toContain('#202c2b')->toContain('#d39e46')->not->toContain('#2c9fda');
|
||||
expect($negative)->toContain('#f7f4ef')->toContain('#d39e46')->not->toContain('#2c9fda');
|
||||
});
|
||||
|
||||
it('keeps the blue bow on the real estate logo', function () {
|
||||
$svg = strtolower(file_get_contents(public_path('img/logos/b2in-logo-positive.svg')));
|
||||
|
||||
expect($svg)->toContain('#2c9fda')->not->toContain('#d39e46');
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// Welten-Wechsler in Kopf- und Fusszeile
|
||||
// ========================================
|
||||
|
||||
it('offers the world switcher in header and footer', function () {
|
||||
$html = $this->get('/')->assertSuccessful()->getContent();
|
||||
|
||||
// Desktop-Header, mobiles Menue (nur bei geoeffnetem Menue) und Footer
|
||||
expect(substr_count($html, 'data-world-switch'))->toBeGreaterThanOrEqual(2);
|
||||
expect($html)->toContain('Bereich wechseln');
|
||||
expect(preg_match_all('/data-world-switch.*?>\s*Interior\s*<.*?>\s*Immobilien\s*</s', $html))->toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
it('marks the current world in the switcher', function () {
|
||||
$home = $this->get('/')->assertSuccessful()->getContent();
|
||||
$immobilien = $this->get('/immobilien')->assertSuccessful()->getContent();
|
||||
|
||||
expect($home)->toMatch('/aria-current="page"[^>]*>\s*Interior\s*</');
|
||||
expect($home)->not->toMatch('/aria-current="page"[^>]*>\s*Immobilien\s*</');
|
||||
|
||||
expect($immobilien)->toMatch('/aria-current="page"[^>]*>\s*Immobilien\s*</');
|
||||
expect($immobilien)->not->toMatch('/aria-current="page"[^>]*>\s*Interior\s*</');
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// Themensystem
|
||||
// ========================================
|
||||
|
||||
it('defines the interior palette as default and the blue world as override', function () {
|
||||
$css = file_get_contents(resource_path('css/web/theme-b2in.css'));
|
||||
|
||||
// Interior: Tanne, Creme, Sand, Ocker, Clay, Anthrazit
|
||||
expect($css)->toContain('--primary: 175 16% 15%')
|
||||
->toContain('--background: 38 33% 95%')
|
||||
->toContain('--accent: 28 25% 80%')
|
||||
->toContain('--secondary: 37 62% 55%')
|
||||
->toContain('--clay: 24 15% 57%')
|
||||
->toContain('--foreground: 26 8% 16%')
|
||||
->toContain('.world-immobilien')
|
||||
->toContain('--secondary: 199 74% 49%');
|
||||
});
|
||||
|
||||
it('binds the glow and shadow effects to the accent instead of a fixed blue', function () {
|
||||
$css = file_get_contents(resource_path('css/web/shared-styles.css'));
|
||||
|
||||
expect($css)->not->toContain('rgba(0, 136, 204')
|
||||
->not->toContain('rgba(0, 155, 221')
|
||||
->not->toContain('hsla(199');
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// Abschlussband
|
||||
// ========================================
|
||||
|
||||
it('keeps ocher out of the interior band gradient and uses it for highlighted words instead', function () {
|
||||
$theme = file_get_contents(resource_path('css/web/theme-b2in.css'));
|
||||
$shared = file_get_contents(resource_path('css/web/shared-styles.css'));
|
||||
|
||||
// Interior: Ton in Ton (Tanne), Immobilien: weiter Anthrazit → Dynamic Blue
|
||||
expect($theme)->toContain('--cta-band-end: 175 12% 26%')
|
||||
->toContain('--cta-band-end: 199 74% 42%');
|
||||
|
||||
// Woerter mit `text-primary` im Bandtitel („Ihr nächster Schritt“) laufen auf den Akzent
|
||||
expect($shared)->toContain('.cta-band .text-primary');
|
||||
});
|
||||
|
||||
it('turns highlighted words on accent-colored sections to the primary tone', function () {
|
||||
// /marken: „Lernen Sie unsere Marken kennen“ liegt auf einer Ocker-Flaeche; das Wort
|
||||
// „Marken“ traegt im CMS `text-secondary` und waere sonst Ocker auf Ocker.
|
||||
$shared = file_get_contents(resource_path('css/web/shared-styles.css'));
|
||||
|
||||
expect($shared)->toContain('section.bg-secondary .text-secondary');
|
||||
|
||||
// Livewire haengt seine Attribute vor `class`, deshalb ein Muster statt eines festen Strings.
|
||||
$html = $this->get('/marken')->assertSuccessful()->getContent();
|
||||
|
||||
expect($html)->toMatch('/<section [^>]*class="section-padding bg-secondary/')
|
||||
->toContain('<span class="text-secondary">Marken</span>');
|
||||
});
|
||||
|
|
@ -30,7 +30,7 @@ it('contact form requires privacy acceptance', function () {
|
|||
->set('firstName', 'Max')
|
||||
->set('lastName', 'Mustermann')
|
||||
->set('email', 'max@example.com')
|
||||
->set('subject', 'Allgemein')
|
||||
->set('subject', 'general')
|
||||
->set('message', 'Testnachricht')
|
||||
->set('privacy', false)
|
||||
->call('submit')
|
||||
|
|
@ -47,7 +47,7 @@ it('contact form sends email on valid submission', function () {
|
|||
->set('firstName', 'Max')
|
||||
->set('lastName', 'Mustermann')
|
||||
->set('email', 'max@example.com')
|
||||
->set('subject', 'Allgemein')
|
||||
->set('subject', 'general')
|
||||
->set('message', 'Testnachricht für das Kontaktformular')
|
||||
->set('privacy', true)
|
||||
->call('submit')
|
||||
|
|
@ -65,7 +65,7 @@ it('contact form resets after successful submission', function () {
|
|||
->set('firstName', 'Max')
|
||||
->set('lastName', 'Mustermann')
|
||||
->set('email', 'max@example.com')
|
||||
->set('subject', 'Allgemein')
|
||||
->set('subject', 'general')
|
||||
->set('message', 'Testnachricht')
|
||||
->set('privacy', true)
|
||||
->call('submit')
|
||||
|
|
@ -81,7 +81,7 @@ it('contact form blocks honeypot spam', function () {
|
|||
->set('firstName', 'Max')
|
||||
->set('lastName', 'Mustermann')
|
||||
->set('email', 'max@example.com')
|
||||
->set('subject', 'Allgemein')
|
||||
->set('subject', 'general')
|
||||
->set('message', 'Testnachricht')
|
||||
->set('privacy', true)
|
||||
->set('website', 'https://spam.com')
|
||||
|
|
|
|||
90
tests/Feature/FourthBrandTest.php
Normal file
90
tests/Feature/FourthBrandTest.php
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
<?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);
|
||||
|
||||
// ========================================
|
||||
// Tischlerei als vierte Kernmarke (Arbeitsplan 14.09., Punkt 1.2 – Platzhalter)
|
||||
// ========================================
|
||||
|
||||
beforeEach(function () {
|
||||
$this->seed(\Database\Seeders\CmsBrandSeeder::class);
|
||||
});
|
||||
|
||||
it('creates the joinery as the fourth brand without touching the existing ones', function () {
|
||||
expect(CmsBrand::count())->toBe(4);
|
||||
|
||||
$brand = CmsBrand::where('slug', 'tischlerei')->firstOrFail();
|
||||
|
||||
expect($brand->order)->toBe(4)
|
||||
->and($brand->is_published)->toBeTrue()
|
||||
->and($brand->logo)->toBe('')
|
||||
->and($brand->sections)->toHaveCount(3);
|
||||
|
||||
// Erneutes Seeden legt nichts doppelt an.
|
||||
$this->seed(\Database\Seeders\CmsBrandSeeder::class);
|
||||
expect(CmsBrand::count())->toBe(4);
|
||||
});
|
||||
|
||||
it('shows four tiles on the homepage in a two by two grid', function () {
|
||||
$html = $this->get('/')->assertSuccessful()->getContent();
|
||||
|
||||
expect($html)->toContain('Tischlerei')
|
||||
->toContain('/marken/tischlerei')
|
||||
->toMatch('/<div [^>]*class="grid sm:grid-cols-2 gap-8[^"]*"/')
|
||||
->not->toContain('xl:grid-cols-4');
|
||||
});
|
||||
|
||||
it('lists the joinery on the brands page after the three established brands', function () {
|
||||
$html = $this->get('/marken')->assertSuccessful()->getContent();
|
||||
|
||||
preg_match_all('#wire:key="brand-([a-z-]+)"#', $html, $matches);
|
||||
|
||||
expect($matches[1])->toBe(['cabinet', 'rotpunkt', 'februe', 'tischlerei']);
|
||||
expect($html)->toContain('Sonderlösungen nach Aufmaß');
|
||||
});
|
||||
|
||||
it('serves the joinery detail page with the placeholder texts and the wordmark', function () {
|
||||
$this->get('/marken/tischlerei')
|
||||
->assertSuccessful()
|
||||
->assertSee('<h1 class="text-section-title">Tischlerei</h1>', false)
|
||||
->assertSee('Maßarbeit, die den Raum vollendet.', false)
|
||||
->assertSee('Wo Serienmöbel aufhören.', false)
|
||||
->assertSee('Ergänzung, nicht Konkurrenz.', false)
|
||||
->assertSee('Ihr Raum. Bis ins Detail gefertigt.', false)
|
||||
->assertSee('data-appointment-topic="tischlerei"', false);
|
||||
});
|
||||
|
||||
it('offers the joinery among the other brands of every detail page, three across', function () {
|
||||
$html = $this->get('/marken/cabinet')->assertSuccessful()->getContent();
|
||||
|
||||
expect($html)->toContain('other-tischlerei')
|
||||
->toContain('sm:grid-cols-2 lg:grid-cols-3');
|
||||
});
|
||||
|
||||
it('labels the widget on the joinery page and offers the joinery as a topic everywhere', function () {
|
||||
$this->get('/marken/tischlerei')
|
||||
->assertSuccessful()
|
||||
->assertSee('Persönliche Beratung zur Maßarbeit vereinbaren', false);
|
||||
|
||||
expect(Livewire::test(AppointmentWidget::class)->get('topics'))->toHaveKey('tischlerei');
|
||||
|
||||
$this->get('/contact')
|
||||
->assertSuccessful()
|
||||
->assertSee('value="tischlerei"', false);
|
||||
});
|
||||
|
||||
it('keeps the joinery in the lang fallback while no brands are maintained', function () {
|
||||
CmsBrand::query()->delete();
|
||||
|
||||
$this->get('/')
|
||||
->assertSuccessful()
|
||||
->assertSee('Tischlerei')
|
||||
->assertSee('Maßarbeit, die den Raum vollendet.', false);
|
||||
});
|
||||
|
|
@ -98,7 +98,7 @@ it('describes Febrü with the home office first', function () {
|
|||
// Navigation
|
||||
// ========================================
|
||||
|
||||
it('puts brands before real estate in the navigation and drops the network entry', function () {
|
||||
it('leads the interior navigation with brands and leaves real estate to the world switcher', function () {
|
||||
$html = $this->get('/')->assertSuccessful()->getContent();
|
||||
|
||||
$navStart = (int) strpos($html, 'aria-label="Hauptnavigation"');
|
||||
|
|
@ -106,9 +106,11 @@ it('puts brands before real estate in the navigation and drops the network entry
|
|||
|
||||
expect($nav)->toContain('Marken');
|
||||
expect($nav)->toContain('/marken');
|
||||
expect($nav)->toContain('Immobilien');
|
||||
// „Immobilien“ ist seit der Welten-Trennung kein Navigationspunkt mehr – der Wechsler
|
||||
// in Kopf- und Fusszeile ist der einzige Weg in die Immobilien-Welt.
|
||||
expect($nav)->not->toContain('href="/immobilien"');
|
||||
expect($nav)->not->toContain('Netzwerk');
|
||||
expect(strpos($nav, 'Marken'))->toBeLessThan(strpos($nav, 'Immobilien'));
|
||||
expect(strpos($nav, 'Marken'))->toBeLessThan(strpos($nav, 'Magazin'));
|
||||
});
|
||||
|
||||
it('sends the retired network page to the brands page', function () {
|
||||
|
|
@ -174,10 +176,11 @@ it('does not turn the sponsor tiles into links', function () {
|
|||
// ========================================
|
||||
|
||||
it('uses the interior logo in the header and footer', function () {
|
||||
$this->get('/')
|
||||
->assertSuccessful()
|
||||
->assertSee('b2in-interior-logo-positive.svg')
|
||||
->assertSee('b2in-interior-logo-negative.svg');
|
||||
// Beide Male die positive Variante: der Interior-Footer liegt auf Sand, nicht auf dunklem Grund.
|
||||
$html = $this->get('/')->assertSuccessful()->getContent();
|
||||
|
||||
expect(substr_count($html, 'b2in-interior-logo-positive.svg'))->toBeGreaterThanOrEqual(2);
|
||||
expect($html)->not->toContain('img/logos/b2in-logo-positive.svg');
|
||||
});
|
||||
|
||||
it('ships the interior logo files in both variants', function () {
|
||||
|
|
@ -190,8 +193,9 @@ it('ships the interior logo files in both variants', function () {
|
|||
it('crops the logo viewBox to the artwork', function () {
|
||||
// Die Lieferung lag auf einem A4-Artboard mit 74 % Weissraum. Ungeschnitten wuerde das
|
||||
// Logo bei gleicher CSS-Hoehe rund halb so gross rendern wie das bisherige.
|
||||
// Zuschnitt der Fassung vom 07.09.2026 (Tanne/Ocker): Inhalt 153,3–440,5 × 95,3–320,8.
|
||||
$svg = file_get_contents(public_path('img/logos/b2in-interior-logo-positive.svg'));
|
||||
|
||||
expect($svg)->toContain('viewBox="153 96 289.3 227.5"');
|
||||
expect($svg)->toContain('viewBox="152.5 94.5 289 228"');
|
||||
expect($svg)->not->toContain('595.3 419.5');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -72,13 +72,15 @@ it('immobilien has all five sections', function () {
|
|||
->assertSee('Investoren');
|
||||
});
|
||||
it('marken page lists the core brands', fn () => $this->get('/marken')->assertSee('CABINET')->assertSee('Febrü', false));
|
||||
it('magazin has all five articles', function () {
|
||||
it('magazin has all articles across both worlds', function () {
|
||||
$this->get('/magazin')
|
||||
->assertSee('Escrow-System')
|
||||
->assertSee('Al Jaddaf')
|
||||
->assertSee('Turnkey')
|
||||
->assertSee('Supply-Chain')
|
||||
->assertSee('Local for Local');
|
||||
|
||||
$this->get('/immobilien/magazin')
|
||||
->assertSee('Escrow-System')
|
||||
->assertSee('Al Jaddaf')
|
||||
->assertSee('Turnkey');
|
||||
});
|
||||
|
||||
// ========================================
|
||||
|
|
|
|||
|
|
@ -13,14 +13,23 @@ it('magazin list page loads successfully', function () {
|
|||
->assertSee('Magazin');
|
||||
});
|
||||
|
||||
it('magazin list shows all five articles', function () {
|
||||
it('magazin list shows the interior articles and leaves the dubai ones to the real estate list', function () {
|
||||
$this->get('/magazin')
|
||||
->assertSuccessful()
|
||||
->assertSee('Nach Maß statt von der Stange', false)
|
||||
->assertSee('Supply-Chain-Management')
|
||||
->assertSee('Local for Local')
|
||||
->assertDontSee('Escrow-System')
|
||||
->assertDontSee('Al Jaddaf')
|
||||
->assertDontSee('Turnkey-Investments');
|
||||
|
||||
$this->get('/immobilien/magazin')
|
||||
->assertSuccessful()
|
||||
->assertSee('Escrow-System')
|
||||
->assertSee('Al Jaddaf')
|
||||
->assertSee('Turnkey-Investments')
|
||||
->assertSee('Supply-Chain-Management')
|
||||
->assertSee('Local for Local');
|
||||
->assertDontSee('Supply-Chain-Management')
|
||||
->assertDontSee('Local for Local');
|
||||
});
|
||||
|
||||
it('magazin detail page loads for the first article', function () {
|
||||
|
|
@ -48,7 +57,7 @@ it('returns 404 for an unknown slug instead of silently showing the first articl
|
|||
});
|
||||
|
||||
it('links to the articles by slug, not by id', function () {
|
||||
$this->get('/magazin')
|
||||
$this->get('/immobilien/magazin')
|
||||
->assertSuccessful()
|
||||
->assertSee('/magazin/escrow-system-dubai-investoren');
|
||||
});
|
||||
|
|
@ -119,7 +128,7 @@ it('falls back to the lang file while no articles are maintained', function () {
|
|||
|
||||
$this->get('/magazin')
|
||||
->assertSuccessful()
|
||||
->assertSee('Escrow-System');
|
||||
->assertSee('Local for Local');
|
||||
});
|
||||
|
||||
it('prefers the CMS over the lang file once articles exist', function () {
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ it('shows a breadcrumb from home to brands', function () {
|
|||
it('carries the title and description from the spec', function () {
|
||||
$this->get('/marken')
|
||||
->assertSuccessful()
|
||||
->assertSee('Unsere Marken – CABINET, Rotpunkt Küchen & Febrü | B2in Interior', false)
|
||||
->assertSee('Unsere Marken – CABINET, Rotpunkt Küchen, Febrü & Tischlerei | B2in Interior', false)
|
||||
->assertSee('Ausgewählte Hersteller statt beliebiger Kataloge', false);
|
||||
});
|
||||
|
||||
|
|
@ -96,12 +96,12 @@ it('keeps the brand order from the CMS', function () {
|
|||
};
|
||||
|
||||
expect($headings($this->get('/marken')->assertSuccessful()->getContent()))
|
||||
->toBe(['cabinet', 'rotpunkt', 'februe']);
|
||||
->toBe(['cabinet', 'rotpunkt', 'februe', 'tischlerei']);
|
||||
|
||||
CmsBrand::where('slug', 'februe')->update(['order' => 0]);
|
||||
|
||||
expect($headings($this->get('/marken')->getContent()))
|
||||
->toBe(['februe', 'cabinet', 'rotpunkt']);
|
||||
->toBe(['februe', 'cabinet', 'rotpunkt', 'tischlerei']);
|
||||
});
|
||||
|
||||
it('hides unpublished brands', function () {
|
||||
|
|
|
|||
169
tests/Feature/OriginSectionTest.php
Normal file
169
tests/Feature/OriginSectionTest.php
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Livewire\Web\Components\Sections\OriginSection;
|
||||
use FluxCms\Core\Models\CmsContent;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
/**
|
||||
* Herkunfts-Sektion „Aus der Region gefertigt“ (Arbeitsplan 14.09.2026, Punkt 1.3).
|
||||
*/
|
||||
function originBody(string $html): string
|
||||
{
|
||||
$html = preg_replace('/wire:snapshot="[^"]*"/', '', $html) ?? $html;
|
||||
|
||||
return substr($html, (int) strpos($html, '<main'));
|
||||
}
|
||||
|
||||
it('shows the four locations on the homepage', function () {
|
||||
$this->get('/')
|
||||
->assertSuccessful()
|
||||
->assertSee('id="herkunft"', false)
|
||||
->assertSee('Aus der Region', false)
|
||||
->assertSee('Bünde', false)
|
||||
->assertSee('Herford')
|
||||
->assertSee('Rheda-Wiedenbrück', false)
|
||||
->assertSee('Köln', false);
|
||||
});
|
||||
|
||||
it('sits between the core brands and the founder story on the homepage', function () {
|
||||
$html = originBody($this->get('/')->assertSuccessful()->getContent());
|
||||
|
||||
$brands = strpos($html, 'id="marken"');
|
||||
$origin = strpos($html, 'id="herkunft"');
|
||||
$story = strpos($html, 'Gebaut auf Expertise');
|
||||
|
||||
expect($brands)->not->toBeFalse();
|
||||
expect($origin)->toBeGreaterThan($brands);
|
||||
expect($story)->toBeGreaterThan($origin);
|
||||
});
|
||||
|
||||
it('appears on the brands page after the brand overview and before the network', function () {
|
||||
$this->seed(\Database\Seeders\CmsBrandSeeder::class);
|
||||
|
||||
$html = originBody($this->get('/marken')->assertSuccessful()->getContent());
|
||||
|
||||
$overview = strpos($html, '/marken/cabinet');
|
||||
$origin = strpos($html, 'id="herkunft"');
|
||||
$network = strpos($html, 'unser Netzwerk');
|
||||
|
||||
expect($overview)->not->toBeFalse();
|
||||
expect($origin)->toBeGreaterThan($overview);
|
||||
expect($network)->toBeGreaterThan($origin);
|
||||
});
|
||||
|
||||
it('links every location to its brand page', function () {
|
||||
$this->seed(\Database\Seeders\CmsBrandSeeder::class);
|
||||
|
||||
$html = originBody($this->get('/')->assertSuccessful()->getContent());
|
||||
$section = substr($html, (int) strpos($html, 'id="herkunft"'));
|
||||
$section = substr($section, 0, (int) strpos($section, '</section>'));
|
||||
|
||||
foreach (['/marken/rotpunkt', '/marken/februe', '/marken/tischlerei', '/marken/cabinet'] as $link) {
|
||||
expect($section)->toContain('href="'.$link.'"');
|
||||
}
|
||||
});
|
||||
|
||||
it('drops the link of an unpublished brand but keeps its location', function () {
|
||||
$this->seed(\Database\Seeders\CmsBrandSeeder::class);
|
||||
\App\Models\CmsBrand::query()->where('slug', 'februe')->update(['is_published' => false]);
|
||||
|
||||
$html = originBody($this->get('/')->assertSuccessful()->getContent());
|
||||
$section = substr($html, (int) strpos($html, 'id="herkunft"'));
|
||||
$section = substr($section, 0, (int) strpos($section, '</section>'));
|
||||
|
||||
expect($section)->toContain('Herford')
|
||||
->toContain('href="/marken/cabinet"')
|
||||
->not->toContain('/marken/februe');
|
||||
});
|
||||
|
||||
it('finds the brand page by name when the lang fallback uses other slugs', function () {
|
||||
$html = originBody($this->get('/')->assertSuccessful()->getContent());
|
||||
$section = substr($html, (int) strpos($html, 'id="herkunft"'));
|
||||
$section = substr($section, 0, (int) strpos($section, '</section>'));
|
||||
|
||||
expect($section)->toContain('href="/marken/cabinet"')
|
||||
->toContain('href="/marken/tischlerei"');
|
||||
});
|
||||
|
||||
it('draws Bielefeld as the hub with one spoke per location', function () {
|
||||
$html = $this->get('/')->assertSuccessful()->getContent();
|
||||
|
||||
expect($html)->toContain('class="origin-map"')
|
||||
->toContain('origin-map-hub')
|
||||
->toContain('Ostwestfalen');
|
||||
expect(substr_count($html, 'class="origin-map-spoke"'))->toBe(4);
|
||||
expect(substr_count($html, 'class="origin-map-dot"'))->toBe(4);
|
||||
});
|
||||
|
||||
it('separates manufacturing from sales for CABINET', function () {
|
||||
$this->get('/')
|
||||
->assertSuccessful()
|
||||
->assertSee('Gefertigt in Köln', false)
|
||||
->assertSee('Beratung und Planung in Bielefeld')
|
||||
->assertSee('Fertigung in Köln', false)
|
||||
->assertSee('Aufbau vor Ort', false);
|
||||
});
|
||||
|
||||
it('prefers the CMS content over the lang fallback', function () {
|
||||
CmsContent::create([
|
||||
'group' => 'home',
|
||||
'key' => 'origin_section',
|
||||
'type' => 'json',
|
||||
'value' => ['de' => [
|
||||
'title' => 'Gefertigt in <span class="text-secondary">OWL</span>',
|
||||
'hub' => ['city' => 'Bielefeld', 'x' => 400, 'y' => 175],
|
||||
'locations' => [
|
||||
['city' => 'Verl', 'brand' => 'Testmarke', 'x' => 300, 'y' => 100],
|
||||
],
|
||||
]],
|
||||
'order' => 0,
|
||||
]);
|
||||
|
||||
$this->get('/')
|
||||
->assertSuccessful()
|
||||
->assertSee('Gefertigt in <span class="text-secondary">OWL</span>', false)
|
||||
->assertSee('Verl')
|
||||
->assertSee('Testmarke')
|
||||
->assertDontSee('Rheda-Wiedenbrück');
|
||||
});
|
||||
|
||||
it('skips locations without brand or city instead of breaking the map', function () {
|
||||
Livewire::test(OriginSection::class)
|
||||
->set('content', [
|
||||
'title' => 'Herkunft',
|
||||
'locations' => [
|
||||
['city' => 'Bünde', 'brand' => 'Rotpunkt Küchen', 'x' => 1, 'y' => 1],
|
||||
['city' => '', 'brand' => 'Ohne Stadt'],
|
||||
'kaputt',
|
||||
['brand' => '', 'city' => 'Ohne Marke'],
|
||||
],
|
||||
])
|
||||
->assertSee('Rotpunkt Küchen', false)
|
||||
->assertDontSee('Ohne Stadt')
|
||||
->assertDontSee('Ohne Marke');
|
||||
});
|
||||
|
||||
it('renders without the steps block when the CMS leaves it empty', function () {
|
||||
Livewire::test(OriginSection::class)
|
||||
->set('content', ['title' => 'Herkunft', 'locations' => [], 'steps' => []])
|
||||
->assertSee('Herkunft')
|
||||
->assertDontSee('<ol', false);
|
||||
});
|
||||
|
||||
it('creates the CMS section only when it is missing', function () {
|
||||
$this->seed(\Database\Seeders\OriginSectionCmsSeeder::class);
|
||||
|
||||
$row = CmsContent::query()->where('group', 'home')->where('key', 'origin_section')->first();
|
||||
expect($row)->not->toBeNull();
|
||||
expect($row->getTranslation('value', 'de')['locations'])->toHaveCount(4);
|
||||
|
||||
$row->update(['value' => ['de' => ['title' => 'Vom Admin geändert']]]);
|
||||
$this->seed(\Database\Seeders\OriginSectionCmsSeeder::class);
|
||||
|
||||
expect($row->fresh()->getTranslation('value', 'de')['title'])->toBe('Vom Admin geändert');
|
||||
});
|
||||
339
tests/Feature/WorldSplitTest.php
Normal file
339
tests/Feature/WorldSplitTest.php
Normal file
|
|
@ -0,0 +1,339 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Livewire\Web\Components\Ui\AppointmentWidget;
|
||||
use App\Models\CmsArticle;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
use Livewire\Volt\Volt;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
function mainNavigation(string $html): string
|
||||
{
|
||||
$start = (int) strpos($html, 'aria-label="Hauptnavigation"');
|
||||
|
||||
return substr($html, $start, (int) strpos($html, '</nav>', $start) - $start);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Navigation je Welt
|
||||
// ========================================
|
||||
|
||||
it('shows the interior navigation without a real estate entry', function () {
|
||||
$nav = mainNavigation($this->get('/')->assertSuccessful()->getContent());
|
||||
|
||||
expect($nav)->toContain('/marken')
|
||||
->toContain('/referenzen')
|
||||
->toContain('href="/magazin"')
|
||||
->toContain('/about')
|
||||
->not->toContain('href="/immobilien"');
|
||||
});
|
||||
|
||||
it('shows the real estate navigation on the real estate pages', function (string $path) {
|
||||
$nav = mainNavigation($this->get($path)->assertSuccessful()->getContent());
|
||||
|
||||
expect($nav)->toContain('href="/immobilien"')
|
||||
->toContain('/immobilien#projekte')
|
||||
->toContain('/immobilien/magazin')
|
||||
->toContain('/immobilien/ueber-b2in')
|
||||
->not->toContain('/marken')
|
||||
->not->toContain('/referenzen');
|
||||
})->with([
|
||||
'overview' => '/immobilien',
|
||||
'detail' => '/immobilien/azizi-creek-views-4',
|
||||
'magazine' => '/immobilien/magazin',
|
||||
'about' => '/immobilien/ueber-b2in',
|
||||
'faq' => '/immobilien/faq',
|
||||
'contact' => '/immobilien/kontakt',
|
||||
]);
|
||||
|
||||
it('marks only the most specific navigation entry as active', function () {
|
||||
$nav = mainNavigation($this->get('/immobilien/magazin')->assertSuccessful()->getContent());
|
||||
|
||||
preg_match_all('/<a href="([^"]+)"[^>]*class="[^"]*after:bg-secondary/', $nav, $matches);
|
||||
|
||||
expect($matches[1])->toBe(['/immobilien/magazin']);
|
||||
});
|
||||
|
||||
it('points the footer of the real estate world to its own pages', function () {
|
||||
$html = $this->get('/immobilien')->assertSuccessful()->getContent();
|
||||
|
||||
expect($html)->toContain('href="/immobilien/faq"')
|
||||
->not->toContain('href="/faq"');
|
||||
|
||||
$home = $this->get('/')->assertSuccessful()->getContent();
|
||||
|
||||
expect($home)->toContain('href="/faq"');
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// Magazin: zwei Listen, Detailseite in der Welt des Beitrags
|
||||
// ========================================
|
||||
|
||||
it('renders the real estate magazine in the real estate world', function () {
|
||||
$html = $this->get('/immobilien/magazin')->assertSuccessful()->getContent();
|
||||
|
||||
expect($html)->toMatch('/<body class="[^"]*world-immobilien"/')
|
||||
->toContain('Immobilien-Magazin')
|
||||
->toContain('img/logos/b2in-logo-positive.svg');
|
||||
});
|
||||
|
||||
it('renders a dubai article in the real estate world and leads back to its list', function () {
|
||||
$html = $this->get('/magazin/escrow-system-dubai-investoren')->assertSuccessful()->getContent();
|
||||
|
||||
expect($html)->toMatch('/<body class="[^"]*world-immobilien"/')
|
||||
->toContain('img/logos/b2in-logo-positive.svg')
|
||||
->toContain('href="'.route('immobilien.magazin').'"')
|
||||
->not->toContain('b2in-interior-logo');
|
||||
});
|
||||
|
||||
it('renders an interior article in the interior world', function () {
|
||||
$html = $this->get('/magazin/massanfertigung-warum-der-millimeter-zaehlt')->assertSuccessful()->getContent();
|
||||
|
||||
expect($html)->toMatch('/<body class="[^"]*world-interior"/')
|
||||
->toContain('b2in-interior-logo-positive.svg')
|
||||
->toContain('href="'.route('magazin').'"');
|
||||
});
|
||||
|
||||
it('suggests related articles from the same world only', function () {
|
||||
$html = $this->get('/magazin/escrow-system-dubai-investoren')->assertSuccessful()->getContent();
|
||||
|
||||
expect($html)->toContain('Al Jaddaf')
|
||||
->not->toContain('Local for Local')
|
||||
->not->toContain('Nach Maß statt von der Stange');
|
||||
});
|
||||
|
||||
it('keeps the supply chain article in the interior world', function () {
|
||||
$this->get('/magazin')->assertSuccessful()->assertSee('Supply-Chain-Management');
|
||||
$this->get('/immobilien/magazin')->assertSuccessful()->assertDontSee('Supply-Chain-Management');
|
||||
});
|
||||
|
||||
it('sorts maintained articles into the list of their world', function () {
|
||||
CmsArticle::factory()->create(['slug' => 'wohnzimmer', 'title' => 'Beitrag fürs Wohnzimmer', 'world' => 'interior', 'order' => 1]);
|
||||
CmsArticle::factory()->create(['slug' => 'dubai-marina', 'title' => 'Beitrag zur Dubai Marina', 'world' => 'immobilien', 'order' => 2]);
|
||||
|
||||
$this->get('/magazin')
|
||||
->assertSuccessful()
|
||||
->assertSee('Beitrag fürs Wohnzimmer')
|
||||
->assertDontSee('Beitrag zur Dubai Marina');
|
||||
|
||||
$this->get('/immobilien/magazin')
|
||||
->assertSuccessful()
|
||||
->assertSee('Beitrag zur Dubai Marina')
|
||||
->assertDontSee('Beitrag fürs Wohnzimmer');
|
||||
|
||||
expect(array_keys(CmsArticle::forFrontend('immobilien')))->toBe(['dubai-marina']);
|
||||
expect(array_keys(CmsArticle::forFrontend()))->toBe(['wohnzimmer', 'dubai-marina']);
|
||||
});
|
||||
|
||||
it('assigns the three legacy dubai articles to the real estate world in the lang fallback', function () {
|
||||
$worlds = array_map(fn (array $article) => $article['world'], CmsArticle::forFrontend());
|
||||
|
||||
foreach (CmsArticle::LEGACY_IMMOBILIEN_SLUGS as $slug) {
|
||||
expect($worlds[$slug])->toBe('immobilien');
|
||||
}
|
||||
|
||||
expect($worlds['supply-chain-management-vertragssicherheit'])->toBe('interior');
|
||||
expect($worlds['local-for-local-regionaler-moebelhandel'])->toBe('interior');
|
||||
});
|
||||
|
||||
it('returns 404 for an unknown article on the world-aware detail route', function () {
|
||||
$this->get('/magazin/gibt-es-nicht')->assertNotFound();
|
||||
});
|
||||
|
||||
it('lets the editor choose the world of an article', function () {
|
||||
$this->actingAs(\App\Models\User::factory()->create());
|
||||
|
||||
Volt::test('admin.cms.articles-index')
|
||||
->call('openCreate')
|
||||
->set('slug', 'neuer-beitrag')
|
||||
->set('articleTitle', 'Neuer Beitrag')
|
||||
->set('world', 'immobilien')
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
expect(CmsArticle::where('slug', 'neuer-beitrag')->value('world'))->toBe('immobilien');
|
||||
});
|
||||
|
||||
it('rejects an unknown world in the editor', function () {
|
||||
$this->actingAs(\App\Models\User::factory()->create());
|
||||
|
||||
Volt::test('admin.cms.articles-index')
|
||||
->call('openCreate')
|
||||
->set('slug', 'neuer-beitrag')
|
||||
->set('articleTitle', 'Neuer Beitrag')
|
||||
->set('world', 'shop')
|
||||
->call('save')
|
||||
->assertHasErrors(['world']);
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// Über uns und FAQ der Immobilien-Welt
|
||||
// ========================================
|
||||
|
||||
it('serves a real estate about page with the pre-relaunch story, marked as such', function () {
|
||||
$html = $this->get('/immobilien/ueber-b2in')->assertSuccessful()->getContent();
|
||||
|
||||
expect($html)->toMatch('/<body class="[^"]*world-immobilien"/')
|
||||
->toContain('B2in Immobilien')
|
||||
->toContain('Connecting Design and')
|
||||
->toContain('Die Erweiterung')
|
||||
->not->toContain('Supply-Chain');
|
||||
|
||||
$this->get('/about')
|
||||
->assertSuccessful()
|
||||
->assertDontSee('Connecting Design and');
|
||||
});
|
||||
|
||||
it('serves a real estate faq and keeps those questions out of the interior faq', function () {
|
||||
$this->get('/immobilien/faq')
|
||||
->assertSuccessful()
|
||||
->assertSee('Muss ich für den Immobilienkauf persönlich nach Dubai reisen', false)
|
||||
->assertSee('world-immobilien', false);
|
||||
|
||||
$this->get('/faq')
|
||||
->assertSuccessful()
|
||||
->assertDontSee('persönlich nach Dubai reisen')
|
||||
->assertSee('Maßanfertigung', false);
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// Terminanfrage-Widget
|
||||
// ========================================
|
||||
|
||||
it('preselects the real estate topic in the real estate world', function () {
|
||||
b2in_world_set('immobilien');
|
||||
|
||||
Livewire::test(AppointmentWidget::class)
|
||||
->assertSet('world', 'immobilien')
|
||||
->assertSet('topic', 'immobilien');
|
||||
});
|
||||
|
||||
it('leaves the topic open in the interior world', function () {
|
||||
Livewire::test(AppointmentWidget::class)
|
||||
->assertSet('world', 'interior')
|
||||
->assertSet('topic', '');
|
||||
});
|
||||
|
||||
it('offers real estate as a fixed topic', function () {
|
||||
expect(Livewire::test(AppointmentWidget::class)->get('topics')['immobilien'])->toBe('Immobilien');
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// Kontaktseite je Welt
|
||||
// ========================================
|
||||
|
||||
it('serves a real estate contact page in the real estate world', function () {
|
||||
$html = $this->get('/immobilien/kontakt')->assertSuccessful()->getContent();
|
||||
|
||||
expect($html)->toMatch('/<body class="[^"]*world-immobilien"/')
|
||||
->toContain('Projekt?')
|
||||
->toContain('Exposé und Verfügbarkeit')
|
||||
->toContain('Rathausstraße 11');
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// Betreffe in Gruppen
|
||||
// ========================================
|
||||
|
||||
it('groups the subjects into interior, real estate and b2in with the brands first', function () {
|
||||
$html = $this->get('/contact')->assertSuccessful()->getContent();
|
||||
|
||||
expect($html)->toContain('<optgroup label="Interior">')
|
||||
->toContain('<optgroup label="Immobilien">')
|
||||
->toContain('<optgroup label="B2in">');
|
||||
|
||||
$interior = strpos($html, '<optgroup label="Interior">');
|
||||
$immobilien = strpos($html, '<optgroup label="Immobilien">');
|
||||
$b2in = strpos($html, '<optgroup label="B2in">');
|
||||
|
||||
expect($interior)->toBeLessThan($immobilien)
|
||||
->and($immobilien)->toBeLessThan($b2in);
|
||||
|
||||
// Die Kernmarken stehen am Anfang der Interior-Gruppe, vor dem Beratungstermin.
|
||||
$group = substr($html, $interior, $immobilien - $interior);
|
||||
|
||||
$brandSlugs = array_keys(\App\Models\CmsBrand::forFrontend());
|
||||
expect($brandSlugs)->not->toBeEmpty();
|
||||
|
||||
foreach ($brandSlugs as $slug) {
|
||||
expect($group)->toContain('value="'.$slug.'"');
|
||||
}
|
||||
|
||||
expect($group)->toContain('Küchenplanung');
|
||||
expect(strpos($group, 'value="'.$brandSlugs[0].'"'))->toBeLessThan(strpos($group, 'value="beratung"'));
|
||||
});
|
||||
|
||||
it('puts the real estate group first on the real estate contact page', function () {
|
||||
$html = $this->get('/immobilien/kontakt')->assertSuccessful()->getContent();
|
||||
|
||||
expect(strpos($html, '<optgroup label="Immobilien">'))
|
||||
->toBeLessThan(strpos($html, '<optgroup label="Interior">'));
|
||||
});
|
||||
|
||||
it('accepts a brand as subject and rejects an unknown one', function () {
|
||||
\Illuminate\Support\Facades\Mail::fake();
|
||||
|
||||
$form = fn () => Livewire::test(\App\Livewire\Web\Components\Ui\ContactForm::class)
|
||||
->set('firstName', 'Anna')
|
||||
->set('lastName', 'Muster')
|
||||
->set('email', 'anna@example.com')
|
||||
->set('message', 'Hallo.')
|
||||
->set('privacy', true);
|
||||
|
||||
$brand = array_key_first(\App\Models\CmsBrand::forFrontend());
|
||||
|
||||
$form()->set('subject', $brand)->call('submit')->assertHasNoErrors()->assertSet('success', true);
|
||||
$form()->set('subject', 'gibt-es-nicht')->call('submit')->assertHasErrors(['subject']);
|
||||
});
|
||||
|
||||
it('falls back to a flat subject list for an older CMS entry without groups', function () {
|
||||
\FluxCms\Core\Models\CmsContent::create([
|
||||
'group' => 'contact',
|
||||
'key' => 'contact_form',
|
||||
'type' => 'json',
|
||||
'value' => ['de' => ['form' => ['subjects' => ['' => 'Bitte wählen', 'alt' => 'Alter Betreff']]]],
|
||||
'order' => 0,
|
||||
]);
|
||||
|
||||
$html = $this->get('/contact')->assertSuccessful()->getContent();
|
||||
|
||||
expect($html)->toContain('value="alt"')
|
||||
->toContain('Bitte wählen')
|
||||
->not->toContain('<optgroup');
|
||||
});
|
||||
|
||||
it('sends the contact button of the header to the contact page of the current world', function () {
|
||||
expect($this->get('/immobilien')->getContent())->toContain('href="'.route('immobilien.contact').'"');
|
||||
expect($this->get('/')->getContent())->toContain('href="'.route('contact').'"')
|
||||
->not->toContain(route('immobilien.contact'));
|
||||
});
|
||||
|
||||
it('links the calls to action of the real estate page to its own contact page', function () {
|
||||
$html = $this->get('/immobilien')->assertSuccessful()->getContent();
|
||||
|
||||
expect($html)->not->toContain('href="/contact"')
|
||||
->toContain('href="/immobilien/kontakt"')
|
||||
->toContain('Ob Exposé, Kaufprozess oder Einrichtung nach dem Kauf');
|
||||
|
||||
// Der Interior-Abschluss bleibt, wie er ist.
|
||||
expect($this->get('/')->getContent())->toContain('Ob einzelnes Möbelstück');
|
||||
});
|
||||
|
||||
it('prefixes real estate contact mails so they are recognisable in the inbox', function () {
|
||||
\Illuminate\Support\Facades\Mail::fake();
|
||||
|
||||
Livewire::test(\App\Livewire\Web\Components\Ui\ContactForm::class, ['world' => 'immobilien'])
|
||||
->assertSet('world', 'immobilien')
|
||||
->set('firstName', 'Anna')
|
||||
->set('lastName', 'Muster')
|
||||
->set('email', 'anna@example.com')
|
||||
->set('subject', 'expose')
|
||||
->set('message', 'Bitte um das Exposé.')
|
||||
->set('privacy', true)
|
||||
->call('submit')
|
||||
->assertHasNoErrors()
|
||||
->assertSet('success', true);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue