Files
workinf_Blender_Wasm/tools/web/check-chromium-accessibility.mjs
mes123456 10640aeb3c
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
Govern task context and advance execution pointer
2026-08-20 06:02:43 -04:00

126 lines
8.2 KiB
JavaScript

import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import http from "node:http";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const distRoot = path.join(root, "web/dist");
const reportPath = path.join(root, "tests/golden/M14-04H/chromium-accessibility-report.json");
const manifestPath = path.join(root, "tests/golden/M14-04H/manifest.json");
const mime = new Map([[".html", "text/html; charset=utf-8"], [".js", "text/javascript; charset=utf-8"], [".css", "text/css; charset=utf-8"], [".json", "application/json"], [".wasm", "application/wasm"]]);
const server = http.createServer((request, response) => {
const pathname = decodeURIComponent(new URL(request.url ?? "/", "http://127.0.0.1").pathname);
const relative = pathname === "/" ? "index.html" : pathname.replace(/^\//u, "");
const file = path.resolve(distRoot, relative);
if (!file.startsWith(`${distRoot}${path.sep}`) || !fs.existsSync(file) || !fs.statSync(file).isFile()) {
response.writeHead(404);
response.end("not found");
return;
}
response.statusCode = 200;
response.setHeader("Content-Type", mime.get(path.extname(file)) ?? "application/octet-stream");
response.setHeader("Cross-Origin-Opener-Policy", "same-origin");
response.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
response.setHeader("Cross-Origin-Resource-Policy", "same-origin");
response.setHeader("Content-Security-Policy", "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; worker-src 'self'; connect-src 'self'; font-src 'self'; img-src 'self'; media-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'");
fs.createReadStream(file).pipe(response);
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
const address = server.address();
assert.ok(address && typeof address === "object");
let browser;
try {
const playwright = await import(pathToFileURL(path.join(root, "web/node_modules/playwright/index.mjs")).href);
browser = await playwright.chromium.launch({ headless: true, args: ["--no-sandbox", "--use-gl=swiftshader", "--enable-unsafe-swiftshader"] });
const results = {};
for (const backend of ["main", "offscreen"]) {
const context = await browser.newContext({ viewport: { width: 1280, height: 720 }, hasTouch: true, isMobile: false });
const page = await context.newPage();
await page.goto(`http://127.0.0.1:${address.port}/?offscreen=${backend === "offscreen" ? "1" : "0"}`, { waitUntil: "load" });
await page.waitForFunction(() => document.querySelector("[data-testid=engine-status]")?.textContent === "Engine: ready, open a .blend file", undefined, { timeout: 20_000 });
const focusByTab = async (locator) => {
for (let index = 0; index < 100; index += 1) {
if (await locator.evaluate((element) => element === document.activeElement)) return index;
await page.keyboard.press("Tab");
}
throw new Error(`KEYBOARD_FOCUS_NOT_REACHED:${await locator.getAttribute("aria-label") ?? await locator.textContent() ?? "target"}`);
};
const waitFocused = async (locator) => {
const handle = await locator.elementHandle();
assert.ok(handle);
await page.waitForFunction((element) => document.activeElement === element, handle);
};
const fileMenu = page.getByRole("button", { name: "文件", exact: true });
const fileTabCount = await focusByTab(fileMenu);
await page.keyboard.press("Enter");
const fileItem = page.getByRole("menuitem", { name: "打开", exact: true });
await fileItem.waitFor({ state: "visible" });
assert.equal(await fileItem.evaluate((element) => element === document.activeElement), true);
await page.keyboard.press("Escape");
await waitFocused(fileMenu);
await page.keyboard.press("F3");
const search = page.getByRole("textbox", { name: "搜索操作", exact: true });
await search.waitFor({ state: "visible" });
assert.equal(await search.evaluate((element) => element === document.activeElement), true);
await page.keyboard.press("Escape");
const searchTrigger = page.getByRole("button", { name: "操作搜索", exact: true });
await waitFocused(searchTrigger);
const accessible = await page.evaluate(() => {
const visible = (element) => {
const style = getComputedStyle(element);
const rect = element.getBoundingClientRect();
return style.display !== "none" && style.visibility !== "hidden" && rect.width > 0 && rect.height > 0;
};
const name = (element) => {
const aria = element.getAttribute("aria-label")?.trim();
if (aria) return aria;
const labelledBy = element.getAttribute("aria-labelledby");
if (labelledBy) return labelledBy.split(/\s+/u).map((id) => document.getElementById(id)?.textContent?.trim() ?? "").join(" ").trim();
if (element.id) {
const label = document.querySelector(`label[for="${CSS.escape(element.id)}"]`);
if (label?.textContent?.trim()) return label.textContent.trim();
}
return element.textContent?.trim() ?? "";
};
const missing = [];
for (const element of document.querySelectorAll("button, input, select, textarea, canvas, [role]")) {
if (!visible(element)) continue;
const role = element.getAttribute("role") ?? element.tagName.toLowerCase();
if (!name(element)) missing.push({ role, tag: element.tagName.toLowerCase(), html: element.outerHTML.slice(0, 180) });
}
return { missing, visibleInteractiveCount: [...document.querySelectorAll("button, input, select, textarea, canvas, [role]")].filter(visible).length };
});
assert.deepEqual(accessible.missing, [], JSON.stringify({ backend, accessible }));
results[backend] = { fileTabCount, accessible };
await context.close();
}
const report = { schemaVersion: 1, task: "M14-04H", operation: "CHROMIUM_KEYBOARD_ACCESSIBILITY_BOUNDARY", runtime: "PLAYWRIGHT_CHROMIUM", backends: ["main", "offscreen"], results, guarantees: { menuFocus: "RESTORED", operatorSearchFocus: "RESTORED", accessibleNames: "COMPLETE" }, execution: "DISABLED", nextTask: "M15-01A" };
const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
const relative = (file) => path.relative(root, file).replaceAll(path.sep, "/");
if (process.env.UPDATE_M14_04H_REPORT === "1") {
fs.mkdirSync(path.dirname(reportPath), { recursive: true });
fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`);
const artifactPaths = {
parentManifest: path.join(root, "tests/golden/M14-04G/manifest.json"),
checker: path.join(root, "tools/web/check-chromium-accessibility.mjs"),
app: path.join(root, "web/app/src/app/App.tsx"),
css: path.join(root, "web/app/src/app/app-shell.css"),
referenceE2E: path.join(root, "web/tests/e2e/keyboard-accessibility.spec.ts"),
package: path.join(root, "web/package.json"),
report: reportPath,
};
const artifacts = Object.fromEntries(Object.entries(artifactPaths).map(([name, file]) => [name, { path: relative(file), sha256: sha256(file) }]));
fs.writeFileSync(manifestPath, `${JSON.stringify({ schemaVersion: 1, task: "M14-04H", parentTask: "M14-04G", enablingTask: false, parityStateChange: false, runtime: "PLAYWRIGHT_CHROMIUM", operation: "CHROMIUM_KEYBOARD_ACCESSIBILITY_BOUNDARY", artifacts, nextTask: "M15-01A" }, null, 2)}\n`);
}
assert.deepEqual(JSON.parse(fs.readFileSync(reportPath, "utf8")), report);
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
assert.deepEqual({ task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, { task: "M14-04H", parentTask: "M14-04G", nextTask: "M15-01A" });
for (const artifact of Object.values(manifest.artifacts)) assert.equal(sha256(path.join(root, artifact.path)), artifact.sha256, artifact.path);
process.stdout.write(`chromium-accessibility-ok backends=main,offscreen menuFocus=RESTORED operatorSearchFocus=RESTORED names=COMPLETE execution=DISABLED next=${manifest.nextTask}\n`);
} finally {
await browser?.close();
await new Promise((resolve) => server.close(resolve));
}