563 lines
33 KiB
HTML
563 lines
33 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<title>xyzbc-trt browser smoke</title>
|
|
</head>
|
|
<body>
|
|
<pre id="result">xyzbc_trt_browser_smoke=pending</pre>
|
|
<iframe id="app-frame" title="xyzbc-trt app"></iframe>
|
|
<script type="module">
|
|
const result = document.querySelector("#result");
|
|
const frame = document.querySelector("#app-frame");
|
|
const appSrc = new URLSearchParams(window.location.search).get("app") || "../../app/index.html";
|
|
frame.src = appSrc;
|
|
|
|
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
|
async function runSmoke() {
|
|
await new Promise((resolve, reject) => {
|
|
frame.addEventListener("load", resolve, { once: true });
|
|
frame.addEventListener("error", reject, { once: true });
|
|
});
|
|
|
|
const win = frame.contentWindow;
|
|
const doc = frame.contentDocument;
|
|
const runtimeErrors = [];
|
|
win.addEventListener("error", (event) => {
|
|
runtimeErrors.push(event.message || String(event.error || "window error"));
|
|
});
|
|
win.addEventListener("unhandledrejection", (event) => {
|
|
runtimeErrors.push(event.reason?.message || String(event.reason || "unhandled rejection"));
|
|
});
|
|
|
|
await waitFor(() => win.webRtcp5AxisSimulation, "public simulation API");
|
|
const api = win.webRtcp5AxisSimulation;
|
|
const [kinematics, interpreter, taskHal] = await Promise.all([
|
|
api.kinematicsRuntimeReady,
|
|
api.interpreterRuntimeReady,
|
|
api.taskHalRuntimeReady,
|
|
]);
|
|
|
|
assertRuntime(kinematics, {
|
|
name: "kinematics",
|
|
moduleId: "xyzbc-trt",
|
|
required: ["loaded"],
|
|
});
|
|
if (kinematics.executionContext !== "worker") {
|
|
throw new Error(`kinematics runtime should run in worker: ${JSON.stringify(kinematics)}`);
|
|
}
|
|
assertRuntime(interpreter, {
|
|
name: "interpreter",
|
|
required: ["loaded", "runProgramReady", "plannerRuntimeReady"],
|
|
});
|
|
if (interpreter.executionContext !== "worker") {
|
|
throw new Error(`interpreter runtime should run in worker: ${JSON.stringify(interpreter)}`);
|
|
}
|
|
assertRuntime(taskHal, {
|
|
name: "task/HAL",
|
|
required: ["loaded", "taskRuntimeReady", "motionRuntimeReady", "halRuntimeReady"],
|
|
});
|
|
if (taskHal.executionContext !== "worker") {
|
|
throw new Error(`task/HAL runtime should run in worker: ${JSON.stringify(taskHal)}`);
|
|
}
|
|
|
|
const seedResult = await api.machineFileSeedReady;
|
|
if (seedResult?.save?.storageMode !== "opfs") {
|
|
throw new Error(`machine file seed did not use OPFS: ${JSON.stringify(seedResult?.save || seedResult)}`);
|
|
}
|
|
await waitFor(() => {
|
|
const state = api.getState();
|
|
return !state.interpreterExecutionPending
|
|
&& state.programExecution?.sourceMode === "linuxcnc-interpreter-wasm"
|
|
&& state.machineFileStaging?.status === "staged";
|
|
}, "default xyzbc program preview");
|
|
|
|
const state = api.getState();
|
|
if (state.machineProfile !== "xyzbc-trt" || state.profile?.traj?.coordinates !== "XYZBC") {
|
|
throw new Error(`default profile mismatch: ${state.machineProfile}/${state.profile?.traj?.coordinates}`);
|
|
}
|
|
if (state.profile?.kinematicsModuleId !== "xyzbc-trt") {
|
|
throw new Error(`profile kinematics module mismatch: ${state.profile?.kinematicsModuleId}`);
|
|
}
|
|
if (state.machineFileStaging?.profileId !== "xyzbc-trt") {
|
|
throw new Error(`staging profile mismatch: ${state.machineFileStaging?.profileId}`);
|
|
}
|
|
if (
|
|
state.machineFileStaging?.storageMode !== "opfs" ||
|
|
state.machineFileStaging?.storageCapability?.opfsAvailable !== true ||
|
|
state.machineFileStaging?.storageCapability?.reason !== "opfs_available"
|
|
) {
|
|
throw new Error(`machine files must use browser OPFS: ${JSON.stringify(state.machineFileStaging?.storageCapability)}`);
|
|
}
|
|
if (!state.machineFileStaging?.gcodeSources?.some((source) => source.filename === "xyzbc_switchkins.ngc")) {
|
|
throw new Error("staged sources missing xyzbc_switchkins.ngc");
|
|
}
|
|
if (!state.machineFileStaging?.gcodeSources?.some((source) => source.filename === "boat-xyzbc.ngc")) {
|
|
throw new Error("staged sources missing boat-xyzbc.ngc");
|
|
}
|
|
if (state.programExecution?.summary?.motionEventCount < 1) {
|
|
throw new Error(`default program did not produce canonical motion: ${JSON.stringify(state.programExecution?.summary)}`);
|
|
}
|
|
const selectedSource = state.machineFileStaging.gcodeSources.find((source) => source.filename === "xyzbc_switchkins.ngc");
|
|
const opfsProgramText = await readOpfsText(win.navigator.storage, selectedSource.opfsPath);
|
|
if (!opfsProgramText.includes("M428") || !opfsProgramText.includes("M429")) {
|
|
throw new Error(`OPFS staged program text did not contain switchkins commands: ${selectedSource.opfsPath}`);
|
|
}
|
|
const savedSession = await api.saveSession();
|
|
if (savedSession.storageMode !== "opfs" || savedSession.storageCapability?.opfsAvailable !== true) {
|
|
throw new Error(`session save must use browser OPFS: ${JSON.stringify(savedSession.storageCapability)}`);
|
|
}
|
|
const sessionText = await readOpfsText(win.navigator.storage, savedSession.path);
|
|
if (!sessionText.includes('"machineProfile": "xyzbc-trt"')) {
|
|
throw new Error(`OPFS session snapshot did not persist xyzbc-trt profile: ${savedSession.path}`);
|
|
}
|
|
const savedToolDb = await api.saveToolDb();
|
|
if (savedToolDb.storageMode !== "opfs" || savedToolDb.storageCapability?.opfsAvailable !== true) {
|
|
throw new Error(`tool table save must use browser OPFS: ${JSON.stringify(savedToolDb.storageCapability)}`);
|
|
}
|
|
const toolTableText = await readOpfsText(win.navigator.storage, savedToolDb.path);
|
|
if (!toolTableText.includes("T2") || !toolTableText.includes("Z10")) {
|
|
throw new Error(`OPFS tool table save did not contain T2/Z10: ${savedToolDb.path}`);
|
|
}
|
|
|
|
const expectedAxisRegions = ["menubar", "toolbar", "manual", "main-tabs", "pyvcp", "program", "statusbar"];
|
|
const regions = api.getRegions();
|
|
for (const region of expectedAxisRegions) {
|
|
if (regions[region] !== true || !doc.querySelector(`[data-region="${region}"]`)) {
|
|
throw new Error(`missing AXIS region ${region}: ${JSON.stringify(regions)}`);
|
|
}
|
|
}
|
|
if (!doc.querySelector('[data-shell="axis-xyzbc-trt"]')) {
|
|
throw new Error("AXIS xyzbc-trt shell missing");
|
|
}
|
|
if (!doc.querySelector('[data-action="mdi-input"]') || !doc.querySelector('[data-action="kins-tcp"]')) {
|
|
throw new Error("MDI input or switchkins TCP control missing from AXIS shell");
|
|
}
|
|
const statusbar = doc.querySelector('[data-region="statusbar"]');
|
|
if (!statusbar?.dataset.toolStatus?.startsWith("T2 P2")) {
|
|
throw new Error(`AXIS statusbar did not show active xyzbc tool: ${statusbar?.innerText || ""} / ${statusbar?.dataset.toolStatus || ""}`);
|
|
}
|
|
|
|
const buttonParity = api.getButtonParity();
|
|
if (!Array.isArray(buttonParity) || buttonParity.length < 50) {
|
|
throw new Error(`AXIS button parity list too small: ${buttonParity?.length}`);
|
|
}
|
|
for (const action of ["estop", "power", "run-ready", "run", "pause", "resume", "pause-resume", "step", "stop", "home-all", "jog-plus", "jog-minus", "spindle-reverse", "spindle-stop", "spindle-forward", "kins-identity", "kins-tcp", "kins-userk", "clear-preview"]) {
|
|
if (!buttonParity.some((item) => item.action === action && item.sourceSymbol && item.sourceLines && item.expected)) {
|
|
throw new Error(`missing source-referenced button parity for ${action}`);
|
|
}
|
|
}
|
|
const domControls = [...doc.querySelectorAll("[data-action], [data-menu-command]")];
|
|
for (const action of [...new Set(buttonParity.map((item) => item.action))]) {
|
|
const controls = domControls.filter((control) => (
|
|
control.dataset.action === action ||
|
|
control.dataset.menuCommand === action
|
|
));
|
|
if (controls.length === 0) {
|
|
throw new Error(`button parity action has no DOM control: ${action}`);
|
|
}
|
|
if (!controls.some((control) => control.dataset.axisSourceRef && control.dataset.axisExpectedEffect)) {
|
|
throw new Error(`button parity action has no source-tagged DOM control: ${action}`);
|
|
}
|
|
}
|
|
for (const selector of [
|
|
'[data-action="estop"]',
|
|
'[data-action="power"]',
|
|
'[data-action="run"]',
|
|
'[data-action="toggle-auto-manual"]',
|
|
'[data-action="step"]',
|
|
'[data-action="stop"]',
|
|
'[data-action="jog-plus"]',
|
|
'[data-action="spindle-reverse"]',
|
|
'[data-action="spindle-stop"]',
|
|
'[data-action="spindle-forward"]',
|
|
'[data-action="toggle-flood"]',
|
|
'[data-action="kins-tcp"]',
|
|
'[data-action="clear-preview"]',
|
|
'[data-menu-command="run-ready"]',
|
|
]) {
|
|
const control = doc.querySelector(selector);
|
|
if (!control?.dataset.axisSourceRef || !control?.dataset.axisExpectedEffect) {
|
|
throw new Error(`missing AXIS source metadata on ${selector}: ${JSON.stringify(control?.dataset || {})}`);
|
|
}
|
|
}
|
|
|
|
const canvas = await waitFor(() => doc.querySelector("[data-five-axis-canvas]"), "preview canvas");
|
|
await waitFor(() => canvas.dataset.threeReady === "true" && Number(canvas.dataset.threePathPoints || 0) > 0, "Three.js dataset");
|
|
if (
|
|
canvas.dataset.threeRenderer !== "webgl" ||
|
|
canvas.dataset.threeFrameApi !== "web-rtcp-5axis-motion-frame" ||
|
|
canvas.dataset.threeSceneMode !== "program-preview-and-tool-execution" ||
|
|
canvas.dataset.threeMachineReferenceModel !== "webgl-five-axis-reference" ||
|
|
canvas.dataset.threePreviewScope !== "machine-reference-and-toolpath" ||
|
|
canvas.dataset.threeProgramPreviewSource !== "linuxcnc-interpreter-wasm" ||
|
|
!canvas.dataset.threeToolGlyphAxis ||
|
|
canvas.dataset.threeVismachPinDrivenModel !== "web_threejs_vismach_equivalent_driven_by_xyzbc_trt_hal_pins"
|
|
) {
|
|
throw new Error(`unexpected canvas dataset: ${JSON.stringify(canvas.dataset)}`);
|
|
}
|
|
if (Number(canvas.dataset.threeSceneObjects || 0) < 12) {
|
|
throw new Error(`scene object count too low: ${canvas.dataset.threeSceneObjects}`);
|
|
}
|
|
const vismachPins = JSON.parse(canvas.dataset.threeVismachPins || "{}");
|
|
for (const pin of ["table-x", "saddle-y", "spindle-z", "tilt-b", "rotate-c", "tool-offset", "x-offset", "z-offset"]) {
|
|
if (!Number.isFinite(Number(vismachPins[pin]))) {
|
|
throw new Error(`missing Vismach pin ${pin}: ${JSON.stringify(vismachPins)}`);
|
|
}
|
|
}
|
|
const vismachTransforms = JSON.parse(canvas.dataset.threeVismachTransforms || "{}");
|
|
if (
|
|
vismachTransforms.table?.sourcePins?.[0] !== "table-x" ||
|
|
vismachTransforms.saddle?.sourcePins?.[0] !== "saddle-y" ||
|
|
vismachTransforms.spindle?.sourcePins?.[0] !== "spindle-z" ||
|
|
vismachTransforms.tilt?.sourcePins?.[0] !== "tilt-b" ||
|
|
vismachTransforms.rotary?.sourcePins?.[0] !== "rotate-c" ||
|
|
!vismachTransforms.tool?.sourcePins?.includes("tool-offset")
|
|
) {
|
|
throw new Error(`Vismach transform source pins mismatch: ${JSON.stringify(vismachTransforms)}`);
|
|
}
|
|
assertCanvasNonblank(canvas);
|
|
|
|
await verifyAxisButtonLogic(doc, api);
|
|
|
|
if (runtimeErrors.length > 0) {
|
|
throw new Error(`runtime errors: ${runtimeErrors.join(" | ")}`);
|
|
}
|
|
result.textContent = "xyzbc_trt_browser_smoke=ok";
|
|
}
|
|
|
|
function assertRuntime(readiness, { name, moduleId = null, required }) {
|
|
for (const key of required) {
|
|
if (readiness?.[key] !== true) {
|
|
throw new Error(`${name} runtime missing ${key}: ${JSON.stringify(readiness)}`);
|
|
}
|
|
}
|
|
if (moduleId && readiness?.moduleId !== moduleId) {
|
|
throw new Error(`${name} runtime module mismatch: ${JSON.stringify(readiness)}`);
|
|
}
|
|
}
|
|
|
|
async function waitFor(predicate, label) {
|
|
for (let attempt = 0; attempt < 240; attempt += 1) {
|
|
const value = predicate();
|
|
if (value) return value;
|
|
await wait(50);
|
|
}
|
|
throw new Error(`timed out waiting for ${label}`);
|
|
}
|
|
|
|
function assertCanvasNonblank(canvas) {
|
|
const context = canvas.getContext("webgl2") || canvas.getContext("webgl") || canvas.getContext("2d");
|
|
if (!context) {
|
|
throw new Error("preview canvas has no readable context");
|
|
}
|
|
const width = canvas.width;
|
|
const height = canvas.height;
|
|
if (width <= 0 || height <= 0) {
|
|
throw new Error(`preview canvas has invalid size ${width}x${height}`);
|
|
}
|
|
if (context.readPixels) {
|
|
const pixels = new Uint8Array(4);
|
|
context.readPixels(Math.floor(width / 2), Math.floor(height / 2), 1, 1, context.RGBA, context.UNSIGNED_BYTE, pixels);
|
|
if (pixels.some((value) => value !== 0)) return;
|
|
}
|
|
const imageData = canvas.toDataURL("image/png");
|
|
if (!imageData || imageData.length < 2000) {
|
|
throw new Error("preview canvas appears blank");
|
|
}
|
|
}
|
|
|
|
async function verifyAxisButtonLogic(doc, api) {
|
|
const click = async (selector, label = selector) => {
|
|
const element = doc.querySelector(selector);
|
|
if (!element) throw new Error(`missing button for ${label}`);
|
|
element.click();
|
|
await wait(50);
|
|
return api.getState();
|
|
};
|
|
const waitState = (predicate, label) => waitFor(() => {
|
|
const next = api.getState();
|
|
return predicate(next) ? next : null;
|
|
}, label);
|
|
|
|
await click('[data-action="estop"]', "AXIS estop");
|
|
await waitState((state) => state.machine.estopActive === true && state.runState === "estopped", "estop active");
|
|
await click('[data-action="estop"]', "AXIS estop reset");
|
|
await waitState((state) => state.machine.estopActive === false && state.machine.taskState === "estop-reset", "estop reset");
|
|
|
|
await click('[data-action="power"]', "AXIS power");
|
|
await waitState((state) => state.machine.powerOn === true && state.machine.taskState === "on", "machine power on");
|
|
await click('[data-action="home-all"]', "AXIS home all");
|
|
await waitState((state) => (
|
|
state.machine.allHomed === true
|
|
&& state.machine.mode === "manual"
|
|
&& Math.abs(Number(state.axisPose.x) - 0) < 1e-6
|
|
&& Math.abs(Number(state.axisPose.y) - 0) < 1e-6
|
|
&& Math.abs(Number(state.axisPose.z) - 10) < 1e-6
|
|
&& Math.abs(Number(state.dro.x) - 0) < 1e-6
|
|
&& Math.abs(Number(state.dro.y) - 0) < 1e-6
|
|
&& Math.abs(Number(state.dro.z) - 10) < 1e-6
|
|
), "home all");
|
|
await click('[data-action="toggle-auto-manual"]', "AXIS AUTO mode toolbar toggle");
|
|
await waitState((state) => state.machine.mode === "auto", "toolbar toggle to AUTO");
|
|
await click('[data-action="toggle-auto-manual"]', "AXIS MANUAL mode toolbar toggle");
|
|
await waitState((state) => state.machine.mode === "manual", "toolbar toggle to MANUAL");
|
|
await click('[data-action="run"]', "AXIS immediate-pause run");
|
|
assert(doc.querySelector('[data-tool-id="tbtn_pause"]')?.dataset.action === "pause-resume", "toolbar pause button is LinuxCNC task_pauseresume");
|
|
await click('[data-tool-id="tbtn_pause"]', "AXIS immediate pause/resume button after run click");
|
|
await waitState((state) => (
|
|
state.runState === "paused"
|
|
&& state.machine.interpState === "paused"
|
|
&& state.machine.taskPaused === true
|
|
), "immediate pause after run click");
|
|
assert(doc.querySelector('[data-tool-id="tbtn_pause"]')?.dataset.paused === "true", "toolbar pause button visibly enters paused state");
|
|
assert(doc.querySelector('[data-tool-id="tbtn_pause"]')?.title === "Resume program", "toolbar pause button title switches to resume");
|
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
assert(api.getState().runState === "paused", "immediate pause remains paused after async task/HAL settle");
|
|
await click('[data-action="stop"]', "AXIS stop after immediate pause");
|
|
await waitState((state) => state.runState === "stopped" && state.machine.mode === "auto", "stop after immediate pause");
|
|
await click('[data-action="tab-manual"]', "AXIS manual tab after immediate pause");
|
|
await waitState((state) => state.machine.mode === "manual", "manual mode after immediate pause");
|
|
await click('[data-action="run"]', "AXIS direct run after home");
|
|
await waitState((state) => (
|
|
(state.runState === "running" || state.runState === "complete")
|
|
&& state.machine.mode === "auto"
|
|
&& state.programRuntimeFeedback
|
|
), "direct run after power and home");
|
|
await click('[data-menu-command="pause"]', "AXIS menu Pause after direct run");
|
|
await waitState((state) => (
|
|
state.runState === "paused"
|
|
&& state.machine.interpState === "paused"
|
|
&& state.machine.taskPaused === true
|
|
), "menu Pause after direct run");
|
|
await click('[data-menu-command="resume"]', "AXIS menu Resume after direct Pause");
|
|
await waitState((state) => state.runState === "running" && state.machine.interpState === "reading", "menu Resume after direct Pause");
|
|
await click('[data-tool-id="tbtn_pause"]', "AXIS toolbar pause after direct run");
|
|
await waitState((state) => (
|
|
state.runState === "paused"
|
|
&& state.machine.interpState === "paused"
|
|
&& state.machine.taskPaused === true
|
|
), "pause after direct run");
|
|
const pauseToolbarAfterPause = doc.querySelector('[data-tool-id="tbtn_pause"]');
|
|
assert(pauseToolbarAfterPause?.dataset.action === "pause-resume", "toolbar pause button stays bound to LinuxCNC task_pauseresume while paused");
|
|
assert(pauseToolbarAfterPause?.dataset.paused === "true", "toolbar pause button has paused dataset while paused");
|
|
assert(pauseToolbarAfterPause?.getAttribute("aria-pressed") === "true", "toolbar pause button aria pressed while paused");
|
|
await click('[data-tool-id="tbtn_pause"]', "AXIS toolbar pause/resume resumes while already paused");
|
|
await waitState((state) => state.runState === "running" && state.machine.interpState === "reading", "toolbar pause/resume resumes while paused");
|
|
assert(doc.querySelector('[data-tool-id="tbtn_pause"]')?.dataset.paused === "false", "toolbar pause button leaves paused state after resume");
|
|
assert(doc.querySelector('[data-tool-id="tbtn_pause"]')?.title === "Pause program", "toolbar pause button title switches to pause after resume");
|
|
await click('[data-tool-id="tbtn_pause"]', "AXIS toolbar pause/resume pauses again");
|
|
await waitState((state) => (
|
|
state.runState === "paused"
|
|
&& state.machine.interpState === "paused"
|
|
&& state.machine.taskPaused === true
|
|
), "toolbar pause/resume pauses again");
|
|
const beforeStepSample = api.getState().programExecutionSampleIndex;
|
|
await click('[data-action="step"]', "AXIS step after pause");
|
|
await waitState((state) => (
|
|
(state.runState === "stepping" || state.runState === "paused")
|
|
&& state.machine.interpState === "paused"
|
|
&& state.machine.taskPaused === true
|
|
&& Number(state.programExecutionSampleIndex) > Number(beforeStepSample)
|
|
), "step after pause");
|
|
assert(doc.querySelector('[data-tool-id="tbtn_pause"]')?.dataset.action === "pause-resume", "toolbar pause button stays task_pauseresume after step");
|
|
await click('[data-tool-id="tbtn_pause"]', "AXIS toolbar pause/resume after pause");
|
|
await waitState((state) => state.runState === "running" && state.machine.interpState === "reading", "resume after pause");
|
|
assert(doc.querySelector('[data-tool-id="tbtn_pause"]')?.dataset.action === "pause-resume", "toolbar pause button stays task_pauseresume after resume");
|
|
await click('[data-tool-id="tbtn_pause"]', "AXIS second toolbar pause after resume");
|
|
await waitState((state) => (
|
|
state.runState === "paused"
|
|
&& state.machine.interpState === "paused"
|
|
&& state.machine.taskPaused === true
|
|
), "second pause after resume");
|
|
await click('[data-tool-id="tbtn_pause"]', "AXIS second toolbar resume after pause");
|
|
await waitState((state) => state.runState === "running" && state.machine.interpState === "reading", "second resume after pause");
|
|
await click('[data-action="stop"]', "AXIS stop after direct run");
|
|
await waitState((state) => state.runState === "stopped" && state.machine.mode === "auto", "stop after direct run");
|
|
await click('[data-action="tab-manual"]', "AXIS manual tab after direct run");
|
|
await waitState((state) => state.machine.mode === "manual", "manual mode after direct run");
|
|
|
|
await click('[data-action="tab-mdi"]', "AXIS MDI tab");
|
|
await waitState((state) => state.machine.mode === "mdi", "MDI mode");
|
|
await click('[data-action="kins-tcp"]', "PyVCP TCP");
|
|
await waitState((state) => state.kinsType === "tcp-xyzbc" && state.rtcpState === "on" && state.mdiHistory[0] === "M428", "TCP kins");
|
|
await click('[data-action="kins-identity"]', "PyVCP identity");
|
|
await waitState((state) => state.kinsType === "identity" && state.rtcpState === "off" && state.mdiHistory[0] === "M429", "identity kins");
|
|
await click('[data-action="kins-userk"]', "PyVCP userk");
|
|
await waitState((state) => state.kinsType === "userk" && state.mdiHistory[0] === "M430", "userk kins");
|
|
|
|
await click('[data-action="tab-manual"]', "AXIS manual tab");
|
|
await waitState((state) => state.machine.mode === "manual", "manual mode");
|
|
const joint4 = doc.querySelector('[data-action="select-joint"][value="4"]');
|
|
joint4.checked = true;
|
|
joint4.dispatchEvent(new Event("change", { bubbles: true }));
|
|
await waitState((state) => state.machine.selectedJoint === 4 && state.machine.jogAxis === "c", "select C joint");
|
|
const beforeJogC = api.getState().axisPose.c;
|
|
await click('[data-action="jog-plus"]', "AXIS jog plus");
|
|
await waitState((state) => state.axisPose.c > beforeJogC, "jog C plus");
|
|
await click('[data-action="touch-off"]', "AXIS manual touch off");
|
|
await waitState((state) => (
|
|
state.machine.mode === "manual" &&
|
|
state.runState === "idle" &&
|
|
state.mdiHistory[0] === "G10 L20 P0 C0"
|
|
), "manual touch off");
|
|
await click('[data-action="tool-touch-off"]', "AXIS manual tool touch off");
|
|
await waitState((state) => (
|
|
state.machine.mode === "manual" &&
|
|
state.runState === "idle" &&
|
|
state.mdiHistory[0] === "G43"
|
|
), "manual tool touch off");
|
|
const defaultProgramSource = api.getState().machineFileStaging.gcodeSources.find((source) => source.filename === "xyzbc_switchkins.ngc");
|
|
api.dispatch({ type: "LOAD_LINUXCNC_GCODE_SOURCE", sourceRel: defaultProgramSource.sourceRel });
|
|
await waitState((state) => (
|
|
state.machineFileStaging.selectedGcodeSourceRel === defaultProgramSource.sourceRel &&
|
|
Number(state.programAxisPreviewPath?.sampleCount || 0) > 0
|
|
), "restore default LinuxCNC program after manual touch off");
|
|
|
|
const beforeFeed = api.getState().feed.feedOverride;
|
|
await click('[data-action="feed-override-up"]', "feed override up");
|
|
await waitState((state) => state.feed.feedOverride === Math.min(beforeFeed + 10, 200), "feed override");
|
|
await click('[data-action="spindle-reverse"]', "spindle reverse");
|
|
await waitState((state) => (
|
|
state.spindle.direction === "reverse"
|
|
&& state.spindle.enabled === true
|
|
&& state.linuxCncProcessMonitor.spindle.halPins.on === 1
|
|
&& state.linuxCncProcessMonitor.spindle.halPins.forward === 0
|
|
&& state.linuxCncProcessMonitor.spindle.halPins.reverse === 1
|
|
&& state.linuxCncProcessMonitor.spindle.halPins.speedOut > 0
|
|
), "spindle reverse");
|
|
await waitFor(() => doc.querySelector("[data-active-gcodes]")?.dataset.activeGcodes.includes("M4") ? true : null, "active G-codes reverse spindle");
|
|
await click('[data-action="spindle-stop"]', "spindle stop");
|
|
await waitState((state) => (
|
|
state.spindle.direction === "stop"
|
|
&& state.spindle.enabled === false
|
|
&& state.linuxCncProcessMonitor.spindle.halPins.on === 0
|
|
&& state.linuxCncProcessMonitor.spindle.halPins.forward === 0
|
|
&& state.linuxCncProcessMonitor.spindle.halPins.reverse === 0
|
|
&& state.linuxCncProcessMonitor.spindle.halPins.speedOut === 0
|
|
), "spindle stop");
|
|
await waitFor(() => doc.querySelector("[data-active-gcodes]")?.dataset.activeGcodes.includes("M5") ? true : null, "active G-codes stopped spindle");
|
|
await click('[data-action="spindle-forward"]', "spindle forward");
|
|
await waitState((state) => (
|
|
state.spindle.direction === "forward"
|
|
&& state.spindle.enabled === true
|
|
&& state.linuxCncProcessMonitor.spindle.halPins.on === 1
|
|
&& state.linuxCncProcessMonitor.spindle.halPins.forward === 1
|
|
&& state.linuxCncProcessMonitor.spindle.halPins.reverse === 0
|
|
&& state.linuxCncProcessMonitor.spindle.halPins.speedOut > 0
|
|
), "spindle forward");
|
|
await waitFor(() => doc.querySelector("[data-active-gcodes]")?.dataset.activeGcodes.includes("M3") ? true : null, "active G-codes forward spindle");
|
|
await click('[data-action="spindle-stop"]', "spindle stop after forward");
|
|
await waitState((state) => state.spindle.direction === "stop" && state.spindle.enabled === false, "spindle stop after forward");
|
|
await click('[data-action="toggle-flood"]', "flood coolant");
|
|
await waitState((state) => state.coolant.flood === true, "flood coolant");
|
|
await click('[data-action="block-delete"]', "block delete");
|
|
await waitState((state) => state.gmoccapyGui.optionalBlocks === true, "block delete");
|
|
await click('[data-action="optional-stop"]', "optional stop");
|
|
await waitState((state) => state.gmoccapyGui.optionalStop === true, "optional stop");
|
|
await click('[data-action="ignore-limits"]', "ignore limits");
|
|
await waitState((state) => state.gmoccapyGui.ignoreLimits === true, "ignore limits");
|
|
|
|
await click('[data-action="view-x"]', "view X");
|
|
await waitState((state) => state.preview.selectedView === "x", "view X");
|
|
await click('[data-action="view-y"]', "view Y");
|
|
await waitState((state) => state.preview.selectedView === "y", "view Y");
|
|
await click('[data-action="view-z"]', "view Z");
|
|
await waitState((state) => state.preview.selectedView === "z", "view Z");
|
|
await click('[data-action="view-p"]', "view P");
|
|
await waitState((state) => state.preview.selectedView === "iso", "view P");
|
|
await click('[data-action="clear-preview"]', "clear preview");
|
|
await waitState((state) => state.preview.pathPoints === 0, "clear preview");
|
|
await click('[data-action="reload"]', "reload program after clear preview");
|
|
await waitState((state) => Number(state.programAxisPreviewPath?.sampleCount || 0) > 0 && state.preview.pathPoints > 0, "reload restored real path samples");
|
|
|
|
doc.querySelector('[data-menu-command="run-ready"]').click();
|
|
await waitState((state) => state.machine.powerOn === true && state.machine.allHomed === true && state.machine.mode === "auto", "run-ready menu");
|
|
const beforeRun = api.getState();
|
|
await click('[data-action="run"]', "AXIS run");
|
|
const runState = await waitState((state) => (
|
|
(state.runState === "running" || state.runState === "complete")
|
|
&& state.programRuntimeFeedback
|
|
&& state.activeLine >= beforeRun.activeLine
|
|
), "program run execution");
|
|
if (runState.programExecutionSourceMode !== "linuxcnc-task-motion-hal-wasm") {
|
|
throw new Error(`RUN did not use task/HAL execution path: ${runState.programExecutionSourceMode}`);
|
|
}
|
|
if (
|
|
runState.programUiExecution?.source !== "programAxisPreviewPath.samples" ||
|
|
runState.programUiExecution?.samplePeriodMs !== 50 ||
|
|
runState.programAxisPreviewPath?.gcodeExecutionProcess?.executionStepCount !== 128 ||
|
|
runState.programAxisPreviewPath?.gcodeExecutionProcess?.summary?.motionStepCount !== 29 ||
|
|
!["xyzbc_switchkins_sub.ngc", "helix_bc.ngc"].includes(runState.programUiExecution?.sourceFile)
|
|
) {
|
|
throw new Error(`RUN did not expose real expanded G-code execution: ${JSON.stringify(runState.programUiExecution)}`);
|
|
}
|
|
const programPane = doc.querySelector('[data-region="program"]');
|
|
const processMonitor = doc.querySelector('[data-linuxcnc-process-monitor]');
|
|
if (
|
|
programPane?.dataset.liveSourceFile !== runState.programUiExecution.sourceFile ||
|
|
Number(programPane?.dataset.liveSampleIndex) !== runState.programUiExecution.sampleIndex ||
|
|
programPane?.dataset.gcodeExecutionStatus !== "ok" ||
|
|
Number(programPane?.dataset.gcodeExecutionStepCount) !== 128 ||
|
|
Number(programPane?.dataset.gcodeMotionStepCount) !== 29 ||
|
|
Number(programPane?.dataset.gcodeParameterStepCount) !== 41 ||
|
|
processMonitor?.dataset.liveSourceFile !== runState.programUiExecution.sourceFile ||
|
|
Number(processMonitor?.dataset.liveSourceLine) !== runState.programUiExecution.line
|
|
) {
|
|
throw new Error(`live G-code DOM did not match state: program=${JSON.stringify(programPane?.dataset || {})} monitor=${JSON.stringify(processMonitor?.dataset || {})}`);
|
|
}
|
|
if (!doc.querySelector("[data-gcode-process]") || !doc.querySelector("[data-gcode-step-list] li.active")) {
|
|
throw new Error("expanded G-code process panel did not render active execution step");
|
|
}
|
|
const liveToolAxis = JSON.parse(doc.querySelector("[data-five-axis-canvas]").dataset.threeToolAxis || "{}");
|
|
const liveToolGlyphAxis = JSON.parse(doc.querySelector("[data-five-axis-canvas]").dataset.threeToolGlyphAxis || "{}");
|
|
const liveToolhead = JSON.parse(doc.querySelector("[data-five-axis-canvas]").dataset.threeToolhead || "{}");
|
|
const expectedToolhead = {
|
|
x: Number(runState.programUiExecution?.tcp?.x || 0) / 1000,
|
|
y: Number(runState.programUiExecution?.tcp?.y || 0) / 1000,
|
|
z: Number(runState.programUiExecution?.tcp?.z || 0) / 1000,
|
|
};
|
|
if (
|
|
Math.abs(liveToolhead.x - expectedToolhead.x) > 1e-6 ||
|
|
Math.abs(liveToolhead.y - expectedToolhead.y) > 1e-6 ||
|
|
Math.abs(liveToolhead.z - expectedToolhead.z) > 1e-6
|
|
) {
|
|
throw new Error(`canvas toolhead not synchronized with live TCP: ${JSON.stringify({ liveToolhead, expectedToolhead, tcp: runState.programUiExecution?.tcp })}`);
|
|
}
|
|
if (
|
|
Math.abs(liveToolAxis.x - runState.toolAxisVector.x) > 1e-3 ||
|
|
Math.abs(liveToolAxis.y - runState.toolAxisVector.y) > 1e-3 ||
|
|
Math.abs(liveToolAxis.z - runState.toolAxisVector.z) > 1e-3
|
|
) {
|
|
throw new Error(`canvas tool axis not synchronized with live sample: ${JSON.stringify({ liveToolAxis, state: runState.toolAxisVector })}`);
|
|
}
|
|
if (
|
|
Math.abs(liveToolGlyphAxis.x - runState.toolAxisVector.x) > 1e-3 ||
|
|
Math.abs(liveToolGlyphAxis.y - runState.toolAxisVector.y) > 1e-3 ||
|
|
Math.abs(liveToolGlyphAxis.z - runState.toolAxisVector.z) > 1e-3
|
|
) {
|
|
throw new Error(`canvas tool glyph axis not synchronized with live sample: ${JSON.stringify({ liveToolGlyphAxis, state: runState.toolAxisVector })}`);
|
|
}
|
|
}
|
|
|
|
async function readOpfsText(storage, path) {
|
|
if (!storage?.getDirectory) {
|
|
throw new Error("browser OPFS getDirectory is unavailable");
|
|
}
|
|
let current = await storage.getDirectory();
|
|
const parts = String(path).split("/").filter(Boolean);
|
|
for (const part of parts.slice(0, -1)) {
|
|
current = await current.getDirectoryHandle(part);
|
|
}
|
|
const fileHandle = await current.getFileHandle(parts.at(-1));
|
|
return fileHandle.getFile().then((file) => file.text());
|
|
}
|
|
|
|
runSmoke().catch((error) => {
|
|
result.textContent = `xyzbc_trt_browser_smoke=fail ${error.stack || error.message || error}`;
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|