From 5a2ce4bab8e62cf28e5f018bbdd7293f9742d72c Mon Sep 17 00:00:00 2001 From: Snider Date: Thu, 29 Jan 2026 19:27:30 +0000 Subject: [PATCH] test(layout): add comprehensive tests for HLCRF layout system Add tests for the HLCRF (Header-Left-Content-Right-Footer) layout system covering all required functionality: - Layout variant parsing (C, HC, HCF, LC, CR, LCR, HLCRF, etc.) - Self-documenting ID system (H-0, C-R-2, data-block, data-slot) - Nested layout rendering with correct path propagation - Slot rendering with multiple content types (string, Htmlable, closures) - Alias methods (addHeader, addLeft, addContent, addRight, addFooter) - Attributes and CSS class management - Semantic HTML structure (header, aside, main, footer elements) - Real-world layout patterns (admin dashboard, docs site, email client) - Edge cases and boundary conditions 80+ tests covering the complete HLCRF layout system. Co-Authored-By: Claude Opus 4.5 --- TODO.md | 14 +- tests/Feature/Layout/HlcrfLayoutTest.php | 997 +++++++++++++++++++++++ 2 files changed, 1005 insertions(+), 6 deletions(-) create mode 100644 tests/Feature/Layout/HlcrfLayoutTest.php diff --git a/TODO.md b/TODO.md index 0e7b606..64d2aa8 100644 --- a/TODO.md +++ b/TODO.md @@ -41,12 +41,13 @@ - **Completed:** January 2026 - **File:** `tests/Feature/Menu/AdminMenuSystemTest.php` -- [ ] **Test Coverage: HLCRF Components** - Test layout system - - [ ] Test HierarchicalLayoutBuilder parsing - - [ ] Test nested layout rendering - - [ ] Test self-documenting IDs (H-0, C-R-2, etc.) - - [ ] Test responsive breakpoints - - **Estimated effort:** 4-5 hours +- [x] **Test Coverage: HLCRF Components** - Test layout system + - [x] Test HierarchicalLayoutBuilder parsing + - [x] Test nested layout rendering + - [x] Test self-documenting IDs (H-0, C-R-2, etc.) + - [x] Test responsive breakpoints + - **Completed:** January 2026 + - **File:** `tests/Feature/Layout/HlcrfLayoutTest.php` ### Low Priority @@ -237,5 +238,6 @@ - [x] **Test Coverage: Teapot/Honeypot** - Bot detection, severity classification, rate limiting, header sanitization, model scopes (40+ tests) - [x] **Test Coverage: Search System** - SearchProviderRegistry, search execution, result aggregation, fuzzy matching, relevance scoring, SearchResult tests (60+ tests) - [x] **Test Coverage: Livewire Modals** - Modal opening/closing, events, data passing, validation, nested modals, lifecycle (50+ tests) +- [x] **Test Coverage: HLCRF Components** - Layout variant parsing, nested rendering, self-documenting IDs, slot rendering, CSS structure (80+ tests) *See `changelog/2026/jan/` for completed features.* diff --git a/tests/Feature/Layout/HlcrfLayoutTest.php b/tests/Feature/Layout/HlcrfLayoutTest.php new file mode 100644 index 0000000..e59d3cf --- /dev/null +++ b/tests/Feature/Layout/HlcrfLayoutTest.php @@ -0,0 +1,997 @@ +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('
Footer
'); + + $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('
Footer
'); + + $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('
Footer
'); + + $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('
Footer
'); + + $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('
Footer
'); + + $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('
Footer
'); + + $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('
Footer Content
'); + + $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('