Files
cnc_wams/wasm-port/tests/browser/real_simulation_page_smoke.html

1218 lines
63 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>LinuxCNC Real Simulation Page Smoke</title>
</head>
<body>
<pre id="status">running</pre>
<pre id="axis-diagnostics-artifact" hidden>{}</pre>
<script type="module">
import {
gcodeProgramPath,
machineFilePaths,
saveGcodeProgram,
saveMachineSessionSnapshot,
saveMachineTextFiles,
} from "../../runtime/sdk/src/index.js";
const status = document.getElementById("status");
async function loadFrame(src) {
const frame = document.createElement("iframe");
frame.src = src;
frame.width = "1280";
frame.height = "800";
document.body.appendChild(frame);
await new Promise((resolve, reject) => {
frame.addEventListener("load", resolve, { once: true });
frame.addEventListener("error", reject, { once: true });
});
return frame;
}
async function waitForState(frame) {
for (let i = 0; i < 200; i += 1) {
const state = frame.contentWindow?.linuxCncRealSimulationState;
if (state?.summary?.ready) {
return state;
}
await new Promise((resolve) => setTimeout(resolve, 50));
}
throw new Error("real simulation page did not expose ready state");
}
function assertRenderedState(doc, state) {
const polyline = doc.querySelector("[data-toolpath-polyline]");
const threeCanvas = doc.querySelector("[data-toolpath-three]");
const rows = [...doc.querySelectorAll("[data-motion-row]")];
const programRows = [...doc.querySelectorAll("[data-program-line]")];
const canonicalOutput = doc.querySelector("[data-canonical-output]")?.textContent ?? "";
if (doc.body.dataset.simulationReady !== "true") {
throw new Error(`simulation body readiness flag not true for ${state.program?.id}`);
}
if (state.apiName !== "real-browser-simulation-state") {
throw new Error("simulation state API name drift");
}
if (state.summary.motionEventCount < 2) {
throw new Error(`simulation motion event count too low for ${state.program?.id}`);
}
if (!canonicalOutput.includes("canon_event=")) {
throw new Error(`simulation canonical output missing events for ${state.program?.id}`);
}
if (!polyline?.getAttribute("points")) {
throw new Error(`simulation toolpath polyline missing points for ${state.program?.id}`);
}
if (rows.length !== state.motion.length) {
throw new Error(`simulation motion table row count drift for ${state.program?.id}`);
}
if (programRows.length !== state.summary.programLineCount) {
throw new Error(`simulation program row count drift for ${state.program?.id}`);
}
if (!doc.querySelector("[data-toolpath-svg]")?.getAttribute("viewBox")) {
throw new Error(`simulation SVG viewBox missing for ${state.program?.id}`);
}
if (
threeCanvas?.dataset.threeReady !== "true" ||
threeCanvas?.dataset.threeRevision !== "183" ||
Number(threeCanvas?.dataset.threePathPoints ?? 0) !== state.motion.length ||
Number(threeCanvas?.dataset.threeGridLines ?? 0) < 4 ||
Number(threeCanvas?.dataset.threeAxisLines ?? 0) !== 3 ||
threeCanvas?.dataset.threeAxisLabels !== "3" ||
threeCanvas?.dataset.threeOrientationTriadObjects !== "6" ||
threeCanvas?.dataset.threeWorkPlanes !== "1" ||
threeCanvas?.dataset.threeEnvelopeEdges !== "12" ||
Number(threeCanvas?.dataset.threeSceneObjects ?? 0) < 20 ||
Number(threeCanvas?.dataset.threeToolMarkerObjects ?? 0) < 3 ||
Number(threeCanvas?.dataset.threeScaleBarObjects ?? 0) !== 4 ||
!threeCanvas?.dataset.threeViewBox ||
!threeCanvas?.dataset.threeMachineEnvelope ||
!threeCanvas?.dataset.threeToolhead ||
!threeCanvas?.dataset.threeToolGeometry ||
!threeCanvas?.dataset.threeScaleBar ||
!threeCanvas?.dataset.threeOrientationTriad ||
!threeCanvas?.dataset.threeCanvasSize
) {
throw new Error(`simulation Three.js preview missing rendered path for ${state.program?.id}`);
}
const toolhead = JSON.parse(threeCanvas.dataset.threeToolhead);
const envelope = JSON.parse(threeCanvas.dataset.threeMachineEnvelope);
const toolGeometry = JSON.parse(threeCanvas.dataset.threeToolGeometry);
const scaleBar = JSON.parse(threeCanvas.dataset.threeScaleBar);
const orientationTriad = JSON.parse(threeCanvas.dataset.threeOrientationTriad);
if (
!Number.isFinite(toolhead.x) ||
!Number.isFinite(toolhead.y) ||
!Number.isFinite(envelope.minZ) ||
envelope.maxZ <= envelope.minZ ||
!Number.isFinite(toolGeometry.bodyLength) ||
toolGeometry.bodyLength <= 0 ||
!Number.isFinite(scaleBar.length) ||
scaleBar.length <= 0 ||
!Number.isFinite(orientationTriad.length) ||
orientationTriad.length <= 0
) {
throw new Error(`simulation Three.js preview missing structured toolhead/envelope state for ${state.program?.id}`);
}
const gl = threeCanvas.getContext("webgl2") || threeCanvas.getContext("webgl");
if (!gl) {
throw new Error("simulation Three.js preview missing WebGL context");
}
const pixel = new Uint8Array(4);
gl.readPixels(Math.floor(threeCanvas.width / 2), Math.floor(threeCanvas.height / 2), 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixel);
if (pixel[0] === 0 && pixel[1] === 0 && pixel[2] === 0 && pixel[3] === 0) {
throw new Error("simulation Three.js preview canvas pixel check was blank");
}
if (doc.body.dataset.simulationProgramId !== state.program?.id) {
throw new Error(`simulation body program id drift for ${state.program?.id}`);
}
}
function assertAxisShell(doc) {
for (const region of [
"titlebar",
"menubar",
"toolbar",
"toolbar-file-controls",
"workspace",
"manual-mdi",
"mdi-history",
"preview-dro",
"preview-controls",
"preview-stage",
"preview-hud",
"preview-legend",
"active-codes",
"gcode-pane",
"program-editor",
"recent-programs",
"machine-state",
"diagnostics",
"tool-table",
"limits-home",
"status-history",
"statusbar",
]) {
if (!doc.querySelector(`[data-axis-shell="${region}"]`)) {
throw new Error(`AXIS-style shell missing ${region}`);
}
}
for (const tab of ["manual", "mdi", "preview", "dro"]) {
if (!doc.querySelector(`[data-axis-tab="${tab}"]`)) {
throw new Error(`AXIS-style shell missing ${tab} tab`);
}
if (!doc.querySelector(`[data-axis-panel="${tab}"]`)) {
throw new Error(`AXIS-style shell missing ${tab} panel`);
}
}
const menuText = doc.querySelector('[data-axis-shell="menubar"]')?.textContent ?? "";
for (const label of ["File", "Machine", "View", "Help"]) {
if (!menuText.includes(label)) {
throw new Error(`AXIS-style menu missing ${label}`);
}
}
const statusText = doc.querySelector('[data-axis-shell="statusbar"]')?.textContent ?? "";
if (
!statusText.includes("ESTOP") ||
!statusText.includes("Tool") ||
!statusText.includes("Program") ||
!statusText.includes("Line") ||
!statusText.includes("Position: Relative Actual") ||
!statusText.includes("Preview")
) {
throw new Error(`AXIS-style statusbar drift: ${statusText}`);
}
}
try {
const frame = await loadFrame("../../runtime/ui/simulation/index.html");
const doc = frame.contentDocument;
const state = await waitForState(frame);
const canonicalOutput = doc.querySelector("[data-canonical-output]")?.textContent ?? "";
const api = frame.contentWindow?.linuxCncRealSimulationApi;
assertAxisShell(doc);
assertRenderedState(doc, state);
if (!canonicalOutput.includes("canon_event=STRAIGHT_FEED line=2 x=1 y=0 z=0")) {
throw new Error("simulation canonical output missing LinuxCNC feed event");
}
if (!doc.querySelector("[data-toolpath-polyline]")?.getAttribute("points")?.includes("1,0")) {
throw new Error("simulation toolpath polyline missing expected point");
}
if (doc.querySelector('[data-axis="x"]')?.textContent !== "0.000") {
throw new Error("simulation final X axis drift");
}
if (doc.querySelector('[data-axis="y"]')?.textContent !== "0.000") {
throw new Error("simulation final Y axis drift");
}
if (!api?.getPrograms || !api?.runProgramById || !api?.reloadCurrentProgram) {
throw new Error("simulation API missing program controls");
}
if (!api?.getDroState || !api?.getModalState) {
throw new Error("simulation API missing AXIS DRO/modal controls");
}
if (!api?.fitPreview || !api?.resetPreview || !api?.zoomPreview || !api?.panPreview || !api?.getPreviewState || !api?.setPreviewViewMode || !api?.getPreviewLayerState || !api?.setPreviewLayer || !api?.getPreviewCursorState) {
throw new Error("simulation API missing AXIS preview controls");
}
if (!api?.getOpfsProgramState || !api?.saveProgramToOpfs || !api?.loadProgramFromOpfs) {
throw new Error("simulation API missing AXIS OPFS program persistence controls");
}
if (!api?.getMachineReadiness || !api?.checkMachineSessionReadiness) {
throw new Error("simulation API missing AXIS machine/session readiness controls");
}
if (!api?.getMachineStatusState) {
throw new Error("simulation API missing AXIS machine status controls");
}
if (!api?.getMachineSessionLoadState || !api?.loadReadyMachineSession) {
throw new Error("simulation API missing AXIS machine/session load controls");
}
if (!api?.getRunMode || !api?.getRunControlState || !api?.setUseLoadedSession || !api?.getRunSummary) {
throw new Error("simulation API missing AXIS run-mode controls");
}
if (!api?.getAxisStatusbarState) {
throw new Error("simulation API missing AXIS statusbar controls");
}
if (!api?.getDiagnosticsState || !api?.handleAxisShortcut) {
throw new Error("simulation API missing AXIS diagnostics/shortcut controls");
}
if (!api?.getStatusHistory || !api?.getToolTableSummary) {
throw new Error("simulation API missing AXIS status history/tool table summary controls");
}
if (!api?.getLimitsHomeState || !api?.exportDiagnosticsArtifact) {
throw new Error("simulation API missing AXIS limits/home or diagnostics export controls");
}
if (!api?.applyAxisViewFromUrl) {
throw new Error("simulation API missing AXIS URL view controls");
}
if (!api?.runProgramText || !api?.loadProgramText || !api?.loadProgramFile || !api?.runMdiProgramText) {
throw new Error("simulation API missing real G-code loading controls");
}
if (!api?.getMdiHistory || !api?.clearMdiHistory) {
throw new Error("simulation API missing AXIS MDI history controls");
}
if (!api?.getRecentPrograms || !api?.clearRecentPrograms) {
throw new Error("simulation API missing AXIS recent program controls");
}
if (!api?.getProgramText || !api?.setProgramText || !api?.runEditorProgramText) {
throw new Error("simulation API missing editor text controls");
}
if (!api?.resetPlayback || !api?.stepPlayback || !api?.finishPlayback) {
throw new Error("simulation API missing playback controls");
}
if (!api?.isPlaybackRunning) {
throw new Error("simulation API missing playback running state");
}
const programs = api.getPrograms();
if (programs.length < 5) {
throw new Error("simulation test program inventory too small");
}
const initialRecentPrograms = api.getRecentPrograms();
if (
initialRecentPrograms.length !== 1 ||
initialRecentPrograms[0].source !== "builtin" ||
doc.body.dataset.recentProgramCount !== "1" ||
!doc.querySelector("[data-recent-program-list]")?.textContent.includes(initialRecentPrograms[0].label)
) {
throw new Error(`simulation initial built-in run should populate recent programs: ${JSON.stringify(initialRecentPrograms)}`);
}
api.clearRecentPrograms();
if (api.getRecentPrograms().length !== 0 || doc.body.dataset.recentProgramCount !== "0" || !doc.querySelector("[data-recent-program-list]")?.textContent.includes("n/a")) {
throw new Error(`simulation recent programs clear did not reset API/DOM state: ${JSON.stringify(api.getRecentPrograms())}`);
}
if (!doc.querySelector("[data-open-program]") || !doc.querySelector("[data-open-program-file]")) {
throw new Error("simulation page missing open program controls");
}
if (!doc.querySelector("[data-program-editor]") || !doc.querySelector("[data-run-editor-program]")) {
throw new Error("simulation page missing editable G-code controls");
}
if (!doc.querySelector("[data-recent-program-list]") || !doc.querySelector("[data-clear-recent-programs]")) {
throw new Error("simulation page missing recent program controls");
}
if (!doc.querySelector("[data-opfs-program-filename]") || !doc.querySelector("[data-save-program-opfs]") || !doc.querySelector("[data-load-program-opfs]")) {
throw new Error("simulation page missing OPFS program persistence controls");
}
if (!doc.querySelector('[data-axis-shell="machine-session"]') || !doc.querySelector("[data-check-machine-session]") || !doc.querySelector("[data-load-machine-session]") || !doc.querySelector("[data-use-loaded-session]")) {
throw new Error("simulation page missing machine/session readiness controls");
}
if (!doc.querySelector("[data-axis-machine-control-state]")?.textContent.includes("not bound")) {
throw new Error("simulation page should explicitly mark unbound real-machine controls");
}
if (!doc.querySelector("[data-mdi-program-text]") || !doc.querySelector("[data-load-mdi-program]") || !doc.querySelector("[data-run-mdi-program]") || !doc.querySelector("[data-mdi-history-list]") || !doc.querySelector("[data-clear-mdi-history]")) {
throw new Error("simulation page missing MDI program text controls");
}
if (api.getRunMode().mode !== "standalone" || doc.body.dataset.runMode !== "standalone" || !doc.querySelector('[data-axis-shell="statusbar"]')?.textContent.includes("Standalone")) {
throw new Error("simulation initial run mode should be standalone");
}
if (api.getRunSummary().executionMode !== "linuxcnc-wasm" || doc.querySelector('[data-run-summary="execution-mode"]')?.textContent !== "linuxcnc-wasm") {
throw new Error("simulation initial run summary should report standalone LinuxCNC WASM execution");
}
const initialRunControl = api.getRunControlState();
if (
initialRunControl.phase !== "fallback" ||
doc.body.dataset.runControlPhase !== "fallback" ||
!doc.querySelector("[data-run-control-state]")?.textContent.includes("no loaded machine session")
) {
throw new Error(`simulation initial run control should explain standalone fallback: ${JSON.stringify(initialRunControl)}`);
}
const initialMachineStatus = api.getMachineStatusState();
if (
initialMachineStatus?.source !== "linuxcnc-canonical-events" ||
initialMachineStatus.spindle.state !== "n/a" ||
doc.querySelector('[data-machine-status="spindle-state"]')?.textContent !== "n/a"
) {
throw new Error(`simulation initial machine status should expose explicit unavailable state: ${JSON.stringify(initialMachineStatus)}`);
}
const modalState = api.getModalState();
if (modalState?.source !== "linuxcnc-update-tag" || !modalState.raw?.includes("canon_event=UPDATE_TAG")) {
throw new Error(`simulation modal state should come from LinuxCNC UPDATE_TAG: ${JSON.stringify(modalState)}`);
}
const modalText = doc.querySelector("[data-modal-rows]")?.textContent ?? "";
if (!modalText.includes("Plane: plane=170") || !modalText.includes("Origin: origin=530")) {
throw new Error(`simulation modal display missing LinuxCNC fields: ${modalText}`);
}
const diagnosticsState = api.getDiagnosticsState();
if (
diagnosticsState.lastEvent === "n/a" ||
diagnosticsState.lastError !== "none" ||
!doc.querySelector('[data-diagnostics="last-event"]')?.textContent.includes("canon_event=")
) {
throw new Error(`simulation diagnostics should expose last LinuxCNC event: ${JSON.stringify(diagnosticsState)}`);
}
const initialHistory = api.getStatusHistory();
if (!initialHistory.some(({ kind }) => kind === "run") || !doc.querySelector("[data-status-history]")?.textContent.includes("run:")) {
throw new Error(`simulation status history should record initial run: ${JSON.stringify(initialHistory)}`);
}
const limitsHome = api.getLimitsHomeState();
if (
limitsHome.source !== "unavailable" ||
limitsHome.axes.x.homed !== "n/a" ||
doc.querySelector('[data-limits-home-row="x"]')?.children.length !== 5 ||
!doc.querySelector("[data-limits-home-summary]")?.textContent.includes("source unavailable")
) {
throw new Error(`simulation limits/home state should remain explicit unavailable: ${JSON.stringify(limitsHome)}`);
}
const diagnosticsArtifact = api.exportDiagnosticsArtifact();
if (
diagnosticsArtifact.apiName !== "real-browser-simulation-diagnostics-artifact" ||
diagnosticsArtifact.statusHistory.length === 0 ||
diagnosticsArtifact.limitsHome.source !== "unavailable" ||
diagnosticsArtifact.preview.renderer !== "threejs" ||
diagnosticsArtifact.toolTable.state !== "not-loaded"
) {
throw new Error(`simulation diagnostics artifact initial state drift: ${JSON.stringify(diagnosticsArtifact)}`);
}
const droState = api.getDroState();
if (droState?.source !== "linuxcnc-canonical-motion") {
throw new Error(`simulation DRO source drift: ${JSON.stringify(droState)}`);
}
for (const field of ["distanceToGo", "workOffsetG54", "g92Offset", "toolLengthOffset"]) {
if (droState[field].x !== "n/a") {
throw new Error(`simulation DRO ${field} should remain explicit n/a when unavailable`);
}
}
if (droState.velocity !== "n/a" || doc.querySelector("[data-dro-velocity]")?.textContent !== "n/a") {
throw new Error("simulation DRO velocity should remain explicit n/a when unavailable");
}
const previewExtents = doc.querySelector("[data-preview-extents]");
const previewOrigin = doc.querySelector("[data-preview-origin]");
const extentsLabel = doc.querySelector("[data-preview-extents-label]")?.textContent ?? "";
if (
!previewExtents ||
Number(previewExtents.getAttribute("width")) <= 0 ||
Number(previewExtents.getAttribute("height")) <= 0 ||
previewOrigin?.getAttribute("cx") !== "0" ||
previewOrigin?.getAttribute("cy") !== "0" ||
!extentsLabel.includes("X ") ||
!extentsLabel.includes("Y ")
) {
throw new Error("simulation preview missing AXIS extents/origin overlay");
}
const fitViewBox = doc.querySelector("[data-toolpath-svg]")?.getAttribute("viewBox");
api.zoomPreview(1.5);
const zoomedViewBox = doc.querySelector("[data-toolpath-svg]")?.getAttribute("viewBox");
if (!fitViewBox || zoomedViewBox === fitViewBox || !doc.querySelector("[data-toolpath-polyline]")?.getAttribute("points")) {
throw new Error("simulation preview zoom did not change a populated toolpath viewBox");
}
api.resetPreview();
const resetViewBox = doc.querySelector("[data-toolpath-svg]")?.getAttribute("viewBox");
if (resetViewBox !== fitViewBox || doc.querySelector("[data-preview-zoom]")?.textContent !== "100%") {
throw new Error(`simulation preview reset did not restore fit viewBox: ${resetViewBox} !== ${fitViewBox}`);
}
api.panPreview(0.2, 0);
const pannedViewBox = doc.querySelector("[data-toolpath-svg]")?.getAttribute("viewBox");
if (pannedViewBox === fitViewBox || !doc.querySelector("[data-toolpath-polyline]")?.getAttribute("points")) {
throw new Error("simulation preview pan did not move a populated toolpath viewBox");
}
const pannedPreview = api.getPreviewState();
if (
pannedPreview.renderer !== "threejs" ||
pannedPreview.previewVersion !== 3 ||
pannedPreview.three.revision !== "183" ||
pannedPreview.three.axisLines !== 3 ||
pannedPreview.three.workPlanes !== 1 ||
pannedPreview.three.envelopeEdges !== 12 ||
pannedPreview.three.toolMarkerObjects < 3 ||
pannedPreview.three.scaleBarObjects !== 4 ||
pannedPreview.layers.tool !== true ||
pannedPreview.layers.envelope !== true ||
pannedPreview.layers.scale !== true ||
pannedPreview.three.visibleLayers?.tool !== true ||
pannedPreview.three.pathPoints !== state.motion.length ||
!pannedPreview.three.machineEnvelope ||
!pannedPreview.three.toolhead ||
!pannedPreview.three.toolGeometry ||
!pannedPreview.three.scaleBar ||
!pannedPreview.hud?.tool?.includes("X") ||
!pannedPreview.hud?.envelope?.includes("Z") ||
!pannedPreview.hud?.renderer?.includes("Three.js r183") ||
pannedPreview.pan.x <= 0 ||
!doc.querySelector("[data-preview-pan]")?.textContent.includes("x")
) {
throw new Error(`simulation preview pan state did not update API/DOM: ${JSON.stringify(pannedPreview)}`);
}
const hudText = doc.querySelector('[data-axis-shell="preview-hud"]')?.textContent ?? "";
if (!hudText.includes("Envelope") || !hudText.includes("Renderer") || !hudText.includes("Three.js r183")) {
throw new Error(`simulation preview HUD missing AXIS renderer/envelope state: ${hudText}`);
}
const initialLayers = api.getPreviewLayerState();
if (!initialLayers.traverse || !initialLayers.feed || !initialLayers.arc || !initialLayers.tool || !initialLayers.envelope || !initialLayers.scale) {
throw new Error(`simulation preview layers should default to visible: ${JSON.stringify(initialLayers)}`);
}
const hiddenToolPreview = api.setPreviewLayer("tool", false);
if (
hiddenToolPreview.layers.tool !== false ||
hiddenToolPreview.three.visibleLayers.tool !== false ||
hiddenToolPreview.three.toolMarkerObjects !== 0 ||
hiddenToolPreview.three.toolGeometry?.objectCount !== 0 ||
doc.querySelector('[data-preview-layer="tool"]')?.checked !== false
) {
throw new Error(`simulation preview tool layer did not hide tool geometry: ${JSON.stringify(hiddenToolPreview.three)}`);
}
const hiddenEnvelopePreview = api.setPreviewLayer("envelope", false);
if (
hiddenEnvelopePreview.layers.envelope !== false ||
hiddenEnvelopePreview.three.visibleLayers.envelope !== false ||
hiddenEnvelopePreview.three.envelopeEdges !== 0 ||
doc.querySelector('[data-preview-layer="envelope"]')?.checked !== false
) {
throw new Error(`simulation preview envelope layer did not hide envelope: ${JSON.stringify(hiddenEnvelopePreview.three)}`);
}
const hiddenScalePreview = api.setPreviewLayer("scale", false);
if (
hiddenScalePreview.layers.scale !== false ||
hiddenScalePreview.three.visibleLayers.scale !== false ||
hiddenScalePreview.three.scaleBarObjects !== 0 ||
hiddenScalePreview.three.scaleBar?.length !== 0 ||
doc.querySelector('[data-preview-layer="scale"]')?.checked !== false
) {
throw new Error(`simulation preview scale layer did not hide scale bar: ${JSON.stringify(hiddenScalePreview.three)}`);
}
api.setPreviewLayer("tool", true);
api.setPreviewLayer("envelope", true);
const restoredLayerPreview = api.setPreviewLayer("scale", true);
if (
restoredLayerPreview.layers.tool !== true ||
restoredLayerPreview.layers.envelope !== true ||
restoredLayerPreview.layers.scale !== true ||
restoredLayerPreview.three.toolMarkerObjects < 3 ||
restoredLayerPreview.three.envelopeEdges !== 12 ||
restoredLayerPreview.three.scaleBarObjects !== 4
) {
throw new Error(`simulation preview layers did not restore AXIS default view: ${JSON.stringify(restoredLayerPreview.three)}`);
}
const statusbarState = api.getAxisStatusbarState();
if (
statusbarState.apiName !== "real-browser-simulation-axis-statusbar-state" ||
!statusbarState.preview.includes("Top") ||
doc.querySelector('[data-statusbar="preview"]')?.textContent !== statusbarState.preview ||
doc.body.dataset.axisStatusbarPreview !== statusbarState.preview
) {
throw new Error(`simulation AXIS statusbar state drift: ${JSON.stringify(statusbarState)}`);
}
const reloadedState = await api.reloadCurrentProgram();
if (
reloadedState.apiName !== "real-browser-simulation-state" ||
reloadedState.summary.ready !== true ||
!doc.querySelector("[data-canonical-output]")?.textContent.includes("canon_event=")
) {
throw new Error(`simulation reload current program did not execute through LinuxCNC WASM: ${JSON.stringify(reloadedState.summary)}`);
}
const isoPreview = api.setPreviewViewMode("iso");
if (
isoPreview.viewMode !== "iso" ||
doc.querySelector("[data-toolpath-three]")?.dataset.threeViewMode !== "iso" ||
doc.querySelector("[data-preview-view-label]")?.textContent !== "Iso"
) {
throw new Error(`simulation Three.js preview view mode did not switch to Iso: ${JSON.stringify(isoPreview)}`);
}
const sidePreview = api.setPreviewViewMode("side");
if (sidePreview.viewMode !== "side" || doc.querySelector("[data-preview-view-mode=\"side\"]")?.dataset.active !== "true") {
throw new Error(`simulation Three.js preview view mode did not switch to Side: ${JSON.stringify(sidePreview)}`);
}
api.setPreviewViewMode("top");
api.fitPreview();
const fitPreviewState = api.getPreviewState();
if (doc.querySelector("[data-toolpath-svg]")?.getAttribute("viewBox") !== fitViewBox || fitPreviewState.pan.x !== 0 || fitPreviewState.pan.y !== 0) {
throw new Error("simulation preview fit did not clear pan offset");
}
const threeCanvas = doc.querySelector("[data-toolpath-three]");
const beforeWheelZoom = api.getPreviewState().zoom;
threeCanvas.dispatchEvent(new frame.contentWindow.WheelEvent("wheel", { deltaY: -100, bubbles: true, cancelable: true }));
if (api.getPreviewState().zoom <= beforeWheelZoom) {
throw new Error("simulation Three.js wheel zoom did not update preview state");
}
const dragStart = api.getPreviewState();
const rect = threeCanvas.getBoundingClientRect();
threeCanvas.dispatchEvent(new frame.contentWindow.PointerEvent("pointerdown", {
pointerId: 7,
clientX: rect.left + rect.width / 2,
clientY: rect.top + rect.height / 2,
bubbles: true,
cancelable: true,
}));
threeCanvas.dispatchEvent(new frame.contentWindow.PointerEvent("pointermove", {
pointerId: 7,
clientX: rect.left + rect.width / 2 + 40,
clientY: rect.top + rect.height / 2 + 20,
bubbles: true,
cancelable: true,
}));
threeCanvas.dispatchEvent(new frame.contentWindow.PointerEvent("pointerup", {
pointerId: 7,
clientX: rect.left + rect.width / 2 + 40,
clientY: rect.top + rect.height / 2 + 20,
bubbles: true,
cancelable: true,
}));
const dragEnd = api.getPreviewState();
if (dragEnd.pan.x === dragStart.pan.x && dragEnd.pan.y === dragStart.pan.y) {
throw new Error("simulation Three.js drag pan did not update preview state");
}
const cursorState = api.getPreviewCursorState();
if (
cursorState.apiName !== "real-browser-simulation-preview-cursor-state" ||
cursorState.active !== true ||
!Number.isFinite(cursorState.x) ||
!Number.isFinite(cursorState.y) ||
!cursorState.label.includes("X") ||
doc.querySelector("[data-preview-crosshair]")?.dataset.active !== "true" ||
!doc.querySelector("[data-preview-crosshair-label]")?.textContent.includes("X") ||
!doc.querySelector("[data-preview-hud=\"cursor\"]")?.textContent.includes("X")
) {
throw new Error(`simulation Three.js cursor/crosshair state did not update: ${JSON.stringify(cursorState)}`);
}
threeCanvas.dispatchEvent(new frame.contentWindow.PointerEvent("pointerleave", {
pointerId: 7,
clientX: rect.left + rect.width / 2 + 40,
clientY: rect.top + rect.height / 2 + 20,
bubbles: true,
cancelable: true,
}));
if (api.getPreviewCursorState().active !== false || doc.querySelector("[data-preview-crosshair]")?.dataset.active !== "false") {
throw new Error("simulation Three.js cursor/crosshair state did not clear on pointer leave");
}
threeCanvas.dispatchEvent(new frame.contentWindow.MouseEvent("dblclick", { bubbles: true, cancelable: true }));
const doubleClickFit = api.getPreviewState();
if (doubleClickFit.pan.x !== 0 || doubleClickFit.pan.y !== 0 || doubleClickFit.zoom !== 1) {
throw new Error("simulation Three.js double-click fit did not reset preview state");
}
const initialEditor = api.getProgramText();
if (!initialEditor.text.includes("G1 X1 Y0 Z0 F100")) {
throw new Error("simulation editor did not receive initial built-in program text");
}
doc.querySelector('[data-axis-tab="mdi"]')?.click();
if (!doc.querySelector('[data-axis-panel="manual"]')?.hidden || doc.querySelector('[data-axis-panel="mdi"]')?.hidden) {
throw new Error("AXIS-style Manual/MDI tab switch failed");
}
doc.querySelector('[data-axis-tab="manual"]')?.click();
if (doc.querySelector('[data-axis-panel="manual"]')?.hidden || !doc.querySelector('[data-axis-panel="mdi"]')?.hidden) {
throw new Error("AXIS-style Manual tab restore failed");
}
doc.body.dispatchEvent(new frame.contentWindow.KeyboardEvent("keydown", { key: "F5", bubbles: true, cancelable: true }));
if (doc.querySelector('[data-axis-panel="mdi"]')?.hidden || doc.body.dataset.lastShortcut !== "F5 MDI") {
throw new Error("AXIS-style F5 shortcut did not switch to MDI");
}
doc.body.dispatchEvent(new frame.contentWindow.KeyboardEvent("keydown", { key: "F3", bubbles: true, cancelable: true }));
if (doc.querySelector('[data-axis-panel="manual"]')?.hidden || doc.body.dataset.lastShortcut !== "F3 Manual") {
throw new Error("AXIS-style F3 shortcut did not switch to Manual");
}
doc.querySelector('[data-axis-tab="dro"]')?.click();
if (!doc.querySelector('[data-axis-panel="preview"]')?.hidden || doc.querySelector('[data-axis-panel="dro"]')?.hidden) {
throw new Error("AXIS-style Preview/DRO tab switch failed");
}
if (doc.querySelector('[data-axis-shell="dro-readout"]')?.textContent.includes("undefined")) {
throw new Error("AXIS-style DRO should not render undefined fields");
}
doc.querySelector('[data-axis-tab="preview"]')?.click();
if (doc.querySelector('[data-axis-panel="preview"]')?.hidden || !doc.querySelector('[data-axis-panel="dro"]')?.hidden) {
throw new Error("AXIS-style Preview tab restore failed");
}
doc.body.dispatchEvent(new frame.contentWindow.KeyboardEvent("keydown", { key: "F7", bubbles: true, cancelable: true }));
if (doc.querySelector('[data-axis-panel="dro"]')?.hidden || doc.body.dataset.lastShortcut !== "F7 DRO") {
throw new Error("AXIS-style F7 shortcut did not switch to DRO");
}
doc.body.dispatchEvent(new frame.contentWindow.KeyboardEvent("keydown", { key: "F6", bubbles: true, cancelable: true }));
if (doc.querySelector('[data-axis-panel="preview"]')?.hidden || doc.body.dataset.lastShortcut !== "F6 Preview") {
throw new Error("AXIS-style F6 shortcut did not switch to Preview");
}
const urlView = api.applyAxisViewFromUrl("?axis_left=mdi&axis_right=dro");
if (
urlView.left !== "mdi" ||
urlView.right !== "dro" ||
doc.querySelector('[data-axis-panel="mdi"]')?.hidden ||
doc.querySelector('[data-axis-panel="dro"]')?.hidden ||
doc.body.dataset.axisLeftView !== "mdi" ||
doc.body.dataset.axisRightView !== "dro"
) {
throw new Error(`AXIS-style URL view state did not switch panels: ${JSON.stringify(urlView)}`);
}
api.applyAxisViewFromUrl("?axis_left=manual&axis_right=preview");
api.resetPlayback();
if (doc.body.dataset.playbackIndex !== "0") {
throw new Error("simulation playback did not reset to first frame");
}
const firstHead = doc.querySelector("[data-toolpath-head]");
const firstCx = firstHead?.getAttribute("cx");
const firstCy = firstHead?.getAttribute("cy");
const firstExecutedPoints = doc.querySelector("[data-toolpath-executed-polyline]")?.getAttribute("points") ?? "";
const firstThreeExecutedPoints = doc.querySelector("[data-toolpath-three]")?.dataset.threeExecutedPoints ?? "";
if (!doc.querySelector('[data-program-line="1"]') || doc.querySelector('[data-program-line="2"]')?.dataset.active !== "false") {
throw new Error("simulation reset playback did not render first active program line");
}
const secondFrame = api.stepPlayback(1);
if (secondFrame.index !== 1 || secondFrame.activeLine !== 2) {
throw new Error(`simulation step playback drift: ${JSON.stringify(secondFrame)}`);
}
const secondExecutedPoints = doc.querySelector("[data-toolpath-executed-polyline]")?.getAttribute("points") ?? "";
if (secondExecutedPoints === firstExecutedPoints || !secondExecutedPoints.includes("1,0")) {
throw new Error("simulation executed toolpath did not advance on step");
}
if (doc.querySelector("[data-toolpath-three]")?.dataset.threeExecutedPoints === firstThreeExecutedPoints) {
throw new Error("simulation Three.js executed toolpath did not advance on step");
}
if (firstHead?.getAttribute("cx") === firstCx && firstHead?.getAttribute("cy") === firstCy) {
throw new Error("simulation toolhead did not move on playback step");
}
if (doc.querySelector('[data-program-line="2"]')?.dataset.active !== "true") {
throw new Error("simulation step playback did not highlight active G-code line");
}
if (doc.querySelector('[data-motion-row="1"]')?.dataset.active !== "true") {
throw new Error("simulation step playback did not highlight active motion row");
}
doc.querySelector("[data-playback-next]")?.click();
if (doc.body.dataset.playbackIndex !== "2") {
throw new Error("AXIS-style toolbar step button did not advance playback");
}
doc.body.dispatchEvent(new frame.contentWindow.KeyboardEvent("keydown", { key: "ArrowRight", bubbles: true, cancelable: true }));
if (doc.body.dataset.playbackIndex !== "3" || doc.body.dataset.lastShortcut !== "ArrowRight Step") {
throw new Error("AXIS-style ArrowRight shortcut did not advance playback");
}
doc.body.dispatchEvent(new frame.contentWindow.KeyboardEvent("keydown", { key: "ArrowLeft", bubbles: true, cancelable: true }));
if (doc.body.dataset.playbackIndex !== "2" || doc.body.dataset.lastShortcut !== "ArrowLeft Step") {
throw new Error("AXIS-style ArrowLeft shortcut did not reverse playback");
}
api.zoomPreview(1.5);
doc.body.dispatchEvent(new frame.contentWindow.KeyboardEvent("keydown", { key: "f", bubbles: true, cancelable: true }));
if (api.getPreviewState().zoom !== 1 || doc.body.dataset.lastShortcut !== "F Fit") {
throw new Error("AXIS-style fit shortcut did not reset preview zoom");
}
doc.body.dispatchEvent(new frame.contentWindow.KeyboardEvent("keydown", { key: " ", bubbles: true, cancelable: true }));
if (!api.isPlaybackRunning() || doc.body.dataset.lastShortcut !== "Space Play") {
throw new Error("AXIS-style Space shortcut did not start playback");
}
doc.body.dispatchEvent(new frame.contentWindow.KeyboardEvent("keydown", { key: " ", bubbles: true, cancelable: true }));
if (api.isPlaybackRunning() || doc.body.dataset.lastShortcut !== "Space Pause") {
throw new Error("AXIS-style Space shortcut did not pause playback");
}
doc.body.dispatchEvent(new frame.contentWindow.KeyboardEvent("keydown", { key: "r", bubbles: true, cancelable: true }));
for (let i = 0; i < 120 && api.getStatusHistory()[0]?.kind !== "run"; i += 1) {
await new Promise((resolve) => setTimeout(resolve, 25));
}
if (
doc.body.dataset.lastShortcut !== "R Run" ||
api.getStatusHistory()[0]?.kind !== "run" ||
!api.getStatusHistory().some(({ kind, message }) => kind === "shortcut" && message === "R Run")
) {
throw new Error(`AXIS-style R shortcut did not record run action: ${JSON.stringify(api.getStatusHistory())}`);
}
const finishedFrame = api.finishPlayback();
if (finishedFrame.index !== state.motion.length - 1 || doc.body.dataset.playbackComplete !== "true") {
throw new Error("simulation finish playback did not reach final frame");
}
for (const program of programs) {
const nextState = await api.runProgramById(program.id);
assertRenderedState(doc, nextState);
const resetFrame = api.resetPlayback();
if (resetFrame.index !== 0 || doc.body.dataset.playbackIndex !== "0") {
throw new Error(`simulation playback reset failed for ${program.id}`);
}
if (nextState.program.id !== program.id) {
throw new Error(`simulation API returned wrong program id for ${program.id}`);
}
for (const motionType of program.expectedMotionTypes) {
if (!nextState.summary.motionTypes.includes(motionType)) {
throw new Error(`simulation program ${program.id} missing motion type ${motionType}`);
}
}
}
const arcState = await api.runProgramById("arc-g2-g3");
const arcPoints = doc.querySelector("[data-toolpath-polyline]")?.getAttribute("points") ?? "";
if (!arcState.summary.motionTypes.includes("ARC_FEED")) {
throw new Error("arc simulation did not produce ARC_FEED");
}
if (!arcPoints.includes("1,-1") || !arcPoints.includes("0,0")) {
throw new Error(`arc simulation toolpath points did not use canonical arc endpoints: ${arcPoints}`);
}
const arcPreview = api.getPreviewState();
if (arcPreview.three.arcSamplePoints <= arcState.motion.filter(({ type }) => type === "ARC_FEED").length * 2) {
throw new Error(`arc simulation Three.js preview did not sample canonical arcs: ${JSON.stringify(arcPreview.three)}`);
}
const drillState = await api.runProgramById("drill-g81");
if (drillState.motion.filter(({ type }) => type === "STRAIGHT_FEED").length < 3) {
throw new Error("drill simulation did not produce expected feed plunges");
}
const customProgramText = [
"G90 G17 G0 X0 Y0 Z0",
"G1 X2.5 Y0.5 Z0 F75",
"G1 X2.5 Y1.5 Z0",
"M2",
"",
].join("\n");
const customState = await api.runProgramText(customProgramText, {
label: "operator-real-part.ngc",
source: "text",
sourceLabel: "Operator pasted G-code",
});
assertRenderedState(doc, customState);
if (customState.program?.id !== "custom" || customState.program?.source !== "text") {
throw new Error("custom text program state metadata drift");
}
if (!customState.resultText.includes("canon_event=STRAIGHT_FEED line=2 x=2.5 y=0.5 z=0")) {
throw new Error("custom text program did not execute through LinuxCNC WASM");
}
if (doc.body.dataset.simulationProgramId !== "custom" || doc.body.dataset.simulationProgramSource !== "text") {
throw new Error("custom text program body dataset drift");
}
if (!doc.querySelector('[data-axis-shell="statusbar"]')?.textContent.includes("Operator pasted G-code")) {
throw new Error("custom text program source did not render in statusbar");
}
if (!api.getProgramText().text.includes("G1 X2.5 Y0.5 Z0 F75")) {
throw new Error("custom text program did not sync into editable G-code pane");
}
const customRecentPrograms = api.getRecentPrograms();
if (
customRecentPrograms.length !== 1 ||
customRecentPrograms[0].source !== "text" ||
customRecentPrograms[0].label !== "operator-real-part.ngc" ||
!doc.querySelector("[data-recent-program-list]")?.textContent.includes("operator-real-part.ngc")
) {
throw new Error(`custom text program did not populate AXIS recent programs: ${JSON.stringify(customRecentPrograms)}`);
}
api.setProgramText("G90 G17 G0 X0 Y0 Z0\nM2\n", {
label: "temporary-editor.ngc",
source: "editor",
sourceLabel: "Editor text",
});
doc.querySelector("[data-recent-program-item]")?.click();
if (!api.getProgramText().text.includes("X2.5 Y0.5") || api.getProgramText().metadata.source !== "recent") {
throw new Error(`recent program item did not restore text into editor: ${JSON.stringify(api.getProgramText())}`);
}
const machineStatusProgram = [
"S1200 M3",
"M5",
"M7",
"M8",
"M9",
"M48",
"M50 P0",
"M50 P1",
"T2",
"M6",
"G43 H2",
"G49",
"M61 Q2",
"G90 G17 G0 X0 Y0 Z0",
"G1 X1 Y1 Z0 F50",
"M2",
"",
].join("\n");
const machineStatusRun = await api.runProgramText(machineStatusProgram, {
label: "axis-machine-status.ngc",
source: "text",
sourceLabel: "AXIS machine status probe",
});
assertRenderedState(doc, machineStatusRun);
const machineStatus = api.getMachineStatusState();
if (
machineStatus.spindle.state !== "off" ||
machineStatus.spindle.direction !== "cw" ||
machineStatus.spindle.speed !== "1200" ||
machineStatus.coolant.mist !== "off" ||
machineStatus.coolant.flood !== "off" ||
machineStatus.tool.selected !== "2" ||
machineStatus.tool.current !== "2" ||
machineStatus.tool.lengthOffset !== "z=0.000" ||
machineStatus.overrides.feed !== "enabled"
) {
throw new Error(`simulation machine status did not derive from LinuxCNC canonical output: ${JSON.stringify(machineStatus)}`);
}
if (
doc.querySelector('[data-machine-status="spindle-speed"]')?.textContent !== "1200" ||
doc.querySelector('[data-machine-status="coolant-flood"]')?.textContent !== "off" ||
doc.querySelector('[data-machine-status="tool-current"]')?.textContent !== "2"
) {
throw new Error("simulation machine status did not render into AXIS panels");
}
const fileProgram = new frame.contentWindow.File(
[customProgramText.replace("X2.5", "X3.25")],
"real-loaded-file.ngc",
{ type: "text/plain" },
);
const fileState = await api.loadProgramFile(fileProgram);
assertRenderedState(doc, fileState);
if (fileState.program?.source !== "file" || fileState.program?.filename !== "real-loaded-file.ngc") {
throw new Error("loaded file program metadata drift");
}
if (!fileState.resultText.includes("canon_event=STRAIGHT_FEED line=2 x=3.25 y=0.5 z=0")) {
throw new Error("loaded file program did not execute changed G-code through LinuxCNC WASM");
}
if (!api.getProgramText().metadata.filename || !api.getProgramText().text.includes("G1 X3.25 Y0.5 Z0 F75")) {
throw new Error("loaded file program did not sync into editor state");
}
const loadedText = api.loadProgramText(
"G90 G17 G0 X0 Y0 Z0\nG1 X3.5 Y1.75 Z0 F82\nM2\n",
{
label: "loaded-through-api.ngc",
source: "loaded-text",
sourceLabel: "Loaded through API",
},
);
if (!loadedText.text.includes("X3.5 Y1.75") || api.getProgramText().metadata.source !== "loaded-text") {
throw new Error(`loadProgramText did not stage editor text: ${JSON.stringify(loadedText)}`);
}
if (doc.body.dataset.simulationProgramSource !== "loaded-text" || !doc.querySelector('[data-axis-shell="statusbar"]')?.textContent.includes("Loaded through API")) {
throw new Error("loadProgramText did not render source metadata");
}
doc.querySelector("[data-mdi-program-text]").value = [
"G90 G17 G0 X0 Y0 Z0",
"G1 X4.5 Y1.125 Z0 F85",
"M2",
"",
].join("\n");
doc.querySelector("[data-load-mdi-program]")?.click();
if (!api.getProgramText().text.includes("X4.5 Y1.125") || !doc.querySelector("[data-mdi-status]")?.textContent.includes("Loaded 3 MDI lines")) {
throw new Error("MDI load control did not stage text into editor");
}
const mdiState = await api.runMdiProgramText();
assertRenderedState(doc, mdiState);
if (mdiState.program?.source !== "mdi" || !mdiState.resultText.includes("canon_event=STRAIGHT_FEED line=2 x=4.5 y=1.125 z=0")) {
throw new Error("MDI text did not execute through LinuxCNC WASM");
}
const mdiHistory = api.getMdiHistory();
if (
mdiHistory.length !== 1 ||
!mdiHistory[0].summary.includes("X4.5 Y1.125") ||
doc.body.dataset.mdiHistoryCount !== "1" ||
!doc.querySelector("[data-mdi-history-list]")?.textContent.includes("X4.5 Y1.125")
) {
throw new Error(`MDI history did not record successful LinuxCNC-backed run: ${JSON.stringify(mdiHistory)}`);
}
doc.querySelector("[data-mdi-program-text]").value = "G90 G17 G0 X0 Y0 Z0\nM2\n";
doc.querySelector("[data-mdi-history-item]")?.click();
if (!doc.querySelector("[data-mdi-program-text]")?.value.includes("X4.5 Y1.125")) {
throw new Error("MDI history item did not restore text into the MDI pane");
}
api.clearMdiHistory();
if (api.getMdiHistory().length !== 0 || doc.body.dataset.mdiHistoryCount !== "0" || !doc.querySelector("[data-mdi-history-list]")?.textContent.includes("n/a")) {
throw new Error(`MDI history clear did not reset API/DOM state: ${JSON.stringify(api.getMdiHistory())}`);
}
api.setProgramText(
[
"G90 G17 G0 X0 Y0 Z0",
"G1 X4.75 Y2.25 Z0 F95",
"M2",
"",
].join("\n"),
{
label: "edited-in-axis-pane.ngc",
source: "editor",
sourceLabel: "Editor text",
},
);
const editorState = api.getProgramText();
if (!editorState.text.includes("X4.75") || editorState.metadata.label !== "edited-in-axis-pane.ngc") {
throw new Error("simulation editable G-code state did not accept setProgramText");
}
const editorRunState = await api.runEditorProgramText();
assertRenderedState(doc, editorRunState);
if (!editorRunState.resultText.includes("canon_event=STRAIGHT_FEED line=2 x=4.75 y=2.25 z=0")) {
throw new Error("editor G-code did not execute changed text through LinuxCNC WASM");
}
if (!doc.querySelector("[data-program-editor]")?.value.includes("X4.75")) {
throw new Error("editor DOM did not retain edited G-code text");
}
const opfsProgramText = [
"G90 G17 G0 X0 Y0 Z0",
"G1 X6.5 Y1.25 Z0 F105",
"M2",
"",
].join("\n");
api.setProgramText(opfsProgramText, {
label: "axis-opfs-roundtrip.ngc",
source: "editor",
sourceLabel: "Editor text",
});
doc.querySelector("[data-opfs-program-filename]").value = "axis-opfs-roundtrip.ngc";
const savedOpfs = await api.saveProgramToOpfs();
if (savedOpfs.opfsPath !== "linuxcnc/gcode/axis-opfs-roundtrip.ngc" || !savedOpfs.ready) {
throw new Error(`simulation OPFS save metadata drift: ${JSON.stringify(savedOpfs)}`);
}
api.setProgramText("G90 G17 G0 X0 Y0 Z0\nG1 X0.125 Y0.125 Z0 F10\nM2\n", {
label: "unsaved-editor-change.ngc",
source: "editor",
sourceLabel: "Editor text",
});
const loadedOpfsState = await api.loadProgramFromOpfs("axis-opfs-roundtrip.ngc");
assertRenderedState(doc, loadedOpfsState);
if (loadedOpfsState.program?.source !== "opfs" || loadedOpfsState.program?.opfsPath !== "linuxcnc/gcode/axis-opfs-roundtrip.ngc") {
throw new Error(`simulation OPFS load metadata drift: ${JSON.stringify(loadedOpfsState.program)}`);
}
if (!loadedOpfsState.resultText.includes("canon_event=STRAIGHT_FEED line=2 x=6.5 y=1.25 z=0")) {
throw new Error("OPFS loaded program did not execute saved G-code through LinuxCNC WASM");
}
if (api.getProgramText().metadata.source !== "opfs" || !api.getProgramText().text.includes("X6.5 Y1.25")) {
throw new Error("OPFS loaded program did not sync into editable G-code pane");
}
if (doc.body.dataset.simulationProgramSource !== "opfs" || !doc.querySelector('[data-axis-shell="statusbar"]')?.textContent.includes("Loaded OPFS: linuxcnc/gcode/axis-opfs-roundtrip.ngc")) {
throw new Error("OPFS loaded program source did not render in statusbar");
}
const opfsState = api.getOpfsProgramState();
if (opfsState.status !== "Loaded OPFS: linuxcnc/gcode/axis-opfs-roundtrip.ngc") {
throw new Error(`simulation OPFS state status drift: ${JSON.stringify(opfsState)}`);
}
const machineId = "axis-browser-readiness";
const sessionId = "axis-browser-readiness-session";
const snapshotFilename = "axis-browser-readiness.json";
const gcodeFilename = "axis-browser-readiness.ngc";
const paths = machineFilePaths(machineId);
const initialReadiness = await api.checkMachineSessionReadiness({
machineId,
sessionId,
snapshotFilename,
gcodeFilename,
});
if (initialReadiness.ready || !initialReadiness.missing.includes("ini") || !initialReadiness.missing.includes("gcode")) {
throw new Error(`simulation initial machine readiness should be blocked: ${JSON.stringify(initialReadiness)}`);
}
if (doc.body.dataset.machineSessionReady !== "false" || doc.querySelector("[data-machine-readiness-phase]")?.textContent !== "blocked") {
throw new Error("simulation blocked machine readiness did not render");
}
await saveMachineTextFiles(machineId, {
ini: `[EMC]
MACHINE = axis-browser-readiness
[RS274NGC]
PARAMETER_FILE = linuxcnc.var
[EMCIO]
TOOL_TABLE = tool.tbl
`,
parameters: "5161 0.0\n5162 0.0\n5220 1\n5221 0.0\n5399 0.0\n",
toolTable: "T1 P1 Z0.0 D0.125 ;axis readiness\n",
});
await saveGcodeProgram(gcodeFilename, "G0 X0 Y0\nG1 X1 Y1 F20\nM2\n");
await saveMachineSessionSnapshot(sessionId, machineId, {
filename: snapshotFilename,
gcodeFilename,
createdAt: "2026-06-16T00:00:00.000Z",
metadata: {
workflow: "axis-real-simulation-readiness-smoke",
},
});
const readyReadiness = await api.checkMachineSessionReadiness({
machineId,
sessionId,
snapshotFilename,
gcodeFilename,
});
if (!readyReadiness.ready || readyReadiness.phase !== "ready" || readyReadiness.missing.length !== 0) {
throw new Error(`simulation machine readiness should be ready: ${JSON.stringify(readyReadiness)}`);
}
if (readyReadiness.paths.ini !== paths.ini || readyReadiness.paths.gcode !== gcodeProgramPath(gcodeFilename)) {
throw new Error(`simulation machine readiness paths drift: ${JSON.stringify(readyReadiness.paths)}`);
}
if (api.getMachineReadiness().machineId !== machineId || doc.body.dataset.machineSessionReady !== "true") {
throw new Error("simulation ready machine readiness state did not sync to API/body");
}
if (
doc.querySelector("[data-machine-readiness-phase]")?.textContent !== "ready" ||
doc.querySelector("[data-machine-readiness-missing]")?.textContent !== "none" ||
doc.querySelector('[data-machine-readiness-path="ini"]')?.textContent !== paths.ini ||
doc.querySelector('[data-machine-readiness-path="gcode"]')?.textContent !== gcodeProgramPath(gcodeFilename)
) {
throw new Error("simulation ready machine readiness did not render expected paths");
}
const loadedSession = await api.loadReadyMachineSession({
machineId,
sessionId,
snapshotFilename,
gcodeFilename,
iniWasmPath: "/work/axis-smoke/machine.ini",
parameterWasmPath: "/work/axis-smoke/linuxcnc.var",
toolTableWasmPath: "/work/axis-smoke/tool.tbl",
});
if (!loadedSession.loaded || loadedSession.phase !== "loaded") {
throw new Error(`simulation machine session should load into WASM: ${JSON.stringify(loadedSession)}`);
}
if (
loadedSession.ini.wasmPath !== "/work/axis-smoke/machine.ini" ||
loadedSession.parameters.wasmPath !== "/work/axis-smoke/linuxcnc.var" ||
loadedSession.toolTable.wasmPath !== "/work/axis-smoke/tool.tbl"
) {
throw new Error(`simulation machine session WASM path drift: ${JSON.stringify(loadedSession)}`);
}
if (
api.getMachineSessionLoadState().machineId !== machineId ||
doc.body.dataset.machineSessionLoaded !== "true" ||
doc.querySelector('[data-machine-session-loaded-path="ini"]')?.textContent !== "/work/axis-smoke/machine.ini" ||
doc.querySelector('[data-machine-session-loaded-path="parameters"]')?.textContent !== "/work/axis-smoke/linuxcnc.var" ||
doc.querySelector('[data-machine-session-loaded-path="toolTable"]')?.textContent !== "/work/axis-smoke/tool.tbl"
) {
throw new Error("simulation machine session load state did not sync to API/body/DOM");
}
const toolTableSummary = api.getToolTableSummary();
if (
toolTableSummary.state !== "loaded" ||
toolTableSummary.wasmPath !== "/work/axis-smoke/tool.tbl" ||
toolTableSummary.toolCount !== 1 ||
!toolTableSummary.firstTool.includes("T1 P1") ||
!doc.querySelector('[data-tool-table-summary="paths"]')?.textContent.includes("/work/axis-smoke/tool.tbl") ||
!doc.querySelector('[data-tool-table-summary="tool"]')?.textContent.includes("axis readiness") ||
doc.querySelector('[data-tool-table-row="1"]')?.children.length !== 5 ||
!doc.querySelector('[data-tool-table-row="1"]')?.textContent.includes("axis readiness")
) {
throw new Error(`simulation tool table summary did not render loaded session tool table: ${JSON.stringify(toolTableSummary)}`);
}
if (!api.getStatusHistory().some(({ kind, message }) => kind === "session" && message.includes("/work/axis-smoke/tool.tbl"))) {
throw new Error(`simulation status history should record loaded tool table session: ${JSON.stringify(api.getStatusHistory())}`);
}
const loadedDiagnosticsArtifact = api.exportDiagnosticsArtifact();
if (
loadedDiagnosticsArtifact.toolTable.state !== "loaded" ||
loadedDiagnosticsArtifact.toolTable.rows.length !== 1 ||
loadedDiagnosticsArtifact.limitsHome.axes.x.fault !== "n/a" ||
!loadedDiagnosticsArtifact.statusHistory.some(({ kind }) => kind === "session")
) {
throw new Error(`simulation diagnostics artifact did not include session/tool/limits state: ${JSON.stringify(loadedDiagnosticsArtifact)}`);
}
if (api.getRunSummary().sessionLoadPhase !== "loaded" || doc.querySelector('[data-run-summary="session-state"]')?.textContent !== "ready / loaded") {
throw new Error(`simulation run summary did not reflect ready loaded session: ${JSON.stringify(api.getRunSummary())}`);
}
const loadedRunControl = api.getRunControlState();
if (loadedRunControl.phase !== "loaded" || loadedRunControl.loadedIniPath !== "/work/axis-smoke/machine.ini") {
throw new Error(`simulation run control should report loaded session availability before session-backed run: ${JSON.stringify(loadedRunControl)}`);
}
api.setProgramText(
[
"G90 G17 G0 X0 Y0 Z0",
"G1 X7.25 Y3.5 Z0 F115",
"M2",
"",
].join("\n"),
{
label: "axis-session-editor.ngc",
source: "editor",
sourceLabel: "Editor text",
},
);
const sessionEditorRun = await api.runEditorProgramText();
assertRenderedState(doc, sessionEditorRun);
if (sessionEditorRun.execution?.mode !== "linuxcnc-wasm-with-ini" || sessionEditorRun.execution?.iniPath !== "/work/axis-smoke/machine.ini") {
throw new Error(`simulation editor run did not use loaded session INI: ${JSON.stringify(sessionEditorRun.execution)}`);
}
if (!sessionEditorRun.resultText.includes("canon_event=STRAIGHT_FEED line=2 x=7.25 y=3.5 z=0")) {
throw new Error("simulation editor run with loaded session did not execute expected G-code");
}
if (!sessionEditorRun.program?.sourceLabel.includes("Session INI: /work/axis-smoke/machine.ini")) {
throw new Error(`simulation editor run source label missing loaded session INI: ${sessionEditorRun.program?.sourceLabel}`);
}
if (
api.getRunMode().mode !== "session-backed" ||
api.getRunMode().iniPath !== "/work/axis-smoke/machine.ini" ||
doc.body.dataset.runMode !== "session-backed" ||
!doc.querySelector('[data-axis-shell="statusbar"]')?.textContent.includes("Session-backed: /work/axis-smoke/machine.ini")
) {
throw new Error("simulation run mode did not switch to session-backed after loaded-session execution");
}
if (
api.getRunSummary().executionMode !== "linuxcnc-wasm-with-ini" ||
api.getRunSummary().sessionIniPath !== "/work/axis-smoke/machine.ini" ||
doc.querySelector('[data-run-summary="session-ini"]')?.textContent !== "/work/axis-smoke/machine.ini" ||
api.getRunControlState().phase !== "ready"
) {
throw new Error(`simulation run summary missing session-backed execution details: ${JSON.stringify(api.getRunSummary())}`);
}
const standaloneMode = api.setUseLoadedSession(false);
if (standaloneMode.mode !== "standalone" || standaloneMode.useLoadedSession !== false || doc.querySelector("[data-use-loaded-session]")?.checked !== false) {
throw new Error(`simulation run-mode toggle did not disable loaded session: ${JSON.stringify(standaloneMode)}`);
}
api.setProgramText(
[
"G90 G17 G0 X0 Y0 Z0",
"G1 X7.75 Y3.75 Z0 F116",
"M2",
"",
].join("\n"),
{
label: "axis-standalone-after-session.ngc",
source: "editor",
sourceLabel: "Editor text",
},
);
const standaloneEditorRun = await api.runEditorProgramText();
assertRenderedState(doc, standaloneEditorRun);
if (standaloneEditorRun.execution?.mode !== "linuxcnc-wasm" || standaloneEditorRun.execution?.iniPath !== null) {
throw new Error(`simulation standalone toggle run should not use loaded session INI: ${JSON.stringify(standaloneEditorRun.execution)}`);
}
if (api.getRunMode().mode !== "standalone" || doc.body.dataset.runMode !== "standalone") {
throw new Error("simulation run mode should stay standalone after disabled-session run");
}
if (api.getRunSummary().executionMode !== "linuxcnc-wasm" || doc.querySelector('[data-run-summary="session-ini"]')?.textContent !== "n/a") {
throw new Error(`simulation run summary should return to standalone details: ${JSON.stringify(api.getRunSummary())}`);
}
if (api.getRunControlState().reason !== "Loaded machine session bypassed by Use Session toggle") {
throw new Error(`simulation run control should explain loaded-session bypass: ${JSON.stringify(api.getRunControlState())}`);
}
const restoredSessionMode = api.setUseLoadedSession(true);
if (restoredSessionMode.useLoadedSession !== true || doc.querySelector("[data-use-loaded-session]")?.checked !== true) {
throw new Error(`simulation run-mode toggle did not re-enable loaded session: ${JSON.stringify(restoredSessionMode)}`);
}
api.setProgramText(
[
"G90 G17 G0 X0 Y0 Z0",
"G1 X8.5 Y4.25 Z0 F125",
"M2",
"",
].join("\n"),
{
label: "axis-session-opfs.ngc",
source: "editor",
sourceLabel: "Editor text",
},
);
doc.querySelector("[data-opfs-program-filename]").value = "axis-session-opfs.ngc";
await api.saveProgramToOpfs();
api.setProgramText("G90 G17 G0 X0 Y0 Z0\nG1 X0.5 Y0.5 Z0 F10\nM2\n", {
label: "unsaved-session-editor-change.ngc",
source: "editor",
sourceLabel: "Editor text",
});
const sessionOpfsRun = await api.loadProgramFromOpfs("axis-session-opfs.ngc");
assertRenderedState(doc, sessionOpfsRun);
if (sessionOpfsRun.execution?.mode !== "linuxcnc-wasm-with-ini" || sessionOpfsRun.execution?.iniPath !== "/work/axis-smoke/machine.ini") {
throw new Error(`simulation OPFS run did not use loaded session INI: ${JSON.stringify(sessionOpfsRun.execution)}`);
}
if (!sessionOpfsRun.resultText.includes("canon_event=STRAIGHT_FEED line=2 x=8.5 y=4.25 z=0")) {
throw new Error("simulation OPFS run with loaded session did not execute saved G-code");
}
if (!sessionOpfsRun.program?.sourceLabel.includes("Loaded OPFS: linuxcnc/gcode/axis-session-opfs.ngc") || !sessionOpfsRun.program?.sourceLabel.includes("Session INI: /work/axis-smoke/machine.ini")) {
throw new Error(`simulation OPFS run source label missing OPFS/session details: ${sessionOpfsRun.program?.sourceLabel}`);
}
if (
api.getRunSummary().opfsProgramPath !== "linuxcnc/gcode/axis-session-opfs.ngc" ||
doc.querySelector('[data-run-summary="opfs-program"]')?.textContent !== "linuxcnc/gcode/axis-session-opfs.ngc"
) {
throw new Error(`simulation run summary missing OPFS program path: ${JSON.stringify(api.getRunSummary())}`);
}
document.getElementById("axis-diagnostics-artifact").textContent = JSON.stringify(api.exportDiagnosticsArtifact());
status.textContent = "browser_real_simulation_page_smoke=ok";
} catch (error) {
status.textContent = `browser_real_simulation_page_smoke=fail ${error.stack || error.message}`;
throw error;
}
</script>
</body>
</html>