345 lines
19 KiB
HTML
345 lines
19 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<title>xyzbc-trt all button coverage</title>
|
|
</head>
|
|
<body>
|
|
<pre id="result">xyzbc_trt_all_buttons=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";
|
|
|
|
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
|
async function runAllButtons() {
|
|
result.textContent = "xyzbc_trt_all_buttons=loading-frame";
|
|
const frameLoaded = new Promise((resolve, reject) => {
|
|
frame.addEventListener("load", resolve, { once: true });
|
|
frame.addEventListener("error", reject, { once: true });
|
|
});
|
|
frame.src = appSrc;
|
|
await frameLoaded;
|
|
|
|
result.textContent = "xyzbc_trt_all_buttons=waiting-api";
|
|
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;
|
|
result.textContent = "xyzbc_trt_all_buttons=waiting-runtimes";
|
|
await waitFor(() => {
|
|
const state = api.getState();
|
|
return state.kinematicsRuntime?.loaded === true &&
|
|
state.interpreterRuntime?.loaded === true &&
|
|
state.taskHalRuntime?.loaded === true;
|
|
}, "runtime loaded state");
|
|
result.textContent = "xyzbc_trt_all_buttons=waiting-program";
|
|
if (api.getState().machineFileStaging?.status !== "staged") {
|
|
api.dispatch({ type: "STAGE_MACHINE_FILES_REQUEST" });
|
|
}
|
|
await waitFor(() => api.getState().machineFileStaging?.status === "staged", "machine file staging");
|
|
const source = api.getState().machineFileStaging?.gcodeSources?.find((item) => item.filename === "xyzbc_switchkins.ngc") ||
|
|
api.getState().machineFileStaging?.gcodeSources?.[0];
|
|
if (source?.sourceRel) {
|
|
api.dispatch({ type: "LOAD_LINUXCNC_GCODE_SOURCE", sourceRel: source.sourceRel });
|
|
}
|
|
await waitFor(() => {
|
|
const state = api.getState();
|
|
return state.machineFileStaging?.status === "staged" &&
|
|
!state.interpreterExecutionPending &&
|
|
Number(state.programAxisPreviewPath?.sampleCount || 0) > 0;
|
|
}, "staged default program");
|
|
|
|
result.textContent = "xyzbc_trt_all_buttons=checking-dom";
|
|
assertButtonParityDomCoverage(doc, api.getButtonParity());
|
|
result.textContent = "xyzbc_trt_all_buttons=checking-menu";
|
|
await verifyMenuButtons(doc, api);
|
|
result.textContent = "xyzbc_trt_all_buttons=checking-toolbar";
|
|
await verifyToolbarButtons(doc, api);
|
|
result.textContent = "xyzbc_trt_all_buttons=checking-manual-mdi-pyvcp";
|
|
await verifyManualMdiPyvcpButtons(doc, api);
|
|
result.textContent = "xyzbc_trt_all_buttons=checking-overrides";
|
|
await verifyOverrideCoolantAndViewButtons(doc, api);
|
|
|
|
if (runtimeErrors.length > 0) {
|
|
throw new Error(`runtime errors: ${runtimeErrors.join(" | ")}`);
|
|
}
|
|
result.textContent = `xyzbc_trt_all_buttons=ok checked=${api.getButtonParity().length}`;
|
|
}
|
|
|
|
function assertButtonParityDomCoverage(doc, buttonParity) {
|
|
if (!Array.isArray(buttonParity) || buttonParity.length < 50) {
|
|
throw new Error(`button parity list too small: ${buttonParity?.length}`);
|
|
}
|
|
const controls = [...doc.querySelectorAll("[data-action], [data-menu-command]")];
|
|
for (const item of buttonParity) {
|
|
const matches = controls.filter((control) => (
|
|
control.dataset.action === item.action ||
|
|
control.dataset.menuCommand === item.action
|
|
));
|
|
if (matches.length === 0) {
|
|
throw new Error(`button parity item has no DOM control: ${item.id}/${item.action}`);
|
|
}
|
|
if (!matches.some((control) => control.dataset.axisSourceRef && control.dataset.axisExpectedEffect)) {
|
|
throw new Error(`button parity item has no source-tagged DOM control: ${item.id}/${item.action}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function verifyMenuButtons(doc, api) {
|
|
await click(doc, '[data-menu-command="stage"]', "menu Stage LinuxCNC files");
|
|
await waitState(api, (state) => state.machineFileStaging?.status === "staged", "menu stage completed");
|
|
|
|
await click(doc, '[data-menu-command="save-session"]', "menu Save session");
|
|
await waitState(api, (state) => (
|
|
state.sessionPersistence?.status === "saved" &&
|
|
Boolean(state.sessionPersistence?.path) &&
|
|
Boolean(state.sessionPersistence?.savedAt)
|
|
), "menu save session completed");
|
|
|
|
await click(doc, '[data-menu-command="restore-session"]', "menu Restore session");
|
|
await waitState(api, (state) => (
|
|
state.sessionPersistence?.status === "restored" &&
|
|
Boolean(state.sessionPersistence?.path) &&
|
|
Boolean(state.sessionPersistence?.restoredAt)
|
|
), "menu restore session completed");
|
|
|
|
await click(doc, '[data-menu-command="reload"]', "menu Reload");
|
|
await waitState(api, (state) => Number(state.programAxisPreviewPath?.sampleCount || 0) > 0, "menu reload restored preview");
|
|
|
|
await click(doc, '[data-menu-command="view-z"]', "menu View Z");
|
|
await waitState(api, (state) => state.preview.selectedView === "z", "menu view z");
|
|
await click(doc, '[data-menu-command="view-y"]', "menu View Y");
|
|
await waitState(api, (state) => state.preview.selectedView === "y", "menu view y");
|
|
await click(doc, '[data-menu-command="view-x"]', "menu View X");
|
|
await waitState(api, (state) => state.preview.selectedView === "x", "menu view x");
|
|
await click(doc, '[data-menu-command="view-p"]', "menu View P");
|
|
await waitState(api, (state) => state.preview.selectedView === "iso", "menu view p");
|
|
await click(doc, '[data-menu-command="clear-preview"]', "menu Clear Live Plot");
|
|
await waitState(api, (state) => state.preview.pathPoints === 0, "menu clear preview");
|
|
|
|
await click(doc, '[data-menu-command="audit"]', "menu Run parity audit");
|
|
await waitState(api, (state) => (
|
|
state.interpreterExecutionPending === false &&
|
|
Number(state.programAxisPreviewPath?.sampleCount || 0) > 0 &&
|
|
state.taskHalRuntime?.loaded === true
|
|
), "menu audit completed observable work");
|
|
}
|
|
|
|
async function verifyToolbarButtons(doc, api) {
|
|
await ensureResetPoweredHomedManual(doc, api);
|
|
|
|
await click(doc, '[data-action="toggle-auto-manual"]', "toolbar AUTO");
|
|
await waitState(api, (state) => state.machine.mode === "auto", "toolbar auto");
|
|
await click(doc, '[data-action="toggle-auto-manual"]', "toolbar MANUAL");
|
|
await waitState(api, (state) => state.machine.mode === "manual", "toolbar manual");
|
|
|
|
await click(doc, '[data-action="reload"]', "toolbar reload");
|
|
await waitState(api, (state) => Number(state.programAxisPreviewPath?.sampleCount || 0) > 0, "toolbar reload");
|
|
|
|
await click(doc, '[data-action="run"]', "toolbar run");
|
|
await waitState(api, (state) => state.runState === "running" || state.runState === "complete", "toolbar run started");
|
|
await click(doc, '[data-tool-id="tbtn_pause"]', "toolbar pause");
|
|
await waitState(api, (state) => state.runState === "paused", "toolbar pause");
|
|
await click(doc, '[data-action="step"]', "toolbar step");
|
|
await waitState(api, (state) => state.machine.taskPaused === true && Number(state.programExecutionSampleIndex) >= 0, "toolbar step");
|
|
await click(doc, '[data-tool-id="tbtn_pause"]', "toolbar resume");
|
|
await waitState(api, (state) => state.runState === "running", "toolbar resume");
|
|
await click(doc, '[data-action="stop"]', "toolbar stop");
|
|
await waitState(api, (state) => state.runState === "stopped", "toolbar stop");
|
|
|
|
await click(doc, '[data-action="estop"]', "toolbar estop");
|
|
await waitState(api, (state) => state.machine.estopActive === true, "toolbar estop");
|
|
await click(doc, '[data-action="estop"]', "toolbar reset");
|
|
await waitState(api, (state) => state.machine.estopActive === false, "toolbar reset");
|
|
if (api.getState().machine.powerOn !== true) {
|
|
await click(doc, '[data-action="power"]', "toolbar power on after reset");
|
|
await waitState(api, (state) => state.machine.powerOn === true, "toolbar power on after reset");
|
|
}
|
|
await click(doc, '[data-action="power"]', "toolbar power off");
|
|
await waitState(api, (state) => state.machine.powerOn === false, "toolbar power off");
|
|
await click(doc, '[data-action="power"]', "toolbar power on");
|
|
await waitState(api, (state) => state.machine.powerOn === true, "toolbar power on");
|
|
}
|
|
|
|
async function verifyManualMdiPyvcpButtons(doc, api) {
|
|
await ensureResetPoweredHomedManual(doc, api);
|
|
|
|
const joint3 = doc.querySelector('[data-action="select-joint"][value="3"]');
|
|
joint3.checked = true;
|
|
joint3.dispatchEvent(new Event("change", { bubbles: true }));
|
|
await waitState(api, (state) => state.machine.selectedJoint === 3 && state.machine.jogAxis === "b", "select B joint");
|
|
|
|
const jogIncrement = doc.querySelector('[data-action="jog-increment"]');
|
|
jogIncrement.value = "0.1";
|
|
jogIncrement.dispatchEvent(new Event("change", { bubbles: true }));
|
|
await waitState(api, (state) => Number(state.machine.jogIncrement) === 0.1, "jog increment");
|
|
|
|
const beforeJogB = api.getState().axisPose.b;
|
|
await click(doc, '[data-action="jog-plus"]', "manual jog plus");
|
|
await waitState(api, (state) => state.axisPose.b > beforeJogB, "manual jog plus changed B");
|
|
const afterJogPlusB = api.getState().axisPose.b;
|
|
await click(doc, '[data-action="jog-minus"]', "manual jog minus");
|
|
await waitState(api, (state) => state.axisPose.b < afterJogPlusB, "manual jog minus changed B");
|
|
|
|
await click(doc, '[data-action="touch-off"]', "manual touch off");
|
|
await waitState(api, (state) => state.mdiHistory[0] === "G10 L20 P0 B0" && state.machine.mode === "manual", "manual touch off");
|
|
await click(doc, '[data-action="tool-touch-off"]', "manual tool touch off");
|
|
await waitState(api, (state) => state.mdiHistory[0] === "G43" && state.machine.mode === "manual", "manual tool touch off");
|
|
|
|
await click(doc, '[data-action="tab-mdi"]', "MDI tab");
|
|
await waitState(api, (state) => state.machine.mode === "mdi", "mdi tab");
|
|
const mdiInput = doc.querySelector('[data-action="mdi-input"]');
|
|
mdiInput.value = "G90";
|
|
mdiInput.dispatchEvent(new Event("input", { bubbles: true }));
|
|
doc.querySelector('[data-action="mdi-form"]').dispatchEvent(new Event("submit", { bubbles: true, cancelable: true }));
|
|
await waitState(api, (state) => state.mdiHistory[0] === "G90", "mdi submit");
|
|
await click(doc, '[data-action="mdi-history"][data-command="G90"]', "MDI history G90");
|
|
await waitState(api, (state) => state.mdiHistory[0] === "G90", "mdi history");
|
|
|
|
await click(doc, '[data-action="kins-tcp"]', "PyVCP TCP");
|
|
await waitState(api, (state) => state.kinsType === "tcp-xyzbc" && state.mdiHistory[0] === "M428", "pyvcp tcp");
|
|
await click(doc, '[data-action="kins-identity"]', "PyVCP identity");
|
|
await waitState(api, (state) => state.kinsType === "identity" && state.mdiHistory[0] === "M429", "pyvcp identity");
|
|
await click(doc, '[data-action="kins-userk"]', "PyVCP userk");
|
|
await waitState(api, (state) => state.kinsType === "userk" && state.mdiHistory[0] === "M430", "pyvcp userk");
|
|
}
|
|
|
|
async function verifyOverrideCoolantAndViewButtons(doc, api) {
|
|
await ensureResetPoweredHomedManual(doc, api);
|
|
|
|
const feedBefore = api.getState().feed.feedOverride;
|
|
await click(doc, '[data-action="feed-override-up"]', "feed override up");
|
|
await waitState(api, (state) => state.feed.feedOverride === Math.min(feedBefore + 10, 200), "feed override up");
|
|
await click(doc, '[data-action="feed-override-down"]', "feed override down");
|
|
await waitState(api, (state) => state.feed.feedOverride === feedBefore, "feed override down");
|
|
|
|
const rapidBefore = api.getState().feed.rapidOverride;
|
|
await click(doc, '[data-action="rapid-override-up"]', "rapid override up");
|
|
await waitState(api, (state) => state.feed.rapidOverride === Math.min(rapidBefore + 10, 200), "rapid override up");
|
|
await click(doc, '[data-action="rapid-override-down"]', "rapid override down");
|
|
await waitState(api, (state) => state.feed.rapidOverride === rapidBefore, "rapid override down");
|
|
|
|
const spindleOverrideBefore = api.getState().spindle.override;
|
|
await click(doc, '[data-action="spindle-override-up"]', "spindle override up");
|
|
await waitState(api, (state) => state.spindle.override === Math.min(spindleOverrideBefore + 10, 150), "spindle override up");
|
|
await click(doc, '[data-action="spindle-override-down"]', "spindle override down");
|
|
await waitState(api, (state) => state.spindle.override === spindleOverrideBefore, "spindle override down");
|
|
|
|
await click(doc, '[data-action="spindle-reverse"]', "spindle reverse");
|
|
await waitState(api, (state) => state.spindle.direction === "reverse" && state.spindle.enabled === true, "spindle reverse");
|
|
await click(doc, '[data-action="spindle-stop"]', "spindle stop");
|
|
await waitState(api, (state) => state.spindle.direction === "stop" && state.spindle.enabled === false, "spindle stop");
|
|
await click(doc, '[data-action="spindle-forward"]', "spindle forward");
|
|
await waitState(api, (state) => state.spindle.direction === "forward" && state.spindle.enabled === true, "spindle forward");
|
|
|
|
await click(doc, '[data-action="toggle-flood"]', "toggle flood");
|
|
await waitState(api, (state) => state.coolant.flood === true, "toggle flood");
|
|
await click(doc, '[data-action="toggle-mist"]', "toggle mist");
|
|
await waitState(api, (state) => state.coolant.mist === true, "toggle mist");
|
|
await click(doc, '[data-action="block-delete"]', "block delete");
|
|
await waitState(api, (state) => state.gmoccapyGui.optionalBlocks === true, "block delete");
|
|
await click(doc, '[data-action="optional-stop"]', "optional stop");
|
|
await waitState(api, (state) => state.gmoccapyGui.optionalStop === true, "optional stop");
|
|
await click(doc, '[data-action="ignore-limits"]', "ignore limits");
|
|
await waitState(api, (state) => state.gmoccapyGui.ignoreLimits === true, "ignore limits");
|
|
|
|
await click(doc, '[data-action="view-z"]', "view z");
|
|
await waitState(api, (state) => state.preview.selectedView === "z", "view z");
|
|
await click(doc, '[data-action="view-y"]', "view y");
|
|
await waitState(api, (state) => state.preview.selectedView === "y", "view y");
|
|
await click(doc, '[data-action="view-x"]', "view x");
|
|
await waitState(api, (state) => state.preview.selectedView === "x", "view x");
|
|
await click(doc, '[data-action="view-p"]', "view p");
|
|
await waitState(api, (state) => state.preview.selectedView === "iso", "view p");
|
|
await click(doc, '[data-action="clear-preview"]', "clear preview");
|
|
await waitState(api, (state) => state.preview.pathPoints === 0, "clear preview");
|
|
await click(doc, '[data-action="reload"]', "reload after clear preview");
|
|
await waitState(api, (state) => state.preview.pathPoints > 0, "reload after clear preview");
|
|
}
|
|
|
|
async function ensureResetPoweredHomedManual(doc, api) {
|
|
let state = api.getState();
|
|
if (state.machine.estopActive === true || state.machine.taskState === "estop") {
|
|
await click(doc, '[data-action="estop"]', "ensure reset estop");
|
|
await waitState(api, (next) => next.machine.estopActive === false, "ensure estop reset");
|
|
}
|
|
state = api.getState();
|
|
if (state.machine.powerOn !== true) {
|
|
await click(doc, '[data-action="power"]', "ensure power on");
|
|
await waitState(api, (next) => next.machine.powerOn === true, "ensure power on");
|
|
}
|
|
if (api.getState().machine.mode !== "manual") {
|
|
await click(doc, '[data-action="tab-manual"]', "ensure manual before home");
|
|
await waitState(api, (next) => next.machine.mode === "manual", "ensure manual before home");
|
|
}
|
|
state = api.getState();
|
|
if (state.machine.allHomed !== true) {
|
|
await click(doc, '[data-action="home-all"]', "ensure home all");
|
|
await waitState(api, (next) => next.machine.allHomed === true, "ensure home all");
|
|
}
|
|
if (api.getState().machine.mode !== "manual") {
|
|
await click(doc, '[data-action="tab-manual"]', "ensure manual");
|
|
await waitState(api, (next) => next.machine.mode === "manual", "ensure manual mode");
|
|
}
|
|
if (api.getState().runState === "running" || api.getState().runState === "paused" || api.getState().runState === "stepping") {
|
|
await click(doc, '[data-action="stop"]', "ensure stopped");
|
|
await waitState(api, (next) => next.runState === "stopped" || next.runState === "idle", "ensure stopped");
|
|
await click(doc, '[data-action="tab-manual"]', "manual after stop");
|
|
}
|
|
}
|
|
|
|
async function click(doc, selector, label) {
|
|
const element = await waitFor(() => doc.querySelector(selector), label);
|
|
element.click();
|
|
await wait(80);
|
|
return element;
|
|
}
|
|
|
|
function waitState(api, predicate, label) {
|
|
return waitFor(() => {
|
|
const state = api.getState();
|
|
return predicate(state) ? state : null;
|
|
}, label, () => summarizeState(api.getState()));
|
|
}
|
|
|
|
async function waitFor(predicate, label, describe = null) {
|
|
for (let attempt = 0; attempt < 300; attempt += 1) {
|
|
const value = predicate();
|
|
if (value) return value;
|
|
await wait(50);
|
|
}
|
|
const suffix = describe ? ` ${describe()}` : "";
|
|
throw new Error(`timed out waiting for ${label}${suffix}`);
|
|
}
|
|
|
|
function summarizeState(state) {
|
|
return JSON.stringify({
|
|
runState: state.runState,
|
|
mode: state.machine?.mode,
|
|
taskState: state.machine?.taskState,
|
|
powerOn: state.machine?.powerOn,
|
|
allHomed: state.machine?.allHomed,
|
|
kinsType: state.kinsType,
|
|
mdiHistory: state.mdiHistory?.slice?.(0, 3),
|
|
operatorMessage: state.operatorMessage,
|
|
});
|
|
}
|
|
|
|
runAllButtons().catch((error) => {
|
|
result.textContent = `xyzbc_trt_all_buttons=fail ${error.stack || error.message || error}`;
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|