Add harness workflow and Material showcase design system
This commit is contained in:
@@ -0,0 +1,238 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { createRequire } from "node:module";
|
||||
|
||||
import { chromium } from "@playwright/test";
|
||||
|
||||
import { repoRoot } from "./core.mjs";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const { startStorybookServer, stopStorybookServer } = require(
|
||||
path.resolve(repoRoot, "tests/e2e/support/storybook-server.cjs")
|
||||
);
|
||||
|
||||
const baseURL = "http://127.0.0.1:6006";
|
||||
const axeSourcePath = require.resolve("axe-core/axe.min.js");
|
||||
const reportDir = path.join(repoRoot, ".artifacts", "a11y");
|
||||
const reportPath = path.join(reportDir, "storybook-a11y.json");
|
||||
|
||||
const stories = [
|
||||
{
|
||||
id: "components-button--playground",
|
||||
label: "Button playground"
|
||||
},
|
||||
{
|
||||
id: "components-combobox--controlled",
|
||||
label: "Combobox controlled"
|
||||
},
|
||||
{
|
||||
id: "components-data-table--playground",
|
||||
label: "Data table playground"
|
||||
},
|
||||
{
|
||||
id: "components-datepicker--playground",
|
||||
label: "Date picker playground",
|
||||
prepare: async (page) => {
|
||||
await page.getByRole("combobox", { name: "Launch date" }).click();
|
||||
await page.getByRole("grid").waitFor({ state: "visible" });
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "components-dialog--playground",
|
||||
label: "Dialog playground",
|
||||
prepare: async (page) => {
|
||||
await page.getByRole("button", { name: "Open approval dialog" }).click();
|
||||
await page.getByRole("dialog", { name: "Launch this release?" }).waitFor({ state: "visible" });
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "components-dropdownmenu--states",
|
||||
label: "Dropdown menu states",
|
||||
prepare: async (page) => {
|
||||
await page.getByRole("button", { name: "Review lane menu" }).click();
|
||||
await page.getByRole("menu").waitFor({ state: "visible" });
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "components-form--launch-settings",
|
||||
label: "Form launch settings",
|
||||
query: "globals=motion:reduced"
|
||||
},
|
||||
{
|
||||
id: "components-popover--playground",
|
||||
label: "Popover playground",
|
||||
prepare: async (page) => {
|
||||
await page.getByRole("button", { name: "Inspect summary" }).click();
|
||||
await page.getByText("Release health").waitFor({ state: "visible" });
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "components-sheet--playground",
|
||||
label: "Sheet playground",
|
||||
prepare: async (page) => {
|
||||
await page.getByRole("button", { name: "Open right sheet" }).click();
|
||||
await page.getByRole("dialog", { name: "Launch settings" }).waitFor({ state: "visible" });
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
function buildStoryUrl(story) {
|
||||
const params = new URLSearchParams({
|
||||
id: story.id,
|
||||
viewMode: "story"
|
||||
});
|
||||
|
||||
if (story.query) {
|
||||
for (const [key, value] of new URLSearchParams(story.query).entries()) {
|
||||
params.set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
return `${baseURL}/iframe.html?${params.toString()}`;
|
||||
}
|
||||
|
||||
async function gotoStory(page, story) {
|
||||
await page.goto(buildStoryUrl(story));
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
await page.waitForFunction(
|
||||
(storyId) => {
|
||||
const title = document.title.toLowerCase();
|
||||
const root = document.getElementById("storybook-root");
|
||||
|
||||
return (
|
||||
title.includes(String(storyId).toLowerCase()) ||
|
||||
Boolean(root && root.childElementCount > 0)
|
||||
);
|
||||
},
|
||||
story.id,
|
||||
{ timeout: 30_000 }
|
||||
);
|
||||
}
|
||||
|
||||
function summarizeResults(results) {
|
||||
return results.map((result) => ({
|
||||
description: result.description,
|
||||
help: result.help,
|
||||
helpUrl: result.helpUrl,
|
||||
id: result.id,
|
||||
impact: result.impact,
|
||||
nodeCount: result.nodes.length,
|
||||
nodes: result.nodes.map((node) => ({
|
||||
failureSummary: node.failureSummary,
|
||||
html: node.html,
|
||||
target: node.target
|
||||
}))
|
||||
}));
|
||||
}
|
||||
|
||||
async function runAxe(page) {
|
||||
return page.evaluate(async () => {
|
||||
const root = document.getElementById("storybook-root") ?? document.body;
|
||||
return window.axe.run(root, {
|
||||
resultTypes: ["violations", "incomplete"]
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const axeSource = await fs.readFile(axeSourcePath, "utf8");
|
||||
await fs.mkdir(reportDir, { recursive: true });
|
||||
|
||||
await startStorybookServer();
|
||||
|
||||
const browser = await chromium.launch({
|
||||
headless: true
|
||||
});
|
||||
const context = await browser.newContext();
|
||||
await context.addInitScript({ content: axeSource });
|
||||
|
||||
const report = {
|
||||
checkedAt: new Date().toISOString(),
|
||||
errors: [],
|
||||
incompleteCount: 0,
|
||||
stories: [],
|
||||
storyCount: stories.length,
|
||||
violationCount: 0
|
||||
};
|
||||
|
||||
try {
|
||||
const page = await context.newPage();
|
||||
|
||||
for (const story of stories) {
|
||||
process.stdout.write(`[a11y] checking ${story.id}\n`);
|
||||
|
||||
try {
|
||||
await gotoStory(page, story);
|
||||
|
||||
if (story.prepare) {
|
||||
await story.prepare(page);
|
||||
}
|
||||
|
||||
const result = await runAxe(page);
|
||||
const violations = summarizeResults(result.violations);
|
||||
const incomplete = summarizeResults(result.incomplete);
|
||||
|
||||
report.violationCount += violations.length;
|
||||
report.incompleteCount += incomplete.length;
|
||||
report.stories.push({
|
||||
id: story.id,
|
||||
incomplete,
|
||||
label: story.label,
|
||||
url: buildStoryUrl(story),
|
||||
violations
|
||||
});
|
||||
|
||||
process.stdout.write(
|
||||
`[a11y] ${story.id}: ${violations.length} violations, ${incomplete.length} incomplete\n`
|
||||
);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
|
||||
report.errors.push({
|
||||
id: story.id,
|
||||
label: story.label,
|
||||
message
|
||||
});
|
||||
report.stories.push({
|
||||
error: message,
|
||||
id: story.id,
|
||||
incomplete: [],
|
||||
label: story.label,
|
||||
url: buildStoryUrl(story),
|
||||
violations: []
|
||||
});
|
||||
|
||||
process.stdout.write(`[a11y] ${story.id}: error: ${message}\n`);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await context.close();
|
||||
await browser.close();
|
||||
await stopStorybookServer();
|
||||
}
|
||||
|
||||
await fs.writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
|
||||
process.stdout.write(`[a11y] Report written to ${path.relative(repoRoot, reportPath)}\n`);
|
||||
|
||||
if (report.incompleteCount > 0) {
|
||||
process.stdout.write(
|
||||
`[a11y] ${report.incompleteCount} incomplete results detected. Review the JSON report before merge.\n`
|
||||
);
|
||||
}
|
||||
|
||||
if (report.errors.length > 0) {
|
||||
process.stdout.write(
|
||||
`[a11y] ${report.errors.length} story checks failed before axe completed. Failing validation.\n`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (report.violationCount > 0) {
|
||||
process.stdout.write(
|
||||
`[a11y] ${report.violationCount} violations detected. Failing validation.\n`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
await main();
|
||||
Reference in New Issue
Block a user