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 { 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 { 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 { 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"); });