fix(a11y): close cross-app interaction gaps
This commit is contained in:
@@ -2,6 +2,38 @@ import { expect, test, type Page } from "@playwright/test";
|
||||
|
||||
const apiUrl = "http://localhost:5302/api";
|
||||
|
||||
type Rgba = { red: number; green: number; blue: number; alpha: number };
|
||||
|
||||
function parseCssColour(value: string): Rgba {
|
||||
const parts = value.match(/[\d.]+/g)?.map(Number) ?? [];
|
||||
expect(parts.length, `expected an rgb/rgba colour but received ${value}`).toBeGreaterThanOrEqual(3);
|
||||
return { red: parts[0], green: parts[1], blue: parts[2], alpha: parts[3] ?? 1 };
|
||||
}
|
||||
|
||||
function composite(foreground: Rgba, background: Rgba): Rgba {
|
||||
return {
|
||||
red: foreground.red * foreground.alpha + background.red * (1 - foreground.alpha),
|
||||
green: foreground.green * foreground.alpha + background.green * (1 - foreground.alpha),
|
||||
blue: foreground.blue * foreground.alpha + background.blue * (1 - foreground.alpha),
|
||||
alpha: 1,
|
||||
};
|
||||
}
|
||||
|
||||
function relativeLuminance(colour: Rgba) {
|
||||
const linear = [colour.red, colour.green, colour.blue].map((channel) => {
|
||||
const value = channel / 255;
|
||||
return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
|
||||
});
|
||||
return linear[0] * 0.2126 + linear[1] * 0.7152 + linear[2] * 0.0722;
|
||||
}
|
||||
|
||||
function contrastRatio(foreground: Rgba, background: Rgba) {
|
||||
const foregroundLuminance = relativeLuminance(foreground);
|
||||
const backgroundLuminance = relativeLuminance(background);
|
||||
return (Math.max(foregroundLuminance, backgroundLuminance) + 0.05)
|
||||
/ (Math.min(foregroundLuminance, backgroundLuminance) + 0.05);
|
||||
}
|
||||
|
||||
async function login(page: Page) {
|
||||
await page.goto("/login");
|
||||
await page.getByLabel("Email").fill("e2e@example.test");
|
||||
@@ -153,7 +185,19 @@ test("the dedicated application workspace survives deep links, long data and uns
|
||||
await expect(page.getByRole("row", { name: new RegExp(`Open ${title}`, "i") })).toBeFocused();
|
||||
|
||||
await page.goto("/jobs/2147483647");
|
||||
await expect(page.getByRole("alert").filter({ hasText: /Not Found|Could not open this application/i })).toBeVisible();
|
||||
const missingJobAlert = page.getByRole("alert").filter({ hasText: /Not Found|Could not open this application/i });
|
||||
await expect(missingJobAlert).toBeVisible();
|
||||
const alertColours = await missingJobAlert.evaluate((element) => {
|
||||
const styles = window.getComputedStyle(element);
|
||||
const parentStyles = window.getComputedStyle(element.parentElement!);
|
||||
return {
|
||||
foreground: styles.color,
|
||||
background: styles.backgroundColor,
|
||||
parentBackground: parentStyles.backgroundColor,
|
||||
};
|
||||
});
|
||||
const alertBackground = composite(parseCssColour(alertColours.background), parseCssColour(alertColours.parentBackground));
|
||||
expect(contrastRatio(parseCssColour(alertColours.foreground), alertBackground)).toBeGreaterThanOrEqual(4.5);
|
||||
await expect(page.getByRole("button", { name: "Back to applications" })).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -182,8 +226,21 @@ test("a public CV renders anonymously and downloads as PDF", async ({ page }) =>
|
||||
const published = await publish.json();
|
||||
|
||||
await page.context().clearCookies();
|
||||
await page.setViewportSize({ width: 375, height: 900 });
|
||||
await page.goto(`/cv/${published.publicSlug}`);
|
||||
await expect(page.getByTitle("Public CV")).toBeVisible();
|
||||
const publicCvFrame = page.getByTitle("Public CV");
|
||||
await expect(publicCvFrame).toBeVisible();
|
||||
const overflow = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth);
|
||||
expect(overflow).toBeLessThanOrEqual(1);
|
||||
const publicCvMetrics = await publicCvFrame.evaluate((element) => {
|
||||
const frame = element as HTMLIFrameElement;
|
||||
return {
|
||||
renderedWidth: frame.getBoundingClientRect().width,
|
||||
innerOverflow: (frame.contentDocument?.documentElement.scrollWidth ?? 0) - (frame.contentDocument?.documentElement.clientWidth ?? 0),
|
||||
};
|
||||
});
|
||||
expect(publicCvMetrics.renderedWidth).toBeLessThanOrEqual(343);
|
||||
expect(publicCvMetrics.innerOverflow).toBeLessThanOrEqual(1);
|
||||
await expect(page.getByRole("link", { name: "Download PDF" })).toHaveAttribute("href", /\/pdf$/);
|
||||
const pdf = await page.request.get(`${apiUrl}/public-cv/${published.publicSlug}/pdf`);
|
||||
expect(pdf.ok()).toBeTruthy();
|
||||
|
||||
Reference in New Issue
Block a user