继续实现仿真测试程序

结论:真实浏览器仿真页面新增多测试程序选择和运行能力,覆盖直线、Z 轴轮廓、增量、圆弧和 G81 钻孔,并接入 Node、browser 与 release gate 验证。
This commit is contained in:
2026-06-16 21:13:09 +08:00
parent 24e1fc18b1
commit ddd5b78999
10 changed files with 461 additions and 41 deletions

View File

@@ -33,44 +33,95 @@
throw new Error("real simulation page did not expose ready state");
}
try {
const frame = await loadFrame("../../runtime/ui/simulation/index.html");
const doc = frame.contentDocument;
const state = await waitForState(frame);
function assertRenderedState(doc, state) {
const polyline = doc.querySelector("[data-toolpath-polyline]");
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");
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 < 5) {
throw new Error("simulation motion event count too low");
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 (doc.body.dataset.simulationProgramId !== state.program?.id) {
throw new Error(`simulation body program id drift for ${state.program?.id}`);
}
}
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;
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 (!polyline?.getAttribute("points")?.includes("1,0")) {
if (!doc.querySelector("[data-toolpath-polyline]")?.getAttribute("points")?.includes("1,0")) {
throw new Error("simulation toolpath polyline missing expected point");
}
if (rows.length !== state.motion.length) {
throw new Error("simulation motion table row count drift");
}
if (programRows.length < 6) {
throw new Error("simulation program rows missing");
}
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 (!doc.querySelector("[data-toolpath-svg]")?.getAttribute("viewBox")) {
throw new Error("simulation SVG viewBox missing");
if (!api?.getPrograms || !api?.runProgramById) {
throw new Error("simulation API missing program controls");
}
const programs = api.getPrograms();
if (programs.length < 5) {
throw new Error("simulation test program inventory too small");
}
for (const program of programs) {
const nextState = await api.runProgramById(program.id);
assertRenderedState(doc, nextState);
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 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");
}
status.textContent = "browser_real_simulation_page_smoke=ok";

View File

@@ -0,0 +1,83 @@
import assert from "node:assert/strict";
import {
DEFAULT_SIMULATION_PROGRAM,
DEFAULT_SIMULATION_PROGRAM_ID,
createSimulationSummary,
createToolpathPolylinePoints,
getSimulationTestProgram,
getSimulationTestPrograms,
parseLinuxCncCanonicalMotion,
} from "../../../runtime/ui/simulation/simulation-app.js";
const programs = getSimulationTestPrograms();
assert.equal(DEFAULT_SIMULATION_PROGRAM_ID, "square-linear");
assert.equal(getSimulationTestProgram(DEFAULT_SIMULATION_PROGRAM_ID).text, DEFAULT_SIMULATION_PROGRAM);
assert.ok(programs.length >= 5, "real simulation program inventory should cover multiple examples");
for (const program of programs) {
assert.ok(program.id, "program id should be present");
assert.ok(program.label, `program ${program.id} should have a label`);
assert.ok(program.category, `program ${program.id} should have a category`);
assert.ok(program.text.includes("M2"), `program ${program.id} should end through LinuxCNC program end`);
assert.ok(
program.expectedMotionTypes.length > 0,
`program ${program.id} should declare expected LinuxCNC motion types`,
);
assert.equal(getSimulationTestProgram(program.id).id, program.id);
}
const ids = new Set(programs.map(({ id }) => id));
assert.equal(ids.size, programs.length, "program ids should be unique");
assert.ok(ids.has("arc-g2-g3"), "program inventory should include an arc fixture");
assert.ok(ids.has("drill-g81"), "program inventory should include a canned-cycle fixture");
assert.throws(
() => getSimulationTestProgram("missing-program"),
/unknown simulation test program/,
"unknown program ids should be rejected",
);
const arcProgram = getSimulationTestProgram("arc-g2-g3");
const arcCanonical = [
"canon_event=UPDATE_TAG line=1 g0=-1 motion=0 plane=170 origin=530 feed=0 speed=0 flags=1048994",
"canon_event=STRAIGHT_TRAVERSE line=1 x=0 y=0 z=0 a=0 b=0 c=0 u=0 v=0 w=0",
"canon_event=ARC_FEED line=3 first_end=1 second_end=1 first_axis=1 second_axis=0 rotation=-1 axis_end_point=0 a=0 b=0 c=0 u=0 v=0 w=0",
"canon_event=ARC_FEED line=4 first_end=0 second_end=0 first_axis=1 second_axis=0 rotation=1 axis_end_point=0 a=0 b=0 c=0 u=0 v=0 w=0",
].join("\n");
const arcMotion = parseLinuxCncCanonicalMotion(arcCanonical, arcProgram.text);
assert.equal(arcMotion.length, 3);
assert.equal(arcMotion[1].type, "ARC_FEED");
assert.equal(arcMotion[1].statement, "G2 X1 Y1 I1 J0 F80");
assert.deepEqual(
{ x: arcMotion[1].axes.x, y: arcMotion[1].axes.y, z: arcMotion[1].axes.z },
{ x: 1, y: 1, z: 0 },
"arc parser should map LinuxCNC canonical arc endpoints onto active-plane axes",
);
assert.equal(createToolpathPolylinePoints(arcMotion), "0,0 1,-1 0,0");
const xzArcCanonical = [
"canon_event=UPDATE_TAG line=1 g0=-1 motion=0 plane=180 origin=530 feed=0 speed=0 flags=1048994",
"canon_event=STRAIGHT_TRAVERSE line=1 x=0 y=0 z=0 a=0 b=0 c=0 u=0 v=0 w=0",
"canon_event=ARC_FEED line=2 first_end=2 second_end=3 first_axis=1 second_axis=0 rotation=-1 axis_end_point=4 a=0 b=0 c=0 u=0 v=0 w=0",
].join("\n");
const xzArcMotion = parseLinuxCncCanonicalMotion(xzArcCanonical, "G18 G0 X0 Y0 Z0\nG2 X2 Z3 I1 K0\nM2\n");
assert.deepEqual(
{ x: xzArcMotion[1].axes.x, y: xzArcMotion[1].axes.y, z: xzArcMotion[1].axes.z },
{ x: 2, y: 4, z: 3 },
"G18 arc endpoints should use X/Z plus Y axis endpoint",
);
const summary = createSimulationSummary({
programText: arcProgram.text,
resultText: arcCanonical,
motion: arcMotion,
});
assert.equal(summary.ready, true);
assert.ok(summary.motionTypes.includes("ARC_FEED"));
assert.equal(summary.finalAxes.x, 0);
assert.equal(summary.finalAxes.y, 0);
console.log("real_simulation_programs_node_smoke=ok");

View File

@@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/../../.." && pwd)"
node "$ROOT_DIR/tests/ui/node/verify_real_simulation_programs.mjs"

View File

@@ -19,5 +19,6 @@ ROOT_DIR="$(cd "$(dirname "$0")/../../.." && pwd)"
"$ROOT_DIR/tests/ui/node/verify_ini_panel_ui_shell.sh"
"$ROOT_DIR/tests/ui/node/verify_ini_panel_shell_control_page_contract.sh"
"$ROOT_DIR/tests/ui/node/verify_ini_panel_entry_docs.sh"
"$ROOT_DIR/tests/ui/node/verify_real_simulation_programs.sh"
echo "ui_node_smokes=ok"