VisualContentSeeder::class, '--force' => true]); }); it('uses a five-seven desktop spread with a bounded reading column and full-bleed media', function (): void { $page = $this->visit('/', [ 'reducedMotion' => 'reduce', ])->resize(1440, 1000); $page->script('() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)))'); $page->script('() => document.fonts.ready'); $layout = $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 heading = hero?.querySelector('h1'); const image = media?.querySelector('img'); 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, fillsRemainingViewport: (heroRect?.bottom ?? 0) >= window.innerHeight - 1, mediaTouchesHeroTop: Math.abs((mediaRect?.top ?? -1) - (heroRect?.top ?? -2)) <= 1, mediaTouchesHeroBottom: Math.abs((mediaRect?.bottom ?? -1) - (heroRect?.bottom ?? -2)) <= 1, mediaTouchesViewportRight: Math.abs((mediaRect?.right ?? -1) - window.innerWidth) <= 1, splitImageObjectFit: image ? getComputedStyle(image).objectFit : null, columnRatio: Number(((contentRect?.width ?? 0) / (mediaRect?.width ?? 1)).toFixed(3)), readingColumnWithinLimit: (heading?.getBoundingClientRect().width ?? Number.POSITIVE_INFINITY) <= 470, titleClipsHorizontally: (() => { if (!heading) { return true; } const range = document.createRange(); range.selectNodeContents(heading); return Array.from(range.getClientRects()).some((line) => line.right > heading.getBoundingClientRect().right + 1); })(), horizontalOverflow: document.documentElement.scrollWidth > window.innerWidth, }; } JS); expect($layout)->toBe([ 'hasContentColumn' => true, 'startsBelowHeader' => true, 'fillsRemainingViewport' => true, 'mediaTouchesHeroTop' => true, 'mediaTouchesHeroBottom' => true, 'mediaTouchesViewportRight' => true, 'splitImageObjectFit' => 'cover', 'columnRatio' => 0.714, 'readingColumnWithinLimit' => true, 'titleClipsHorizontally' => false, 'horizontalOverflow' => false, ]); }); it('keeps the home hero linear through tablet with a full-width four-by-five crop', function (): void { $page = $this->visit('/', [ 'reducedMotion' => 'reduce', ])->resize(1023, 1000); $page->script('() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)))'); $layout = $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, mediaFillsViewportWidth: Math.abs((mediaRect?.width ?? 0) - window.innerWidth) <= 1, mediaAspectRatio: Number(((mediaRect?.width ?? 0) / (mediaRect?.height ?? 1)).toFixed(3)), horizontalOverflow: document.documentElement.scrollWidth > window.innerWidth, }; } JS); expect($layout)->toBe([ 'hasContentColumn' => true, 'mediaFollowsContent' => true, 'mediaFillsViewportWidth' => true, 'mediaAspectRatio' => 0.8, 'horizontalOverflow' => false, ]); }); it('stacks the home hero media after its content as a full-width four-by-five crop on mobile', function (): void { $page = $this->visit('/', [ 'reducedMotion' => 'reduce', ])->resize(390, 844); $page->script('() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)))'); $layout = $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, mediaFillsViewportWidth: Math.abs((mediaRect?.width ?? 0) - window.innerWidth) <= 1, mediaAspectRatio: Number(((mediaRect?.width ?? 0) / (mediaRect?.height ?? 1)).toFixed(3)), horizontalOverflow: document.documentElement.scrollWidth > window.innerWidth, }; } JS); expect($layout)->toBe([ 'hasContentColumn' => true, 'mediaFollowsContent' => true, 'mediaFillsViewportWidth' => true, 'mediaAspectRatio' => 0.8, 'horizontalOverflow' => false, ]); }); it('keeps the full home cadence accessible and contained at both viewports', function (): void { foreach ([[1440, 1000], [390, 844]] as [$width, $height]) { $page = $this->visit('/', [ 'reducedMotion' => 'reduce', ])->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, ]); } });