Initial commit: Rapport Website (Hugo + Hextra)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import AxeBuilder from "@axe-core/playwright";
|
||||
|
||||
const WCAG_TAGS = ["wcag2a", "wcag2aa", "wcag22aa"];
|
||||
// TODO: Re-enable once known baseline issues are resolved and tracked.
|
||||
const DISABLED_RULES = ["color-contrast", "target-size"];
|
||||
const EXCLUDED_SELECTORS = [
|
||||
// Third-party player internals are outside the theme's control and can change
|
||||
// independently, while the iframe element itself remains covered by page HTML.
|
||||
"iframe[src*=\"youtube.com/embed\"]",
|
||||
"iframe[src*=\"youtube-nocookie.com/embed\"]",
|
||||
];
|
||||
|
||||
type Violation = Awaited<
|
||||
ReturnType<InstanceType<typeof AxeBuilder>["analyze"]>
|
||||
>["violations"][number];
|
||||
|
||||
function decodeXmlEntities(value: string): string {
|
||||
return value
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
function parseLocUrlsFromSitemap(xml: string): string[] {
|
||||
const locRegex = /<loc>\s*([^<]+?)\s*<\/loc>/gi;
|
||||
const urls: string[] = [];
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
while ((match = locRegex.exec(xml)) !== null) {
|
||||
urls.push(decodeXmlEntities(match[1]));
|
||||
}
|
||||
|
||||
return urls;
|
||||
}
|
||||
|
||||
async function getEnglishPages(baseURL: string): Promise<string[]> {
|
||||
const sitemapUrl = `${baseURL}/en/sitemap.xml`;
|
||||
const response = await fetch(sitemapUrl);
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch sitemap (${response.status} ${response.statusText}) at ${sitemapUrl}`,
|
||||
);
|
||||
}
|
||||
|
||||
const xml = await response.text();
|
||||
const pages = parseLocUrlsFromSitemap(xml)
|
||||
.map((url) => {
|
||||
try {
|
||||
return new URL(url).pathname;
|
||||
} catch {
|
||||
return url;
|
||||
}
|
||||
});
|
||||
|
||||
if (pages.length === 0) {
|
||||
throw new Error(`Sitemap at ${sitemapUrl} returned no URLs.`);
|
||||
}
|
||||
|
||||
return pages;
|
||||
}
|
||||
|
||||
function formatViolation(v: Violation): string {
|
||||
return `• ${v.id} (${v.impact}) — ${v.nodes.length} element(s)\n ${v.help}\n ${v.helpUrl}`;
|
||||
}
|
||||
|
||||
test("all English pages pass axe-core WCAG AA", async ({ page, baseURL }) => {
|
||||
const pages = await getEnglishPages(baseURL!);
|
||||
const failures: string[] = [];
|
||||
|
||||
for (const path of pages) {
|
||||
await test.step(path, async () => {
|
||||
await page.goto(path, { waitUntil: "load" });
|
||||
|
||||
const axe = new AxeBuilder({ page })
|
||||
.withTags(WCAG_TAGS)
|
||||
.disableRules(DISABLED_RULES);
|
||||
|
||||
for (const selector of EXCLUDED_SELECTORS) {
|
||||
axe.exclude(selector);
|
||||
}
|
||||
|
||||
const results = await axe.analyze();
|
||||
|
||||
if (results.violations.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
failures.push(
|
||||
`--- ${path} ---\n${results.violations
|
||||
.map(formatViolation)
|
||||
.join("\n\n")}`,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
expect(
|
||||
failures,
|
||||
`Accessibility violations found:\n\n${failures.join("\n\n")}`,
|
||||
).toHaveLength(0);
|
||||
});
|
||||
Reference in New Issue
Block a user