import assert from "node:assert/strict"; import { pathToFileURL } from "node:url"; const modulePath = process.env.PLAYWRIGHT_MODULE; if (!modulePath) { console.log(JSON.stringify({ schema: "rtcp-program-library-browser.v1", status: "blocked", code: "playwright_unavailable" })); process.exit(0); } const imported = await import(pathToFileURL(modulePath).href); const playwright = imported.default ?? imported; const browser = await playwright.chromium.launch({ headless: true, executablePath: process.env.BROWSER_EXECUTABLE_PATH ?? "/usr/bin/google-chrome", args: ["--no-sandbox", "--use-angle=swiftshader", "--enable-unsafe-swiftshader"] }); const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } }); const errors = []; page.on("pageerror", (error) => errors.push(`pageerror:${error.message}`)); page.on("console", (message) => { if (message.type() === "error") errors.push(`console:${message.text()}`); }); const control = (id) => page.locator(`[data-axis-control="${id}"]`); const main = page.locator("main.axis-app"); const waitReady = () => page.waitForFunction(() => { const element = document.querySelector("main.axis-app"); return element?.dataset.axisStateSource === "linuxcnc-wasm" && element?.dataset.axisProgramLoading === "false"; }, null, { timeout: 45000 }); const waitPath = (path) => page.waitForFunction((expected) => { const element = document.querySelector("main.axis-app"); return element?.dataset.axisProgramLoading === "false" && element?.dataset.axisProgramPath === expected && element?.dataset.axisProgramStatus === "loaded"; }, path, { timeout: 45000 }); const report = { schema: "rtcp-program-library-browser.v1", status: "running", checks: [] }; try { await page.goto(process.env.RTCP_LIBRARY_URL ?? "http://127.0.0.1:5174/", { waitUntil: "domcontentloaded", timeout: 30000 }); await waitReady(); assert.match(await control("workspace-rtcp-programs").textContent(), /AC(XYZAC) RTCP 程序 \(15\)/); await control("workspace-rtcp-programs").click(); assert.equal(await control("rtcp-program-scope").inputValue(), "machine"); assert.equal(await control("rtcp-program-select").locator("option").count(), 15); assert.deepEqual(await control("rtcp-program-select").locator("optgroup").evaluateAll((nodes) => nodes.map((node) => ({ label: node.label, count: node.querySelectorAll("option").length }))), [ { label: "TRT 旋转倾斜台 XYZAC/XYZBC (12)", count: 12 }, { label: "gmoccapy TRT 示例 (3)", count: 3 }, ]); report.checks.push("AC machine defaults to 15 XYZAC/common programs"); await control("rtcp-program-scope").selectOption("all"); assert.equal(await control("rtcp-program-select").locator("option").count(), 32); assert.deepEqual(await control("rtcp-program-select").locator("optgroup").evaluateAll((nodes) => nodes.map((node) => ({ label: node.label, count: node.querySelectorAll("option").length }))), [ { label: "TRT 旋转倾斜台 XYZAC/XYZBC (16)", count: 16 }, { label: "gmoccapy TRT 示例 (5)", count: 5 }, { label: "XYZAB 双旋转台 (3)", count: 3 }, { label: "Bridge Mill / 5axiskins (4)", count: 4 }, { label: "Hexapod genhexkins RTCP (3)", count: 3 }, { label: "独立五轴动态刀长示例 (1)", count: 1 }, ]); report.checks.push("all-source scope retains 32 programs and 6 source groups"); await control("rtcp-program-select").selectOption("trt-remap-m428"); assert.equal(await control("rtcp-program-load").isDisabled(), true); await control("rtcp-program-source").click(); await page.waitForFunction(() => document.querySelector(".rtcp-program-source")?.value.includes("motion.switchkins-type")); report.checks.push("support remap is source-readable and execution-disabled"); await control("rtcp-program-select").selectOption("xyzab-tdr-demo"); assert.equal(await control("rtcp-program-load").isDisabled(), true); assert.match(await page.locator(".rtcp-incompatible").textContent(), /需要 XYZAB/); report.checks.push("mismatched XYZAB program is visible and execution-disabled"); const xyzacPath = "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins_test_3.ngc"; await control("rtcp-program-select").selectOption("trt-xyzac-switchkins-test-3"); assert.equal(await control("rtcp-program-load").isDisabled(), false); await control("rtcp-program-load").click(); await waitPath(xyzacPath); assert.match(await main.getAttribute("data-axis-program-source-hash"), /^fnv1a64:/); assert.ok(Number(await page.locator(".xyzac-tool-preview").getAttribute("data-xyzac-toolpath-count")) > 0); report.checks.push("XYZAC switchkins test loaded through WASM with canonical source hash and preview"); await page.keyboard.press("Escape"); const xyzbcCase = "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini"; await control("workspace-case-select").selectOption(xyzbcCase); await page.waitForFunction((expected) => document.querySelector('[data-axis-control="workspace-case-select"]')?.value === expected && document.querySelector("main.axis-app")?.dataset.axisProgramLoading === "false", xyzbcCase, { timeout: 45000 }); assert.match(await control("workspace-rtcp-programs").textContent(), /BC(XYZBC) RTCP 程序 \(9\)/); await control("workspace-rtcp-programs").click(); assert.equal(await control("rtcp-program-scope").inputValue(), "machine"); assert.equal(await control("rtcp-program-select").locator("option").count(), 9); const xyzbcPath = "linuxcnc/configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/examples/boat-xyzbc.ngc"; await control("rtcp-program-select").selectOption("gmoccapy-boat-xyzbc"); assert.equal(await control("rtcp-program-load").isDisabled(), false); await control("rtcp-program-load").click(); await waitPath(xyzbcPath); assert.ok(Number(await page.locator(".xyzac-tool-preview").getAttribute("data-xyzac-toolpath-count")) > 0); report.checks.push("BC machine defaults to 9 XYZBC/common programs and loads gmoccapy XYZBC through WASM"); assert.deepEqual(errors, []); report.status = "verified"; } catch (error) { report.status = "failed"; report.error = error instanceof Error ? error.stack : String(error); await page.screenshot({ path: "/tmp/rtcp-program-library-failure.png", fullPage: true }); throw error; } finally { report.browser_errors = errors; report.final = await main.evaluate((element) => ({ coordinates: element.querySelector('[data-axis-control="workspace-case-select"]')?.value, program_path: element.dataset.axisProgramPath, program_status: element.dataset.axisProgramStatus, source_hash: element.dataset.axisProgramSourceHash })).catch(() => null); await browser.close(); } console.log(JSON.stringify(report));