Files
Portfolio/tests/browser/accessibility-reflow.spec.ts

105 lines
3.5 KiB
TypeScript

import type { Page } from "@playwright/test";
import { clickNavigationLink, expect, test } from "../helpers/serve-dist";
const textSpacingOverride = `
* {
line-height: 1.5 !important;
letter-spacing: 0.12em !important;
word-spacing: 0.16em !important;
}
p { margin-block-end: 2em !important; }
`;
async function assertReflowGeometry(page: Page): Promise<void> {
const result = await page.evaluate(() => {
const root = document.documentElement;
const horizontalCandidates = [
...document.querySelectorAll<HTMLElement>(
".site-header, .portfolio-main, .portfolio-section, .site-footer, .drive-controls, .journey-hud, h1, h2, h3, p, a, button, input, select, figure, img, video, table",
),
].filter((element) => {
const style = getComputedStyle(element);
const rect = element.getBoundingClientRect();
return (
style.display !== "none" &&
style.visibility !== "hidden" &&
rect.width > 0 &&
rect.height > 0
);
});
const clippedText = [
...document.querySelectorAll<HTMLElement>(
"h1, h2, h3, p, li, dt, dd, a, button, label, figcaption, caption, th, td",
),
].filter((element) => {
if (element.classList.contains("visually-hidden")) return false;
const style = getComputedStyle(element);
return style.overflow === "hidden" || style.textOverflow === "ellipsis";
});
const horizontalFailures = horizontalCandidates.flatMap((element) => {
const rect = element.getBoundingClientRect();
if (rect.left >= -1 && rect.right <= root.clientWidth + 1) return [];
return [
{
element: element.outerHTML.slice(0, 140),
left: rect.left,
right: rect.right,
clientWidth: root.clientWidth,
},
];
});
return {
clientWidth: root.clientWidth,
scrollWidth: root.scrollWidth,
horizontalFailures,
clippedText: clippedText.map((element) =>
element.outerHTML.slice(0, 140),
),
};
});
expect(result.scrollWidth).toBeLessThanOrEqual(result.clientWidth + 1);
expect(result.horizontalFailures).toEqual([]);
expect(result.clippedText).toEqual([]);
await clickNavigationLink(page, "#contact");
await expect(page.locator("#contact-heading")).toBeFocused();
await expect(page.locator('#contact a[href^="mailto:"]')).toBeVisible();
}
test("reflows at a 320 CSS pixel mobile viewport without lost content or controls", async ({
page,
distUrl,
}, testInfo) => {
test.skip(
testInfo.project.name !== "mobile-chromium",
"Mobile CSS viewport gate.",
);
await page.setViewportSize({ width: 320, height: 800 });
await page.goto(distUrl, { waitUntil: "domcontentloaded" });
await assertReflowGeometry(page);
});
test("reflows in desktop Chromium at the effective 320 CSS pixel width produced by 400 percent zoom", async ({
page,
distUrl,
}, testInfo) => {
test.skip(
testInfo.project.name !== "desktop-chromium",
"Desktop zoom-equivalent gate.",
);
await page.setViewportSize({ width: 320, height: 225 });
await page.goto(distUrl, { waitUntil: "domcontentloaded" });
await assertReflowGeometry(page);
});
test("preserves content and controls with the WCAG text-spacing override", async ({
page,
distUrl,
}) => {
await page.setViewportSize({ width: 320, height: 800 });
await page.goto(distUrl, { waitUntil: "domcontentloaded" });
await page.addStyleTag({ content: textSpacingOverride });
await assertReflowGeometry(page);
});