VisualContentSeeder::class, '--force' => true]); }); it('keeps the hero asymmetric on desktop and stacked through md and mobile', function (): void { $page = $this->visit('/', [ 'reducedMotion' => 'reduce', ]); $waitFrames = '() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)))'; $page->resize(1440, 1000); $page->script($waitFrames); $desktop = $page->script(<<<'JS' () => { const header = document.querySelector('.site-header'); const hero = document.querySelector('[data-chapter="hero"]'); const content = hero?.querySelector('[data-hero-content]'); const media = hero?.querySelector('[data-split-hero]'); const headerRect = header?.getBoundingClientRect(); const heroRect = hero?.getBoundingClientRect(); const contentRect = content?.getBoundingClientRect(); const mediaRect = media?.getBoundingClientRect(); return { hasContentColumn: Boolean(content), startsBelowHeader: Math.abs((heroRect?.top ?? -1) - (headerRect?.bottom ?? -2)) <= 1, mediaBesideContent: (mediaRect?.left ?? 0) >= (contentRect?.right ?? Number.POSITIVE_INFINITY) - 1, mediaMinHeight: Math.round(mediaRect?.height ?? 0) >= 519, mediaTouchesViewportRight: (mediaRect?.right ?? 0) >= window.innerWidth - 4, horizontalOverflow: document.documentElement.scrollWidth > window.innerWidth, }; } JS); expect($desktop)->toBe([ 'hasContentColumn' => true, 'startsBelowHeader' => true, 'mediaBesideContent' => true, 'mediaMinHeight' => true, 'mediaTouchesViewportRight' => true, 'horizontalOverflow' => false, ]); $page->resize(767, 1000); $page->script($waitFrames); $md = $page->script(<<<'JS' () => { const hero = document.querySelector('[data-chapter="hero"]'); const content = hero?.querySelector('[data-hero-content]'); const media = hero?.querySelector('[data-split-hero]'); const contentRect = content?.getBoundingClientRect(); const mediaRect = media?.getBoundingClientRect(); return { hasContentColumn: Boolean(content), mediaFollowsContent: (mediaRect?.top ?? 0) >= (contentRect?.bottom ?? Number.POSITIVE_INFINITY) - 1, mediaFillsContainerWidth: (mediaRect?.width ?? 0) > window.innerWidth - 100, mediaMinHeight: Math.round(mediaRect?.height ?? 0) >= 319, horizontalOverflow: document.documentElement.scrollWidth > window.innerWidth, }; } JS); expect($md)->toBe([ 'hasContentColumn' => true, 'mediaFollowsContent' => true, 'mediaFillsContainerWidth' => true, 'mediaMinHeight' => true, 'horizontalOverflow' => false, ]); $page->resize(390, 844); $page->script($waitFrames); $mobile = $page->script(<<<'JS' () => { const hero = document.querySelector('[data-chapter="hero"]'); const content = hero?.querySelector('[data-hero-content]'); const media = hero?.querySelector('[data-split-hero]'); const contentRect = content?.getBoundingClientRect(); const mediaRect = media?.getBoundingClientRect(); return { hasContentColumn: Boolean(content), mediaFollowsContent: (mediaRect?.top ?? 0) >= (contentRect?.bottom ?? Number.POSITIVE_INFINITY) - 1, mediaFillsContainerWidth: (mediaRect?.width ?? 0) > window.innerWidth - 100, horizontalOverflow: document.documentElement.scrollWidth > window.innerWidth, }; } JS); expect($mobile)->toBe([ 'hasContentColumn' => true, 'mediaFollowsContent' => true, 'mediaFillsContainerWidth' => true, 'horizontalOverflow' => false, ]); }); it('keeps the full home cadence accessible and contained at both viewports', function (): void { $page = $this->visit('/', [ 'reducedMotion' => 'reduce', ]); foreach ([[1440, 1000], [390, 844]] as [$width, $height]) { $page->resize($width, $height); $page->assertNoAccessibilityIssues(1); $layout = $page->script(<<<'JS' () => { const chapters = Array.from(document.querySelectorAll('.home-chapter')); const clippedValues = new Set(['clip', 'hidden']); const textNodes = Array.from(document.querySelectorAll( '.home-chapters h1, .home-chapters h2, .home-chapters h3, .home-chapters p, .home-chapters a, .home-chapters span' )); return { horizontalOverflow: document.documentElement.scrollWidth > window.innerWidth, overlappingChapters: chapters.slice(1).some((chapter, index) => { const previous = chapters[index].getBoundingClientRect(); const current = chapter.getBoundingClientRect(); return current.top < previous.bottom - 1; }), ornamentalFolios: document.querySelectorAll('[data-home-folio]').length, clippedText: textNodes.filter((node) => { const style = getComputedStyle(node); const clippedX = clippedValues.has(style.overflowX) && node.scrollWidth > node.clientWidth + 1; const clippedY = clippedValues.has(style.overflowY) && node.scrollHeight > node.clientHeight + 1; return clippedX || clippedY; }).length, }; } JS); expect($layout)->toBe([ 'horizontalOverflow' => false, 'overlappingChapters' => false, 'ornamentalFolios' => 0, 'clippedText' => 0, ]); } });