Add OPFS G-code staging and browser simulation checks

This commit is contained in:
2026-06-22 10:56:21 +08:00
parent 8321055934
commit 61e2fe8441
19 changed files with 1869 additions and 35 deletions

View File

@@ -34,6 +34,16 @@
}
return win.webRtcp5AxisSimulation.getState();
}
async function waitForMachineFileStaging(win) {
for (let attempt = 0; attempt < 120; attempt += 1) {
const state = win.webRtcp5AxisSimulation.getState();
if (state.machineFileStaging?.status === "staged" && state.machineFileStaging?.save?.fileCount >= 5) {
return state;
}
await wait(25);
}
return win.webRtcp5AxisSimulation.getState();
}
async function runSmoke() {
await new Promise((resolve, reject) => {
@@ -44,6 +54,14 @@
const doc = frame.contentDocument;
const win = frame.contentWindow;
const runtimeErrors = [];
win.addEventListener("error", (event) => {
runtimeErrors.push(event.message || String(event.error || "window error"));
});
win.addEventListener("unhandledrejection", (event) => {
runtimeErrors.push(event.reason?.message || String(event.reason || "unhandled rejection"));
});
win.__WEB_RTCP_FORCE_OPFS_UNAVAILABLE__ = true;
const regions = [
"titlebar",
"preview",
@@ -301,6 +319,20 @@
if (!doc.querySelector('[data-program-switchkins-summary]')?.textContent.includes("0 events")) {
throw new Error("program switchkins summary DOM did not render zero-event state");
}
if (!win.webRtcp5AxisSimulation.machineFileSeedReady) {
throw new Error("missing automatic machine file seed promise");
}
await win.webRtcp5AxisSimulation.machineFileSeedReady;
const autoStagedState = await waitForMachineFileStaging(win);
if (autoStagedState.machineFileStaging?.save?.summary?.gcodeFileCount !== 16) {
throw new Error(`automatic machine file seeding did not preload full project G-code set: ${JSON.stringify(autoStagedState.machineFileStaging?.save?.summary)}`);
}
if (autoStagedState.machineFileStaging?.gcodeSources?.length < 8) {
throw new Error(`automatic machine file seeding did not expose LinuxCNC 5-axis demos: ${JSON.stringify(autoStagedState.machineFileStaging)}`);
}
if (!doc.querySelector('[data-machine-file-staging="status"]')?.textContent.includes("staged")) {
throw new Error("automatic machine file staging status did not render");
}
win.webRtcp5AxisSimulation.dispatch({
type: "LOAD_PROGRAM",
filename: "operator-arc-demo.ngc",
@@ -328,12 +360,27 @@
if (stagedMachineFiles.save.status !== "saved" || stagedMachineFiles.save.fileCount < 5) {
throw new Error(`machine file staging did not save files: ${JSON.stringify(stagedMachineFiles.save)}`);
}
if (stagedMachineFiles.save.summary?.gcodeFileCount !== 16) {
throw new Error(`manual machine file staging did not preserve full project G-code set: ${JSON.stringify(stagedMachineFiles.save.summary)}`);
}
if (
stagedMachineFiles.save.storageMode !== "memory-fallback" ||
stagedMachineFiles.save.storageCapability?.opfsUnavailable !== true
) {
throw new Error(`machine file staging did not use OPFS fallback: ${JSON.stringify(stagedMachineFiles.save.storageCapability)}`);
}
if (!stagedMachineFiles.save.files.some((file) => file.sourceRel.endsWith("remap_subs/428remap.ngc"))) {
throw new Error("machine file staging did not include M428 remap");
}
if (!doc.querySelector('[data-machine-file-staging="status"]')?.textContent.includes("staged")) {
throw new Error("machine file staging status did not render");
}
if (
!doc.querySelector('[data-machine-file-staging="status"]')?.textContent.includes("memory-fallback") ||
!doc.querySelector('[data-machine-file-staging="status"]')?.textContent.includes("forced_unavailable")
) {
throw new Error("machine file staging status did not render OPFS fallback reason");
}
const sourceSelect = doc.querySelector('[data-action="select-linuxcnc-gcode-source"]');
if (!sourceSelect || sourceSelect.options.length < 4) {
throw new Error("LinuxCNC 5-axis G-code source selector did not render staged demos");
@@ -355,6 +402,45 @@
if (!doc.querySelector('[data-program-source]')?.textContent.includes("linuxcnc-vendored-5axis-gcode")) {
throw new Error("program source DOM did not show LinuxCNC vendored source");
}
if (linuxCncSourceState.programExecution?.summary?.motionEventCount < 1000) {
throw new Error(`LinuxCNC source program did not produce canonical motion: ${JSON.stringify(linuxCncSourceState.programExecution?.summary)}`);
}
if (linuxCncSourceState.programExecution?.summary?.plannerRuntimeReady !== true) {
throw new Error(`LinuxCNC source program did not produce TP timing: ${JSON.stringify(linuxCncSourceState.programExecution?.plannerTiming)}`);
}
if (linuxCncSourceState.programExecutionTiming?.samples?.length < 1) {
throw new Error("LinuxCNC source program did not expose TP samples");
}
if (linuxCncSourceState.programExecution?.summary?.switchkinsEventCount < 2) {
throw new Error("LinuxCNC source program did not preserve switchkins events");
}
if (linuxCncSourceState.rtcpState !== "on" || linuxCncSourceState.kinsType !== "tcp-xyzac") {
throw new Error(`LinuxCNC source program did not apply initial RTCP switchkins state: ${linuxCncSourceState.rtcpState}/${linuxCncSourceState.kinsType}`);
}
canvas = doc.querySelector("[data-five-axis-canvas]");
if (
canvas.dataset.threeProgramPreviewSource !== "linuxcnc-interpreter-wasm" ||
Number(canvas.dataset.threePathPoints ?? 0) < 1 ||
Number(canvas.dataset.threeExecutedPathPoints ?? 0) < 1 ||
canvas.dataset.threeToolExecutionMarker !== "true"
) {
throw new Error(`Three.js preview did not expose LinuxCNC source program path and execution trace: ${JSON.stringify(canvas.dataset)}`);
}
if (
canvas.dataset.threeSceneMode !== "program-preview-and-tool-execution" ||
canvas.dataset.threeToolpathPreviewSource !== "linuxcnc_interpreter_canonical_motion" ||
canvas.dataset.threeToolExecutionTraceSource !== "linuxcnc_tp_samples_or_task_motion_hal_feedback" ||
canvas.dataset.threePathFitBounds !== "ok" ||
canvas.dataset.threeCurrentSegmentHighlight !== "ok" ||
canvas.dataset.threeRapidFeedVisualDistinction !== "ok" ||
canvas.dataset.threeNoGcodeSemanticsGeneration !== "ok"
) {
throw new Error(`Three.js M20 source/runtime display gate failed: ${JSON.stringify(canvas.dataset)}`);
}
if (Number(canvas.dataset.threeRapidPathPoints ?? 0) < 1 || Number(canvas.dataset.threeFeedPathPoints ?? 0) < 1) {
throw new Error(`Three.js rapid/feed distinction did not expose path points: ${JSON.stringify(canvas.dataset)}`);
}
assertCanvasNonblank(canvas, "desktop LinuxCNC source Three.js preview");
win.webRtcp5AxisSimulation.dispatch({ type: "RUN_MACHINE_FILE_PROGRAM" });
const machineFileRunState = await waitForMachineFileExecution(win);
if (machineFileRunState.machineFileExecution?.summary?.machineFileExecutionReady !== true) {
@@ -383,12 +469,18 @@
if (!["opfs", "memory-fallback"].includes(savedSession.storageMode)) {
throw new Error(`saveSession used unexpected storage mode: ${savedSession.storageMode}`);
}
if (savedSession.storageMode !== "memory-fallback" || savedSession.storageCapability?.opfsUnavailable !== true) {
throw new Error(`saveSession did not use OPFS fallback: ${JSON.stringify(savedSession.storageCapability)}`);
}
if (!doc.querySelector('[data-session-persistence="status"]')?.textContent.includes("saved")) {
throw new Error("session save status did not render");
}
if (!doc.querySelector('[data-session-persistence="status"]')?.textContent.includes(savedSession.storageMode)) {
throw new Error("session save status did not render storage mode");
}
if (!doc.querySelector('[data-session-persistence="status"]')?.textContent.includes("opfs unavailable")) {
throw new Error("session save status did not render OPFS unavailable fallback");
}
if (doc.querySelector('[data-active-program-line]')?.textContent !== "Current line 2") {
throw new Error("loaded program did not render first LinuxCNC motion line");
}
@@ -413,10 +505,22 @@
canvas = doc.querySelector("[data-five-axis-canvas]");
if (
Number(canvas.dataset.threeExecutedPathPoints ?? 0) < 1 ||
canvas.dataset.threeToolExecutionMarker !== "true"
canvas.dataset.threeToolExecutionMarker !== "true" ||
canvas.dataset.threeCurrentSegmentHighlight !== "ok"
) {
throw new Error(`Three.js preview did not display tool execution progress: ${JSON.stringify(canvas.dataset)}`);
}
const originalFrameStyle = frame.getAttribute("style") || "";
frame.style.width = "390px";
frame.style.height = "760px";
await wait(100);
canvas = doc.querySelector("[data-five-axis-canvas]");
if (canvas.dataset.threeReady !== "true" || canvas.dataset.threePathFitBounds !== "ok") {
throw new Error(`mobile Three.js preview did not retain fit bounds: ${JSON.stringify(canvas.dataset)}`);
}
assertCanvasNonblank(canvas, "mobile LinuxCNC source Three.js preview");
frame.setAttribute("style", originalFrameStyle);
await wait(50);
const taskHalRunState = win.webRtcp5AxisSimulation.getState();
if (taskHalRunState.fullExecutionBoundary?.semanticBoundary !== "linuxcnc_task_motion_hal_wasm_simulation_runtime") {
throw new Error(`full execution boundary did not promote task/HAL simulation runtime: ${JSON.stringify(taskHalRunState.fullExecutionBoundary)}`);
@@ -427,6 +531,27 @@
if (!doc.querySelector('[data-full-execution-boundary="status"]')?.textContent.includes("full ready")) {
throw new Error("full execution boundary status did not render full ready state");
}
const hostNativeBoundary = doc.querySelector('[data-full-execution-boundary="host-native"]')?.textContent || "";
for (const requiredHostNativeState of [
"hardware drive false",
"host realtime false",
"external user-M false",
"tool DB false",
]) {
if (!hostNativeBoundary.includes(requiredHostNativeState)) {
throw new Error(`host/native boundary diagnostic did not render ${requiredHostNativeState}: ${hostNativeBoundary}`);
}
}
for (const [field, expected] of Object.entries({
hardwareDrive: false,
hostRealtimeKernel: false,
externalUserMProcessReady: false,
toolDbProcessReady: false,
})) {
if (taskHalRunState.fullExecutionBoundary?.[field] !== expected) {
throw new Error(`full execution boundary ${field} drifted: ${JSON.stringify(taskHalRunState.fullExecutionBoundary)}`);
}
}
if (!doc.querySelector('[data-task-hal-runtime="readiness"]')?.textContent.includes("sync")) {
throw new Error("task/HAL readiness diagnostic did not render sync");
}
@@ -545,6 +670,9 @@
if (uiRestoredSession.status !== "restored" || uiRestoredSession.storageMode !== uiSavedSession.storageMode) {
throw new Error(`UI Restore Session did not restore from default storage fallback: ${JSON.stringify(uiRestoredSession)}`);
}
if (uiRestoredSession.storageMode !== "memory-fallback") {
throw new Error(`UI Restore Session did not preserve memory fallback mode: ${JSON.stringify(uiRestoredSession)}`);
}
if (win.webRtcp5AxisSimulation.getState().machine.allHomed !== true) {
doc.querySelector('[data-action="mode-manual"]').click();
await wait(50);
@@ -673,6 +801,9 @@
if (win.webRtcp5AxisSimulation.getState().runState !== "estopped") {
throw new Error("E-STOP did not update run state");
}
if (runtimeErrors.length > 0) {
throw new Error(`public HTTP fallback smoke saw uncaught runtime errors: ${runtimeErrors.join(" | ")}`);
}
result.textContent = "gmoccapy_shell_smoke=ok";
}