feat(harness): centralize browser coverage contract
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -3,6 +3,7 @@ import path from "node:path";
|
|||||||
import { createRequire } from "node:module";
|
import { createRequire } from "node:module";
|
||||||
|
|
||||||
import { chromium } from "@playwright/test";
|
import { chromium } from "@playwright/test";
|
||||||
|
import harnessStoryContract from "../../tests/e2e/support/story-harness-contract.json" with { type: "json" };
|
||||||
|
|
||||||
import { repoRoot } from "./core.mjs";
|
import { repoRoot } from "./core.mjs";
|
||||||
|
|
||||||
@@ -16,65 +17,36 @@ const axeSourcePath = require.resolve("axe-core/axe.min.js");
|
|||||||
const reportDir = path.join(repoRoot, ".artifacts", "a11y");
|
const reportDir = path.join(repoRoot, ".artifacts", "a11y");
|
||||||
const reportPath = path.join(reportDir, "storybook-a11y.json");
|
const reportPath = path.join(reportDir, "storybook-a11y.json");
|
||||||
|
|
||||||
const stories = [
|
const a11yPrepareHandlers = {
|
||||||
{
|
"open-date-picker": async (page) => {
|
||||||
id: "components-button--playground",
|
await page.getByRole("combobox", { name: "Launch date" }).click();
|
||||||
label: "Button playground"
|
await page.getByRole("dialog", { name: "Launch date calendar" }).waitFor({ state: "visible" });
|
||||||
},
|
},
|
||||||
{
|
"open-dialog": async (page) => {
|
||||||
id: "components-combobox--controlled",
|
await page.getByRole("button", { name: "Open approval dialog" }).click();
|
||||||
label: "Combobox controlled"
|
await page.getByRole("dialog", { name: "Launch this release?" }).waitFor({ state: "visible" });
|
||||||
},
|
},
|
||||||
{
|
"open-dropdown-menu": async (page) => {
|
||||||
id: "components-data-table--playground",
|
await page.getByRole("button", { name: "Review lane menu" }).click();
|
||||||
label: "Data table playground"
|
await page.getByRole("menu").waitFor({ state: "visible" });
|
||||||
},
|
},
|
||||||
{
|
"open-popover": async (page) => {
|
||||||
id: "components-datepicker--playground",
|
await page.getByRole("button", { name: "Inspect summary" }).click();
|
||||||
label: "Date picker playground",
|
await page.getByText("Release health").waitFor({ state: "visible" });
|
||||||
prepare: async (page) => {
|
|
||||||
await page.getByRole("combobox", { name: "Launch date" }).click();
|
|
||||||
await page.getByRole("dialog", { name: "Launch date calendar" }).waitFor({ state: "visible" });
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
"open-sheet": async (page) => {
|
||||||
id: "components-dialog--playground",
|
await page.getByRole("button", { name: "Open right sheet" }).click();
|
||||||
label: "Dialog playground",
|
await page.getByRole("dialog", { name: "Launch settings" }).waitFor({ state: "visible" });
|
||||||
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" });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
];
|
};
|
||||||
|
|
||||||
|
const stories = harnessStoryContract.stories.filter((story) => story.suites.includes("a11y"));
|
||||||
|
|
||||||
|
for (const story of stories) {
|
||||||
|
if (story.a11yPrepare && !(story.a11yPrepare in a11yPrepareHandlers)) {
|
||||||
|
throw new Error(`Unknown a11y prepare handler "${story.a11yPrepare}" for ${story.id}.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function buildStoryUrl(story) {
|
function buildStoryUrl(story) {
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
@@ -215,8 +187,8 @@ async function main() {
|
|||||||
try {
|
try {
|
||||||
await gotoStory(page, story);
|
await gotoStory(page, story);
|
||||||
|
|
||||||
if (story.prepare) {
|
if (story.a11yPrepare) {
|
||||||
await story.prepare(page);
|
await a11yPrepareHandlers[story.a11yPrepare](page);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await runAxe(page);
|
const result = await runAxe(page);
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
// @vitest-environment node
|
||||||
|
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import harnessStoryContract from "../../tests/e2e/support/story-harness-contract.json";
|
||||||
|
|
||||||
|
describe("story harness contract", () => {
|
||||||
|
it("uses unique story ids", () => {
|
||||||
|
const storyIds = harnessStoryContract.stories.map((story) => story.id);
|
||||||
|
|
||||||
|
expect(new Set(storyIds).size).toBe(storyIds.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("requires a reason for every curated story", () => {
|
||||||
|
expect(harnessStoryContract.stories.every((story) => story.reason.trim().length > 0)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("requires smoke scenarios for docs-smoke coverage", () => {
|
||||||
|
expect(
|
||||||
|
harnessStoryContract.stories
|
||||||
|
.filter((story) => story.suites.includes("docs-smoke"))
|
||||||
|
.every((story) => Boolean(story.smokeScenario))
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps every story on at least one supported suite", () => {
|
||||||
|
const supportedSuites = new Set(["a11y", "docs-smoke"]);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
harnessStoryContract.stories.every(
|
||||||
|
(story) => story.suites.length > 0 && story.suites.every((suite) => supportedSuites.has(suite))
|
||||||
|
)
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,76 +1,97 @@
|
|||||||
import { expect, test, type Page } from "@playwright/test";
|
import { expect, test, type Page } from "@playwright/test";
|
||||||
|
import harnessStoryContract from "./support/story-harness-contract.json";
|
||||||
|
|
||||||
async function gotoStory(page: Page, storyId: string) {
|
type StoryHarnessEntry = (typeof harnessStoryContract.stories)[number];
|
||||||
await page.goto(`/iframe.html?id=${storyId}&viewMode=story`);
|
|
||||||
await expect(page).toHaveTitle(new RegExp(storyId, "i"), { timeout: 15_000 });
|
const smokeStories = harnessStoryContract.stories.filter((story) => story.suites.includes("docs-smoke"));
|
||||||
|
|
||||||
|
async function gotoStory(page: Page, story: StoryHarnessEntry) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.goto(`/iframe.html?${params.toString()}`);
|
||||||
|
await expect(page).toHaveTitle(new RegExp(story.id, "i"), { timeout: 15_000 });
|
||||||
}
|
}
|
||||||
|
|
||||||
test("storybook button, select, and static-motion form stories stay interactive", async ({
|
const smokeScenarios: Record<string, (page: Page) => Promise<void>> = {
|
||||||
page
|
"button-playground": async (page) => {
|
||||||
}) => {
|
const button = page.getByRole("button", { name: "Save changes" });
|
||||||
await page.goto("/");
|
await expect(button).toBeVisible();
|
||||||
await expect(page).toHaveTitle(/storybook/i);
|
await button.focus();
|
||||||
|
await expect(button).toBeFocused();
|
||||||
|
},
|
||||||
|
"select-playground": async (page) => {
|
||||||
|
const selectTrigger = page.locator('[data-slot="trigger"]').first();
|
||||||
|
await expect(selectTrigger).toBeVisible();
|
||||||
|
await selectTrigger.click();
|
||||||
|
await expect(page.getByRole("option", { name: "Legal review" })).toBeVisible();
|
||||||
|
},
|
||||||
|
"launch-settings-form": async (page) => {
|
||||||
|
await page.getByRole("textbox", { name: "Email address" }).fill("team@cadence.dev");
|
||||||
|
await page.getByRole("combobox", { name: "Review lane" }).click();
|
||||||
|
await page.getByRole("option", { name: "Legal" }).click();
|
||||||
|
await page.getByRole("textbox", { name: "Launch summary" }).fill(
|
||||||
|
"This release coordinates approvals, copy, and rollout risks."
|
||||||
|
);
|
||||||
|
await page.getByRole("button", { name: "Save settings" }).click();
|
||||||
|
await expect(page.locator("pre code").last()).toContainText('"role": "legal"');
|
||||||
|
},
|
||||||
|
"data-table-playground": async (page) => {
|
||||||
|
const table = page.getByRole("table", { name: "Routing lanes" });
|
||||||
|
await expect(table).toBeVisible();
|
||||||
|
await expect(page.getByRole("searchbox", { name: "Search routing lanes" })).toBeVisible();
|
||||||
|
|
||||||
await gotoStory(page, "components-button--playground");
|
const sortableHeader = page.getByRole("columnheader", { name: /owner/i });
|
||||||
const button = page.getByRole("button", { name: "Save changes" });
|
await sortableHeader.getByRole("button").click();
|
||||||
await expect(button).toBeVisible();
|
await expect(sortableHeader).toHaveAttribute("aria-sort", "ascending");
|
||||||
await button.focus();
|
|
||||||
await expect(button).toBeFocused();
|
|
||||||
|
|
||||||
await gotoStory(page, "components-select--playground");
|
await page.getByRole("checkbox", { name: /select row/i }).first().click();
|
||||||
const selectTrigger = page.locator('[data-slot="trigger"]').first();
|
await expect(page.getByRole("button", { name: "Clear selection" })).toBeVisible();
|
||||||
await expect(selectTrigger).toBeVisible();
|
|
||||||
await selectTrigger.click();
|
|
||||||
await expect(page.getByRole("option", { name: "Legal review" })).toBeVisible();
|
|
||||||
|
|
||||||
await page.goto("/iframe.html?id=components-form--launch-settings&viewMode=story&globals=motion:reduced");
|
const nextButton = page.getByRole("button", { name: "Next" });
|
||||||
await expect(page).toHaveTitle(/components-form--launch-settings/i, { timeout: 15_000 });
|
await expect(nextButton).toBeEnabled();
|
||||||
await page.getByRole("textbox", { name: "Email address" }).fill("team@cadence.dev");
|
await nextButton.click();
|
||||||
await page.getByRole("combobox", { name: "Review lane" }).click();
|
await expect(page.getByRole("button", { name: "Previous" })).toBeEnabled();
|
||||||
await page.getByRole("option", { name: "Legal" }).click();
|
},
|
||||||
await page.getByRole("textbox", { name: "Launch summary" }).fill(
|
"dialog-playground": async (page) => {
|
||||||
"This release coordinates approvals, copy, and rollout risks."
|
await page.getByRole("button", { name: "Open approval dialog" }).click();
|
||||||
);
|
await expect(page.getByRole("dialog", { name: "Launch this release?" })).toBeVisible();
|
||||||
await page.getByRole("button", { name: "Save settings" }).click();
|
await page.getByRole("button", { name: "Close dialog" }).click();
|
||||||
await expect(page.locator("pre code").last()).toContainText('"role": "legal"');
|
await expect(page.getByRole("dialog")).toHaveCount(0);
|
||||||
});
|
},
|
||||||
|
"popover-playground": async (page) => {
|
||||||
|
await page.getByRole("button", { name: "Inspect summary" }).click();
|
||||||
|
await expect(page.getByText("Release health")).toBeVisible();
|
||||||
|
await page.getByRole("button", { name: "Dismiss" }).click();
|
||||||
|
await expect(page.getByText("Release health")).toHaveCount(0);
|
||||||
|
},
|
||||||
|
"sheet-playground": async (page) => {
|
||||||
|
await page.getByRole("button", { name: "Open right sheet" }).click();
|
||||||
|
await expect(page.getByRole("dialog", { name: "Launch settings" })).toBeVisible();
|
||||||
|
await page.getByRole("button", { name: "Close sheet" }).click();
|
||||||
|
await expect(page.getByRole("dialog")).toHaveCount(0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
test("storybook data table story stays interactive", async ({ page }) => {
|
for (const story of smokeStories) {
|
||||||
await gotoStory(page, "components-data-table--playground");
|
const scenario = story.smokeScenario ? smokeScenarios[story.smokeScenario] : null;
|
||||||
|
|
||||||
const table = page.getByRole("table", { name: "Routing lanes" });
|
if (!scenario) {
|
||||||
await expect(table).toBeVisible();
|
throw new Error(`Missing smoke scenario "${story.smokeScenario ?? "undefined"}" for ${story.id}.`);
|
||||||
await expect(page.getByRole("searchbox", { name: "Search routing lanes" })).toBeVisible();
|
}
|
||||||
|
|
||||||
const sortableHeader = page.getByRole("columnheader", { name: /owner/i });
|
test(`storybook ${story.label} stays interactive`, async ({ page }) => {
|
||||||
await sortableHeader.getByRole("button").click();
|
await page.goto("/");
|
||||||
await expect(sortableHeader).toHaveAttribute("aria-sort", "ascending");
|
await expect(page).toHaveTitle(/storybook/i);
|
||||||
|
await gotoStory(page, story);
|
||||||
await page.getByRole("checkbox", { name: /select row/i }).first().click();
|
await scenario(page);
|
||||||
await expect(page.getByRole("button", { name: "Clear selection" })).toBeVisible();
|
});
|
||||||
|
}
|
||||||
const nextButton = page.getByRole("button", { name: "Next" });
|
|
||||||
await expect(nextButton).toBeEnabled();
|
|
||||||
await nextButton.click();
|
|
||||||
await expect(page.getByRole("button", { name: "Previous" })).toBeEnabled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("storybook overlay stories stay interactive", async ({ page }) => {
|
|
||||||
await gotoStory(page, "components-dialog--playground");
|
|
||||||
await page.getByRole("button", { name: "Open approval dialog" }).click();
|
|
||||||
await expect(page.getByRole("dialog", { name: "Launch this release?" })).toBeVisible();
|
|
||||||
await page.getByRole("button", { name: "Close dialog" }).click();
|
|
||||||
await expect(page.getByRole("dialog")).toHaveCount(0);
|
|
||||||
|
|
||||||
await gotoStory(page, "components-popover--playground");
|
|
||||||
await page.getByRole("button", { name: "Inspect summary" }).click();
|
|
||||||
await expect(page.getByText("Release health")).toBeVisible();
|
|
||||||
await page.getByRole("button", { name: "Dismiss" }).click();
|
|
||||||
await expect(page.getByText("Release health")).toHaveCount(0);
|
|
||||||
|
|
||||||
await gotoStory(page, "components-sheet--playground");
|
|
||||||
await page.getByRole("button", { name: "Open right sheet" }).click();
|
|
||||||
await expect(page.getByRole("dialog", { name: "Launch settings" })).toBeVisible();
|
|
||||||
await page.getByRole("button", { name: "Close sheet" }).click();
|
|
||||||
await expect(page.getByRole("dialog")).toHaveCount(0);
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
{
|
||||||
|
"stories": [
|
||||||
|
{
|
||||||
|
"id": "components-button--playground",
|
||||||
|
"label": "Button playground",
|
||||||
|
"reason": "Covers the default trigger and focusable control contract.",
|
||||||
|
"smokeScenario": "button-playground",
|
||||||
|
"suites": ["a11y", "docs-smoke"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "components-select--playground",
|
||||||
|
"label": "Select playground",
|
||||||
|
"reason": "Covers a representative composed trigger and option list flow.",
|
||||||
|
"smokeScenario": "select-playground",
|
||||||
|
"suites": ["docs-smoke"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "components-combobox--controlled",
|
||||||
|
"label": "Combobox controlled",
|
||||||
|
"reason": "Covers the searchable command-driven picker surface.",
|
||||||
|
"suites": ["a11y"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "components-data-table--playground",
|
||||||
|
"label": "Data table playground",
|
||||||
|
"reason": "Covers sorting, selection, pagination, and search on a dense composite surface.",
|
||||||
|
"smokeScenario": "data-table-playground",
|
||||||
|
"suites": ["a11y", "docs-smoke"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"a11yPrepare": "open-date-picker",
|
||||||
|
"id": "components-datepicker--playground",
|
||||||
|
"label": "Date picker playground",
|
||||||
|
"reason": "Covers the calendar overlay and day-grid interaction surface.",
|
||||||
|
"suites": ["a11y"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"a11yPrepare": "open-dialog",
|
||||||
|
"id": "components-dialog--playground",
|
||||||
|
"label": "Dialog playground",
|
||||||
|
"reason": "Covers modal focus management and close behavior.",
|
||||||
|
"smokeScenario": "dialog-playground",
|
||||||
|
"suites": ["a11y", "docs-smoke"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"a11yPrepare": "open-dropdown-menu",
|
||||||
|
"id": "components-dropdownmenu--states",
|
||||||
|
"label": "Dropdown menu states",
|
||||||
|
"reason": "Covers menu semantics on the current review surface.",
|
||||||
|
"suites": ["a11y"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "components-form--launch-settings",
|
||||||
|
"label": "Form launch settings",
|
||||||
|
"query": "globals=motion:reduced",
|
||||||
|
"reason": "Covers a representative reduced-motion form flow with validation and submission state.",
|
||||||
|
"smokeScenario": "launch-settings-form",
|
||||||
|
"suites": ["a11y", "docs-smoke"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"a11yPrepare": "open-popover",
|
||||||
|
"id": "components-popover--playground",
|
||||||
|
"label": "Popover playground",
|
||||||
|
"reason": "Covers anchored disclosure content and dismiss behavior.",
|
||||||
|
"smokeScenario": "popover-playground",
|
||||||
|
"suites": ["a11y", "docs-smoke"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"a11yPrepare": "open-sheet",
|
||||||
|
"id": "components-sheet--playground",
|
||||||
|
"label": "Sheet playground",
|
||||||
|
"reason": "Covers drawer-style overlay semantics and close behavior.",
|
||||||
|
"smokeScenario": "sheet-playground",
|
||||||
|
"suites": ["a11y", "docs-smoke"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user