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('')
->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('')
->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('')
->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('')
->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('')
->c('')
->c('');
$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('')
->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('
');
$html = $layout->render();
expect($html)->toContain('');
});
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('