186 lines
8.1 KiB
PHP
186 lines
8.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use Database\Seeders\VisualContentSeeder;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Artisan;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
beforeEach(function (): void {
|
|
Artisan::call('db:seed', ['--class' => 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,
|
|
]);
|
|
}
|
|
});
|