267 lines
13 KiB
HTML
267 lines
13 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<title>xyzbc-trt browser smoke</title>
|
|
</head>
|
|
<body>
|
|
<pre id="result">xyzbc_trt_browser_smoke=pending</pre>
|
|
<iframe id="app-frame" title="xyzbc-trt app"></iframe>
|
|
<script type="module">
|
|
const result = document.querySelector("#result");
|
|
const frame = document.querySelector("#app-frame");
|
|
const appSrc = new URLSearchParams(window.location.search).get("app") || "../../app/index.html";
|
|
frame.src = appSrc;
|
|
|
|
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
|
async function runSmoke() {
|
|
await new Promise((resolve, reject) => {
|
|
frame.addEventListener("load", resolve, { once: true });
|
|
frame.addEventListener("error", reject, { once: true });
|
|
});
|
|
|
|
const win = frame.contentWindow;
|
|
const doc = frame.contentDocument;
|
|
const runtimeErrors = [];
|
|
win.addEventListener("error", (event) => {
|
|
runtimeErrors.push(event.message || String(event.error || "window error"));
|
|
});
|
|
win.addEventListener("unhandledrejection", (event) => {
|
|
runtimeErrors.push(event.reason?.message || String(event.reason || "unhandled rejection"));
|
|
});
|
|
|
|
await waitFor(() => win.webRtcp5AxisSimulation, "public simulation API");
|
|
const api = win.webRtcp5AxisSimulation;
|
|
const [kinematics, interpreter, taskHal] = await Promise.all([
|
|
api.kinematicsRuntimeReady,
|
|
api.interpreterRuntimeReady,
|
|
api.taskHalRuntimeReady,
|
|
]);
|
|
|
|
assertRuntime(kinematics, {
|
|
name: "kinematics",
|
|
moduleId: "xyzbc-trt",
|
|
required: ["loaded"],
|
|
});
|
|
if (kinematics.executionContext !== "worker") {
|
|
throw new Error(`kinematics runtime should run in worker: ${JSON.stringify(kinematics)}`);
|
|
}
|
|
assertRuntime(interpreter, {
|
|
name: "interpreter",
|
|
required: ["loaded", "runProgramReady", "plannerRuntimeReady"],
|
|
});
|
|
if (interpreter.executionContext !== "worker") {
|
|
throw new Error(`interpreter runtime should run in worker: ${JSON.stringify(interpreter)}`);
|
|
}
|
|
assertRuntime(taskHal, {
|
|
name: "task/HAL",
|
|
required: ["loaded", "taskRuntimeReady", "motionRuntimeReady", "halRuntimeReady"],
|
|
});
|
|
if (taskHal.executionContext !== "worker") {
|
|
throw new Error(`task/HAL runtime should run in worker: ${JSON.stringify(taskHal)}`);
|
|
}
|
|
|
|
const seedResult = await api.machineFileSeedReady;
|
|
if (seedResult?.save?.storageMode !== "opfs") {
|
|
throw new Error(`machine file seed did not use OPFS: ${JSON.stringify(seedResult?.save || seedResult)}`);
|
|
}
|
|
await waitFor(() => {
|
|
const state = api.getState();
|
|
return !state.interpreterExecutionPending
|
|
&& state.programExecution?.sourceMode === "linuxcnc-interpreter-wasm"
|
|
&& state.machineFileStaging?.status === "staged";
|
|
}, "default xyzbc program preview");
|
|
|
|
const state = api.getState();
|
|
if (state.machineProfile !== "xyzbc-trt" || state.profile?.traj?.coordinates !== "XYZBC") {
|
|
throw new Error(`default profile mismatch: ${state.machineProfile}/${state.profile?.traj?.coordinates}`);
|
|
}
|
|
if (state.profile?.kinematicsModuleId !== "xyzbc-trt") {
|
|
throw new Error(`profile kinematics module mismatch: ${state.profile?.kinematicsModuleId}`);
|
|
}
|
|
if (state.machineFileStaging?.profileId !== "xyzbc-trt") {
|
|
throw new Error(`staging profile mismatch: ${state.machineFileStaging?.profileId}`);
|
|
}
|
|
if (
|
|
state.machineFileStaging?.storageMode !== "opfs" ||
|
|
state.machineFileStaging?.storageCapability?.opfsAvailable !== true ||
|
|
state.machineFileStaging?.storageCapability?.reason !== "opfs_available"
|
|
) {
|
|
throw new Error(`machine files must use browser OPFS: ${JSON.stringify(state.machineFileStaging?.storageCapability)}`);
|
|
}
|
|
if (!state.machineFileStaging?.gcodeSources?.some((source) => source.filename === "xyzbc_switchkins.ngc")) {
|
|
throw new Error("staged sources missing xyzbc_switchkins.ngc");
|
|
}
|
|
if (!state.machineFileStaging?.gcodeSources?.some((source) => source.filename === "boat-xyzbc.ngc")) {
|
|
throw new Error("staged sources missing boat-xyzbc.ngc");
|
|
}
|
|
if (state.programExecution?.summary?.motionEventCount < 1) {
|
|
throw new Error(`default program did not produce canonical motion: ${JSON.stringify(state.programExecution?.summary)}`);
|
|
}
|
|
const selectedSource = state.machineFileStaging.gcodeSources.find((source) => source.filename === "xyzbc_switchkins.ngc");
|
|
const opfsProgramText = await readOpfsText(win.navigator.storage, selectedSource.opfsPath);
|
|
if (!opfsProgramText.includes("M428") || !opfsProgramText.includes("M429")) {
|
|
throw new Error(`OPFS staged program text did not contain switchkins commands: ${selectedSource.opfsPath}`);
|
|
}
|
|
const savedSession = await api.saveSession();
|
|
if (savedSession.storageMode !== "opfs" || savedSession.storageCapability?.opfsAvailable !== true) {
|
|
throw new Error(`session save must use browser OPFS: ${JSON.stringify(savedSession.storageCapability)}`);
|
|
}
|
|
const sessionText = await readOpfsText(win.navigator.storage, savedSession.path);
|
|
if (!sessionText.includes('"machineProfile": "xyzbc-trt"')) {
|
|
throw new Error(`OPFS session snapshot did not persist xyzbc-trt profile: ${savedSession.path}`);
|
|
}
|
|
const savedToolDb = await api.saveToolDb();
|
|
if (savedToolDb.storageMode !== "opfs" || savedToolDb.storageCapability?.opfsAvailable !== true) {
|
|
throw new Error(`tool table save must use browser OPFS: ${JSON.stringify(savedToolDb.storageCapability)}`);
|
|
}
|
|
const toolTableText = await readOpfsText(win.navigator.storage, savedToolDb.path);
|
|
if (!toolTableText.includes("T2") || !toolTableText.includes("Z10")) {
|
|
throw new Error(`OPFS tool table save did not contain T2/Z10: ${savedToolDb.path}`);
|
|
}
|
|
|
|
const expectedAxisSections = {
|
|
".preview-panel": "preview-toolpath-and-machine-model",
|
|
".dro-panel": "coordinates-dro",
|
|
".gcode-panel": "program-and-mdi",
|
|
".status-sidebar": "status-mode-and-switchkins",
|
|
".info-tabs": "execution-tool-and-runtime-state",
|
|
".override-panel": "feed-rapid-override",
|
|
".spindle-coolant-panel": "spindle-coolant",
|
|
".bottom-controls": "run-jog-home-controls",
|
|
};
|
|
for (const [selector, section] of Object.entries(expectedAxisSections)) {
|
|
const element = doc.querySelector(selector);
|
|
if (!element) {
|
|
throw new Error(`missing AXIS UI section ${selector}`);
|
|
}
|
|
if (
|
|
element.dataset.axisUiSection !== section ||
|
|
element.dataset.axisUiProfile !== "xyzbc-trt" ||
|
|
element.dataset.axisUiCoordinates !== "XYZBC"
|
|
) {
|
|
throw new Error(`AXIS section dataset mismatch ${selector}: ${JSON.stringify(element.dataset)}`);
|
|
}
|
|
}
|
|
|
|
const firstScreen = await waitFor(
|
|
() => doc.querySelector('[data-axis-main-equivalence="xyzbc_trt_axis_first_screen_program_coordinates_status_mdi_switchkins_override_tool_preview_execution"]'),
|
|
"AXIS first-screen summary",
|
|
);
|
|
const capabilities = Array.from(firstScreen.querySelectorAll("[data-axis-ui-capability]"));
|
|
const capabilityIds = capabilities.map((element) => element.dataset.axisUiCapability);
|
|
for (const id of ["program", "coordinates", "status", "mdi", "switchkins", "override", "tool", "preview", "execution", "axis-buttons"]) {
|
|
if (!capabilityIds.includes(id)) {
|
|
throw new Error(`missing first-screen capability ${id}: ${capabilityIds.join(",")}`);
|
|
}
|
|
}
|
|
if (firstScreen.dataset.axisFirstScreenReady !== "true") {
|
|
throw new Error(`first-screen summary not ready: ${firstScreen.dataset.axisFirstScreenMissing}`);
|
|
}
|
|
if (!doc.querySelector('[data-action="mdi-command"]') || !doc.querySelector('[data-action="kins-tcp"]')) {
|
|
throw new Error("MDI input or switchkins TCP control missing from first screen");
|
|
}
|
|
|
|
const canvas = await waitFor(() => doc.querySelector("[data-five-axis-canvas]"), "preview canvas");
|
|
await waitFor(() => canvas.dataset.threeReady === "true" && Number(canvas.dataset.threePathPoints || 0) > 0, "Three.js dataset");
|
|
if (
|
|
canvas.dataset.threeRenderer !== "webgl" ||
|
|
canvas.dataset.threeFrameApi !== "web-rtcp-5axis-motion-frame" ||
|
|
canvas.dataset.threeSceneMode !== "program-preview-and-tool-execution" ||
|
|
canvas.dataset.threeMachineReferenceModel !== "webgl-five-axis-reference" ||
|
|
canvas.dataset.threePreviewScope !== "machine-reference-and-toolpath" ||
|
|
canvas.dataset.threeProgramPreviewSource !== "linuxcnc-interpreter-wasm" ||
|
|
canvas.dataset.threeVismachPinDrivenModel !== "web_threejs_vismach_equivalent_driven_by_xyzbc_trt_hal_pins"
|
|
) {
|
|
throw new Error(`unexpected canvas dataset: ${JSON.stringify(canvas.dataset)}`);
|
|
}
|
|
if (Number(canvas.dataset.threeSceneObjects || 0) < 12) {
|
|
throw new Error(`scene object count too low: ${canvas.dataset.threeSceneObjects}`);
|
|
}
|
|
const vismachPins = JSON.parse(canvas.dataset.threeVismachPins || "{}");
|
|
for (const pin of ["table-x", "saddle-y", "spindle-z", "tilt-b", "rotate-c", "tool-offset", "x-offset", "z-offset"]) {
|
|
if (!Number.isFinite(Number(vismachPins[pin]))) {
|
|
throw new Error(`missing Vismach pin ${pin}: ${JSON.stringify(vismachPins)}`);
|
|
}
|
|
}
|
|
const vismachTransforms = JSON.parse(canvas.dataset.threeVismachTransforms || "{}");
|
|
if (
|
|
vismachTransforms.table?.sourcePins?.[0] !== "table-x" ||
|
|
vismachTransforms.saddle?.sourcePins?.[0] !== "saddle-y" ||
|
|
vismachTransforms.spindle?.sourcePins?.[0] !== "spindle-z" ||
|
|
vismachTransforms.tilt?.sourcePins?.[0] !== "tilt-b" ||
|
|
vismachTransforms.rotary?.sourcePins?.[0] !== "rotate-c" ||
|
|
!vismachTransforms.tool?.sourcePins?.includes("tool-offset")
|
|
) {
|
|
throw new Error(`Vismach transform source pins mismatch: ${JSON.stringify(vismachTransforms)}`);
|
|
}
|
|
assertCanvasNonblank(canvas);
|
|
|
|
if (runtimeErrors.length > 0) {
|
|
throw new Error(`runtime errors: ${runtimeErrors.join(" | ")}`);
|
|
}
|
|
result.textContent = "xyzbc_trt_browser_smoke=ok";
|
|
}
|
|
|
|
function assertRuntime(readiness, { name, moduleId = null, required }) {
|
|
for (const key of required) {
|
|
if (readiness?.[key] !== true) {
|
|
throw new Error(`${name} runtime missing ${key}: ${JSON.stringify(readiness)}`);
|
|
}
|
|
}
|
|
if (moduleId && readiness?.moduleId !== moduleId) {
|
|
throw new Error(`${name} runtime module mismatch: ${JSON.stringify(readiness)}`);
|
|
}
|
|
}
|
|
|
|
async function waitFor(predicate, label) {
|
|
for (let attempt = 0; attempt < 240; attempt += 1) {
|
|
const value = predicate();
|
|
if (value) return value;
|
|
await wait(50);
|
|
}
|
|
throw new Error(`timed out waiting for ${label}`);
|
|
}
|
|
|
|
function assertCanvasNonblank(canvas) {
|
|
const context = canvas.getContext("webgl2") || canvas.getContext("webgl") || canvas.getContext("2d");
|
|
if (!context) {
|
|
throw new Error("preview canvas has no readable context");
|
|
}
|
|
const width = canvas.width;
|
|
const height = canvas.height;
|
|
if (width <= 0 || height <= 0) {
|
|
throw new Error(`preview canvas has invalid size ${width}x${height}`);
|
|
}
|
|
if (context.readPixels) {
|
|
const pixels = new Uint8Array(4);
|
|
context.readPixels(Math.floor(width / 2), Math.floor(height / 2), 1, 1, context.RGBA, context.UNSIGNED_BYTE, pixels);
|
|
if (pixels.some((value) => value !== 0)) return;
|
|
}
|
|
const imageData = canvas.toDataURL("image/png");
|
|
if (!imageData || imageData.length < 2000) {
|
|
throw new Error("preview canvas appears blank");
|
|
}
|
|
}
|
|
|
|
async function readOpfsText(storage, path) {
|
|
if (!storage?.getDirectory) {
|
|
throw new Error("browser OPFS getDirectory is unavailable");
|
|
}
|
|
let current = await storage.getDirectory();
|
|
const parts = String(path).split("/").filter(Boolean);
|
|
for (const part of parts.slice(0, -1)) {
|
|
current = await current.getDirectoryHandle(part);
|
|
}
|
|
const fileHandle = await current.getFileHandle(parts.at(-1));
|
|
return fileHandle.getFile().then((file) => file.text());
|
|
}
|
|
|
|
runSmoke().catch((error) => {
|
|
result.textContent = `xyzbc_trt_browser_smoke=fail ${error.stack || error.message || error}`;
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|