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.
121 lines
7.7 KiB
JavaScript
121 lines
7.7 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({ headless: true, executablePath: process.env.BROWSER_EXECUTABLE_PATH ?? "/usr/bin/google-chrome", args: ["--disable-gpu"] });
|
|
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
|
|
const errors = [];
|
|
page.on("pageerror", (error) => errors.push(error.message));
|
|
await page.addInitScript(() => {
|
|
const NativeWorker = window.Worker;
|
|
window.stepTimes = [];
|
|
window.workerErrors = [];
|
|
window.Worker = class extends NativeWorker {
|
|
pending = new Map();
|
|
constructor(...args) {
|
|
super(...args);
|
|
this.addEventListener("message", (event) => {
|
|
const value = event.data, request = this.pending.get(value.seq);
|
|
if (request?.kind === "SIM_STEP") window.stepTimes.push(performance.now() - request.time);
|
|
if (value.kind === "ERROR") window.workerErrors.push(value.payload?.gate_reason ?? value.payload?.message_key ?? value.payload?.code);
|
|
if (request) this.pending.delete(value.seq);
|
|
});
|
|
}
|
|
postMessage(value, ...args) {
|
|
this.pending.set(value.seq, { kind: value.kind, time: performance.now() });
|
|
super.postMessage(value, ...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 { path: main.dataset.axisProgramPath, loading: main.dataset.axisProgramLoading, status: main.dataset.axisProgramStatus, mode: main.dataset.axisTaskMode, queue: Number(main.dataset.axisTpQueueDepth), line: Number(main.dataset.axisCurrentLine), tick: Number(viewer?.dataset.xyzacTick), epoch: Number(viewer?.dataset.xyzacLivePathEpoch), live: Number(viewer?.dataset.xyzacLiveTraceCount), preview: Number(viewer?.dataset.xyzacToolpathCount), draws: Number(viewer?.dataset.xyzacPreviewDrawCalls), vertices: Number(viewer?.dataset.xyzacPreviewVertices), pose: JSON.parse(viewer?.dataset.xyzacLivePose ?? "{}"), error: main.querySelector(".diagnostic-error")?.textContent ?? null };
|
|
});
|
|
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(100); }
|
|
throw new Error(`${name}: ${JSON.stringify(latest)}`);
|
|
};
|
|
const sampleRun = async () => {
|
|
await control("toolbar-run").click();
|
|
await wait((s) => s.status === "running", "second run");
|
|
await page.evaluate(() => { window.stepTimes = []; });
|
|
const before = await state(), started = performance.now();
|
|
await page.waitForTimeout(5000);
|
|
const after = await state(), wall = performance.now() - started;
|
|
const simulated = (after.tick - before.tick) * 10;
|
|
const latencies = await page.evaluate(() => window.stepTimes);
|
|
const p95 = latencies.sort((a, b) => a - b)[Math.floor(latencies.length * .95)];
|
|
assert.ok(simulated / wall >= .75, `simulation fell behind real time: ${simulated}/${wall}`);
|
|
assert.ok(p95 < 150, `Worker p95 ${p95} ms`);
|
|
assert.ok(after.line > before.line && after.live > 0 && after.queue > 0);
|
|
const pauseStart = performance.now();
|
|
await control("toolbar-pause-resume").click();
|
|
const paused = await wait((s) => s.status === "paused", "pause");
|
|
const pauseMs = performance.now() - pauseStart;
|
|
await page.waitForTimeout(350);
|
|
assert.deepEqual((await state()).pose, paused.pose);
|
|
await control("toolbar-pause-resume").click();
|
|
await wait((s) => s.status === "running", "resume");
|
|
const stopStart = performance.now();
|
|
await control("toolbar-stop").click();
|
|
await wait((s) => s.status === "aborted" && s.queue === 0, "stop");
|
|
const stopMs = performance.now() - stopStart;
|
|
assert.ok(pauseMs < 1500 && stopMs < 1500);
|
|
return { wall_ms: wall, simulated_ms: simulated, ratio: simulated / wall, p95_step_ms: p95, pause_ms: pauseMs, stop_ms: stopMs, line: after.line };
|
|
};
|
|
try {
|
|
await page.goto(url, { waitUntil: "domcontentloaded" });
|
|
await wait((s) => s.loading === "false" && s.preview > 0, "startup");
|
|
await control("toolbar-power").click();
|
|
await control("manual-home-all").click();
|
|
await control("toolbar-run").click();
|
|
process.stderr.write("[second-program] running configured first program to completion\n");
|
|
const first = await wait((s) => s.status === "completed" && s.queue === 0 && s.live > 0, "first complete", 180000);
|
|
// Chips begins at Z10. The configured first program ends at Z10, so its
|
|
// zero-motion positioning block correctly produces no preview segment.
|
|
const expectedSegments = Math.abs(first.pose.z - 10) < 1e-9 ? 4683 : 4684;
|
|
const loadStart = performance.now();
|
|
await control("workspace-load-3d-chips").click();
|
|
const chips = await wait((s) => s.path === "linuxcnc/nc_files/3D_Chips.ngc" && s.loading === "false" && s.preview === expectedSegments, "chips replacement", 15000);
|
|
const loadMs = performance.now() - loadStart;
|
|
assert.equal(chips.epoch, first.epoch);
|
|
assert.ok(chips.live >= first.live, "native Open must retain live history");
|
|
assert.ok(chips.draws <= 2 && chips.vertices === expectedSegments * 2);
|
|
assert.deepEqual(chips.pose, first.pose, "file replacement changed machine pose");
|
|
process.stderr.write(`[second-program] replaced: ${loadMs.toFixed(0)} ms, ${chips.draws} static draw calls, native live trace retained\n`);
|
|
await control("toolbar-clear-live-plot").click();
|
|
const explicitlyCleared = await wait((s) => s.live === 0 && s.epoch === chips.epoch + 1, "native explicit clear");
|
|
const execution = await sampleRun();
|
|
const stopped = await state();
|
|
await control("toolbar-reload").click();
|
|
const same = await wait((s) => s.status === "loaded" && s.loading === "false", "same reload");
|
|
assert.equal(same.epoch, stopped.epoch, "same source Reload must retain trace");
|
|
assert.ok(same.live >= stopped.live);
|
|
await control("toolbar-clear-live-plot").click();
|
|
await wait((s) => s.live === 0 && s.epoch > same.epoch, "explicit clear");
|
|
for (const width of [1440, 390]) {
|
|
await page.setViewportSize({ width, height: width === 390 ? 844 : 1000 });
|
|
await control("toolbar-view-p").click();
|
|
await page.waitForTimeout(250);
|
|
const colors = await page.locator("[data-xyzac-preview] canvas").evaluate((canvas) => {
|
|
const gl = canvas.getContext("webgl2") ?? canvas.getContext("webgl");
|
|
const pixels = new Uint8Array(canvas.width * canvas.height * 4);
|
|
gl.readPixels(0, 0, canvas.width, canvas.height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
|
|
const colors = new Set();
|
|
for (let i = 0; i < pixels.length; i += 16) colors.add((pixels[i] << 16) | (pixels[i + 1] << 8) | pixels[i + 2]);
|
|
return colors.size;
|
|
});
|
|
assert.ok(colors > 20);
|
|
assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1));
|
|
assert.ok(await page.evaluate(() => document.documentElement.scrollHeight < 5000), "long editor expanded the entire page");
|
|
await page.screenshot({ path: `/tmp/axis-chips-replacement-${width}.png`, fullPage: true });
|
|
}
|
|
assert.deepEqual(errors, []);
|
|
assert.deepEqual(await page.evaluate(() => window.workerErrors), []);
|
|
console.log(JSON.stringify({ schema: "xyzac-second-program-browser.v1", status: "verified", url, first_live_points: first.live, load_retained_live_points: chips.live, new_epoch: explicitlyCleared.epoch, cleared_live_points: explicitlyCleared.live, load_ms: loadMs, preview_segments: chips.preview, static_draw_calls: chips.draws, execution, browser_errors: errors }));
|
|
} finally { await browser.close(); }
|