Document source UI gates, explicit native live-plot clearing, and remaining parity boundaries. Fix pause/resume transport continuity, repeated stepping, coordinate previews, and spindle override state. Optimize large program snapshots and rendering, and cover the control flows with native Tcl, WASM, and browser checks. Validation: 22 native tests and source/browser checks pass. Worker/App regression: 113 of 123 pass; 10 pre-existing failures remain documented with no new failures.
123 lines
7.4 KiB
JavaScript
123 lines
7.4 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { pathToFileURL } from "node:url";
|
|
|
|
assert.ok(process.env.PLAYWRIGHT_MODULE, "PLAYWRIGHT_MODULE is required");
|
|
const { chromium } = await import(pathToFileURL(process.env.PLAYWRIGHT_MODULE).href);
|
|
const url = process.env.XYZAC_PANEL_URL ?? "http://127.0.0.1:5175/";
|
|
const browser = await chromium.launch({ executablePath: "/usr/bin/google-chrome", headless: true, args: ["--disable-gpu"] });
|
|
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
|
|
const browserErrors = [], checks = [];
|
|
page.on("pageerror", (error) => browserErrors.push(error.message));
|
|
await page.addInitScript(() => {
|
|
window.axisPauseRequests = []; window.axisPauseResults = [];
|
|
const NativeWorker = window.Worker;
|
|
window.Worker = class extends NativeWorker {
|
|
constructor(...args) {
|
|
super(...args);
|
|
this.addEventListener("message", ({ data }) => window.axisPauseResults.push({ seq: data.seq, kind: data.kind, status: data.payload?.status, error: data.payload?.gate_reason ?? data.payload?.message_key ?? data.payload?.code }));
|
|
}
|
|
postMessage(data, ...args) {
|
|
if (!data.kind.startsWith("UI_STATE_")) window.axisPauseRequests.push({ seq: data.seq, kind: data.kind });
|
|
super.postMessage(data, ...args);
|
|
}
|
|
};
|
|
});
|
|
const control = (id) => page.locator(`[data-axis-control="${id}"]`);
|
|
const state = () => page.locator("main.axis-app").evaluate((main) => {
|
|
const viewer = main.querySelector("[data-xyzac-preview]");
|
|
return { loading: main.dataset.axisProgramLoading, program: main.dataset.axisProgramStatus, path: main.dataset.axisProgramPath,
|
|
mode: main.dataset.axisTaskMode, operation: main.querySelector("[data-axis-state-operation]")?.dataset.axisStateOperation,
|
|
pose: viewer?.dataset.xyzacLivePose, tick: Number(viewer?.dataset.xyzacTick), epoch: viewer?.dataset.xyzacLivePathEpoch,
|
|
queue: Number(main.dataset.axisTpQueueDepth), command: main.querySelector(".command-status")?.textContent,
|
|
error: main.querySelector(".diagnostic-error")?.textContent };
|
|
});
|
|
const wait = async (predicate, name, timeout = 30000) => {
|
|
const start = performance.now(); let latest;
|
|
while (performance.now() - start < timeout) { latest = await state(); if (predicate(latest)) return latest; await page.waitForTimeout(50); }
|
|
throw new Error(`${name}: ${JSON.stringify(latest)}`);
|
|
};
|
|
const action = async (id, kind) => {
|
|
const seq = await page.evaluate(() => window.axisPauseRequests.at(-1)?.seq ?? 0);
|
|
await control(id).click();
|
|
await page.waitForFunction(({ seq, kind }) => window.axisPauseRequests.some((request) => request.seq > seq && request.kind === kind && window.axisPauseResults.some((r) => r.seq === request.seq && r.kind === "COMMAND_RESULT")), { seq, kind });
|
|
};
|
|
const clearFocus = () => page.evaluate(() => { document.activeElement?.blur(); });
|
|
const moveAfterResume = async (paused) => {
|
|
await action("toolbar-pause-resume", "PROGRAM_RESUME");
|
|
const resumed = await wait((s) => s.operation === "RUNNING" && s.tick >= paused.tick + 10 && s.pose !== paused.pose, "resume must move");
|
|
await page.waitForTimeout(250);
|
|
assert.ok((await state()).tick > resumed.tick, "sampling stopped after one resume frame");
|
|
return resumed;
|
|
};
|
|
try {
|
|
await page.goto(url);
|
|
await wait((s) => s.loading === "false" && s.program === "loaded", "startup");
|
|
await action("toolbar-power", "POWER"); await action("manual-home-all", "HOME");
|
|
for (const program of ["configured", "chips"]) {
|
|
if (program === "chips") {
|
|
await action("workspace-load-3d-chips", "PROGRAM_LOAD");
|
|
await wait((s) => s.path === "linuxcnc/nc_files/3D_Chips.ngc" && s.loading === "false", "chips load");
|
|
}
|
|
await action("toolbar-run", "PROGRAM_RUN");
|
|
await wait((s) => s.operation === "RUNNING" && s.queue > 0, "running");
|
|
for (let round = 0; round < 3; round++) {
|
|
await action("toolbar-pause-resume", "PROGRAM_PAUSE");
|
|
const paused = await wait((s) => s.operation === "PAUSED", "paused");
|
|
await action("toolbar-clear-live-plot", "VISUAL_CLEAR");
|
|
for (const id of ["override-spindle", "override-max-velocity", "override-feed", "override-jog-speed"]) {
|
|
await control(id).focus(); await page.keyboard.press("ArrowLeft"); await page.waitForTimeout(150);
|
|
await page.keyboard.press("ArrowRight"); await page.waitForTimeout(150);
|
|
}
|
|
await page.waitForTimeout(150);
|
|
assert.equal((await state()).pose, paused.pose, "a paused control changed position");
|
|
assert.ok(await control("toolbar-pause-resume").isEnabled());
|
|
await moveAfterResume(paused);
|
|
}
|
|
await clearFocus();
|
|
// A stale/duplicate Resume is a task rejection, not a connection failure.
|
|
await page.keyboard.press("s");
|
|
await wait((s) => s.command?.includes("PROGRAM_RESUME blocked"), "rejected running resume");
|
|
const refused = await state();
|
|
await wait((s) => s.tick > refused.tick + 5, "refused command must not stop a running task");
|
|
await action("toolbar-pause-resume", "PROGRAM_PAUSE");
|
|
const paused = await state();
|
|
await clearFocus(); await page.keyboard.press("p");
|
|
await wait((s) => s.command?.includes("PROGRAM_PAUSE blocked"), "duplicate pause rejection");
|
|
await moveAfterResume(paused);
|
|
await action("toolbar-stop", "PROGRAM_ABORT");
|
|
await wait((s) => s.operation === "IDLE" && s.queue === 0, "stop after resume");
|
|
checks.push(`${program}: repeated pause/clear/overrides/resume + rejection/stop`);
|
|
}
|
|
await action("toolbar-run", "PROGRAM_RUN");
|
|
await wait((s) => s.queue > 0, "step precondition");
|
|
await action("toolbar-pause-resume", "PROGRAM_PAUSE");
|
|
const beforeStep = await state();
|
|
await action("toolbar-step", "PROGRAM_STEP");
|
|
await wait((s) => s.pose !== beforeStep.pose, "paused Step moves");
|
|
const afterStep = await state();
|
|
await moveAfterResume(afterStep);
|
|
await action("toolbar-stop", "PROGRAM_ABORT");
|
|
checks.push("paused Step then Resume then Stop");
|
|
const requests = await page.evaluate(() => window.axisPauseRequests);
|
|
requests.forEach((request, index) => assert.equal(request.seq, index + 1));
|
|
const errors = await page.evaluate(() => window.axisPauseResults.filter((r) => r.kind === "ERROR"));
|
|
assert.equal(errors.length, 4, JSON.stringify(errors));
|
|
assert.ok(errors.every((r) => !r.error?.startsWith("protocol.")), JSON.stringify(errors));
|
|
assert.deepEqual(browserErrors, []);
|
|
await page.screenshot({ path: "/tmp/axis-pause-resume-desktop.png", fullPage: true });
|
|
await page.setViewportSize({ width: 390, height: 844 });
|
|
await action("toolbar-run", "PROGRAM_RUN");
|
|
await wait((s) => s.queue > 0, "mobile running");
|
|
await action("toolbar-pause-resume", "PROGRAM_PAUSE");
|
|
const mobilePaused = await state();
|
|
await action("toolbar-clear-live-plot", "VISUAL_CLEAR");
|
|
await moveAfterResume(mobilePaused);
|
|
await action("toolbar-stop", "PROGRAM_ABORT");
|
|
await page.screenshot({ path: "/tmp/axis-pause-resume-mobile.png", fullPage: true });
|
|
checks.push("mobile pause/clear/resume/stop");
|
|
console.log(JSON.stringify({ schema: "xyzac-pause-resume-browser.v1", status: "verified", url, checks, ordered_commands: requests.length, expected_task_rejections: errors.length, browser_errors: browserErrors }));
|
|
} catch (error) {
|
|
process.stderr.write(JSON.stringify({ state: await state(), recent: await page.evaluate(() => ({ requests: window.axisPauseRequests.slice(-8), results: window.axisPauseResults.slice(-8) })) }) + "\n");
|
|
throw error;
|
|
} finally { await browser.close(); }
|