101 lines
4.9 KiB
TypeScript
101 lines
4.9 KiB
TypeScript
import AxeBuilder from "@axe-core/playwright";
|
|
import { expect, test, type Locator, type Page } from "@playwright/test";
|
|
import path from "node:path";
|
|
|
|
const basicBlend = path.resolve(import.meta.dirname, "../../../tests/files/web/basic_scene.blend");
|
|
const app = (page: Page) => page.locator("main.blender-app");
|
|
|
|
async function focusWithKeyboard(page: Page, target: Locator, backwards = false): Promise<void> {
|
|
for (let index = 0; index < 80; index += 1) {
|
|
if (await target.evaluate((element) => element === document.activeElement)) return;
|
|
await page.keyboard.press(backwards ? "Shift+Tab" : "Tab");
|
|
}
|
|
throw new Error(`Keyboard focus did not reach ${await target.getAttribute("aria-label") ?? await target.textContent() ?? "target"}`);
|
|
}
|
|
|
|
async function expectVisibleKeyboardFocus(target: Locator): Promise<void> {
|
|
await expect(target).toBeFocused();
|
|
await expect.poll(() => target.evaluate((element) => {
|
|
const style = getComputedStyle(element);
|
|
return style.outlineStyle !== "none" && Number.parseFloat(style.outlineWidth) >= 2;
|
|
})).toBe(true);
|
|
}
|
|
|
|
async function expectNoSeriousAccessibilityViolations(page: Page, checkpoint: string): Promise<void> {
|
|
const results = await new AxeBuilder({ page })
|
|
.withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22aa"])
|
|
.analyze();
|
|
const violations = results.violations
|
|
.filter((violation) => violation.impact === "critical" || violation.impact === "serious")
|
|
.map((violation) => ({
|
|
id: violation.id,
|
|
impact: violation.impact,
|
|
help: violation.help,
|
|
targets: violation.nodes.map((node) => node.target.join(" ")),
|
|
}));
|
|
expect(violations, `${checkpoint}: ${JSON.stringify(violations, null, 2)}`).toEqual([]);
|
|
}
|
|
|
|
test("M7-16 completes the P0 project loop without pointer input and passes the accessibility gate", async ({ page }) => {
|
|
test.setTimeout(120_000);
|
|
await page.goto("/");
|
|
await expect(page.getByTestId("engine-status")).toContainText("ready, open a .blend file", { timeout: 20_000 });
|
|
await expectNoSeriousAccessibilityViolations(page, "empty project");
|
|
|
|
const fileMenu = page.getByRole("button", { name: "文件", exact: true });
|
|
await focusWithKeyboard(page, fileMenu);
|
|
await expectVisibleKeyboardFocus(fileMenu);
|
|
await page.keyboard.press("Enter");
|
|
await expect(page.getByRole("menu", { name: "文件" })).toBeVisible();
|
|
await expect(page.getByRole("menuitem", { name: "打开" })).toBeFocused();
|
|
|
|
const fileChooserPromise = page.waitForEvent("filechooser");
|
|
await page.keyboard.press("Enter");
|
|
const fileChooser = await fileChooserPromise;
|
|
await fileChooser.setFiles(basicBlend);
|
|
await expect(page.getByText("BasicCube", { exact: true })).toBeVisible({ timeout: 30_000 });
|
|
await expect(app(page)).toHaveAttribute("data-user-action-open-status", "SUCCEEDED");
|
|
await expect(page.getByTestId("scene-stats")).toContainText("Objects 3");
|
|
|
|
await page.keyboard.press("F3");
|
|
const search = page.getByRole("textbox", { name: "搜索操作" });
|
|
await expectVisibleKeyboardFocus(search);
|
|
await page.keyboard.insertText("Add Cube");
|
|
await page.keyboard.press("Enter");
|
|
await expect(page.getByTestId("scene-stats")).toContainText("Objects 4");
|
|
await expect(app(page)).toHaveAttribute("data-dirty", "true");
|
|
|
|
const blendDownload = page.waitForEvent("download");
|
|
await page.keyboard.press("Control+s");
|
|
await expect((await blendDownload).suggestedFilename()).toBe("blender-web.blend");
|
|
await expect(app(page)).toHaveAttribute("data-user-action-save-status", "SUCCEEDED");
|
|
await expect(app(page)).toHaveAttribute("data-dirty", "false");
|
|
|
|
await page.keyboard.press("F3");
|
|
await expect(search).toBeFocused();
|
|
await page.keyboard.insertText("Close Project");
|
|
await page.keyboard.press("Enter");
|
|
await expect(page.getByTestId("scene-stats")).toContainText("Objects 0");
|
|
|
|
await page.reload();
|
|
await expect(page.getByTestId("engine-status")).toContainText("ready, open a .blend file", { timeout: 20_000 });
|
|
const recentProjects = page.getByRole("combobox", { name: "最近项目" });
|
|
await focusWithKeyboard(page, recentProjects);
|
|
await expectVisibleKeyboardFocus(recentProjects);
|
|
await page.keyboard.press("ArrowDown");
|
|
await page.keyboard.press("Enter");
|
|
await expect(page.getByTestId("scene-stats")).toContainText("Objects 4", { timeout: 30_000 });
|
|
await expect(app(page)).toHaveAttribute("data-dirty", "false");
|
|
await expectNoSeriousAccessibilityViolations(page, "reopened project");
|
|
|
|
const renderMenu = page.getByRole("button", { name: "渲染", exact: true });
|
|
await focusWithKeyboard(page, renderMenu, true);
|
|
await expectVisibleKeyboardFocus(renderMenu);
|
|
await page.keyboard.press("Enter");
|
|
await expect(page.getByRole("menuitem", { name: "导出 GLB" })).toBeFocused();
|
|
const glbDownload = page.waitForEvent("download");
|
|
await page.keyboard.press("Enter");
|
|
await expect((await glbDownload).suggestedFilename()).toBe("blender-web.glb");
|
|
await expect(app(page)).toHaveAttribute("data-user-action-export-status", "SUCCEEDED");
|
|
});
|