1336 lines
70 KiB
HTML
1336 lines
70 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?.getVirtualHalState ||
|
|
!api?.applyVirtualHalAction ||
|
|
!api?.getVirtualHalDroState ||
|
|
!api?.getVirtualHalLimitsHomeState ||
|
|
!api?.getVirtualHalMachineStatusState ||
|
|
!api?.getVirtualHalPinRegistry ||
|
|
!api?.readVirtualHalPin ||
|
|
!api?.writeVirtualHalPin ||
|
|
!api?.applyVirtualHalPinUpdates ||
|
|
!api?.getVirtualHalIntegrityReport ||
|
|
!api?.executeVirtualHalcmd ||
|
|
!api?.executeVirtualHalCommand ||
|
|
!api?.stepVirtualHalMotionController ||
|
|
!api?.getVirtualHalSimulationRuntimeReport ||
|
|
!api?.getVirtualHalSimulationReplacementReport ||
|
|
!api?.getVirtualRealtimeHalRuntimeReport
|
|
) {
|
|
throw new Error("simulation API missing browser virtual HAL 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("No recent programs")) {
|
|
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("browser virtual HAL active")) {
|
|
throw new Error("simulation page should explicitly mark browser virtual HAL 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 !== "off" ||
|
|
doc.querySelector('[data-machine-status="spindle-state"]')?.textContent !== "off"
|
|
) {
|
|
throw new Error(`simulation initial machine status should expose AXIS default 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 === "pending" ||
|
|
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 !== "browser-virtual-hal" ||
|
|
limitsHome.axes.x.homed !== "no" ||
|
|
doc.querySelector('[data-limits-home-row="x"]')?.children.length !== 5 ||
|
|
!doc.querySelector("[data-limits-home-summary]")?.textContent.includes("source browser-virtual-hal")
|
|
) {
|
|
throw new Error(`simulation limits/home state should expose browser virtual HAL rows: ${JSON.stringify(limitsHome)}`);
|
|
}
|
|
const virtualHalInitial = api.getVirtualHalState();
|
|
if (
|
|
virtualHalInitial.source !== "browser-virtual-hal" ||
|
|
virtualHalInitial.component !== "axisui" ||
|
|
virtualHalInitial.task.state !== "ESTOP" ||
|
|
!virtualHalInitial.axisuiPins.some(({ name }) => name === "jog.x") ||
|
|
!virtualHalInitial.axisuiPins.some(({ name }) => name === "jog.increment")
|
|
) {
|
|
throw new Error(`simulation virtual HAL initial state drift: ${JSON.stringify(virtualHalInitial)}`);
|
|
}
|
|
const initialVirtualHalRegistry = api.getVirtualHalPinRegistry();
|
|
if (
|
|
initialVirtualHalRegistry.apiName !== "linuxcnc-wasm-virtual-hal-pin-registry" ||
|
|
!initialVirtualHalRegistry.byName["halui.machine.on"]?.writable ||
|
|
!initialVirtualHalRegistry.byName["motion.tooloffset.z"]?.writable ||
|
|
!api.getVirtualHalIntegrityReport().complete
|
|
) {
|
|
throw new Error(`simulation virtual HAL registry/integrity drift: ${JSON.stringify(initialVirtualHalRegistry)}`);
|
|
}
|
|
const diagnosticsArtifact = api.exportDiagnosticsArtifact();
|
|
if (
|
|
diagnosticsArtifact.apiName !== "real-browser-simulation-diagnostics-artifact" ||
|
|
diagnosticsArtifact.statusHistory.length === 0 ||
|
|
diagnosticsArtifact.virtualHal.source !== "browser-virtual-hal" ||
|
|
diagnosticsArtifact.virtualHalSimulationReplacement?.ready !== true ||
|
|
diagnosticsArtifact.limitsHome.source !== "browser-virtual-hal" ||
|
|
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 !== "0.000") {
|
|
throw new Error(`simulation DRO ${field} should use zeroed AXIS default when unavailable`);
|
|
}
|
|
}
|
|
if (droState.velocity !== "0.000" || doc.querySelector("[data-dro-velocity]")?.textContent !== "0.000") {
|
|
throw new Error("simulation DRO velocity should use zeroed AXIS default 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");
|
|
}
|
|
doc.body.dispatchEvent(new frame.contentWindow.KeyboardEvent("keydown", { key: "F1", bubbles: true, cancelable: true }));
|
|
if (api.getVirtualHalState().task.state !== "ESTOP_RESET" || doc.body.dataset.virtualHalTaskState !== "ESTOP_RESET" || doc.body.dataset.lastShortcut !== "F1 ESTOP") {
|
|
throw new Error(`AXIS-style F1 did not reset browser virtual HAL ESTOP: ${JSON.stringify(api.getVirtualHalState())}`);
|
|
}
|
|
doc.body.dispatchEvent(new frame.contentWindow.KeyboardEvent("keydown", { key: "F2", bubbles: true, cancelable: true }));
|
|
if (api.getVirtualHalState().task.state !== "ON" || doc.body.dataset.virtualHalTaskState !== "ON" || doc.body.dataset.lastShortcut !== "F2 Power") {
|
|
throw new Error(`AXIS-style F2 did not power browser virtual HAL machine: ${JSON.stringify(api.getVirtualHalState())}`);
|
|
}
|
|
doc.body.dispatchEvent(new frame.contentWindow.KeyboardEvent("keydown", { key: "Home", ctrlKey: true, bubbles: true, cancelable: true }));
|
|
if (api.getVirtualHalLimitsHomeState().axes.x.homed !== "yes" || doc.body.dataset.lastShortcut !== "Ctrl-Home Home All") {
|
|
throw new Error(`AXIS-style Ctrl-Home did not home browser virtual HAL axes: ${JSON.stringify(api.getVirtualHalLimitsHomeState())}`);
|
|
}
|
|
doc.querySelector('[data-jog-increment]').value = "0.01 in";
|
|
doc.querySelector('[data-axis-jog="+"]')?.click();
|
|
if (api.getVirtualHalState().pins["jog.x"] !== true || api.getVirtualHalDroState().actual.x !== "0.010" || doc.querySelector('[data-dro-actual="x"]')?.textContent !== "0.010") {
|
|
throw new Error(`AXIS-style jog did not update browser virtual HAL DRO/pins: ${JSON.stringify(api.getVirtualHalState())}`);
|
|
}
|
|
doc.querySelector('[data-axis-spindle="cw"]')?.click();
|
|
doc.querySelector('[data-axis-coolant="flood"]').checked = true;
|
|
doc.querySelector('[data-axis-coolant="flood"]').dispatchEvent(new frame.contentWindow.Event("change", { bubbles: true }));
|
|
const virtualMachineStatus = api.getVirtualHalMachineStatusState();
|
|
if (
|
|
virtualMachineStatus.spindle.state !== "on" ||
|
|
virtualMachineStatus.spindle.direction !== "cw" ||
|
|
virtualMachineStatus.coolant.flood !== "on" ||
|
|
doc.querySelector('[data-axis-spindle-status]')?.dataset.state !== "ready" ||
|
|
doc.querySelector('[data-axis-coolant-status]')?.dataset.state !== "ready"
|
|
) {
|
|
throw new Error(`AXIS-style spindle/coolant did not update browser virtual HAL machine status: ${JSON.stringify(virtualMachineStatus)}`);
|
|
}
|
|
api.writeVirtualHalPin({ name: "spindle.0.speed-out", type: "HAL_FLOAT", value: 1500 });
|
|
api.applyVirtualHalPinUpdates([
|
|
{ name: "motion.tooloffset.z", type: "HAL_FLOAT", value: 1.5 },
|
|
{ name: "halui.tool.number", type: "HAL_U32", value: 8 },
|
|
]);
|
|
if (
|
|
api.readVirtualHalPin("spindle.0.speed-out")?.value !== 1500 ||
|
|
api.readVirtualHalPin("motion.tooloffset.z")?.value !== 1.5 ||
|
|
api.readVirtualHalPin("halui.tool.number")?.value !== 8 ||
|
|
api.getVirtualHalIntegrityReport().complete !== true
|
|
) {
|
|
throw new Error(`AXIS-style virtual HAL pin service did not update/read pins: ${JSON.stringify(api.getVirtualHalPinRegistry())}`);
|
|
}
|
|
const virtualHalcmdResult = api.executeVirtualHalcmd("loadrt trivkins\naddf motion-controller servo-thread\nsetp axis.x.pos-cmd 1.75\nshow pin axis.x.*");
|
|
if (
|
|
virtualHalcmdResult.apiName !== "linuxcnc-wasm-virtual-halcmd-result" ||
|
|
virtualHalcmdResult.ok !== true ||
|
|
!virtualHalcmdResult.output.includes("axis.x.pos-cmd") ||
|
|
api.readVirtualHalPin("axis.x.pos-cmd")?.value !== 1.75
|
|
) {
|
|
throw new Error(`AXIS-style virtual halcmd did not execute: ${JSON.stringify(virtualHalcmdResult)}`);
|
|
}
|
|
const motionControllerStep = api.stepVirtualHalMotionController({
|
|
target: { x: 2.25 },
|
|
maxVelocity: 10,
|
|
dt: 0.1,
|
|
cycleCount: 2,
|
|
});
|
|
if (
|
|
motionControllerStep.apiName !== "linuxcnc-wasm-virtual-motion-controller-step" ||
|
|
motionControllerStep.ok !== true ||
|
|
motionControllerStep.state.position.x <= 1.75
|
|
) {
|
|
throw new Error(`AXIS-style virtual motion controller did not step: ${JSON.stringify(motionControllerStep)}`);
|
|
}
|
|
const virtualHalReplacement = api.getVirtualHalSimulationReplacementReport();
|
|
if (
|
|
virtualHalReplacement.apiName !== "linuxcnc-wasm-virtual-hal-simulation-replacement-report" ||
|
|
virtualHalReplacement.ready !== true ||
|
|
virtualHalReplacement.replacements["linuxcnc-realtime-hal"].ready !== true ||
|
|
virtualHalReplacement.replacements.halcmd.ready !== true ||
|
|
virtualHalReplacement.replacements["motion-controller"].ready !== true ||
|
|
api.getVirtualRealtimeHalRuntimeReport().ready !== true ||
|
|
api.getVirtualHalSimulationRuntimeReport().replacesHostRuntimeForSimulation !== true
|
|
) {
|
|
throw new Error(`AXIS-style virtual HAL simulation replacement report drift: ${JSON.stringify(virtualHalReplacement)}`);
|
|
}
|
|
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("No MDI history")) {
|
|
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 !== "none" ||
|
|
!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 !== "-") {
|
|
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>
|