b2in/tests/Feature/OriginSectionTest.php
Kevin Adametz 90aa079cf7 CMS: Standorte an der Marke, Seitenbaum mit Vorschau, Referenzen-Modul
Nachtrag 11: Herkunfts-Sektion liest Ort, Entfernung und Rolle aus der
Markenverwaltung (drei neue Felder an cms_brands, BrandOriginSeeder),
Kartenpositionen im Code, „Marken“ in der Sidebar.

Nachtrag 12: Inhalte-Editor zeigt die Seiten der Website mit ihren
Sektionen in Reihenfolge (config/cms_pages.php, CmsPageTree), Felder in
Klartext, fehlende Sektionen aus der Sprachdatei anlegbar, Vorschau der
echten Seite im iframe mit Sprung zur Sektion; Anker an allen lebenden
Sektionen nachgezogen.

Nachtrag 13: Referenzen als eigenes Modul (CmsReference, Portal-Seite,
/referenzen mit Fallback auf die Markengalerien); Immobilien-Projekte
aus Sidebar und CMS-Übersicht genommen, Route bleibt.

Dazu .gitignore für _b2in_interior_shop/ und docs/.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-15 14:58:45 +00:00

236 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\Sections\OriginSection;
use App\Models\CmsBrand;
use Database\Seeders\BrandOriginSeeder;
use Database\Seeders\CmsBrandSeeder;
use Database\Seeders\OriginSectionCmsSeeder;
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, Nachtrag 11).
*
* Die Standorte kommen aus den Marken (origin_city, origin_distance, origin_role),
* die Kartenpositionen aus OriginSection::MAP_POSITIONS.
*/
function originBody(string $html): string
{
$html = preg_replace('/wire:snapshot="[^"]*"/', '', $html) ?? $html;
return substr($html, (int) strpos($html, '<main'));
}
function originSection(string $html): string
{
$html = originBody($html);
$section = substr($html, (int) strpos($html, 'id="herkunft"'));
return substr($section, 0, (int) strpos($section, '</section>'));
}
// ========================================
// Standorte aus den Marken
// ========================================
it('shows the four locations from the seeded brands on the homepage', function () {
$this->seed(CmsBrandSeeder::class);
$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)
->assertSee('rund 200 km');
});
it('links every location to its brand page', function () {
$this->seed(CmsBrandSeeder::class);
$section = originSection($this->get('/')->assertSuccessful()->getContent());
foreach (['/marken/rotpunkt', '/marken/februe', '/marken/tischlerei', '/marken/cabinet'] as $link) {
expect($section)->toContain('href="'.$link.'"');
}
});
it('drops an unpublished brand from the locations entirely', function () {
$this->seed(CmsBrandSeeder::class);
CmsBrand::query()->where('slug', 'februe')->update(['is_published' => false]);
$section = originSection($this->get('/')->assertSuccessful()->getContent());
// „Herford“ steht auch im Alt-Text der Karte – geprüft wird die Kachel.
expect($section)->toContain('href="/marken/cabinet"')
->toContain('wire:key="origin-cabinet"')
->not->toContain('wire:key="origin-februe"')
->not->toContain('/marken/februe');
expect(substr_count($section, 'class="origin-map-dot"'))->toBe(3);
});
it('leaves out a brand whose origin city is empty', function () {
$this->seed(CmsBrandSeeder::class);
CmsBrand::query()->where('slug', 'februe')->update(['origin_city' => ['de' => '', 'en' => '']]);
$section = originSection($this->get('/')->assertSuccessful()->getContent());
expect($section)->toContain('wire:key="origin-rotpunkt"')
->not->toContain('wire:key="origin-februe"');
expect(substr_count($section, 'class="origin-map-dot"'))->toBe(3);
});
it('shows a brand without a map position in the tiles only', function () {
$this->seed(CmsBrandSeeder::class);
CmsBrand::factory()->create([
'slug' => 'neue-marke',
'name' => ['de' => 'Neue Marke'],
'origin_city' => ['de' => 'Verl'],
'is_published' => true,
'order' => 9,
]);
$section = originSection($this->get('/')->assertSuccessful()->getContent());
expect($section)->toContain('Verl')
->toContain('wire:key="origin-neue-marke"');
expect(substr_count($section, 'class="origin-map-dot"'))->toBe(4);
});
it('uses the lang fallback brands without links while no brand is maintained', function () {
$section = originSection($this->get('/')->assertSuccessful()->getContent());
expect($section)->toContain('Bünde')
->toContain('Köln')
->not->toContain('href="/marken/');
expect(substr_count($section, 'class="origin-map-dot"'))->toBe(4);
});
it('draws Bielefeld as the hub with one spoke per location', function () {
$this->seed(CmsBrandSeeder::class);
$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('hides map and tiles but keeps the steps when no brand has an origin', function () {
CmsBrand::factory()->create(['slug' => 'cabinet', 'origin_city' => null, 'is_published' => true]);
$section = originSection($this->get('/')->assertSuccessful()->getContent());
expect($section)->not->toContain('origin-map')
->toContain('Fertigung in Köln');
});
// ========================================
// Platzierung und Ablauf-Block
// ========================================
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(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('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 for the texts but keeps the locations from the brands', function () {
$this->seed(CmsBrandSeeder::class);
CmsContent::create([
'group' => 'home',
'key' => 'origin_section',
'type' => 'json',
'value' => ['de' => [
'title' => 'Gefertigt in <span class="text-secondary">OWL</span>',
'link_text' => 'Zur Manufaktur',
'hub' => ['city' => 'Bielefeld', 'x' => 400, 'y' => 175],
]],
'order' => 0,
]);
$this->get('/')
->assertSuccessful()
->assertSee('Gefertigt in <span class="text-secondary">OWL</span>', false)
->assertSee('Zur Manufaktur')
->assertSee('Rheda-Wiedenbrück', false);
});
it('renders without the steps block when the CMS leaves it empty', function () {
Livewire::test(OriginSection::class)
->set('content', ['title' => 'Herkunft', 'steps' => []])
->assertSee('Herkunft')
->assertDontSee('<ol', false);
});
// ========================================
// Seeder
// ========================================
it('creates the CMS section only when it is missing and strips the legacy locations', function () {
$this->seed(OriginSectionCmsSeeder::class);
$row = CmsContent::query()->where('group', 'home')->where('key', 'origin_section')->first();
expect($row)->not->toBeNull();
expect($row->getTranslation('value', 'de'))->not->toHaveKey('locations');
$row->update(['value' => ['de' => [
'title' => 'Vom Admin geändert',
'locations' => [['city' => 'Alt', 'brand' => 'Alt']],
]]]);
$this->seed(OriginSectionCmsSeeder::class);
$value = $row->fresh()->getTranslation('value', 'de');
expect($value['title'])->toBe('Vom Admin geändert');
expect($value)->not->toHaveKey('locations');
});
it('fills the origin of existing brands only where it is empty', function () {
CmsBrand::factory()->create(['slug' => 'cabinet', 'origin_city' => null]);
CmsBrand::factory()->create(['slug' => 'rotpunkt', 'origin_city' => ['de' => 'Eigener Ort']]);
$this->seed(BrandOriginSeeder::class);
expect(CmsBrand::where('slug', 'cabinet')->first()->getTranslation('origin_city', 'de'))->toBe('Köln');
expect(CmsBrand::where('slug', 'cabinet')->first()->getTranslation('origin_distance', 'en'))->toBe('about 200 km');
expect(CmsBrand::where('slug', 'rotpunkt')->first()->getTranslation('origin_city', 'de'))->toBe('Eigener Ort');
expect(CmsBrand::where('slug', 'rotpunkt')->first()->getTranslation('origin_distance', 'de', false))->toBeEmpty();
});