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>
236 lines
8.6 KiB
PHP
236 lines
8.6 KiB
PHP
<?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();
|
||
});
|