1510 lines
82 KiB
HTML
1510 lines
82 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<title>gmoccapy shell browser smoke</title>
|
|
</head>
|
|
<body>
|
|
<pre id="result">gmoccapy_shell_smoke=pending</pre>
|
|
<iframe id="app-frame" title="gmoccapy shell"></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 waitForInterpreterExecution(win) {
|
|
for (let attempt = 0; attempt < 40; attempt += 1) {
|
|
const state = win.webRtcp5AxisSimulation.getState();
|
|
if (!state.interpreterExecutionPending && state.programExecutionSourceMode === "linuxcnc-interpreter-wasm") {
|
|
return state;
|
|
}
|
|
await wait(25);
|
|
}
|
|
return win.webRtcp5AxisSimulation.getState();
|
|
}
|
|
async function waitForMachineFileExecution(win) {
|
|
for (let attempt = 0; attempt < 60; attempt += 1) {
|
|
const state = win.webRtcp5AxisSimulation.getState();
|
|
if (!state.interpreterExecutionPending && state.machineFileExecution) {
|
|
return state;
|
|
}
|
|
await wait(25);
|
|
}
|
|
return win.webRtcp5AxisSimulation.getState();
|
|
}
|
|
async function waitForMachineFileStaging(win, profileId = null) {
|
|
for (let attempt = 0; attempt < 120; attempt += 1) {
|
|
const state = win.webRtcp5AxisSimulation.getState();
|
|
const stagedForProfile = !profileId || state.machineFileStaging?.profileId === profileId;
|
|
if (stagedForProfile && state.machineFileStaging?.status === "staged" && state.machineFileStaging?.save?.fileCount >= 5) {
|
|
return state;
|
|
}
|
|
await wait(25);
|
|
}
|
|
return win.webRtcp5AxisSimulation.getState();
|
|
}
|
|
function assertActiveGcodeRowVisible(doc, expectedLine) {
|
|
const activeRow = doc.querySelector(".gcode-row.active");
|
|
const list = doc.querySelector(".gcode-list");
|
|
if (!activeRow || !list) {
|
|
throw new Error("missing active G-code row or list");
|
|
}
|
|
if (expectedLine && activeRow.dataset.programLine !== String(expectedLine)) {
|
|
throw new Error(`active G-code row should be line ${expectedLine}, got ${activeRow.dataset.programLine}`);
|
|
}
|
|
if (activeRow.dataset.lineStatus !== "running") {
|
|
throw new Error(`active G-code row should be running, got ${activeRow.dataset.lineStatus}`);
|
|
}
|
|
const activeBackground = getComputedStyle(activeRow).backgroundColor.replace(/\s/g, "");
|
|
if (activeBackground !== "rgb(33,165,83)") {
|
|
throw new Error(`active G-code row should be green, got ${activeBackground}`);
|
|
}
|
|
const previousLine = Number(activeRow.dataset.programLine || 0) - 1;
|
|
const previousRow = doc.querySelector(`.gcode-row[data-program-line="${previousLine}"]`);
|
|
if (previousRow) {
|
|
const previousBackground = getComputedStyle(previousRow).backgroundColor.replace(/\s/g, "");
|
|
if (previousRow.dataset.lineStatus !== "done") {
|
|
throw new Error(`executed G-code row should be done, got ${previousRow.dataset.lineStatus}`);
|
|
}
|
|
if (previousBackground !== "rgb(238,238,238)") {
|
|
throw new Error(`executed G-code row should be light gray, got ${previousBackground}`);
|
|
}
|
|
}
|
|
const executionText = activeRow.querySelector("[data-line-execution]")?.textContent || "";
|
|
if (
|
|
executionText &&
|
|
!/\b(running|done|paused|stopped|mdi|ready)\b\s+\|\s+F\s+[-.\d]+\s+\|\s+cycle\s+\d+\/\d+/.test(executionText)
|
|
) {
|
|
throw new Error(`G-code line execution text has unexpected format: ${executionText}`);
|
|
}
|
|
const rowBounds = activeRow.getBoundingClientRect();
|
|
const listBounds = list.getBoundingClientRect();
|
|
if (rowBounds.bottom < listBounds.top || rowBounds.top > listBounds.bottom) {
|
|
throw new Error(`active G-code row ${activeRow.dataset.programLine} is not visible after auto-scroll`);
|
|
}
|
|
}
|
|
|
|
function assertSidebarModeVisualState(doc, { manualActive, autoActive }) {
|
|
const expected = [
|
|
[doc.querySelector('[data-action="mode-manual"]'), "MANUAL", manualActive],
|
|
[doc.querySelector('[data-action="mode-auto"]'), "AUTO", autoActive],
|
|
];
|
|
for (const [button, label, active] of expected) {
|
|
if (!button) {
|
|
throw new Error(`missing ${label} mode button`);
|
|
}
|
|
if (button.dataset.active !== String(active)) {
|
|
throw new Error(`${label} mode button active flag should be ${active}, got ${button.dataset.active}`);
|
|
}
|
|
const style = getComputedStyle(button);
|
|
const backgroundImage = style.backgroundImage;
|
|
const color = style.color.replace(/\s/g, "");
|
|
if (active) {
|
|
if (!backgroundImage.includes("rgb(174, 233, 174)") || color !== "rgb(7,95,22)") {
|
|
throw new Error(`${label} active mode button should render green, got ${backgroundImage} / ${color}`);
|
|
}
|
|
} else if (!backgroundImage.includes("rgb(221, 217, 209)") || color !== "rgb(48,48,48)") {
|
|
throw new Error(`${label} inactive mode button should render gray, got ${backgroundImage} / ${color}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function runSmoke() {
|
|
await new Promise((resolve, reject) => {
|
|
frame.addEventListener("load", resolve, { once: true });
|
|
frame.addEventListener("error", reject, { once: true });
|
|
});
|
|
await wait(250);
|
|
|
|
const doc = frame.contentDocument;
|
|
const win = frame.contentWindow;
|
|
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"));
|
|
});
|
|
win.__WEB_RTCP_FORCE_OPFS_UNAVAILABLE__ = true;
|
|
const regions = [
|
|
"titlebar",
|
|
"preview",
|
|
"dro",
|
|
"gcode",
|
|
"status-sidebar",
|
|
"info-tabs",
|
|
"override",
|
|
"spindle-coolant",
|
|
"bottom-controls",
|
|
];
|
|
|
|
for (const region of regions) {
|
|
const element = doc.querySelector(`[data-region="${region}"]`);
|
|
if (!element) {
|
|
throw new Error(`missing region: ${region}`);
|
|
}
|
|
if (!element.textContent.trim() && region !== "preview") {
|
|
throw new Error(`empty region: ${region}`);
|
|
}
|
|
}
|
|
|
|
if (!doc.querySelector(".toolpath-preview")) {
|
|
throw new Error("missing toolpath preview");
|
|
}
|
|
let canvas = doc.querySelector("[data-five-axis-canvas]");
|
|
if (!canvas) {
|
|
throw new Error("missing Three.js preview canvas");
|
|
}
|
|
if (
|
|
canvas.dataset.threeReady !== "true" ||
|
|
canvas.dataset.threeFrameApi !== "web-rtcp-5axis-motion-frame" ||
|
|
canvas.dataset.threeSceneMode !== "program-preview-and-tool-execution" ||
|
|
canvas.dataset.threePreviewScope !== "machine-reference-and-toolpath" ||
|
|
canvas.dataset.threeMachineReferenceModel !== "webgl-five-axis-reference" ||
|
|
canvas.dataset.threeCameraControls !== "orbit-pan-zoom" ||
|
|
Number(canvas.dataset.threePathPoints ?? 0) < 64 ||
|
|
Number(canvas.dataset.threeSceneObjects ?? 0) < 12 ||
|
|
!canvas.dataset.threeToolhead ||
|
|
!canvas.dataset.threeToolAxis ||
|
|
!canvas.dataset.threeTcpPose ||
|
|
canvas.dataset.threeToolExecutionMarker !== "true" ||
|
|
canvas.dataset.threeTcpMarker !== "sphere" ||
|
|
canvas.dataset.threeToolAxisMarker !== "line"
|
|
) {
|
|
throw new Error(`Three.js preview did not expose ready render state: ${JSON.stringify(canvas.dataset)}`);
|
|
}
|
|
assertCanvasNonblank(canvas, "initial Three.js preview");
|
|
if (!doc.querySelector(".dro-row")) {
|
|
throw new Error("missing DRO rows");
|
|
}
|
|
if (!doc.querySelector(".gcode-row.active")) {
|
|
throw new Error("missing active gcode row");
|
|
}
|
|
if (!win.webRtcp5AxisSimulation) {
|
|
throw new Error("missing public simulation API");
|
|
}
|
|
if (win.React || win.Vue || win.angular || win.Svelte) {
|
|
throw new Error("forbidden frontend framework global detected");
|
|
}
|
|
if (doc.querySelector("[data-reactroot], [data-v-app], [ng-version], [svelte]")) {
|
|
throw new Error("forbidden frontend framework DOM marker detected");
|
|
}
|
|
const packageJson = await fetch("../../app/package.json").then((response) => response.json());
|
|
const dependencyNames = [
|
|
...Object.keys(packageJson.dependencies || {}),
|
|
...Object.keys(packageJson.devDependencies || {}),
|
|
];
|
|
const forbiddenDependencies = ["react", "vue", "@angular/core", "svelte"];
|
|
const forbidden = dependencyNames.filter((name) => forbiddenDependencies.includes(name));
|
|
if (forbidden.length > 0) {
|
|
throw new Error(`forbidden frontend framework dependency detected: ${forbidden.join(", ")}`);
|
|
}
|
|
|
|
const runtimeReadiness = await win.webRtcp5AxisSimulation.kinematicsRuntimeReady;
|
|
if (runtimeReadiness.loaded !== true || runtimeReadiness.moduleId !== "xyzac-trt") {
|
|
throw new Error(`LinuxCNC kinematics runtime did not load in browser: ${JSON.stringify(runtimeReadiness)}`);
|
|
}
|
|
if (runtimeReadiness.executionContext !== "worker") {
|
|
throw new Error(`LinuxCNC kinematics runtime should run in worker: ${JSON.stringify(runtimeReadiness)}`);
|
|
}
|
|
const interpreterReadiness = await win.webRtcp5AxisSimulation.interpreterRuntimeReady;
|
|
if (interpreterReadiness.loaded !== true || interpreterReadiness.runProgramReady !== true) {
|
|
throw new Error(`LinuxCNC interpreter runtime did not load in browser: ${JSON.stringify(interpreterReadiness)}`);
|
|
}
|
|
if (interpreterReadiness.executionContext !== "worker") {
|
|
throw new Error(`LinuxCNC interpreter runtime should run in worker: ${JSON.stringify(interpreterReadiness)}`);
|
|
}
|
|
if (
|
|
interpreterReadiness.plannerRuntimeReady !== true ||
|
|
interpreterReadiness.plannerSemanticBoundary !== "linuxcnc_tp_queue_runtime_timing_from_canonical_motion"
|
|
) {
|
|
throw new Error(`LinuxCNC TP planner runtime did not load in browser worker: ${JSON.stringify(interpreterReadiness)}`);
|
|
}
|
|
const taskHalReadiness = await win.webRtcp5AxisSimulation.taskHalRuntimeReady;
|
|
if (
|
|
taskHalReadiness.loaded !== true ||
|
|
taskHalReadiness.taskRuntimeReady !== true ||
|
|
taskHalReadiness.motionRuntimeReady !== true ||
|
|
taskHalReadiness.halRuntimeReady !== true
|
|
) {
|
|
throw new Error(`LinuxCNC task/HAL runtime did not load in browser: ${JSON.stringify(taskHalReadiness)}`);
|
|
}
|
|
if (taskHalReadiness.executionContext !== "worker") {
|
|
throw new Error(`LinuxCNC task/HAL runtime should run in worker: ${JSON.stringify(taskHalReadiness)}`);
|
|
}
|
|
const state = win.webRtcp5AxisSimulation.getState();
|
|
if (state.sourceMode !== "source-derived-kinematics-wasm") {
|
|
throw new Error(`unexpected source mode: ${state.sourceMode}`);
|
|
}
|
|
if (state.kinematicsExecutionContext !== "worker") {
|
|
throw new Error(`unexpected kinematics execution context: ${state.kinematicsExecutionContext}`);
|
|
}
|
|
const profileSelector = doc.querySelector('[data-action="select-profile"]');
|
|
if (!profileSelector || profileSelector.options.length < 2) {
|
|
throw new Error("missing five-axis profile selector");
|
|
}
|
|
const titlebar = doc.querySelector('[data-region="titlebar"]');
|
|
const currentLineIndicator = titlebar?.querySelector("[data-titlebar-current-line]");
|
|
if (!titlebar || !currentLineIndicator) {
|
|
throw new Error("missing stable titlebar nodes");
|
|
}
|
|
win.webRtcp5AxisSimulation.dispatch({ type: "SET_VIEW", view: "x" });
|
|
await wait(50);
|
|
if (profileSelector !== doc.querySelector('[data-action="select-profile"]')) {
|
|
throw new Error("titlebar profile selector should not be remounted on state updates");
|
|
}
|
|
if (currentLineIndicator !== doc.querySelector("[data-titlebar-current-line]")) {
|
|
throw new Error("titlebar current-line indicator should not be remounted on state updates");
|
|
}
|
|
win.webRtcp5AxisSimulation.dispatch({ type: "RESET_VIEW" });
|
|
await wait(50);
|
|
profileSelector.value = "xyzbc-trt";
|
|
profileSelector.dispatchEvent(new Event("change", { bubbles: true }));
|
|
await wait(500);
|
|
await win.webRtcp5AxisSimulation.refreshKinematicsFrame({ operatorMessage: "browser smoke refreshed xyzbc frame" });
|
|
const xyzbcState = win.webRtcp5AxisSimulation.getState();
|
|
if (xyzbcState.machineProfile !== "xyzbc-trt" || xyzbcState.profile.traj.coordinates !== "XYZBC") {
|
|
throw new Error("profile selector did not switch to XYZBC");
|
|
}
|
|
if (xyzbcState.kinematicsRuntimeReadiness?.moduleId !== "xyzbc-trt") {
|
|
throw new Error(`XYZBC kinematics runtime did not load: ${JSON.stringify(xyzbcState.kinematicsRuntimeReadiness)}`);
|
|
}
|
|
const gmoccapyTrtProfileSelector = doc.querySelector('[data-action="select-profile"]');
|
|
gmoccapyTrtProfileSelector.value = "gmoccapy-xyzac-trt";
|
|
gmoccapyTrtProfileSelector.dispatchEvent(new Event("change", { bubbles: true }));
|
|
const gmoccapyTrtStagedState = await waitForMachineFileStaging(win, "gmoccapy-xyzac-trt");
|
|
await win.webRtcp5AxisSimulation.refreshKinematicsFrame({ operatorMessage: "browser smoke refreshed gmoccapy xyzac TRT frame" });
|
|
const gmoccapyTrtState = win.webRtcp5AxisSimulation.getState();
|
|
if (gmoccapyTrtState.machineProfile !== "gmoccapy-xyzac-trt") {
|
|
throw new Error(`profile selector did not switch to gmoccapy TRT: ${gmoccapyTrtState.machineProfile}`);
|
|
}
|
|
if (gmoccapyTrtState.profile.display.display !== "gmoccapy") {
|
|
throw new Error(`gmoccapy TRT profile did not preserve DISPLAY=gmoccapy: ${JSON.stringify(gmoccapyTrtState.profile.display)}`);
|
|
}
|
|
if (gmoccapyTrtState.profile.display.openFile !== "./examples/impeller-7bl-xyzac.ngc") {
|
|
throw new Error(`gmoccapy TRT profile did not preserve OPEN_FILE: ${gmoccapyTrtState.profile.display.openFile}`);
|
|
}
|
|
if (gmoccapyTrtState.kinematicsRuntimeReadiness?.moduleId !== "xyzac-trt") {
|
|
throw new Error(`gmoccapy TRT kinematics runtime did not load xyzac-trt: ${JSON.stringify(gmoccapyTrtState.kinematicsRuntimeReadiness)}`);
|
|
}
|
|
if (gmoccapyTrtState.kinsType !== "tcp-xyzac" || gmoccapyTrtState.rtcpState !== "on") {
|
|
throw new Error(`gmoccapy TRT default kinematics should be fixed TCP, got ${gmoccapyTrtState.kinsType}/${gmoccapyTrtState.rtcpState}`);
|
|
}
|
|
if (!gmoccapyTrtState.iniConfigReadiness?.path?.includes("configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/xyzac-trt.ini")) {
|
|
throw new Error(`gmoccapy TRT INI readiness did not use gmoccapy config: ${JSON.stringify(gmoccapyTrtState.iniConfigReadiness)}`);
|
|
}
|
|
if (gmoccapyTrtStagedState.machineFileStaging?.plan?.demoDirectory !== "examples") {
|
|
throw new Error(`gmoccapy TRT staging did not use examples directory: ${JSON.stringify(gmoccapyTrtStagedState.machineFileStaging?.plan)}`);
|
|
}
|
|
if (!gmoccapyTrtStagedState.machineFileStaging?.gcodeSources?.some((source) => source.filename === "impeller-7bl-xyzac.ngc" && source.sourceRel.includes("/examples/"))) {
|
|
throw new Error(`gmoccapy TRT staging did not expose LinuxCNC examples: ${JSON.stringify(gmoccapyTrtStagedState.machineFileStaging?.gcodeSources)}`);
|
|
}
|
|
if (!gmoccapyTrtState.machineProject?.gcodeDirectory?.includes("configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/examples")) {
|
|
throw new Error(`gmoccapy TRT project directory did not track examples: ${JSON.stringify(gmoccapyTrtState.machineProject)}`);
|
|
}
|
|
if (gmoccapyTrtState.rtcpFrame?.profileId !== "gmoccapy-xyzac-trt" || gmoccapyTrtState.rtcpFrame?.rtcpState !== "on") {
|
|
throw new Error(`gmoccapy TRT RTCP frame did not stay on fixed TRT: ${JSON.stringify(gmoccapyTrtState.rtcpFrame)}`);
|
|
}
|
|
const restoredProfileSelector = doc.querySelector('[data-action="select-profile"]');
|
|
restoredProfileSelector.value = "xyzac-trt";
|
|
restoredProfileSelector.dispatchEvent(new Event("change", { bubbles: true }));
|
|
await waitForMachineFileStaging(win, "xyzac-trt");
|
|
await win.webRtcp5AxisSimulation.refreshKinematicsFrame({ operatorMessage: "browser smoke restored xyzac frame" });
|
|
const restoredProfileState = win.webRtcp5AxisSimulation.getState();
|
|
if (restoredProfileState.machineProfile !== "xyzac-trt") {
|
|
throw new Error(`unexpected profile: ${restoredProfileState.machineProfile}`);
|
|
}
|
|
if (state.rtcpFrame?.apiName !== "web-rtcp-5axis-motion-frame") {
|
|
throw new Error("missing RTCP frame state");
|
|
}
|
|
if (state.rtcpFrame?.readiness?.linuxCncKinematicsReady !== true) {
|
|
throw new Error("browser RTCP frame must use LinuxCNC kinematics readiness");
|
|
}
|
|
if (!doc.querySelector('[data-rtcp-value="tcp"]')?.textContent.includes("TCP")) {
|
|
throw new Error("missing TCP DRO strip");
|
|
}
|
|
if (!doc.querySelector('[data-rtcp-diagnostic="boundary"]')?.textContent.includes("linuxcnc_kinematics_wasm_c_abi")) {
|
|
throw new Error("missing RTCP boundary diagnostic");
|
|
}
|
|
if (!doc.querySelector('[data-linuxcnc-ini="status"]')?.textContent.includes("xyzac-trt.ini")) {
|
|
throw new Error("missing LinuxCNC INI status diagnostic");
|
|
}
|
|
if (!doc.querySelector('[data-linuxcnc-ini="kins"]')?.textContent.includes("xyzac-trt-kins")) {
|
|
throw new Error("missing LinuxCNC INI kinematics diagnostic");
|
|
}
|
|
if (!doc.querySelector('[data-linuxcnc-ini="limits"]')?.textContent.includes("X[-200,200]")) {
|
|
throw new Error("missing LinuxCNC INI axis limits diagnostic");
|
|
}
|
|
if (!doc.querySelector('[data-linuxcnc-task-policy="boundary"]')?.textContent.includes("linuxcnc_task_state_mode_command_gate")) {
|
|
throw new Error("missing LinuxCNC task policy boundary diagnostic");
|
|
}
|
|
if (!doc.querySelector('[data-linuxcnc-task-policy="source"]')?.textContent.includes("linuxcnc/src/emc/task/emctaskmain.cc")) {
|
|
throw new Error("missing LinuxCNC emctaskmain source diagnostic");
|
|
}
|
|
for (const [selector, iconName] of [
|
|
['[data-action="estop"]', "main_switch_off"],
|
|
['[data-action="power"]', "power_off"],
|
|
['[data-action="mode-manual"]', "mode_manual_active"],
|
|
['[data-action="mode-auto"]', "mode_auto_inactive"],
|
|
['[data-action="mode-mdi"]', "mode_mdi_inactive"],
|
|
['[data-action="RUN"]', "play"],
|
|
['[data-action="STOP"]', "stop"],
|
|
['[data-action="PAUSE"]', "pause"],
|
|
['[data-action="HOME"]', "ref_all"],
|
|
['[data-action="toggle-flood"]', "coolant_flood_inactive"],
|
|
['[data-action="toggle-mist"]', "coolant_mist_inactive"],
|
|
['[data-action="view-x"]', "tool_axis_x"],
|
|
]) {
|
|
const button = doc.querySelector(selector);
|
|
if (!button || button.dataset.iconName !== iconName || !button.querySelector("img")) {
|
|
throw new Error(`missing gmoccapy icon ${selector} expected ${iconName}, got ${button?.dataset.iconName}`);
|
|
}
|
|
}
|
|
for (const [selector, pin, panel, index] of [
|
|
['[data-action="estop"]', "gmoccapy.v-button.button-0", "right", "0"],
|
|
['[data-action="power"]', "gmoccapy.v-button.button-1", "right", "1"],
|
|
['[data-action="mode-manual"]', "gmoccapy.v-button.button-2", "right", "2"],
|
|
['[data-action="mode-mdi"]', "gmoccapy.v-button.button-3", "right", "3"],
|
|
['[data-action="mode-auto"]', "gmoccapy.v-button.button-4", "right", "4"],
|
|
['[data-action="mode-jog"]', "gmoccapy.v-button.button-5", "right", "5"],
|
|
['[data-action="HOME"]', "gmoccapy.h-button.button-0", "bottom", "0"],
|
|
]) {
|
|
const button = doc.querySelector(selector);
|
|
if (
|
|
!button ||
|
|
button.dataset.gmoccapyHalPin !== pin ||
|
|
button.dataset.gmoccapyNativePanel !== panel ||
|
|
button.dataset.gmoccapyNativeButtonIndex !== index
|
|
) {
|
|
throw new Error(`gmoccapy hard-button mapping mismatch ${selector}: ${JSON.stringify(button?.dataset)}`);
|
|
}
|
|
}
|
|
if (!doc.querySelector('[data-gmoccapy-comm="boundary"]')?.textContent.includes("native_gmoccapy_nml_hal_reference")) {
|
|
throw new Error("missing gmoccapy communication boundary diagnostic");
|
|
}
|
|
if (!doc.querySelector('[data-gmoccapy-comm="native-command"]')?.textContent.includes("NML emcCommand")) {
|
|
throw new Error("missing gmoccapy NML command diagnostic");
|
|
}
|
|
if (!doc.querySelector('[data-gmoccapy-comm="web-path"]')?.textContent.includes("store.dispatch")) {
|
|
throw new Error("missing gmoccapy Web runtime mapping");
|
|
}
|
|
const filePageDiagnostic = doc.querySelector('[data-gmoccapy-page="file"]')?.textContent || "";
|
|
if (
|
|
!filePageDiagnostic.includes("file page partial") ||
|
|
!filePageDiagnostic.includes("PROGRAM_PREFIX ../../nc_files/") ||
|
|
!filePageDiagnostic.includes("native IconFileSelection") ||
|
|
!filePageDiagnostic.includes("Web OPEN_FILE/staged-source")
|
|
) {
|
|
throw new Error(`missing gmoccapy file page diagnostic: ${filePageDiagnostic}`);
|
|
}
|
|
const macroPageDiagnostic = doc.querySelector('[data-gmoccapy-page="macros"]')?.textContent || "";
|
|
if (
|
|
!macroPageDiagnostic.includes("5 XYZAB macros") ||
|
|
!macroPageDiagnostic.includes("MDI gate") ||
|
|
!macroPageDiagnostic.includes("O-word call")
|
|
) {
|
|
throw new Error(`missing gmoccapy macro page diagnostic: ${macroPageDiagnostic}`);
|
|
}
|
|
const toolEditorDiagnostic = doc.querySelector('[data-gmoccapy-page="tool-editor"]')?.textContent || "";
|
|
if (
|
|
!toolEditorDiagnostic.includes("tool editor diagnostic-only") ||
|
|
!toolEditorDiagnostic.includes("17 tools") ||
|
|
!toolEditorDiagnostic.includes("writeback disabled") ||
|
|
!toolEditorDiagnostic.includes("iocontrol-loopback preserved")
|
|
) {
|
|
throw new Error(`missing gmoccapy tool editor diagnostic: ${toolEditorDiagnostic}`);
|
|
}
|
|
const nativePageMatrixDiagnostic = doc.querySelector('[data-gmoccapy-page="matrix"]')?.textContent || "";
|
|
if (
|
|
!nativePageMatrixDiagnostic.includes("12 native pages") ||
|
|
!nativePageMatrixDiagnostic.includes("5 diagnostic-only") ||
|
|
!nativePageMatrixDiagnostic.includes("2 native-only") ||
|
|
!nativePageMatrixDiagnostic.includes("hard-buttons diagnostic-only")
|
|
) {
|
|
throw new Error(`missing gmoccapy native page matrix diagnostic: ${nativePageMatrixDiagnostic}`);
|
|
}
|
|
if (!doc.querySelector('[data-gmoccapy-hal="postgui"]')?.textContent.includes("tool-change-loop")) {
|
|
throw new Error("missing gmoccapy HAL postgui diagnostic");
|
|
}
|
|
const halInputDiagnostic = doc.querySelector('[data-gmoccapy-hal="operator-inputs"]')?.textContent || "";
|
|
if (
|
|
!halInputDiagnostic.includes("3 operator pins") ||
|
|
!halInputDiagnostic.includes("optional-stop -> block delete") ||
|
|
!halInputDiagnostic.includes("blockdelete -> optional stop") ||
|
|
!halInputDiagnostic.includes("ignore-limits off")
|
|
) {
|
|
throw new Error(`missing gmoccapy HAL operator input diagnostic: ${halInputDiagnostic}`);
|
|
}
|
|
const overrideInputDiagnostic = doc.querySelector('[data-gmoccapy-hal="override-inputs"]')?.textContent || "";
|
|
if (
|
|
!overrideInputDiagnostic.includes("4 override targets") ||
|
|
!overrideInputDiagnostic.includes("counts require enable") ||
|
|
!overrideInputDiagnostic.includes("direct-value requires analog-enable") ||
|
|
!overrideInputDiagnostic.includes("reset rising-edge-only")
|
|
) {
|
|
throw new Error(`missing gmoccapy HAL override diagnostic: ${overrideInputDiagnostic}`);
|
|
}
|
|
const jogInputDiagnostic = doc.querySelector('[data-gmoccapy-hal="jog-inputs"]')?.textContent || "";
|
|
if (
|
|
!jogInputDiagnostic.includes("10 axis jog pins") ||
|
|
!jogInputDiagnostic.includes("6 jog-inc pins") ||
|
|
!jogInputDiagnostic.includes("press/release level") ||
|
|
!jogInputDiagnostic.includes("jog-inc rising-edge-only") ||
|
|
!jogInputDiagnostic.includes("jog-inc-0 continuous")
|
|
) {
|
|
throw new Error(`missing gmoccapy HAL jog diagnostic: ${jogInputDiagnostic}`);
|
|
}
|
|
const messageInputDiagnostic = doc.querySelector('[data-gmoccapy-hal="message-inputs"]')?.textContent || "";
|
|
if (
|
|
!messageInputDiagnostic.includes("3 message pins") ||
|
|
!messageInputDiagnostic.includes("delete-message rising-edge-only") ||
|
|
!messageInputDiagnostic.includes("warning-confirm level-polled")
|
|
) {
|
|
throw new Error(`missing gmoccapy HAL message diagnostic: ${messageInputDiagnostic}`);
|
|
}
|
|
const settingsInputDiagnostic = doc.querySelector('[data-gmoccapy-hal="settings-inputs"]')?.textContent || "";
|
|
if (
|
|
!settingsInputDiagnostic.includes("1 settings pins") ||
|
|
!settingsInputDiagnostic.includes("unlock-settings level-driven") ||
|
|
!settingsInputDiagnostic.includes("unlock-way use") ||
|
|
!settingsInputDiagnostic.includes("setup sensitive")
|
|
) {
|
|
throw new Error(`missing gmoccapy HAL settings diagnostic: ${settingsInputDiagnostic}`);
|
|
}
|
|
const toolUserDiagnostic = doc.querySelector('[data-gmoccapy-hal="tool-user-inputs"]')?.textContent || "";
|
|
if (
|
|
!toolUserDiagnostic.includes("5 tool measurement HAL_OUT pins") ||
|
|
!toolUserDiagnostic.includes("XYZAB no TOOLSENSOR") ||
|
|
!toolUserDiagnostic.includes("0 user message pins") ||
|
|
!toolUserDiagnostic.includes("no MESSAGE_*")
|
|
) {
|
|
throw new Error(`missing gmoccapy HAL tool/user diagnostic: ${toolUserDiagnostic}`);
|
|
}
|
|
const hardButtonDiagnostic = doc.querySelector('[data-gmoccapy-hal="hard-buttons"]')?.textContent || "";
|
|
if (
|
|
!hardButtonDiagnostic.includes("7 right v-button pins") ||
|
|
!hardButtonDiagnostic.includes("4 bottom h-button main refs") ||
|
|
!hardButtonDiagnostic.includes("7 mapped Web buttons") ||
|
|
!hardButtonDiagnostic.includes("rising-edge-only") ||
|
|
!hardButtonDiagnostic.includes("insensitive ignored")
|
|
) {
|
|
throw new Error(`missing gmoccapy hard-button diagnostic: ${hardButtonDiagnostic}`);
|
|
}
|
|
const toolChangeDiagnostic = doc.querySelector('[data-gmoccapy-hal="tool-change"]')?.textContent || "";
|
|
if (
|
|
!toolChangeDiagnostic.includes("iocontrol-loopback") ||
|
|
!toolChangeDiagnostic.includes("manual GUI pins disconnected") ||
|
|
!toolChangeDiagnostic.includes("3 postgui unlinkp")
|
|
) {
|
|
throw new Error(`missing gmoccapy HAL tool-change diagnostic: ${toolChangeDiagnostic}`);
|
|
}
|
|
if (!doc.querySelector('[data-tool-preview="summary"]')?.textContent.includes("T1")) {
|
|
throw new Error("missing tool preview summary");
|
|
}
|
|
if (!doc.querySelector('[data-action="OPEN_FILE"]')) {
|
|
throw new Error("missing G-code file input");
|
|
}
|
|
if (!doc.querySelector('[data-machine-state="summary"]')?.textContent.includes("power off")) {
|
|
throw new Error("initial machine state should show power off");
|
|
}
|
|
const initialModeManualButton = doc.querySelector('[data-action="mode-manual"]');
|
|
const initialModeJogButton = doc.querySelector('[data-action="mode-jog"]');
|
|
const initialModeAutoButton = doc.querySelector('[data-action="mode-auto"]');
|
|
const initialModeMdiButton = doc.querySelector('[data-action="mode-mdi"]');
|
|
if (
|
|
!initialModeManualButton?.disabled ||
|
|
initialModeManualButton.dataset.commandReady !== "false" ||
|
|
initialModeManualButton.getAttribute("aria-disabled") !== "true"
|
|
) {
|
|
throw new Error("MANUAL mode button should be disabled before machine power on");
|
|
}
|
|
if (
|
|
!initialModeJogButton?.disabled ||
|
|
initialModeJogButton.dataset.commandReady !== "false" ||
|
|
initialModeJogButton.getAttribute("aria-disabled") !== "true"
|
|
) {
|
|
throw new Error("JOG mode button should be disabled before machine power on");
|
|
}
|
|
if (
|
|
!initialModeAutoButton?.disabled ||
|
|
initialModeAutoButton.dataset.commandReady !== "false" ||
|
|
initialModeAutoButton.getAttribute("aria-disabled") !== "true"
|
|
) {
|
|
throw new Error("AUTO mode button should be disabled before machine power on");
|
|
}
|
|
if (
|
|
!initialModeMdiButton?.disabled ||
|
|
initialModeMdiButton.dataset.commandReady !== "false" ||
|
|
initialModeMdiButton.getAttribute("aria-disabled") !== "true"
|
|
) {
|
|
throw new Error("MDI mode button should be disabled before machine power on");
|
|
}
|
|
|
|
const initiallyBlockedRunButton = doc.querySelector('[data-action="RUN"]');
|
|
if (
|
|
initiallyBlockedRunButton.disabled ||
|
|
initiallyBlockedRunButton.dataset.commandReady !== "false" ||
|
|
initiallyBlockedRunButton.getAttribute("aria-disabled") !== "true"
|
|
) {
|
|
throw new Error("RUN button should remain clickable and marked blocked before power on");
|
|
}
|
|
initiallyBlockedRunButton.click();
|
|
await wait(50);
|
|
if (!win.webRtcp5AxisSimulation.getState().operatorMessage.includes("blocked")) {
|
|
throw new Error("RUN button should report why execution is blocked before power on");
|
|
}
|
|
doc.querySelector('[data-action="power"]').click();
|
|
await wait(50);
|
|
if (win.webRtcp5AxisSimulation.getState().machine.powerOn !== true) {
|
|
throw new Error("POWER action did not turn machine on");
|
|
}
|
|
if (!doc.querySelector('[data-machine-state="summary"]')?.textContent.includes("power on")) {
|
|
throw new Error("machine state did not render power on");
|
|
}
|
|
const unhomedModeManualButton = doc.querySelector('[data-action="mode-manual"]');
|
|
const unhomedModeJogButton = doc.querySelector('[data-action="mode-jog"]');
|
|
const unhomedModeAutoButton = doc.querySelector('[data-action="mode-auto"]');
|
|
const unhomedModeMdiButton = doc.querySelector('[data-action="mode-mdi"]');
|
|
if (
|
|
unhomedModeManualButton?.disabled ||
|
|
unhomedModeManualButton.dataset.commandReady !== "true" ||
|
|
unhomedModeJogButton?.disabled ||
|
|
unhomedModeJogButton.dataset.commandReady !== "true"
|
|
) {
|
|
throw new Error("MANUAL and JOG mode buttons should be enabled after power on");
|
|
}
|
|
if (
|
|
!unhomedModeAutoButton?.disabled ||
|
|
unhomedModeAutoButton.dataset.commandReady !== "false" ||
|
|
!unhomedModeAutoButton.title.includes("home machine before AUTO")
|
|
) {
|
|
throw new Error("AUTO mode button should remain disabled until homed");
|
|
}
|
|
if (
|
|
!unhomedModeMdiButton?.disabled ||
|
|
unhomedModeMdiButton.dataset.commandReady !== "false" ||
|
|
!unhomedModeMdiButton.title.includes("home machine before MDI")
|
|
) {
|
|
throw new Error("MDI mode button should remain disabled until homed");
|
|
}
|
|
doc.querySelector('[data-action="HOME"]').click();
|
|
await wait(50);
|
|
const homedModeAutoButton = doc.querySelector('[data-action="mode-auto"]');
|
|
const homedModeMdiButton = doc.querySelector('[data-action="mode-mdi"]');
|
|
if (
|
|
homedModeAutoButton?.disabled ||
|
|
homedModeAutoButton.dataset.commandReady !== "true" ||
|
|
homedModeMdiButton?.disabled ||
|
|
homedModeMdiButton.dataset.commandReady !== "true"
|
|
) {
|
|
throw new Error("AUTO and MDI mode buttons should be enabled after homing");
|
|
}
|
|
doc.querySelector('[data-action="mode-auto"]').click();
|
|
await wait(50);
|
|
const autoModeState = win.webRtcp5AxisSimulation.getState();
|
|
if (autoModeState.machine.mode !== "auto") {
|
|
throw new Error(`AUTO mode button did not switch task mode: ${autoModeState.machine.mode}`);
|
|
}
|
|
if (autoModeState.machine.powerOn !== true || autoModeState.machine.taskState !== "on") {
|
|
throw new Error(`AUTO mode button should preserve machine power: ${JSON.stringify(autoModeState.machine)}`);
|
|
}
|
|
const postAutoManualButton = doc.querySelector('[data-action="mode-manual"]');
|
|
if (postAutoManualButton?.disabled || postAutoManualButton.dataset.commandReady !== "true") {
|
|
throw new Error("MANUAL mode button should remain enabled after switching to AUTO");
|
|
}
|
|
assertSidebarModeVisualState(doc, { manualActive: false, autoActive: true });
|
|
if (!doc.querySelector('[data-linuxcnc-task-policy="gates"]')?.textContent.includes("auto")) {
|
|
throw new Error("LinuxCNC task policy did not expose auto run gate");
|
|
}
|
|
|
|
win.webRtcp5AxisSimulation.dispatch({
|
|
type: "LOAD_PROGRAM",
|
|
filename: "operator-demo.ngc",
|
|
content: [
|
|
"G90 G17",
|
|
"G0 X0 Y0 Z0",
|
|
"G1 X10 F100",
|
|
"G1 Y10",
|
|
"G1 X0",
|
|
"G1 Y0",
|
|
"G0 Z5",
|
|
"M5",
|
|
"M2",
|
|
].join("\n"),
|
|
});
|
|
await waitForInterpreterExecution(win);
|
|
if (win.webRtcp5AxisSimulation.getState().activeProgram !== "operator-demo.ngc") {
|
|
throw new Error("LOAD_PROGRAM did not update active program");
|
|
}
|
|
if (win.webRtcp5AxisSimulation.getState().programExecutionSourceMode !== "linuxcnc-interpreter-wasm") {
|
|
throw new Error("LOAD_PROGRAM did not execute through LinuxCNC interpreter WASM");
|
|
}
|
|
if (win.webRtcp5AxisSimulation.getState().programExecution?.summary?.motionEventCount < 5) {
|
|
throw new Error("LinuxCNC interpreter execution did not produce motion events");
|
|
}
|
|
if (win.webRtcp5AxisSimulation.getState().programExecution?.summary?.plannerRuntimeReady !== true) {
|
|
throw new Error("LinuxCNC TP planner timing was not ready for operator program");
|
|
}
|
|
if (win.webRtcp5AxisSimulation.getState().programExecutionTiming?.semanticBoundary !== "linuxcnc_tp_queue_runtime_timing_from_canonical_motion") {
|
|
throw new Error(`program timing did not use LinuxCNC TP queue runtime: ${JSON.stringify(win.webRtcp5AxisSimulation.getState().programExecutionTiming)}`);
|
|
}
|
|
if (win.webRtcp5AxisSimulation.getState().programExecutionTiming?.sampleCount < 2) {
|
|
throw new Error("LinuxCNC TP runtime feedback samples were not exposed");
|
|
}
|
|
if (win.webRtcp5AxisSimulation.getState().programRuntimeFeedback?.semanticBoundary !== "linuxcnc_tp_run_cycle_feedback_without_hardware") {
|
|
throw new Error(`initial runtime feedback did not use LinuxCNC TP sample: ${JSON.stringify(win.webRtcp5AxisSimulation.getState().programRuntimeFeedback)}`);
|
|
}
|
|
canvas = doc.querySelector("[data-five-axis-canvas]");
|
|
if (
|
|
canvas.dataset.threeProgramPreviewSource !== "linuxcnc-interpreter-wasm" ||
|
|
Number(canvas.dataset.threePathPoints ?? 0) < win.webRtcp5AxisSimulation.getState().programExecution.summary.motionEventCount ||
|
|
Number(canvas.dataset.threeExecutedPathPoints ?? 0) < 1
|
|
) {
|
|
throw new Error(`Three.js preview did not consume LinuxCNC program execution path: ${JSON.stringify(canvas.dataset)}`);
|
|
}
|
|
if (!doc.querySelector('[data-program-execution-source]')?.textContent.includes("linuxcnc-interpreter-wasm")) {
|
|
throw new Error("program execution source DOM did not render interpreter source");
|
|
}
|
|
if (!doc.querySelector('[data-program-execution-summary]')?.textContent.includes("motion")) {
|
|
throw new Error("program execution summary DOM did not render motion count");
|
|
}
|
|
if (!doc.querySelector('[data-program-timing="summary"]')?.textContent.match(/\\d+:\\d/)) {
|
|
throw new Error("program timing summary DOM did not render estimated execution time");
|
|
}
|
|
if (!doc.querySelector('[data-program-timing="segment"]')?.textContent.includes("mm/min")) {
|
|
throw new Error("program timing segment DOM did not render segment velocity");
|
|
}
|
|
if (!doc.querySelector('[data-program-runtime-feedback="source"]')?.textContent.includes("linuxcnc-tp-runtime-sample")) {
|
|
throw new Error("program runtime feedback DOM did not render LinuxCNC TP sample source");
|
|
}
|
|
if (!doc.querySelector('[data-program-runtime-feedback="dtg"]')?.textContent.includes("DTG")) {
|
|
throw new Error("program runtime feedback DOM did not render DTG");
|
|
}
|
|
if (!doc.querySelector('[data-program-switchkins-summary]')?.textContent.includes("0 events")) {
|
|
throw new Error("program switchkins summary DOM did not render zero-event state");
|
|
}
|
|
if (!win.webRtcp5AxisSimulation.machineFileSeedReady) {
|
|
throw new Error("missing automatic machine file seed promise");
|
|
}
|
|
await win.webRtcp5AxisSimulation.machineFileSeedReady;
|
|
const autoStagedState = await waitForMachineFileStaging(win);
|
|
if (autoStagedState.machineFileStaging?.save?.summary?.gcodeFileCount !== 16) {
|
|
throw new Error(`automatic machine file seeding did not preload full project G-code set: ${JSON.stringify(autoStagedState.machineFileStaging?.save?.summary)}`);
|
|
}
|
|
if (autoStagedState.machineFileStaging?.gcodeSources?.length < 8) {
|
|
throw new Error(`automatic machine file seeding did not expose LinuxCNC 5-axis demos: ${JSON.stringify(autoStagedState.machineFileStaging)}`);
|
|
}
|
|
if (!doc.querySelector('[data-machine-file-staging="status"]')?.textContent.includes("staged")) {
|
|
throw new Error("automatic machine file staging status did not render");
|
|
}
|
|
const stagedToolTwo = win.webRtcp5AxisSimulation.queryToolDb({ toolNumber: 2 });
|
|
if (!stagedToolTwo || stagedToolTwo.idx !== 2 || stagedToolTwo.pocket !== 2 || stagedToolTwo.offset.z !== 15) {
|
|
throw new Error(`browser tool DB query did not read staged tool.tbl: ${JSON.stringify(stagedToolTwo)}`);
|
|
}
|
|
win.webRtcp5AxisSimulation.editToolDb({
|
|
toolNumber: 5,
|
|
pocket: 55,
|
|
diameter: 11.25,
|
|
toolLength: 31.5,
|
|
});
|
|
const editedBrowserTool = win.webRtcp5AxisSimulation.queryToolDb({ toolNumber: 5 });
|
|
if (
|
|
!editedBrowserTool ||
|
|
editedBrowserTool.idx !== 5 ||
|
|
editedBrowserTool.pocket !== 55 ||
|
|
editedBrowserTool.diameter !== 11.25 ||
|
|
editedBrowserTool.offset.z !== 31.5
|
|
) {
|
|
throw new Error(`browser tool DB edit did not update staged row: ${JSON.stringify(editedBrowserTool)}`);
|
|
}
|
|
const browserToolSave = await win.webRtcp5AxisSimulation.saveToolDb();
|
|
if (
|
|
browserToolSave.storageMode !== "memory-fallback" ||
|
|
browserToolSave.storageCapability?.opfsUnavailable !== true ||
|
|
!browserToolSave.text.includes("T5 P55 D+11.250000 Z+31.500000")
|
|
) {
|
|
throw new Error(`browser tool DB save did not use memory fallback LinuxCNC format: ${JSON.stringify(browserToolSave)}`);
|
|
}
|
|
win.webRtcp5AxisSimulation.dispatch({
|
|
type: "LOAD_PROGRAM",
|
|
filename: "operator-arc-demo.ngc",
|
|
content: [
|
|
"G90 G17",
|
|
"G0 X1 Y0 Z0",
|
|
"G2 X0 Y1 I-1 J0 F60",
|
|
"M2",
|
|
].join("\n"),
|
|
});
|
|
await waitForInterpreterExecution(win);
|
|
const arcState = win.webRtcp5AxisSimulation.getState();
|
|
if (!arcState.programExecution?.summary?.motionTypes?.includes("ARC_FEED")) {
|
|
throw new Error("LinuxCNC interpreter did not produce ARC_FEED for browser arc program");
|
|
}
|
|
if (arcState.programExecution?.summary?.plannerRuntimeReady !== true) {
|
|
throw new Error("LinuxCNC TP planner timing was not ready for browser arc program");
|
|
}
|
|
const arcSegment = arcState.programExecutionTiming?.segments?.find((segment) => segment.type === "ARC_FEED");
|
|
if (!arcSegment || arcSegment.durationSeconds <= 1.4) {
|
|
throw new Error(`LinuxCNC TP arc timing segment missing or too short: ${JSON.stringify(arcState.programExecutionTiming)}`);
|
|
}
|
|
const stagedMachineFiles = await win.webRtcp5AxisSimulation.stageMachineFiles();
|
|
await wait(50);
|
|
if (stagedMachineFiles.save.status !== "saved" || stagedMachineFiles.save.fileCount < 5) {
|
|
throw new Error(`machine file staging did not save files: ${JSON.stringify(stagedMachineFiles.save)}`);
|
|
}
|
|
if (stagedMachineFiles.save.summary?.gcodeFileCount !== 16) {
|
|
throw new Error(`manual machine file staging did not preserve full project G-code set: ${JSON.stringify(stagedMachineFiles.save.summary)}`);
|
|
}
|
|
if (
|
|
stagedMachineFiles.save.storageMode !== "memory-fallback" ||
|
|
stagedMachineFiles.save.storageCapability?.opfsUnavailable !== true
|
|
) {
|
|
throw new Error(`machine file staging did not use OPFS fallback: ${JSON.stringify(stagedMachineFiles.save.storageCapability)}`);
|
|
}
|
|
if (!stagedMachineFiles.save.files.some((file) => file.sourceRel.endsWith("remap_subs/428remap.ngc"))) {
|
|
throw new Error("machine file staging did not include M428 remap");
|
|
}
|
|
if (!doc.querySelector('[data-machine-file-staging="status"]')?.textContent.includes("staged")) {
|
|
throw new Error("machine file staging status did not render");
|
|
}
|
|
if (
|
|
!doc.querySelector('[data-machine-file-staging="status"]')?.textContent.includes("memory-fallback") ||
|
|
!doc.querySelector('[data-machine-file-staging="status"]')?.textContent.includes("forced_unavailable")
|
|
) {
|
|
throw new Error("machine file staging status did not render OPFS fallback reason");
|
|
}
|
|
const sourceSelect = doc.querySelector('[data-action="select-linuxcnc-gcode-source"]');
|
|
if (!sourceSelect || sourceSelect.options.length < 4) {
|
|
throw new Error("LinuxCNC 5-axis G-code source selector did not render staged demos");
|
|
}
|
|
const impellerSource = "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/impeller-7bl-xyzac.ngc";
|
|
sourceSelect.value = impellerSource;
|
|
sourceSelect.dispatchEvent(new Event("change", { bubbles: true }));
|
|
await waitForInterpreterExecution(win);
|
|
const linuxCncSourceState = win.webRtcp5AxisSimulation.getState();
|
|
if (linuxCncSourceState.programSource !== "linuxcnc-vendored-5axis-gcode") {
|
|
throw new Error(`LinuxCNC source program was not loaded: ${linuxCncSourceState.programSource}`);
|
|
}
|
|
if (!linuxCncSourceState.activeProgram.endsWith("impeller-7bl-xyzac.ngc")) {
|
|
throw new Error(`unexpected LinuxCNC source program: ${linuxCncSourceState.activeProgram}`);
|
|
}
|
|
if (!doc.querySelector('[data-linuxcnc-gcode-source="selected"]')?.textContent.includes("impeller-7bl-xyzac.ngc")) {
|
|
throw new Error("selected LinuxCNC G-code source did not render");
|
|
}
|
|
if (!doc.querySelector('[data-program-source]')?.textContent.includes("linuxcnc-vendored-5axis-gcode")) {
|
|
throw new Error("program source DOM did not show LinuxCNC vendored source");
|
|
}
|
|
if (linuxCncSourceState.programExecution?.summary?.motionEventCount < 1000) {
|
|
throw new Error(`LinuxCNC source program did not produce canonical motion: ${JSON.stringify(linuxCncSourceState.programExecution?.summary)}`);
|
|
}
|
|
if (linuxCncSourceState.programExecution?.summary?.plannerRuntimeReady !== true) {
|
|
throw new Error(`LinuxCNC source program did not produce TP timing: ${JSON.stringify(linuxCncSourceState.programExecution?.plannerTiming)}`);
|
|
}
|
|
if (linuxCncSourceState.programExecutionTiming?.samples?.length < 1) {
|
|
throw new Error("LinuxCNC source program did not expose TP samples");
|
|
}
|
|
if (linuxCncSourceState.programExecution?.summary?.switchkinsEventCount < 2) {
|
|
throw new Error("LinuxCNC source program did not preserve switchkins events");
|
|
}
|
|
if (linuxCncSourceState.rtcpState !== "on" || linuxCncSourceState.kinsType !== "tcp-xyzac") {
|
|
throw new Error(`LinuxCNC source program did not apply initial RTCP switchkins state: ${linuxCncSourceState.rtcpState}/${linuxCncSourceState.kinsType}`);
|
|
}
|
|
canvas = doc.querySelector("[data-five-axis-canvas]");
|
|
if (
|
|
canvas.dataset.threeProgramPreviewSource !== "linuxcnc-interpreter-wasm" ||
|
|
Number(canvas.dataset.threePathPoints ?? 0) < 1 ||
|
|
Number(canvas.dataset.threeExecutedPathPoints ?? 0) < 1 ||
|
|
canvas.dataset.threeToolExecutionMarker !== "true"
|
|
) {
|
|
throw new Error(`Three.js preview did not expose LinuxCNC source program path and execution trace: ${JSON.stringify(canvas.dataset)}`);
|
|
}
|
|
if (
|
|
canvas.dataset.threeSceneMode !== "program-preview-and-tool-execution" ||
|
|
canvas.dataset.threePreviewScope !== "machine-reference-and-toolpath" ||
|
|
canvas.dataset.threeMachineReferenceModel !== "webgl-five-axis-reference" ||
|
|
canvas.dataset.threeTcpMarker !== "sphere" ||
|
|
canvas.dataset.threeToolAxisMarker !== "line" ||
|
|
canvas.dataset.threeToolpathPreviewSource !== "linuxcnc_interpreter_canonical_motion" ||
|
|
canvas.dataset.threeToolExecutionTraceSource !== "linuxcnc_tp_samples_or_task_motion_hal_feedback" ||
|
|
canvas.dataset.threePathFitBounds !== "ok" ||
|
|
canvas.dataset.threeCurrentSegmentHighlight !== "ok" ||
|
|
canvas.dataset.threeRapidFeedVisualDistinction !== "ok" ||
|
|
canvas.dataset.threeNoGcodeSemanticsGeneration !== "ok"
|
|
) {
|
|
throw new Error(`Three.js M20 source/runtime display gate failed: ${JSON.stringify(canvas.dataset)}`);
|
|
}
|
|
if (Number(canvas.dataset.threeRapidPathPoints ?? 0) < 1 || Number(canvas.dataset.threeFeedPathPoints ?? 0) < 1) {
|
|
throw new Error(`Three.js rapid/feed distinction did not expose path points: ${JSON.stringify(canvas.dataset)}`);
|
|
}
|
|
assertCanvasNonblank(canvas, "desktop LinuxCNC source Three.js preview");
|
|
win.webRtcp5AxisSimulation.dispatch({ type: "RUN_MACHINE_FILE_PROGRAM" });
|
|
const machineFileRunState = await waitForMachineFileExecution(win);
|
|
if (machineFileRunState.machineFileExecution?.summary?.machineFileExecutionReady !== true) {
|
|
throw new Error(`machine-file backed remap run did not complete: ${JSON.stringify(machineFileRunState.machineFileExecution?.summary)}`);
|
|
}
|
|
if (!machineFileRunState.machineFileExecution.resultText.includes("fiveaxis_file_reached_exit=1")) {
|
|
throw new Error("machine-file backed remap run did not reach exit");
|
|
}
|
|
if (!machineFileRunState.machineFileExecution.machineFilePlan?.selectedProgramFilename?.includes("impeller-7bl-xyzac.ngc")) {
|
|
throw new Error("machine-file backed remap run did not use selected LinuxCNC G-code source");
|
|
}
|
|
if (!doc.querySelector('[data-machine-file-execution="status"]')?.textContent.includes("ready")) {
|
|
throw new Error("machine-file execution status did not render");
|
|
}
|
|
if (machineFileRunState.fullExecutionBoundary?.machineFileBackedRemapReady !== true) {
|
|
throw new Error(`full execution boundary did not record remap readiness: ${JSON.stringify(machineFileRunState.fullExecutionBoundary)}`);
|
|
}
|
|
if (!doc.querySelector('[data-full-execution-boundary="status"]')?.textContent.includes("remap ready")) {
|
|
throw new Error("full execution boundary status did not render remap readiness");
|
|
}
|
|
const savedSession = await win.webRtcp5AxisSimulation.saveSession();
|
|
await wait(50);
|
|
if (savedSession.snapshot.format !== "web-rtcp-5axis-session-snapshot") {
|
|
throw new Error("saveSession did not write a five-axis session snapshot");
|
|
}
|
|
if (!["opfs", "memory-fallback"].includes(savedSession.storageMode)) {
|
|
throw new Error(`saveSession used unexpected storage mode: ${savedSession.storageMode}`);
|
|
}
|
|
if (savedSession.storageMode !== "memory-fallback" || savedSession.storageCapability?.opfsUnavailable !== true) {
|
|
throw new Error(`saveSession did not use OPFS fallback: ${JSON.stringify(savedSession.storageCapability)}`);
|
|
}
|
|
if (!doc.querySelector('[data-session-persistence="status"]')?.textContent.includes("saved")) {
|
|
throw new Error("session save status did not render");
|
|
}
|
|
if (!doc.querySelector('[data-session-persistence="status"]')?.textContent.includes(savedSession.storageMode)) {
|
|
throw new Error("session save status did not render storage mode");
|
|
}
|
|
if (!doc.querySelector('[data-session-persistence="status"]')?.textContent.includes("opfs unavailable")) {
|
|
throw new Error("session save status did not render OPFS unavailable fallback");
|
|
}
|
|
if (doc.querySelector('[data-active-program-line]')?.textContent !== "Executing line 2") {
|
|
throw new Error("loaded program did not render first LinuxCNC motion line");
|
|
}
|
|
assertActiveGcodeRowVisible(doc, 2);
|
|
|
|
win.webRtcp5AxisSimulation.dispatch({ type: "RUN" });
|
|
await wait(250);
|
|
if (win.webRtcp5AxisSimulation.getState().runState !== "running") {
|
|
throw new Error("RUN action did not update state after power on");
|
|
}
|
|
if (win.webRtcp5AxisSimulation.getState().programRuntimeFeedback?.sourceMode !== "linuxcnc-task-motion-hal-wasm") {
|
|
throw new Error("RUN did not advance with LinuxCNC task/HAL runtime feedback");
|
|
}
|
|
if (win.webRtcp5AxisSimulation.getState().feed.currentVelocity <= 0) {
|
|
throw new Error("RUN did not expose LinuxCNC task/HAL current velocity");
|
|
}
|
|
if (Number(doc.querySelector(".gcode-row.active")?.dataset.programLine || 0) < 2) {
|
|
throw new Error("RUN did not highlight a LinuxCNC task/HAL motion line");
|
|
}
|
|
assertActiveGcodeRowVisible(doc);
|
|
const runExecutionText = doc.querySelector(".gcode-row.active [data-line-execution]")?.textContent || "";
|
|
if (!runExecutionText.includes("F ") || !runExecutionText.includes("cycle") || !runExecutionText.includes("linuxcnc-task-motion-hal-wasm")) {
|
|
throw new Error(`RUN did not render current G-code line execution details: ${runExecutionText}`);
|
|
}
|
|
const stopLineBefore = win.webRtcp5AxisSimulation.getState().activeLine;
|
|
doc.querySelector('[data-action="STOP"]').click();
|
|
await wait(150);
|
|
const stoppedState = win.webRtcp5AxisSimulation.getState();
|
|
if (stoppedState.runState !== "stopped" || stoppedState.machine.interpState !== "idle") {
|
|
throw new Error(`STOP did not stop the running G-code program: ${stoppedState.runState}/${stoppedState.machine.interpState}`);
|
|
}
|
|
if (stoppedState.feed.currentVelocity !== 0) {
|
|
throw new Error(`STOP did not zero current velocity: ${stoppedState.feed.currentVelocity}`);
|
|
}
|
|
if (stoppedState.activeLine !== stopLineBefore) {
|
|
throw new Error(`STOP allowed G-code execution to continue: ${stopLineBefore} -> ${stoppedState.activeLine}`);
|
|
}
|
|
doc.querySelector('[data-action="RUN"]').click();
|
|
await wait(250);
|
|
if (win.webRtcp5AxisSimulation.getState().runState !== "running") {
|
|
throw new Error("RUN did not restart after STOP");
|
|
}
|
|
canvas = doc.querySelector("[data-five-axis-canvas]");
|
|
if (
|
|
Number(canvas.dataset.threeExecutedPathPoints ?? 0) < 1 ||
|
|
canvas.dataset.threeToolExecutionMarker !== "true" ||
|
|
canvas.dataset.threeCurrentSegmentHighlight !== "ok"
|
|
) {
|
|
throw new Error(`Three.js preview did not display tool execution progress: ${JSON.stringify(canvas.dataset)}`);
|
|
}
|
|
const originalFrameStyle = frame.getAttribute("style") || "";
|
|
frame.style.width = "390px";
|
|
frame.style.height = "760px";
|
|
await wait(100);
|
|
canvas = doc.querySelector("[data-five-axis-canvas]");
|
|
if (canvas.dataset.threeReady !== "true" || canvas.dataset.threePathFitBounds !== "ok") {
|
|
throw new Error(`mobile Three.js preview did not retain fit bounds: ${JSON.stringify(canvas.dataset)}`);
|
|
}
|
|
assertCanvasNonblank(canvas, "mobile LinuxCNC source Three.js preview");
|
|
frame.setAttribute("style", originalFrameStyle);
|
|
await wait(50);
|
|
const taskHalRunState = win.webRtcp5AxisSimulation.getState();
|
|
if (taskHalRunState.fullExecutionBoundary?.semanticBoundary !== "linuxcnc_task_motion_hal_wasm_simulation_runtime") {
|
|
throw new Error(`full execution boundary did not promote task/HAL simulation runtime: ${JSON.stringify(taskHalRunState.fullExecutionBoundary)}`);
|
|
}
|
|
if (taskHalRunState.fullExecutionBoundary?.fullLinuxCncProgramExecutionReady !== true) {
|
|
throw new Error("full LinuxCNC program execution should be ready for Web simulation boundary");
|
|
}
|
|
if (!doc.querySelector('[data-full-execution-boundary="status"]')?.textContent.includes("full ready")) {
|
|
throw new Error("full execution boundary status did not render full ready state");
|
|
}
|
|
const hostNativeBoundary = doc.querySelector('[data-full-execution-boundary="host-native"]')?.textContent || "";
|
|
for (const requiredHostNativeState of [
|
|
"hardware drive false",
|
|
"host realtime false",
|
|
"external user-M web simulation only",
|
|
"tool DB web simulation only",
|
|
"host external user-M false",
|
|
"host tool DB false",
|
|
"arbitrary user-M false",
|
|
]) {
|
|
if (!hostNativeBoundary.includes(requiredHostNativeState)) {
|
|
throw new Error(`host/native boundary diagnostic did not render ${requiredHostNativeState}: ${hostNativeBoundary}`);
|
|
}
|
|
}
|
|
const toolUserBoundary = doc.querySelector('[data-full-execution-boundary="tool-user-process"]')?.textContent || "";
|
|
for (const requiredToolUserState of [
|
|
"toolDbProcessReady=true web_simulation_only",
|
|
"externalUserMProcessReady=true web_simulation_only",
|
|
]) {
|
|
if (!toolUserBoundary.includes(requiredToolUserState)) {
|
|
throw new Error(`tool/user process diagnostic did not render ${requiredToolUserState}: ${toolUserBoundary}`);
|
|
}
|
|
}
|
|
for (const [field, expected] of Object.entries({
|
|
hardwareDrive: false,
|
|
hostRealtimeKernel: false,
|
|
externalUserMProcessReady: true,
|
|
toolDbProcessReady: true,
|
|
hostExternalUserMProcessReady: false,
|
|
hostToolDbProcessReady: false,
|
|
arbitraryUserMExecution: false,
|
|
})) {
|
|
if (taskHalRunState.fullExecutionBoundary?.[field] !== expected) {
|
|
throw new Error(`full execution boundary ${field} drifted: ${JSON.stringify(taskHalRunState.fullExecutionBoundary)}`);
|
|
}
|
|
}
|
|
if (!doc.querySelector('[data-task-hal-runtime="readiness"]')?.textContent.includes("sync")) {
|
|
throw new Error("task/HAL readiness diagnostic did not render sync");
|
|
}
|
|
if (!doc.querySelector('[data-task-hal-runtime="cycles"]')?.textContent.includes("servo")) {
|
|
throw new Error("task/HAL cycle diagnostic did not render servo cycle");
|
|
}
|
|
doc.querySelector('[data-action="PAUSE"]').click();
|
|
await wait(50);
|
|
if (win.webRtcp5AxisSimulation.getState().runState !== "paused") {
|
|
throw new Error("PAUSE action did not update state");
|
|
}
|
|
doc.querySelector('[data-action="RUN"]').click();
|
|
await wait(50);
|
|
if (!win.webRtcp5AxisSimulation.getState().operatorMessage.includes("resume paused program")) {
|
|
throw new Error("RUN should be blocked while program is paused");
|
|
}
|
|
doc.querySelector('[data-action="RESUME"]').click();
|
|
await wait(50);
|
|
if (win.webRtcp5AxisSimulation.getState().runState !== "running") {
|
|
throw new Error("RESUME action did not restore running state");
|
|
}
|
|
const tcpButton = doc.querySelector('[data-action="kins-tcp"]');
|
|
tcpButton.click();
|
|
await wait(50);
|
|
const rtcpState = win.webRtcp5AxisSimulation.getState();
|
|
if (rtcpState.rtcpState !== "on" || rtcpState.kinsType !== "tcp-xyzac") {
|
|
throw new Error(`TCP mode did not enable RTCP: ${rtcpState.rtcpState}/${rtcpState.kinsType}`);
|
|
}
|
|
if (!doc.querySelector('[data-rtcp-value="state"]')?.textContent.includes("RTCP on")) {
|
|
throw new Error("RTCP DRO strip did not render enabled state");
|
|
}
|
|
if (!doc.querySelector('[data-rtcp-diagnostic="frame"]')?.textContent.includes("on")) {
|
|
throw new Error("RTCP diagnostics did not render enabled frame");
|
|
}
|
|
if (canvas.dataset.threeRtcpState !== "on") {
|
|
throw new Error("Three.js preview did not consume RTCP enabled frame");
|
|
}
|
|
if (doc.querySelector('[data-rtcp-diagnostic="kinematics-ready"]')?.textContent !== "ready") {
|
|
throw new Error("RTCP diagnostics must show LinuxCNC kinematics ready");
|
|
}
|
|
if (doc.querySelector('[data-rtcp-diagnostic="execution-context"]')?.textContent !== "worker") {
|
|
throw new Error("RTCP diagnostics must show worker kinematics context");
|
|
}
|
|
if (!doc.querySelector('[data-linuxcnc-boundary="adapter"]')?.textContent.includes("linuxcnc-boundary-adapter")) {
|
|
throw new Error("missing LinuxCNC boundary adapter diagnostic");
|
|
}
|
|
if (!doc.querySelector('[data-linuxcnc-boundary="panel"]')?.textContent.includes("xyzac-trt-switchkins-pyvcp")) {
|
|
throw new Error("missing PyVCP panel schema diagnostic");
|
|
}
|
|
if (!doc.querySelector('[data-linuxcnc-boundary="profile-summary"]')?.textContent.includes("XYZAC / 5 joints / 10 tools")) {
|
|
throw new Error("missing LinuxCNC profile summary diagnostic");
|
|
}
|
|
if (!doc.querySelector('[data-linuxcnc-boundary="readiness"]')?.textContent.includes("ready")) {
|
|
throw new Error("LinuxCNC boundary readiness should show kinematics ready");
|
|
}
|
|
if (!doc.querySelector('[data-linuxcnc-boundary="interpreter"]')?.textContent.includes("linuxcnc_interpreter_wasm_canonical_events")) {
|
|
throw new Error("missing LinuxCNC interpreter boundary diagnostic");
|
|
}
|
|
if (doc.querySelector('[data-linuxcnc-boundary="interpreter-context"]')?.textContent !== "worker") {
|
|
throw new Error("LinuxCNC interpreter diagnostics must show worker context");
|
|
}
|
|
canvas = doc.querySelector("[data-five-axis-canvas]");
|
|
const tcpPoseBeforeStep = canvas.dataset.threeTcpPose;
|
|
win.webRtcp5AxisSimulation.dispatch({ type: "STEP" });
|
|
await win.webRtcp5AxisSimulation.refreshKinematicsFrame({ operatorMessage: "browser smoke refreshed worker frame" });
|
|
await wait(50);
|
|
if (win.webRtcp5AxisSimulation.getState().activeLine <= rtcpState.activeLine) {
|
|
throw new Error("STEP did not advance RTCP frame line");
|
|
}
|
|
canvas = doc.querySelector("[data-five-axis-canvas]");
|
|
if (canvas.dataset.threeTcpPose === tcpPoseBeforeStep) {
|
|
throw new Error("Three.js preview did not update TCP pose after STEP");
|
|
}
|
|
assertCanvasNonblank(canvas, "updated Three.js preview");
|
|
doc.querySelector('[data-action="STOP"]').click();
|
|
await wait(50);
|
|
if (win.webRtcp5AxisSimulation.getState().machine.interpState !== "idle") {
|
|
throw new Error("STOP did not clear interpreter state");
|
|
}
|
|
doc.querySelector('[data-action="mode-manual"]').click();
|
|
await wait(50);
|
|
assertSidebarModeVisualState(doc, { manualActive: true, autoActive: false });
|
|
doc.querySelector('[data-action="mode-jog"]').click();
|
|
await wait(50);
|
|
const xBeforeJog = win.webRtcp5AxisSimulation.getState().axisPose.x;
|
|
doc.querySelector('[data-action="JOG_X_POS"]').click();
|
|
await wait(50);
|
|
if (win.webRtcp5AxisSimulation.getState().axisPose.x <= xBeforeJog) {
|
|
throw new Error("JOG X+ did not move the axis");
|
|
}
|
|
await win.webRtcp5AxisSimulation.restoreSession();
|
|
await wait(50);
|
|
const restoredState = win.webRtcp5AxisSimulation.getState();
|
|
if (restoredState.axisPose.x !== xBeforeJog) {
|
|
throw new Error("restoreSession did not restore saved axis pose");
|
|
}
|
|
if (restoredState.programExecutionSourceMode !== "linuxcnc-interpreter-wasm") {
|
|
throw new Error("restoreSession did not restore LinuxCNC interpreter execution source");
|
|
}
|
|
if (!doc.querySelector('[data-session-persistence="status"]')?.textContent.includes("restored")) {
|
|
throw new Error("session restore status did not render");
|
|
}
|
|
if (!doc.querySelector('[data-session-persistence="status"]')?.textContent.includes(savedSession.storageMode)) {
|
|
throw new Error("session restore status did not preserve storage mode");
|
|
}
|
|
doc.querySelector('[data-action="SAVE_SESSION"]').click();
|
|
await wait(120);
|
|
const uiSavedSession = win.webRtcp5AxisSimulation.getState().sessionPersistence;
|
|
if (uiSavedSession.status !== "saved" || !["opfs", "memory-fallback"].includes(uiSavedSession.storageMode)) {
|
|
throw new Error(`UI Save Session did not use a supported storage mode: ${JSON.stringify(uiSavedSession)}`);
|
|
}
|
|
doc.querySelector('[data-action="JOG_X_POS"]').click();
|
|
await wait(50);
|
|
doc.querySelector('[data-action="RESTORE_SESSION"]').click();
|
|
await wait(180);
|
|
const uiRestoredSession = win.webRtcp5AxisSimulation.getState().sessionPersistence;
|
|
if (uiRestoredSession.status !== "restored" || uiRestoredSession.storageMode !== uiSavedSession.storageMode) {
|
|
throw new Error(`UI Restore Session did not restore from default storage fallback: ${JSON.stringify(uiRestoredSession)}`);
|
|
}
|
|
if (uiRestoredSession.storageMode !== "memory-fallback") {
|
|
throw new Error(`UI Restore Session did not preserve memory fallback mode: ${JSON.stringify(uiRestoredSession)}`);
|
|
}
|
|
if (win.webRtcp5AxisSimulation.getState().machine.allHomed !== true) {
|
|
doc.querySelector('[data-action="mode-manual"]').click();
|
|
await wait(50);
|
|
doc.querySelector('[data-action="HOME"]').click();
|
|
await wait(50);
|
|
}
|
|
doc.querySelector('[data-action="mode-mdi"]').click();
|
|
const mdiInput = doc.querySelector('[data-action="mdi-command"]');
|
|
if (!mdiInput) {
|
|
throw new Error("missing MDI command input");
|
|
}
|
|
mdiInput.value = "G90 X12.5 Y-4 Z1.25 F900";
|
|
doc.querySelector('[data-action="mdi-submit"]').click();
|
|
await wait(50);
|
|
let mdiState = win.webRtcp5AxisSimulation.getState();
|
|
if (mdiState.runState !== "mdi") {
|
|
throw new Error("MDI action did not update run state");
|
|
}
|
|
if (mdiState.axisPose.x !== 12.5 || mdiState.axisPose.y !== -4 || mdiState.axisPose.z !== 1.25) {
|
|
throw new Error(`MDI input did not move axes: ${JSON.stringify(mdiState.axisPose)}`);
|
|
}
|
|
if (!doc.querySelector(".mdi-history")?.textContent.includes("G90 X12.5")) {
|
|
throw new Error("MDI history did not render executed command");
|
|
}
|
|
doc.querySelector('[data-action="MDI_RUN"]').click();
|
|
await wait(50);
|
|
mdiState = win.webRtcp5AxisSimulation.getState();
|
|
if (mdiState.runState !== "mdi" || mdiState.machine.mdiCommand !== "G90 X12.5 Y-4 Z1.25 F900") {
|
|
throw new Error("bottom MDI button did not execute staged command");
|
|
}
|
|
doc.querySelector('[data-action="reset"]').click();
|
|
await wait(50);
|
|
if (win.webRtcp5AxisSimulation.getState().runState !== "idle") {
|
|
throw new Error("RESET did not return to idle");
|
|
}
|
|
doc.querySelector('[data-action="power"]').click();
|
|
await wait(50);
|
|
if (win.webRtcp5AxisSimulation.getState().machine.powerOn !== true) {
|
|
throw new Error("POWER did not re-enable machine before override tests");
|
|
}
|
|
doc.querySelector('[data-action="mode-manual"]').click();
|
|
await wait(50);
|
|
if (win.webRtcp5AxisSimulation.getState().machine.mode !== "manual") {
|
|
throw new Error("MANUAL mode did not re-enable before HAL jog tests");
|
|
}
|
|
doc.querySelector('[data-action="feed-override-down"]').click();
|
|
await wait(50);
|
|
if (win.webRtcp5AxisSimulation.getState().feed.feedOverride !== 90) {
|
|
throw new Error("feed override button did not update state");
|
|
}
|
|
if (doc.querySelector('[data-value="feed-override"]')?.textContent.trim() !== "90 %") {
|
|
throw new Error("feed override DOM did not update");
|
|
}
|
|
doc.querySelector('[data-action="feed-override-reset"]').click();
|
|
await wait(50);
|
|
if (win.webRtcp5AxisSimulation.getState().feed.feedOverride !== 100) {
|
|
throw new Error("feed override reset button did not update state");
|
|
}
|
|
doc.querySelector('[data-action="rapid-override-up"]').click();
|
|
await wait(50);
|
|
if (win.webRtcp5AxisSimulation.getState().feed.rapidOverride !== 110) {
|
|
throw new Error("rapid override button did not update state");
|
|
}
|
|
doc.querySelector('[data-action="spindle-override-up"]').click();
|
|
await wait(50);
|
|
if (win.webRtcp5AxisSimulation.getState().spindle.override !== 110) {
|
|
throw new Error("spindle override button did not update state");
|
|
}
|
|
doc.querySelector('[data-action="spindle-override-reset"]').click();
|
|
await wait(50);
|
|
if (win.webRtcp5AxisSimulation.getState().spindle.override !== 100) {
|
|
throw new Error("spindle override reset button did not update state");
|
|
}
|
|
doc.querySelector('[data-action="ignore-limits"]').click();
|
|
doc.querySelector('[data-action="block-delete"]').click();
|
|
doc.querySelector('[data-action="optional-stop"]').click();
|
|
await wait(50);
|
|
const guiState = win.webRtcp5AxisSimulation.getState().gmoccapyGui;
|
|
if (guiState.ignoreLimits !== true || guiState.optionalBlocks !== true || guiState.optionalStop !== true) {
|
|
throw new Error(`gmoccapy HAL input buttons did not update state: ${JSON.stringify(guiState)}`);
|
|
}
|
|
if (!doc.querySelector('[data-value="gmoccapy-hal-last"]')?.textContent.includes("optional stop set from Web control")) {
|
|
throw new Error("gmoccapy HAL last input DOM did not update");
|
|
}
|
|
win.webRtcp5AxisSimulation.dispatch({
|
|
type: "GMOCAPY_HAL_PIN",
|
|
pin: "gmoccapy.optional-stop",
|
|
value: false,
|
|
});
|
|
win.webRtcp5AxisSimulation.dispatch({
|
|
type: "GMOCAPY_HAL_PIN",
|
|
pin: "gmoccapy.blockdelete",
|
|
value: false,
|
|
});
|
|
await wait(50);
|
|
const crossedGuiState = win.webRtcp5AxisSimulation.getState().gmoccapyGui;
|
|
if (crossedGuiState.optionalBlocks !== false || crossedGuiState.optionalStop !== false) {
|
|
throw new Error(`gmoccapy HAL crossed optional/blockdelete pins did not update state: ${JSON.stringify(crossedGuiState)}`);
|
|
}
|
|
win.webRtcp5AxisSimulation.dispatch({
|
|
type: "GMOCAPY_HAL_PIN",
|
|
pin: "gmoccapy.jog.jog-inc-2",
|
|
value: false,
|
|
});
|
|
await wait(25);
|
|
if (win.webRtcp5AxisSimulation.getState().machine.jogIncrement !== 1) {
|
|
throw new Error("gmoccapy HAL jog increment falling edge should not change Web machine jog increment");
|
|
}
|
|
win.webRtcp5AxisSimulation.dispatch({
|
|
type: "GMOCAPY_HAL_PIN",
|
|
pin: "gmoccapy.jog.jog-inc-2",
|
|
value: true,
|
|
});
|
|
await wait(25);
|
|
let halJogGuiState = win.webRtcp5AxisSimulation.getState().gmoccapyGui;
|
|
if (win.webRtcp5AxisSimulation.getState().machine.jogIncrement !== 0.1 || halJogGuiState.jogIncrementOutput !== 0.1) {
|
|
throw new Error(`gmoccapy HAL jog-inc pin did not select 0.100 mm: ${JSON.stringify(halJogGuiState)}`);
|
|
}
|
|
win.webRtcp5AxisSimulation.dispatch({
|
|
type: "GMOCAPY_HAL_PIN",
|
|
pin: "gmoccapy.jog.axis.jog-x-plus",
|
|
value: true,
|
|
});
|
|
await wait(50);
|
|
if (
|
|
win.webRtcp5AxisSimulation.getState().runState !== "jogging" ||
|
|
win.webRtcp5AxisSimulation.getState().gmoccapyGui.activeJogPin !== "gmoccapy.jog.axis.jog-x-plus"
|
|
) {
|
|
throw new Error(`gmoccapy HAL jog axis pin did not enter jogging state: ${JSON.stringify(win.webRtcp5AxisSimulation.getState().gmoccapyGui)}`);
|
|
}
|
|
win.webRtcp5AxisSimulation.dispatch({
|
|
type: "GMOCAPY_HAL_PIN",
|
|
pin: "gmoccapy.jog.axis.jog-x-plus",
|
|
value: false,
|
|
});
|
|
await wait(50);
|
|
if (
|
|
win.webRtcp5AxisSimulation.getState().runState !== "idle" ||
|
|
win.webRtcp5AxisSimulation.getState().gmoccapyGui.activeJogPin !== null
|
|
) {
|
|
throw new Error("gmoccapy HAL jog release did not clear active jog pin");
|
|
}
|
|
win.webRtcp5AxisSimulation.dispatch({
|
|
type: "GMOCAPY_HAL_PIN",
|
|
pin: "gmoccapy.jog.turtle-jog",
|
|
value: true,
|
|
});
|
|
await wait(25);
|
|
if (win.webRtcp5AxisSimulation.getState().gmoccapyGui.turtleJog !== true) {
|
|
throw new Error("gmoccapy HAL turtle-jog pin did not update state");
|
|
}
|
|
win.webRtcp5AxisSimulation.dispatch({
|
|
type: "GMOCAPY_HAL_PIN",
|
|
pin: "gmoccapy.delete-message",
|
|
value: false,
|
|
});
|
|
await wait(25);
|
|
if (win.webRtcp5AxisSimulation.getState().gmoccapyGui.deletedMessageCount !== 0) {
|
|
throw new Error("gmoccapy HAL delete-message falling edge should be ignored");
|
|
}
|
|
win.webRtcp5AxisSimulation.dispatch({
|
|
type: "GMOCAPY_HAL_PIN",
|
|
pin: "gmoccapy.delete-message",
|
|
value: true,
|
|
});
|
|
win.webRtcp5AxisSimulation.dispatch({
|
|
type: "GMOCAPY_HAL_PIN",
|
|
pin: "gmoccapy.warning-confirm",
|
|
value: true,
|
|
});
|
|
await wait(50);
|
|
halJogGuiState = win.webRtcp5AxisSimulation.getState().gmoccapyGui;
|
|
if (halJogGuiState.deletedMessageCount !== 1 || halJogGuiState.warningConfirm !== true) {
|
|
throw new Error(`gmoccapy HAL message pins did not update state: ${JSON.stringify(halJogGuiState)}`);
|
|
}
|
|
win.webRtcp5AxisSimulation.dispatch({
|
|
type: "GMOCAPY_HAL_PIN",
|
|
pin: "gmoccapy.unlock-settings",
|
|
value: false,
|
|
});
|
|
await wait(25);
|
|
if (win.webRtcp5AxisSimulation.getState().gmoccapyGui.setupSensitive !== true) {
|
|
throw new Error("gmoccapy HAL unlock-settings should be ignored while unlock_way is use");
|
|
}
|
|
win.webRtcp5AxisSimulation.dispatch({
|
|
type: "GMOCAPY_HAL_PIN",
|
|
pin: "gmoccapy.unlock-settings",
|
|
value: false,
|
|
halUnlockMode: true,
|
|
});
|
|
await wait(25);
|
|
if (win.webRtcp5AxisSimulation.getState().gmoccapyGui.setupSensitive !== false) {
|
|
throw new Error("gmoccapy HAL unlock-settings low did not lock setup in HAL unlock mode");
|
|
}
|
|
win.webRtcp5AxisSimulation.dispatch({
|
|
type: "GMOCAPY_HAL_PIN",
|
|
pin: "gmoccapy.unlock-settings",
|
|
value: true,
|
|
});
|
|
win.webRtcp5AxisSimulation.dispatch({
|
|
type: "GMOCAPY_HAL_PIN",
|
|
pin: "gmoccapy.blockheight",
|
|
value: 12.5,
|
|
});
|
|
await wait(50);
|
|
const toolUserState = win.webRtcp5AxisSimulation.getState().gmoccapyGui;
|
|
if (toolUserState.setupSensitive !== true || toolUserState.blockHeight !== 12.5) {
|
|
throw new Error(`gmoccapy HAL settings/tool pins did not update state: ${JSON.stringify(toolUserState)}`);
|
|
}
|
|
win.webRtcp5AxisSimulation.dispatch({
|
|
type: "GMOCAPY_HAL_PIN",
|
|
pin: "gmoccapy.messages.test",
|
|
value: true,
|
|
});
|
|
await wait(25);
|
|
if (!win.webRtcp5AxisSimulation.getState().operatorMessage.includes("no MESSAGE_*")) {
|
|
throw new Error("gmoccapy HAL user message pin should report no MESSAGE_* entries");
|
|
}
|
|
const updatedJogDiagnostic = doc.querySelector('[data-gmoccapy-hal="jog-inputs"]')?.textContent || "";
|
|
const updatedMessageDiagnostic = doc.querySelector('[data-gmoccapy-hal="message-inputs"]')?.textContent || "";
|
|
const updatedSettingsDiagnostic = doc.querySelector('[data-gmoccapy-hal="settings-inputs"]')?.textContent || "";
|
|
const updatedToolUserDiagnostic = doc.querySelector('[data-gmoccapy-hal="tool-user-inputs"]')?.textContent || "";
|
|
if (!updatedJogDiagnostic.includes("turtle level-driven") || !updatedJogDiagnostic.includes("increment 0.100 mm=0.1")) {
|
|
throw new Error(`gmoccapy HAL jog diagnostic did not refresh: ${updatedJogDiagnostic}`);
|
|
}
|
|
if (!updatedMessageDiagnostic.includes("deleted 1") || !updatedMessageDiagnostic.includes("confirm on")) {
|
|
throw new Error(`gmoccapy HAL message diagnostic did not refresh: ${updatedMessageDiagnostic}`);
|
|
}
|
|
if (!updatedSettingsDiagnostic.includes("unlock-way hal") || !updatedSettingsDiagnostic.includes("pin high")) {
|
|
throw new Error(`gmoccapy HAL settings diagnostic did not refresh: ${updatedSettingsDiagnostic}`);
|
|
}
|
|
if (!updatedToolUserDiagnostic.includes("blockheight 12.5") || !updatedToolUserDiagnostic.includes("no MESSAGE_*")) {
|
|
throw new Error(`gmoccapy HAL tool/user diagnostic did not refresh: ${updatedToolUserDiagnostic}`);
|
|
}
|
|
win.webRtcp5AxisSimulation.dispatch({
|
|
type: "GMOCAPY_HARDWARE_BUTTON",
|
|
pin: "gmoccapy.v-button.button-6",
|
|
value: true,
|
|
});
|
|
await wait(25);
|
|
if (
|
|
win.webRtcp5AxisSimulation.getState().gmoccapyGui.activeNativePage !== "setup" ||
|
|
!win.webRtcp5AxisSimulation.getState().operatorMessage.includes("diagnostic-only: setup")
|
|
) {
|
|
throw new Error("gmoccapy native setup page diagnostic did not update from hard-button");
|
|
}
|
|
win.webRtcp5AxisSimulation.dispatch({
|
|
type: "GMOCAPY_PAGE_ACTION",
|
|
pageId: "file-load",
|
|
actionId: "open",
|
|
});
|
|
win.webRtcp5AxisSimulation.dispatch({
|
|
type: "GMOCAPY_TOOL_EDITOR_ACTION",
|
|
actionId: "save",
|
|
});
|
|
await wait(50);
|
|
const nativePageState = win.webRtcp5AxisSimulation.getState().gmoccapyGui;
|
|
if (nativePageState.filePageStatus !== "open" || nativePageState.toolEditorStatus !== "writeback-blocked") {
|
|
throw new Error(`gmoccapy native page actions did not update state: ${JSON.stringify(nativePageState)}`);
|
|
}
|
|
win.webRtcp5AxisSimulation.dispatch({ type: "TOGGLE_POWER" });
|
|
win.webRtcp5AxisSimulation.dispatch({ type: "HOME" });
|
|
win.webRtcp5AxisSimulation.dispatch({ type: "SET_MODE", mode: "mdi" });
|
|
win.webRtcp5AxisSimulation.dispatch({
|
|
type: "GMOCAPY_RUN_MACRO",
|
|
name: "increment",
|
|
args: { xinc: 1.25, yinc: 2.5 },
|
|
});
|
|
await wait(50);
|
|
const macroDiagnostic = doc.querySelector('[data-gmoccapy-page="macros"]')?.textContent || "";
|
|
if (
|
|
win.webRtcp5AxisSimulation.getState().gmoccapyGui.macroLastCommand !== "O<increment> call [1.25] [2.5]" ||
|
|
!macroDiagnostic.includes("O<increment> call [1.25] [2.5]")
|
|
) {
|
|
throw new Error(`gmoccapy macro dispatch did not update diagnostics: ${macroDiagnostic}`);
|
|
}
|
|
doc.querySelector('[data-action="toggle-flood"]').click();
|
|
doc.querySelector('[data-action="toggle-mist"]').click();
|
|
await wait(50);
|
|
const coolantState = win.webRtcp5AxisSimulation.getState().coolant;
|
|
if (coolantState.flood !== true || coolantState.mist !== true) {
|
|
throw new Error("coolant buttons did not update state");
|
|
}
|
|
doc.querySelector('[data-action="view-x"]').click();
|
|
await wait(50);
|
|
if (win.webRtcp5AxisSimulation.getState().preview.selectedView !== "x") {
|
|
throw new Error("preview view button did not update state");
|
|
}
|
|
canvas = doc.querySelector("[data-five-axis-canvas]");
|
|
canvas.dispatchEvent(new WheelEvent("wheel", { deltaY: -120, bubbles: true, cancelable: true }));
|
|
canvas.dispatchEvent(new PointerEvent("pointerdown", {
|
|
pointerId: 1,
|
|
clientX: 180,
|
|
clientY: 120,
|
|
button: 0,
|
|
bubbles: true,
|
|
}));
|
|
canvas.dispatchEvent(new PointerEvent("pointermove", {
|
|
pointerId: 1,
|
|
clientX: 220,
|
|
clientY: 135,
|
|
button: 0,
|
|
bubbles: true,
|
|
}));
|
|
canvas.dispatchEvent(new PointerEvent("pointerup", {
|
|
pointerId: 1,
|
|
clientX: 220,
|
|
clientY: 135,
|
|
button: 0,
|
|
bubbles: true,
|
|
}));
|
|
await wait(50);
|
|
if (canvas.dataset.threeCameraControls !== "orbit-pan-zoom") {
|
|
throw new Error("Three.js preview did not expose orbit/pan/zoom controls");
|
|
}
|
|
assertCanvasNonblank(canvas, "interactive Three.js preview");
|
|
doc.querySelector('[data-action="clear-preview"]').click();
|
|
await wait(50);
|
|
if (win.webRtcp5AxisSimulation.getState().preview.pathPoints !== 0) {
|
|
throw new Error("clear preview button did not update path points");
|
|
}
|
|
canvas = doc.querySelector("[data-five-axis-canvas]");
|
|
if (Number(canvas.dataset.threePathPoints ?? -1) !== 0) {
|
|
throw new Error(`Three.js preview did not clear toolpath points: ${JSON.stringify(canvas.dataset)}`);
|
|
}
|
|
if (Number(canvas.dataset.threeExecutedPathPoints ?? -1) !== 0) {
|
|
throw new Error(`Three.js preview did not clear executed toolpath points: ${JSON.stringify(canvas.dataset)}`);
|
|
}
|
|
doc.querySelector('[data-action="RELOAD"]').click();
|
|
await wait(50);
|
|
if (win.webRtcp5AxisSimulation.getState().preview.pathPoints !== 8) {
|
|
throw new Error("reload button did not restore path points");
|
|
}
|
|
doc.querySelector('[data-action="FULL"]').click();
|
|
await wait(50);
|
|
if (win.webRtcp5AxisSimulation.getState().preview.fullscreen !== true) {
|
|
throw new Error("fullscreen button did not update state");
|
|
}
|
|
doc.querySelector('[data-action="mode-manual"]').click();
|
|
await wait(50);
|
|
doc.querySelector('[data-action="HOME"]').click();
|
|
await wait(50);
|
|
const homedState = win.webRtcp5AxisSimulation.getState();
|
|
if (homedState.axisPose.x !== 43) {
|
|
throw new Error("home button did not restore fixture origin");
|
|
}
|
|
doc.querySelector('[data-action="estop"]').click();
|
|
await wait(50);
|
|
if (win.webRtcp5AxisSimulation.getState().runState !== "estopped") {
|
|
throw new Error("E-STOP did not update run state");
|
|
}
|
|
if (runtimeErrors.length > 0) {
|
|
throw new Error(`public HTTP fallback smoke saw uncaught runtime errors: ${runtimeErrors.join(" | ")}`);
|
|
}
|
|
|
|
result.textContent = "gmoccapy_shell_smoke=ok";
|
|
}
|
|
|
|
runSmoke().catch((error) => {
|
|
result.textContent = `gmoccapy_shell_smoke=fail ${error.message}`;
|
|
});
|
|
|
|
function assertCanvasNonblank(canvas, context) {
|
|
const stats = canvasPixelStats(canvas, context);
|
|
if (stats.nonBlackRatio <= 0.02) {
|
|
throw new Error(`${context}: non-black pixel ratio too low ${JSON.stringify(stats)}`);
|
|
}
|
|
if (stats.averageLuminance <= 5) {
|
|
throw new Error(`${context}: average luminance too low ${JSON.stringify(stats)}`);
|
|
}
|
|
}
|
|
|
|
function canvasPixelStats(canvas, context) {
|
|
const gl = canvas.getContext("webgl2") || canvas.getContext("webgl");
|
|
if (!gl) {
|
|
throw new Error(`${context}: missing WebGL context`);
|
|
}
|
|
const width = Math.max(canvas.width || 0, 1);
|
|
const height = Math.max(canvas.height || 0, 1);
|
|
const pixels = new Uint8Array(width * height * 4);
|
|
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
|
|
let nonBlack = 0;
|
|
let luminance = 0;
|
|
for (let index = 0; index < pixels.length; index += 4) {
|
|
const luma = pixels[index] * 0.2126 + pixels[index + 1] * 0.7152 + pixels[index + 2] * 0.0722;
|
|
luminance += luma;
|
|
if (luma > 8) nonBlack += 1;
|
|
}
|
|
const total = width * height;
|
|
return {
|
|
width,
|
|
height,
|
|
nonBlackRatio: nonBlack / total,
|
|
averageLuminance: luminance / total,
|
|
};
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|