c('
Content
'); $html = $layout->render(); expect($html)->toContain('data-layout="root"') ->and($html)->toContain('data-slot="C"') ->and($html)->toContain('hlcrf-content'); }); it('parses header-content variant (HC)', function () { $layout = Layout::make('HC') ->h('') ->c('
Content
'); $html = $layout->render(); expect($html)->toContain('data-slot="H"') ->and($html)->toContain('data-slot="C"') ->and($html)->not->toContain('data-slot="L"') ->and($html)->not->toContain('data-slot="R"') ->and($html)->not->toContain('data-slot="F"'); }); it('parses header-content-footer variant (HCF)', function () { $layout = Layout::make('HCF') ->h('') ->c('
Content
') ->f(''); $html = $layout->render(); expect($html)->toContain('data-slot="H"') ->and($html)->toContain('data-slot="C"') ->and($html)->toContain('data-slot="F"') ->and($html)->not->toContain('data-slot="L"') ->and($html)->not->toContain('data-slot="R"'); }); it('parses left-content variant (LC)', function () { $layout = Layout::make('LC') ->l('') ->c('
Content
'); $html = $layout->render(); expect($html)->toContain('data-slot="L"') ->and($html)->toContain('data-slot="C"') ->and($html)->not->toContain('data-slot="H"') ->and($html)->not->toContain('data-slot="R"') ->and($html)->not->toContain('data-slot="F"'); }); it('parses content-right variant (CR)', function () { $layout = Layout::make('CR') ->c('
Content
') ->r(''); $html = $layout->render(); expect($html)->toContain('data-slot="C"') ->and($html)->toContain('data-slot="R"') ->and($html)->not->toContain('data-slot="H"') ->and($html)->not->toContain('data-slot="L"') ->and($html)->not->toContain('data-slot="F"'); }); it('parses three-column variant (LCR)', function () { $layout = Layout::make('LCR') ->l('') ->c('
Content
') ->r(''); $html = $layout->render(); expect($html)->toContain('data-slot="L"') ->and($html)->toContain('data-slot="C"') ->and($html)->toContain('data-slot="R"') ->and($html)->not->toContain('data-slot="H"') ->and($html)->not->toContain('data-slot="F"'); }); it('parses full variant (HLCRF)', function () { $layout = Layout::make('HLCRF') ->h('
Header
') ->l('') ->c('
Content
') ->r('') ->f(''); $html = $layout->render(); expect($html)->toContain('data-slot="H"') ->and($html)->toContain('data-slot="L"') ->and($html)->toContain('data-slot="C"') ->and($html)->toContain('data-slot="R"') ->and($html)->toContain('data-slot="F"'); }); it('normalises lowercase variant to uppercase', function () { $layout = Layout::make('hlcrf') ->h('
Header
') ->l('') ->c('
Content
') ->r('') ->f(''); $html = $layout->render(); expect($html)->toContain('data-slot="H"') ->and($html)->toContain('data-slot="L"') ->and($html)->toContain('data-slot="C"') ->and($html)->toContain('data-slot="R"') ->and($html)->toContain('data-slot="F"'); }); it('uses HCF as default variant', function () { $layout = Layout::make() ->h('
Header
') ->c('
Content
') ->f(''); $html = $layout->render(); expect($html)->toContain('data-slot="H"') ->and($html)->toContain('data-slot="C"') ->and($html)->toContain('data-slot="F"'); }); }); // ============================================================================= // Self-Documenting ID System Tests // ============================================================================= describe('Self-documenting ID system', function () { it('generates correct block IDs for single items', function () { $layout = Layout::make('HLCRF') ->h('
Header
') ->l('') ->c('
Content
') ->r('') ->f(''); $html = $layout->render(); expect($html)->toContain('data-block="H-0"') ->and($html)->toContain('data-block="L-0"') ->and($html)->toContain('data-block="C-0"') ->and($html)->toContain('data-block="R-0"') ->and($html)->toContain('data-block="F-0"'); }); it('generates sequential block IDs for multiple items', function () { $layout = Layout::make('C') ->c('
First
') ->c('
Second
') ->c('
Third
'); $html = $layout->render(); expect($html)->toContain('data-block="C-0"') ->and($html)->toContain('data-block="C-1"') ->and($html)->toContain('data-block="C-2"'); }); it('generates sequential IDs for header items', function () { $layout = Layout::make('HC') ->h('') ->h('') ->h('') ->c('
Content
'); $html = $layout->render(); expect($html)->toContain('data-block="H-0"') ->and($html)->toContain('data-block="H-1"') ->and($html)->toContain('data-block="H-2"'); }); it('generates correct nested IDs for nested layouts', function () { $nested = Layout::make('LR') ->l('Nested Left') ->r('Nested Right'); $outer = Layout::make('C') ->c($nested); $html = $outer->render(); // The nested layout should have IDs prefixed with parent context expect($html)->toContain('data-block="C-0-L-0"') ->and($html)->toContain('data-block="C-0-R-0"'); }); it('generates correct deeply nested IDs', function () { $deepNested = Layout::make('C') ->c('Deep content'); $nested = Layout::make('LR') ->l($deepNested) ->r('Nested Right'); $outer = Layout::make('C') ->c($nested); $html = $outer->render(); expect($html)->toContain('data-block="C-0-L-0-C-0"'); }); it('sets correct data-layout attribute on root', function () { $layout = Layout::make('C') ->c('
Content
'); $html = $layout->render(); expect($html)->toContain('data-layout="root"'); }); it('sets correct data-layout attribute on nested layouts', function () { $nested = Layout::make('C') ->c('Nested content'); $outer = Layout::make('C') ->c($nested); $html = $outer->render(); // Nested layout should have a path-based layout ID expect($html)->toContain('data-layout="C-0"'); }); }); // ============================================================================= // Nested Layout Rendering Tests // ============================================================================= describe('Nested layout rendering', function () { it('renders nested layout within content', function () { $nested = Layout::make('LCR') ->l('') ->c('
Nested Content
') ->r(''); $outer = Layout::make('HCF') ->h('
Header
') ->c($nested) ->f(''); $html = $outer->render(); expect($html)->toContain('Nested Nav') ->and($html)->toContain('Nested Content') ->and($html)->toContain('Nested Aside') ->and($html)->toContain('Header') ->and($html)->toContain('Footer'); }); it('renders multiple levels of nesting', function () { $level3 = Layout::make('C') ->c('Level 3 Content'); $level2 = Layout::make('LC') ->l('Level 2 Sidebar') ->c($level3); $level1 = Layout::make('HCF') ->h('Level 1 Header') ->c($level2) ->f('Level 1 Footer'); $html = $level1->render(); expect($html)->toContain('Level 3 Content') ->and($html)->toContain('Level 2 Sidebar') ->and($html)->toContain('Level 1 Header') ->and($html)->toContain('Level 1 Footer'); }); it('renders nested layouts in different slots', function () { $leftNested = Layout::make('C') ->c('Left nested content'); $rightNested = Layout::make('C') ->c('Right nested content'); $outer = Layout::make('LCR') ->l($leftNested) ->c('Main content') ->r($rightNested); $html = $outer->render(); expect($html)->toContain('Left nested content') ->and($html)->toContain('Main content') ->and($html)->toContain('Right nested content') ->and($html)->toContain('data-block="L-0-C-0"') ->and($html)->toContain('data-block="R-0-C-0"'); }); it('preserves correct path context through multiple nested items', function () { $nested1 = Layout::make('C')->c('First nested'); $nested2 = Layout::make('C')->c('Second nested'); $outer = Layout::make('C') ->c($nested1) ->c($nested2); $html = $outer->render(); expect($html)->toContain('data-layout="C-0"') ->and($html)->toContain('data-layout="C-1"') ->and($html)->toContain('data-block="C-0-C-0"') ->and($html)->toContain('data-block="C-1-C-0"'); }); }); // ============================================================================= // Slot Rendering Tests // ============================================================================= describe('Slot rendering', function () { it('renders string content directly', function () { $layout = Layout::make('C') ->c('Simple string content'); $html = $layout->render(); expect($html)->toContain('Simple string content'); }); it('renders HTML content correctly', function () { $layout = Layout::make('C') ->c('

Title

Body

'); $html = $layout->render(); expect($html)->toContain('

Title

Body

'); }); it('renders Htmlable objects', function () { $htmlable = new class implements Htmlable { public function toHtml(): string { return 'Htmlable Content'; } }; $layout = Layout::make('C') ->c($htmlable); $html = $layout->render(); expect($html)->toContain('Htmlable Content'); }); it('renders callable/closure content', function () { $layout = Layout::make('C') ->c(fn () => '
Closure Content
'); $html = $layout->render(); expect($html)->toContain('
Closure Content
'); }); it('handles null content gracefully', function () { $layout = Layout::make('C') ->c(null) ->c('Valid content'); $html = $layout->render(); expect($html)->toContain('Valid content') ->and($html)->toContain('data-block="C-0"') ->and($html)->toContain('data-block="C-1"'); }); it('does not render empty slots', function () { $layout = Layout::make('LCR') ->c('
Content only
'); $html = $layout->render(); // L and R slots should not appear since they have no content expect($html)->toContain('data-slot="C"') ->and($html)->not->toContain('data-slot="L"') ->and($html)->not->toContain('data-slot="R"'); }); it('supports variadic item addition', function () { $layout = Layout::make('C') ->c('First', 'Second', 'Third'); $html = $layout->render(); expect($html)->toContain('First') ->and($html)->toContain('Second') ->and($html)->toContain('Third') ->and($html)->toContain('data-block="C-0"') ->and($html)->toContain('data-block="C-1"') ->and($html)->toContain('data-block="C-2"'); }); }); // ============================================================================= // Alias Method Tests // ============================================================================= describe('Alias methods', function () { it('addHeader works like h()', function () { $layout = Layout::make('HC') ->addHeader('') ->c('
Content
'); $html = $layout->render(); expect($html)->toContain('Header Content') ->and($html)->toContain('data-block="H-0"'); }); it('addLeft works like l()', function () { $layout = Layout::make('LC') ->addLeft('') ->c('
Content
'); $html = $layout->render(); expect($html)->toContain('Left Content') ->and($html)->toContain('data-block="L-0"'); }); it('addContent works like c()', function () { $layout = Layout::make('C') ->addContent('
Main Content
'); $html = $layout->render(); expect($html)->toContain('Main Content') ->and($html)->toContain('data-block="C-0"'); }); it('addRight works like r()', function () { $layout = Layout::make('CR') ->c('
Content
') ->addRight(''); $html = $layout->render(); expect($html)->toContain('Right Content') ->and($html)->toContain('data-block="R-0"'); }); it('addFooter works like f()', function () { $layout = Layout::make('CF') ->c('
Content
') ->addFooter(''); $html = $layout->render(); expect($html)->toContain('Footer Content') ->and($html)->toContain('data-block="F-0"'); }); it('alias methods support variadic arguments', function () { $layout = Layout::make('C') ->addContent('First', 'Second', 'Third'); $html = $layout->render(); expect($html)->toContain('First') ->and($html)->toContain('Second') ->and($html)->toContain('Third'); }); }); // ============================================================================= // Attributes and CSS Classes Tests // ============================================================================= describe('Attributes and CSS classes', function () { it('includes default hlcrf-layout class', function () { $layout = Layout::make('C') ->c('Content'); $html = $layout->render(); expect($html)->toContain('class="hlcrf-layout"'); }); it('adds custom class with class() method', function () { $layout = Layout::make('C') ->class('custom-class') ->c('Content'); $html = $layout->render(); expect($html)->toContain('class="hlcrf-layout custom-class"'); }); it('accumulates multiple classes', function () { $layout = Layout::make('C') ->class('first-class') ->class('second-class') ->c('Content'); $html = $layout->render(); expect($html)->toContain('hlcrf-layout') ->and($html)->toContain('first-class') ->and($html)->toContain('second-class'); }); it('sets custom attributes with attributes() method', function () { $layout = Layout::make('C') ->attributes(['id' => 'main-layout', 'data-theme' => 'dark']) ->c('Content'); $html = $layout->render(); expect($html)->toContain('id="main-layout"') ->and($html)->toContain('data-theme="dark"'); }); it('merges attributes correctly', function () { $layout = Layout::make('C') ->attributes(['data-first' => 'one']) ->attributes(['data-second' => 'two']) ->c('Content'); $html = $layout->render(); expect($html)->toContain('data-first="one"') ->and($html)->toContain('data-second="two"'); }); it('handles boolean true attributes', function () { $layout = Layout::make('C') ->attributes(['data-visible' => true]) ->c('Content'); $html = $layout->render(); expect($html)->toContain('data-visible'); }); it('excludes boolean false attributes', function () { $layout = Layout::make('C') ->attributes(['data-hidden' => false]) ->c('Content'); $html = $layout->render(); expect($html)->not->toContain('data-hidden'); }); it('excludes null attributes', function () { $layout = Layout::make('C') ->attributes(['data-maybe' => null]) ->c('Content'); $html = $layout->render(); expect($html)->not->toContain('data-maybe'); }); it('escapes attribute values', function () { $layout = Layout::make('C') ->attributes(['data-value' => '']) ->c('Content'); $html = $layout->render(); expect($html)->not->toContain('