Files
Portfolio/tests/browser/keyboard.spec.ts

184 lines
6.0 KiB
TypeScript

import { expect, expectInsetFocusIndicator, test } from "../helpers/serve-dist";
const navHrefs = [
"#start",
"#profile",
"#experience",
"#infrastructure",
"#selected-builds",
"#additional-work",
"#toolkit",
"#contact",
] as const;
const assertVisibleUnobscuredFocus = async (
page: import("@playwright/test").Page,
): Promise<void> => {
await expect
.poll(() =>
page.evaluate(() => {
const active = document.activeElement;
if (!(active instanceof HTMLElement)) return false;
const rect = active.getBoundingClientRect();
const left = Math.max(0, rect.left);
const right = Math.min(window.innerWidth, rect.right);
const top = Math.max(0, rect.top);
const bottom = Math.min(window.innerHeight, rect.bottom);
if (right <= left || bottom <= top) return false;
const x = left + (right - left) / 2;
const y = top + (bottom - top) / 2;
const topElement = document.elementFromPoint(x, y);
const style = getComputedStyle(active);
return (
topElement !== null &&
(active === topElement ||
active.contains(topElement) ||
topElement.contains(active)) &&
style.outlineStyle !== "none" &&
Number.parseFloat(style.outlineWidth) >= 2
);
}),
)
.toBe(true);
};
test("tabs from the skip link through navigation and every contact action", async ({
page,
distUrl,
}, testInfo) => {
test.skip(
testInfo.project.name === "mobile-chromium",
"The compact disclosure path is covered by the adaptive keyboard traversal suite.",
);
await page.goto(distUrl, { waitUntil: "domcontentloaded" });
const expectedTargets = [
{ selector: ".skip-link", href: "#portfolio-content" },
...navHrefs.map((href) => ({ selector: `nav a[href="${href}"]`, href })),
{ selector: ".hero-journey__action", href: "#selected-builds" },
{
selector: 'a[href="https://github.com/LucaPisl/footy-tracker"]',
href: "https://github.com/LucaPisl/footy-tracker",
},
{
selector:
'a[href="https://github.com/LucaPisl/opinion-dynamics-simulation"]',
href: "https://github.com/LucaPisl/opinion-dynamics-simulation",
},
{
selector:
'a[href="https://github.com/LucaPisl/CurrencyConverterAvalonia"]',
href: "https://github.com/LucaPisl/CurrencyConverterAvalonia",
},
{
selector: 'a[href="https://github.com/LucaPisl/LinuxModdingGuide"]',
href: "https://github.com/LucaPisl/LinuxModdingGuide",
},
{
selector: '#contact a[href^="mailto:"]',
href: "mailto:luca.pislaru@gmail.com",
},
{
selector: '#contact a[href="https://github.com/LucaPisl"]',
href: "https://github.com/LucaPisl",
},
{
selector: '#contact a[href="https://gitlab.com/luca_pisl"]',
href: "https://gitlab.com/luca_pisl",
},
{
selector: '#contact a[href="https://linkedin.com/in/luca-pisl"]',
href: "https://linkedin.com/in/luca-pisl",
},
];
for (const target of expectedTargets) {
await page.keyboard.press("Tab");
const locator = page.locator(target.selector);
await expect(locator).toBeFocused();
await expect(locator).toHaveAttribute("href", target.href);
await assertVisibleUnobscuredFocus(page);
}
});
test("activating the skip link focuses the visible unobscured portfolio content", async ({
page,
distUrl,
}) => {
await page.goto(distUrl, { waitUntil: "domcontentloaded" });
await page.keyboard.press("Tab");
await expect(page.locator(".skip-link")).toBeFocused();
await page.keyboard.press("Enter");
await expect(page).toHaveURL(`${distUrl}/#portfolio-content`);
await expect(page.locator("#portfolio-content")).toBeFocused();
await expect(page.locator("#portfolio-content")).toHaveAttribute(
"tabindex",
"-1",
);
await expectInsetFocusIndicator(page, "#portfolio-content");
});
test("explicit section activation scrolls to and focuses its heading", async ({
page,
distUrl,
}, testInfo) => {
test.skip(
testInfo.project.name === "mobile-chromium",
"The full keyboard path already covers the mobile project.",
);
await page.goto(distUrl, { waitUntil: "domcontentloaded" });
for (let index = 0; index < 5; index += 1) await page.keyboard.press("Tab");
await expect(page.locator('nav a[href="#infrastructure"]')).toBeFocused();
await page.keyboard.press("Enter");
await expect(page).toHaveURL(`${distUrl}/#infrastructure`);
await expect(page.locator("#infrastructure")).toBeInViewport();
await expect(page.locator("#infrastructure-heading")).toHaveAttribute(
"tabindex",
"-1",
);
await expect(page.locator("#infrastructure-heading")).toBeFocused();
await assertVisibleUnobscuredFocus(page);
});
test("manual wheel and PageDown scrolling never move focus", async ({
page,
distUrl,
}, testInfo) => {
test.skip(
testInfo.project.name === "mobile-chromium",
"The focus invariant is engine-level rather than viewport-specific.",
);
await page.goto(distUrl, { waitUntil: "domcontentloaded" });
await page.keyboard.press("Tab");
await page.keyboard.press("Tab");
const startLink = page.locator('nav a[href="#start"]');
await expect(startLink).toBeFocused();
await page.mouse.wheel(0, 900);
await expect(startLink).toBeFocused();
await page.keyboard.press("PageDown");
await expect(startLink).toBeFocused();
});
test("direct fragments do not steal focus while reduced-motion navigation remains accessible", async ({
page,
distUrl,
}, testInfo) => {
test.skip(
testInfo.project.name === "mobile-chromium",
"The full keyboard path already covers the mobile project.",
);
await page.emulateMedia({ reducedMotion: "reduce" });
await page.goto(`${distUrl}/#toolkit`, { waitUntil: "domcontentloaded" });
await expect(page.locator("#toolkit-heading")).not.toBeFocused();
await page.locator('nav a[href="#contact"]').click();
await expect(page).toHaveURL(`${distUrl}/#contact`);
await expect(page.locator("#contact-heading")).toBeFocused();
await expect(page.locator("#contact")).toBeInViewport();
});