feat: sync latest run execution updates
This commit is contained in:
@@ -8,11 +8,15 @@ const REPO_ROOT = path.resolve("/home/meswork/cnc_wams");
|
||||
const QA_ROOT = path.resolve("/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test");
|
||||
const OUTPUT_DIR = path.join(QA_ROOT, "output");
|
||||
const SCREENSHOT_DIR = path.join(QA_ROOT, "screenshots", "toolpath-preview-cases");
|
||||
const RUN_FEEDBACK_SCREENSHOT_DIR = path.join(QA_ROOT, "screenshots", "run-preconditions-feedback");
|
||||
const METER_SCENE_SCREENSHOT_DIR = path.join(QA_ROOT, "screenshots", "meter-scene-evidence");
|
||||
const CHROME_PATH = process.env.CHROME_PATH || "/usr/bin/google-chrome";
|
||||
const FIXTURE_URL = "/web-rtcp-5axis-sim-plan/app/index.html";
|
||||
|
||||
await fs.mkdir(OUTPUT_DIR, { recursive: true });
|
||||
await fs.mkdir(SCREENSHOT_DIR, { recursive: true });
|
||||
await fs.mkdir(RUN_FEEDBACK_SCREENSHOT_DIR, { recursive: true });
|
||||
await fs.mkdir(METER_SCENE_SCREENSHOT_DIR, { recursive: true });
|
||||
|
||||
const MIME_TYPES = {
|
||||
".css": "text/css; charset=utf-8",
|
||||
@@ -144,6 +148,22 @@ async function captureCase(name) {
|
||||
return screenshotPath;
|
||||
}
|
||||
|
||||
async function captureRunFeedbackFrame(name) {
|
||||
const screenshotPath = path.join(RUN_FEEDBACK_SCREENSHOT_DIR, `${name}.png`);
|
||||
await page.screenshot({ path: screenshotPath, fullPage: true });
|
||||
return screenshotPath;
|
||||
}
|
||||
|
||||
async function captureMeterSceneFrame(name) {
|
||||
const screenshotPath = path.join(METER_SCENE_SCREENSHOT_DIR, `${name}.png`);
|
||||
const canvas = await page.$("[data-five-axis-canvas]");
|
||||
if (!canvas) {
|
||||
throw new Error(`missing canvas for ${name}`);
|
||||
}
|
||||
await canvas.screenshot({ path: screenshotPath });
|
||||
return screenshotPath;
|
||||
}
|
||||
|
||||
async function recordCase(name, summary, checks = []) {
|
||||
const dataset = await getCanvasDataset();
|
||||
const state = await page.evaluate(() => JSON.parse(JSON.stringify(window.webRtcp5AxisSimulation.getState())));
|
||||
@@ -211,6 +231,44 @@ function previewChecks(dataset, {
|
||||
return checks;
|
||||
}
|
||||
|
||||
function meterSceneChecks(dataset, pixelStats = null) {
|
||||
const bounds = parseJson(dataset.threePathBoundsMeters);
|
||||
return [
|
||||
check("scene units are meters", dataset.threeSceneUnits === "m", `sceneUnits=${dataset.threeSceneUnits}`),
|
||||
check("linear scale visible", Number(dataset.threeLinearUnitScaleToMeters || 0) > 0, `scale=${dataset.threeLinearUnitScaleToMeters}`),
|
||||
check("path fit bounds", dataset.threePathFitBounds === "ok", `fit=${dataset.threePathFitBounds}`),
|
||||
check("path bounds in meters", Number(bounds?.maxSpan || 0) > 0 && Number(bounds?.maxSpan || 0) < 5, `bounds=${dataset.threePathBoundsMeters}`),
|
||||
check("canvas nonblank", !pixelStats || pixelStats.nonBlackRatio > 0.015, `nonBlack=${pixelStats?.nonBlackRatio ?? "-"}`),
|
||||
];
|
||||
}
|
||||
|
||||
function mixedUnitsChecks(dataset, state) {
|
||||
const motion = state.programExecution?.motion || [];
|
||||
const units = [...new Set(motion.map((event) => event.linearUnits).filter(Boolean))];
|
||||
const xMeters = motion.map((event) => sceneMeterX(event)).filter(Number.isFinite);
|
||||
return [
|
||||
check("G20/G21 motion units", units.includes("inch") && units.includes("mm"), `units=${units.join(",")}`),
|
||||
check("scene units are meters", dataset.threeSceneUnits === "m", `sceneUnits=${dataset.threeSceneUnits}`),
|
||||
check("mixed unit path visible", Number(dataset.threePathPoints || 0) >= 2, `pathPoints=${dataset.threePathPoints}`),
|
||||
check("1 inch equals 25.4 mm in scene", xMeters.some((value) => Math.abs(value - 0.0254) < 1e-9), `xMeters=${xMeters.join(",")}`),
|
||||
check("no unit fallback in canonical motion", state.programExecutionSourceMode === "linuxcnc-interpreter-wasm", `source=${state.programExecutionSourceMode}`),
|
||||
];
|
||||
}
|
||||
|
||||
function sceneMeterX(event) {
|
||||
const factor = event?.linearUnits === "inch" ? 0.0254 : event?.linearUnits === "m" ? 1 : 0.001;
|
||||
const value = Number(event?.axes?.x);
|
||||
return Number.isFinite(value) ? value * factor : null;
|
||||
}
|
||||
|
||||
function parseJson(text) {
|
||||
try {
|
||||
return JSON.parse(text || "null");
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function check(name, pass, detail) {
|
||||
return { name, pass: Boolean(pass), detail };
|
||||
}
|
||||
@@ -222,6 +280,111 @@ function assertChecks(caseName, checks) {
|
||||
}
|
||||
}
|
||||
|
||||
function summarizeRunFeedbackState(state, dataset, elapsedMs, screenshotPath) {
|
||||
const activeRowLine = Number(documentActiveLineFromState(state));
|
||||
return {
|
||||
elapsedMs,
|
||||
screenshotPath,
|
||||
canvas: {
|
||||
threeReady: dataset.threeReady,
|
||||
rtcpState: dataset.threeRtcpState,
|
||||
executedPathPoints: Number(dataset.threeExecutedPathPoints || 0),
|
||||
currentSegmentHighlight: dataset.threeCurrentSegmentHighlight,
|
||||
toolExecutionTraceSource: dataset.threeToolExecutionTraceSource,
|
||||
},
|
||||
state: {
|
||||
profileId: state.machineProfile,
|
||||
iniReady: state.iniConfigReadiness?.ready === true,
|
||||
iniPath: state.iniConfigReadiness?.path || null,
|
||||
coordinates: state.iniConfigReadiness?.coordinates || null,
|
||||
kinematicsModuleId: state.profile?.kinematicsModuleId || null,
|
||||
selectedGcodeSourceRel: state.machineFileStaging?.selectedGcodeSourceRel || null,
|
||||
taskHalSessionProgramPath: state.taskHalSession?.programPath || null,
|
||||
taskHalStatusLoop: state.taskHalStatusLoop || null,
|
||||
runState: state.runState,
|
||||
activeLine: state.activeLine,
|
||||
activeRowLine,
|
||||
activeLineMatchesUi: activeRowLine === Number(state.activeLine),
|
||||
droAxisPose: state.dro ? {
|
||||
x: state.dro.x,
|
||||
y: state.dro.y,
|
||||
z: state.dro.z,
|
||||
a: state.dro.a,
|
||||
b: state.dro.b,
|
||||
c: state.dro.c,
|
||||
} : null,
|
||||
feedbackAxisPose: state.programRuntimeFeedback?.axisPose || null,
|
||||
droMatchesFeedback: axisPoseMatchesDro(state.dro, state.programRuntimeFeedback?.axisPose),
|
||||
rtcpState: state.rtcpState,
|
||||
kinsType: state.kinsType,
|
||||
feedback: state.programRuntimeFeedback ? {
|
||||
sourceMode: state.programRuntimeFeedback.sourceMode,
|
||||
semanticBoundary: state.programRuntimeFeedback.semanticBoundary,
|
||||
line: state.programRuntimeFeedback.line,
|
||||
taskCycle: state.programRuntimeFeedback.taskCycle,
|
||||
servoCycle: state.programRuntimeFeedback.cycle,
|
||||
velocity: state.programRuntimeFeedback.currentVelocityMmPerMin,
|
||||
distanceToGo: state.programRuntimeFeedback.distanceToGo,
|
||||
} : null,
|
||||
feedbackHistoryLength: state.programRuntimeFeedbackHistory?.length || 0,
|
||||
feedbackHistorySourceModes: [...new Set((state.programRuntimeFeedbackHistory || []).map((entry) => entry.sourceMode))],
|
||||
taskHalStatus: state.taskHalStatus ? {
|
||||
taskState: state.taskHalStatus.ui?.taskState || state.taskHalStatus.task?.state || null,
|
||||
taskMode: state.taskHalStatus.ui?.taskMode || state.taskHalStatus.task?.mode || null,
|
||||
interpState: state.taskHalStatus.ui?.interpState || state.taskHalStatus.task?.interpState || null,
|
||||
taskCycle: state.taskHalStatus.ui?.taskCycle || null,
|
||||
servoCycle: state.taskHalStatus.ui?.servoCycle || null,
|
||||
} : null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function documentActiveLineFromState(state) {
|
||||
return state.__activeRowLine ?? null;
|
||||
}
|
||||
|
||||
function axisPoseMatchesDro(dro, axisPose) {
|
||||
if (!dro || !axisPose) return false;
|
||||
return ["x", "y", "z", "a", "b", "c"].every((axis) => (
|
||||
Math.abs(Number(dro[axis] || 0) - Number(axisPose[axis] || 0)) < 1e-9
|
||||
));
|
||||
}
|
||||
|
||||
async function snapshotRunFeedback(elapsedMs, screenshotName) {
|
||||
const screenshotPath = await captureRunFeedbackFrame(screenshotName);
|
||||
const dataset = await getCanvasDataset();
|
||||
const state = await page.evaluate(() => {
|
||||
const snapshot = JSON.parse(JSON.stringify(window.webRtcp5AxisSimulation.getState()));
|
||||
snapshot.__activeRowLine = Number(document.querySelector(".gcode-row.active")?.dataset.programLine || 0);
|
||||
return snapshot;
|
||||
});
|
||||
return summarizeRunFeedbackState(state, dataset, elapsedMs, screenshotPath);
|
||||
}
|
||||
|
||||
function runFeedbackChecks(samples, baseline = null) {
|
||||
const states = samples.map((sample) => sample.state);
|
||||
const histories = states.map((state) => Number(state.feedbackHistoryLength || 0));
|
||||
const ticks = states.map((state) => Number(state.taskHalStatusLoop?.tickCount || 0));
|
||||
const baselineSequence = Number(baseline?.state?.taskHalStatusLoop?.sequence || 0);
|
||||
const sourceModes = states.flatMap((state) => state.feedbackHistorySourceModes || []);
|
||||
return [
|
||||
check("INI ready", states.every((state) => state.iniReady), `iniReady=${states.map((state) => state.iniReady).join(",")}`),
|
||||
check("selected LinuxCNC G-code", states.every((state) => state.selectedGcodeSourceRel?.endsWith("xyzac_switchkins_test_1.ngc")), states.at(-1)?.selectedGcodeSourceRel || "-"),
|
||||
check("task/HAL session opened selected G-code", states.every((state) => state.taskHalSessionProgramPath?.endsWith("xyzac_switchkins_test_1.ngc")), states.at(-1)?.taskHalSessionProgramPath || "-"),
|
||||
check("taskHalStatusLoop 新 RUN sequence", states.every((state) => Number(state.taskHalStatusLoop?.sequence || 0) > baselineSequence), `baselineSequence=${baselineSequence}, sequences=${states.map((state) => state.taskHalStatusLoop?.sequence || 0).join(",")}`),
|
||||
check("taskHalStatusLoop 本次 RUN tickCount", Math.max(...ticks) >= 3, `ticks=${ticks.join(",")}`),
|
||||
check("programRuntimeFeedbackHistory 本次 RUN 采样", Math.max(...histories) >= 3, `history=${histories.join(",")}`),
|
||||
check("feedback sourceMode 来自 task/HAL", sourceModes.length > 0 && sourceModes.every((mode) => mode === "linuxcnc-task-motion-hal-wasm"), `sourceModes=${sourceModes.join(",")}`),
|
||||
check("无 fixture-line-playback feedback", !sourceModes.includes("fixture-line-playback"), `sourceModes=${sourceModes.join(",")}`),
|
||||
check("semantic boundary", states.every((state) => state.feedback?.semanticBoundary === "linuxcnc_task_motion_hal_wasm_simulation_runtime"), states.map((state) => state.feedback?.semanticBoundary || "-").join(",")),
|
||||
check("activeLine 等于 UI 高亮行", states.every((state) => state.activeLineMatchesUi), `active=${states.map((state) => `${state.activeLine}/${state.activeRowLine}`).join(",")}`),
|
||||
check("DRO 等于 runtime feedback axisPose", states.every((state) => state.droMatchesFeedback), "droMatchesFeedback=true for all samples"),
|
||||
check("RTCP canvas 与 state 一致", samples.every((sample) => sample.canvas.rtcpState === sample.state.rtcpState), samples.map((sample) => `${sample.canvas.rtcpState}/${sample.state.rtcpState}`).join(",")),
|
||||
check("runtime cycle 可见", states.every((state) => Number(state.feedback?.taskCycle || 0) > 0 && Number(state.feedback?.servoCycle || 0) > 0), states.map((state) => `${state.feedback?.taskCycle || 0}/${state.feedback?.servoCycle || 0}`).join(",")),
|
||||
check("canvas 执行轨迹", samples.every((sample) => sample.canvas.threeReady === "true" && sample.canvas.currentSegmentHighlight === "ok" && sample.canvas.executedPathPoints >= 1), samples.map((sample) => `${sample.canvas.threeReady}/${sample.canvas.executedPathPoints}/${sample.canvas.currentSegmentHighlight}`).join(",")),
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
await page.goto(`${baseUrl}${FIXTURE_URL}`, { waitUntil: "networkidle2", timeout: 60000 });
|
||||
await page.waitForSelector('[data-shell="gmoccapy-5axis"]', { timeout: 15000 });
|
||||
@@ -303,6 +466,92 @@ try {
|
||||
await captureCase("05-vendored-impeller-toolpath");
|
||||
await recordCase("05-vendored-impeller-toolpath", "LinuxCNC vendored 五轴 impeller 程序:验证长路径、switchkins/RTCP 状态、rapid/feed 图层和 TCP 执行轨迹", checks);
|
||||
|
||||
const meterSceneSamples = [];
|
||||
for (const viewport of [
|
||||
{ name: "desktop", width: 1600, height: 1200, deviceScaleFactor: 1 },
|
||||
{ name: "mobile", width: 390, height: 844, deviceScaleFactor: 2 },
|
||||
]) {
|
||||
await page.setViewport(viewport);
|
||||
await waitForCanvasReady();
|
||||
await wait(600);
|
||||
const screenshotPath = await captureMeterSceneFrame(`08-meter-scene-${viewport.name}`);
|
||||
const sampleDataset = await getCanvasDataset();
|
||||
const pixelStats = await analyzePng(screenshotPath);
|
||||
const sampleChecks = [
|
||||
...previewChecks(sampleDataset, { requirePathPoints: true, requireExecutedPath: true }),
|
||||
...meterSceneChecks(sampleDataset, pixelStats),
|
||||
];
|
||||
assertChecks(`08-meter-scene-${viewport.name}`, sampleChecks);
|
||||
meterSceneSamples.push({
|
||||
viewport,
|
||||
screenshotPath,
|
||||
pixelStats,
|
||||
dataset: sampleDataset,
|
||||
status: sampleChecks.every((item) => item.pass) ? "PASS" : "FAIL",
|
||||
checks: sampleChecks,
|
||||
});
|
||||
}
|
||||
await page.setViewport({ width: 1600, height: 1200, deviceScaleFactor: 1 });
|
||||
const meterSceneChecksAll = meterSceneSamples.flatMap((sample) => sample.checks);
|
||||
const meterSceneReport = {
|
||||
generatedAt: new Date().toISOString(),
|
||||
targetUrl: report.targetUrl,
|
||||
caseName: "08-meter-scene-desktop-mobile",
|
||||
summary: "浏览器米尺度 Three.js 证据:desktop/mobile canvas 非空、路径 bounds 为米、预览稳定居中",
|
||||
status: meterSceneChecksAll.every((item) => item.pass) ? "PASS" : "FAIL",
|
||||
samples: meterSceneSamples,
|
||||
consoleErrors,
|
||||
};
|
||||
await fs.writeFile(
|
||||
path.join(OUTPUT_DIR, "meter-scene-evidence.json"),
|
||||
`${JSON.stringify(meterSceneReport, null, 2)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
report.cases.push({
|
||||
name: "08-meter-scene-desktop-mobile",
|
||||
summary: meterSceneReport.summary,
|
||||
status: meterSceneReport.status,
|
||||
screenshotDir: METER_SCENE_SCREENSHOT_DIR,
|
||||
output: path.join(OUTPUT_DIR, "meter-scene-evidence.json"),
|
||||
sampleCount: meterSceneSamples.length,
|
||||
});
|
||||
|
||||
await page.evaluate(() => {
|
||||
window.webRtcp5AxisSimulation.dispatch({
|
||||
type: "LOAD_PROGRAM",
|
||||
filename: "operator-g20-g21-mixed-units.ngc",
|
||||
content: [
|
||||
"G90 G20",
|
||||
"G1 X1.0 Y0 Z0 F10",
|
||||
"G21",
|
||||
"G1 X25.4 Y25.4 Z0 F254",
|
||||
"M2",
|
||||
].join("\n"),
|
||||
});
|
||||
});
|
||||
await waitForState((state) => (
|
||||
state.activeProgram === "operator-g20-g21-mixed-units.ngc" &&
|
||||
state.programExecutionSourceMode === "linuxcnc-interpreter-wasm" &&
|
||||
(state.programExecution?.motion || []).some((event) => event.linearUnits === "inch") &&
|
||||
(state.programExecution?.motion || []).some((event) => event.linearUnits === "mm")
|
||||
), 20000, "G20/G21 mixed unit program loaded");
|
||||
await waitForCanvasReady();
|
||||
await wait(600);
|
||||
dataset = await getCanvasDataset();
|
||||
const mixedUnitsState = await page.evaluate(() => JSON.parse(JSON.stringify(window.webRtcp5AxisSimulation.getState())));
|
||||
checks = [
|
||||
...previewChecks(dataset, { requirePathPoints: true, requireExecutedPath: true }),
|
||||
...mixedUnitsChecks(dataset, mixedUnitsState),
|
||||
];
|
||||
assertChecks("09-g20-g21-mixed-units-preview", checks);
|
||||
await captureCase("09-g20-g21-mixed-units-preview");
|
||||
await recordCase("09-g20-g21-mixed-units-preview", "G20/G21 混合单位程序:验证 canonical motion 保留 inch/mm,Three.js 统一按米绘制", checks);
|
||||
|
||||
await page.select('[data-action="select-linuxcnc-gcode-source"]', "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/impeller-7bl-xyzac.ngc");
|
||||
await waitForState((state) => state.activeProgram?.endsWith("impeller-7bl-xyzac.ngc"), 25000, "vendored impeller restored before run");
|
||||
await waitForCanvasReady();
|
||||
await wait(800);
|
||||
|
||||
await page.evaluate(() => document.querySelector('[data-action="power"]').click());
|
||||
await waitForState((state) => state.machine.taskState === "on", 10000, "machine powered on");
|
||||
await page.evaluate(() => document.querySelector('[data-action="mode-manual"]').click());
|
||||
@@ -323,6 +572,88 @@ try {
|
||||
await captureCase("06-running-rtcp-toolpath");
|
||||
await recordCase("06-running-rtcp-toolpath", "G-code 运行态:验证 task/HAL runtime feedback 驱动执行轨迹、当前段高亮、TCP 球和刀轴线跟随", checks);
|
||||
|
||||
await page.evaluate(() => document.querySelector('[data-action="STOP"]').click());
|
||||
await waitForState((state) => state.taskHalStatusLoop?.active === false, 10000, "previous run stopped");
|
||||
await page.select('[data-action="select-linuxcnc-gcode-source"]', "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins_test_1.ngc");
|
||||
await waitForState((state) => (
|
||||
state.activeProgram?.endsWith("xyzac_switchkins_test_1.ngc") &&
|
||||
state.taskHalSession?.programPath?.endsWith("xyzac_switchkins_test_1.ngc") &&
|
||||
state.iniConfigReadiness?.ready === true &&
|
||||
state.profile?.kinematicsModuleId === "xyzac-trt"
|
||||
), 25000, "run feedback selected program ready");
|
||||
await waitForCanvasReady();
|
||||
await page.evaluate(() => {
|
||||
const state = window.webRtcp5AxisSimulation.getState();
|
||||
if (state.machine?.taskState !== "on") {
|
||||
document.querySelector('[data-action="power"]').click();
|
||||
}
|
||||
});
|
||||
await waitForState((state) => state.machine.taskState === "on", 10000, "machine powered on for run feedback");
|
||||
await page.evaluate(() => document.querySelector('[data-action="mode-manual"]').click());
|
||||
await waitForState((state) => state.machine.mode === "manual", 10000, "manual mode selected for run feedback");
|
||||
await page.evaluate(() => document.querySelector('[data-action="HOME"]').click());
|
||||
await waitForState((state) => state.machine.allHomed === true, 10000, "machine homed for run feedback");
|
||||
await page.evaluate(() => document.querySelector('[data-action="mode-auto"]').click());
|
||||
await waitForState((state) => state.machine.mode === "auto", 10000, "auto mode selected for run feedback");
|
||||
const runFeedbackBaseline = await snapshotRunFeedback(0, "07-run-preconditions-and-feedback-0000ms-before-run");
|
||||
await page.evaluate(() => document.querySelector('[data-action="RUN"]').click());
|
||||
try {
|
||||
await waitForState((state) => (
|
||||
["running", "complete"].includes(state.runState) &&
|
||||
(state.taskHalStatusLoop?.tickCount > runFeedbackBaseline.state.taskHalStatusLoop.tickCount ||
|
||||
(state.programRuntimeFeedbackHistory?.length || 0) >= 3) &&
|
||||
state.programRuntimeFeedback?.sourceMode === "linuxcnc-task-motion-hal-wasm"
|
||||
), 15000, "run feedback status loop produced feedback");
|
||||
} catch (error) {
|
||||
const failureSnapshot = await snapshotRunFeedback(15000, "07-run-preconditions-and-feedback-timeout");
|
||||
await fs.writeFile(
|
||||
path.join(OUTPUT_DIR, "run-preconditions-feedback-timeout.json"),
|
||||
`${JSON.stringify({
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
baseline: runFeedbackBaseline,
|
||||
failureSnapshot,
|
||||
consoleErrors,
|
||||
}, null, 2)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
const runFeedbackStartedAt = Date.now();
|
||||
const runFeedbackSamples = [];
|
||||
for (const elapsedMs of [200, 500, 1000, 2000, 5000]) {
|
||||
const waitMs = Math.max(runFeedbackStartedAt + elapsedMs - Date.now(), 0);
|
||||
if (waitMs > 0) await wait(waitMs);
|
||||
await waitForCanvasReady();
|
||||
runFeedbackSamples.push(await snapshotRunFeedback(elapsedMs, `07-run-preconditions-and-feedback-${elapsedMs}ms`));
|
||||
}
|
||||
const runFeedbackCaseChecks = runFeedbackChecks(runFeedbackSamples, runFeedbackBaseline);
|
||||
assertChecks("07-run-preconditions-and-feedback", runFeedbackCaseChecks);
|
||||
const runFeedbackReport = {
|
||||
generatedAt: new Date().toISOString(),
|
||||
targetUrl: report.targetUrl,
|
||||
caseName: "07-run-preconditions-and-feedback",
|
||||
summary: "浏览器 RUN 证据:INI/profile/kinematics/task-HAL 同一上下文,taskHalStatusLoop 持续推进,UI 消费 task/HAL/motion feedback",
|
||||
status: runFeedbackCaseChecks.every((item) => item.pass) ? "PASS" : "FAIL",
|
||||
checks: runFeedbackCaseChecks,
|
||||
baseline: runFeedbackBaseline,
|
||||
samples: runFeedbackSamples,
|
||||
consoleErrors,
|
||||
};
|
||||
await fs.writeFile(
|
||||
path.join(OUTPUT_DIR, "run-preconditions-feedback.json"),
|
||||
`${JSON.stringify(runFeedbackReport, null, 2)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
report.cases.push({
|
||||
name: "07-run-preconditions-and-feedback",
|
||||
summary: runFeedbackReport.summary,
|
||||
status: runFeedbackReport.status,
|
||||
checks: runFeedbackCaseChecks,
|
||||
screenshotDir: RUN_FEEDBACK_SCREENSHOT_DIR,
|
||||
output: path.join(OUTPUT_DIR, "run-preconditions-feedback.json"),
|
||||
sampleCount: runFeedbackSamples.length,
|
||||
});
|
||||
|
||||
report.consoleErrors = consoleErrors;
|
||||
await fs.writeFile(
|
||||
path.join(OUTPUT_DIR, "toolpath-preview-cases.json"),
|
||||
|
||||
Reference in New Issue
Block a user