288 lines
11 KiB
JavaScript
288 lines
11 KiB
JavaScript
import { chromium } from "@playwright/test";
|
|
import assert from "node:assert/strict";
|
|
import { readFile, writeFile } from "node:fs/promises";
|
|
|
|
const url = process.env.FDM_URL || "http://127.0.0.1:4178";
|
|
const response = await fetch(url);
|
|
assert.ok(response.ok, "生产预览服务运行中");
|
|
const browser = await chromium.launch({
|
|
executablePath:
|
|
"C:/Users/ikun/AppData/Local/ms-playwright/chromium_headless_shell-1234/chrome-headless-shell-win64/chrome-headless-shell.exe",
|
|
headless: true,
|
|
args: [
|
|
"--enable-webgl",
|
|
"--use-angle=swiftshader",
|
|
"--enable-unsafe-swiftshader",
|
|
],
|
|
});
|
|
const context = await browser.newContext({
|
|
viewport: { width: 1440, height: 900 },
|
|
deviceScaleFactor: 1,
|
|
});
|
|
const page = await context.newPage();
|
|
const errors = [];
|
|
const reports = [];
|
|
page.on("pageerror", (error) => errors.push(error.message));
|
|
const pixels = () => {
|
|
const source = document.querySelector("#scene canvas");
|
|
const canvas = document.createElement("canvas");
|
|
canvas.width = 100;
|
|
canvas.height = 80;
|
|
const context = canvas.getContext("2d");
|
|
context.drawImage(source, 0, 0, 100, 80);
|
|
const data = context.getImageData(0, 0, 100, 80).data;
|
|
let sum = 0;
|
|
const colors = new Set();
|
|
for (let index = 0; index < data.length; index += 4) {
|
|
sum += data[index] + data[index + 1] + data[index + 2];
|
|
colors.add(`${data[index]},${data[index + 1]},${data[index + 2]}`);
|
|
}
|
|
return { sum, colors: colors.size };
|
|
};
|
|
try {
|
|
const cdp = await context.newCDPSession(page);
|
|
await cdp.send("Network.enable");
|
|
await cdp.send("Network.setCacheDisabled", { cacheDisabled: true });
|
|
const start = Date.now();
|
|
await page.goto(url, { waitUntil: "domcontentloaded" });
|
|
await page.waitForSelector("#loading[hidden]", {
|
|
state: "attached",
|
|
timeout: 60000,
|
|
});
|
|
await page.waitForSelector('#prepare-talk[data-ready="true"]', { timeout: 240000 });
|
|
const loadMs = Date.now() - start;
|
|
await page.screenshot({
|
|
path: "artifacts/monitor-desktop.png",
|
|
fullPage: true,
|
|
});
|
|
const initial = await page.evaluate(pixels);
|
|
assert.ok(initial.colors > 40);
|
|
await page.locator("#layer").fill("400");
|
|
await page.waitForTimeout(400);
|
|
assert.equal(await page.locator("#current-layer").textContent(), "400");
|
|
const paused = await page.locator("#timeline").inputValue();
|
|
await page.waitForTimeout(400);
|
|
assert.equal(await page.locator("#timeline").inputValue(), paused);
|
|
await page.locator("#play").click();
|
|
await page.locator("#speed").selectOption("120");
|
|
await page.waitForTimeout(1000);
|
|
assert.notEqual(await page.locator("#timeline").inputValue(), paused);
|
|
await page.locator("#play").click();
|
|
const moving = await page.evaluate(pixels);
|
|
assert.notEqual(moving.sum, initial.sum);
|
|
await page.locator('[data-view="top"]').click();
|
|
await page.waitForTimeout(300);
|
|
assert.notEqual((await page.evaluate(pixels)).sum, moving.sum);
|
|
await page.locator('[data-view="perspective"]').click();
|
|
await page.locator("#layer").fill("714");
|
|
await page.waitForTimeout(300);
|
|
assert.equal(await page.locator("#current-layer").textContent(), "714");
|
|
await page.screenshot({
|
|
path: "artifacts/monitor-complete.png",
|
|
fullPage: true,
|
|
});
|
|
await page.locator("#only-layer").check();
|
|
await page.waitForTimeout(300);
|
|
const isolated = await page.evaluate(pixels);
|
|
await page.locator("#only-layer").uncheck();
|
|
await page.waitForTimeout(300);
|
|
assert.notEqual((await page.evaluate(pixels)).sum, isolated.sum);
|
|
await page.locator("#layer").fill("1");
|
|
await page.waitForTimeout(250);
|
|
assert.equal(await page.locator("#current-layer").textContent(), "1");
|
|
await page.locator("#next-layer").click();
|
|
await page.waitForTimeout(250);
|
|
assert.equal(await page.locator("#current-layer").textContent(), "2");
|
|
await page.locator("#previous-layer").click();
|
|
await page.waitForTimeout(250);
|
|
assert.equal(await page.locator("#current-layer").textContent(), "1");
|
|
await page.locator('[data-technology="nozzle"]').click();
|
|
await page.waitForTimeout(500);
|
|
const assembled = await page.evaluate(pixels);
|
|
await page.locator("#disassemble").click();
|
|
await page.waitForTimeout(1200);
|
|
assert.notEqual((await page.evaluate(pixels)).sum, assembled.sum);
|
|
await page.screenshot({
|
|
path: "artifacts/technology-nozzle.png",
|
|
fullPage: true,
|
|
});
|
|
await page.locator("#assemble").click();
|
|
await page.locator('[data-technology="thermal"]').click();
|
|
await page.locator("#thermal-fault").click();
|
|
await page.waitForTimeout(400);
|
|
assert.ok(
|
|
Number(
|
|
(await page.locator("#nozzle-reading").textContent()).replace("°C", ""),
|
|
) < 210,
|
|
);
|
|
await page.locator("#vision-fault").click();
|
|
await page.waitForTimeout(300);
|
|
assert.ok(
|
|
(await page.locator("#vision-state").textContent()).includes("预警"),
|
|
);
|
|
await page.screenshot({
|
|
path: "artifacts/technology-thermal.png",
|
|
fullPage: true,
|
|
});
|
|
await page.locator("#clear-fault").click();
|
|
await page.locator("#nozzle-target").fill("235");
|
|
await page.waitForTimeout(250);
|
|
assert.ok(
|
|
(await page.locator("#nozzle-target-value").textContent()).includes("235"),
|
|
);
|
|
assert.equal(
|
|
await page.locator("#follow").getAttribute("aria-pressed"),
|
|
"false",
|
|
);
|
|
assert.equal(await page.locator("#nozzle-command").textContent(), "220");
|
|
await page.locator("#reset-thermal").click();
|
|
await page.waitForTimeout(250);
|
|
assert.equal(
|
|
await page.locator("#follow").getAttribute("aria-pressed"),
|
|
"true",
|
|
);
|
|
const downloadPromise = page.waitForEvent("download");
|
|
await page.locator("#export").click();
|
|
const download = await downloadPromise;
|
|
await download.saveAs("artifacts/monitor-export.csv");
|
|
await page.locator("#fullscreen").click();
|
|
await page.locator('[data-technology="model"]').click();
|
|
await page.locator("#workflow").click();
|
|
await page.waitForFunction(
|
|
() => document.querySelector('[data-stage="1"]').classList.contains("active"),
|
|
null,
|
|
{ timeout: 15000 },
|
|
);
|
|
assert.equal(
|
|
await page.locator('[data-stage="1"]').getAttribute("class"),
|
|
"active",
|
|
);
|
|
await page.locator("#workflow").click();
|
|
await page.locator("#layer").fill("300");
|
|
await page.locator("#slow-layer").click();
|
|
await page.waitForTimeout(1200);
|
|
assert.equal(await page.locator("#speed").inputValue(), ".25");
|
|
assert.equal(await page.locator("#only-layer").isChecked(), true);
|
|
assert.equal(await page.locator("#current-layer").textContent(), "300");
|
|
await page.locator("#play").click();
|
|
await page.waitForTimeout(350);
|
|
const pathEvidence = await page.evaluate(() => {
|
|
const scene = document.querySelector("#scene");
|
|
const index = Number(scene.dataset.segment);
|
|
return {
|
|
index,
|
|
position: JSON.parse(scene.dataset.position),
|
|
command: document.querySelector("#raw-command").textContent,
|
|
completed: Number(scene.dataset.completedVertices),
|
|
};
|
|
});
|
|
const originalBytes = await readFile("public/data/print-job.bin");
|
|
const originalPath = new Float32Array(originalBytes.buffer, originalBytes.byteOffset, originalBytes.byteLength / 4);
|
|
const offset = pathEvidence.index * 12;
|
|
pathEvidence.from = [...originalPath.slice(offset, offset + 3)];
|
|
pathEvidence.to = [...originalPath.slice(offset + 3, offset + 6)];
|
|
pathEvidence.line = originalPath[offset + 11];
|
|
for (const [i, axis] of ["x", "y", "z"].entries())
|
|
assert.ok(
|
|
pathEvidence.position[axis] >=
|
|
Math.min(pathEvidence.from[i], pathEvidence.to[i]) - 0.001 &&
|
|
pathEvidence.position[axis] <=
|
|
Math.max(pathEvidence.from[i], pathEvidence.to[i]) + 0.001,
|
|
);
|
|
assert.ok(pathEvidence.command.startsWith("G1"));
|
|
await page.screenshot({
|
|
path: "artifacts/technology-layer.png",
|
|
fullPage: true,
|
|
});
|
|
await page.locator("#only-layer").uncheck();
|
|
await page.locator('[data-view="perspective"]').click();
|
|
assert.ok(await page.evaluate(() => !!document.fullscreenElement));
|
|
await page.locator("#fullscreen").click();
|
|
await page.locator("#layer").fill("580");
|
|
for (const [width, height] of [
|
|
[1920, 1080],
|
|
[1440, 900],
|
|
[1366, 768],
|
|
[1280, 720],
|
|
[390, 844],
|
|
]) {
|
|
await page.setViewportSize({ width, height });
|
|
await page.waitForTimeout(500);
|
|
const layout = await page.evaluate(() => {
|
|
const rect = (selector) =>
|
|
document.querySelector(selector).getBoundingClientRect().toJSON();
|
|
return {
|
|
width: innerWidth,
|
|
height: innerHeight,
|
|
scrollWidth: document.documentElement.scrollWidth,
|
|
scrollHeight: document.documentElement.scrollHeight,
|
|
scene: rect("#scene"),
|
|
process: rect(".process"),
|
|
bottom: rect(".technology-strip"),
|
|
controlBody: rect('[data-panel="model"]'),
|
|
boundary: rect('[data-panel="model"] .tech-footnote'),
|
|
footer: rect("footer"),
|
|
readings: rect(".readings"),
|
|
options: rect(".display-options"),
|
|
};
|
|
});
|
|
assert.ok(layout.scrollWidth <= width + 1, `横向溢出 ${width}`);
|
|
if (width >= 1100) {
|
|
assert.ok(
|
|
layout.scrollHeight <= height + 1,
|
|
`单屏溢出 ${width}: ${layout.scrollHeight}`,
|
|
);
|
|
assert.ok(layout.process.bottom <= layout.bottom.top, "主区和底部重叠");
|
|
assert.ok(
|
|
layout.boundary.bottom <= layout.controlBody.bottom + 1,
|
|
"控制区溢出",
|
|
);
|
|
assert.ok(
|
|
layout.options.bottom <= layout.readings.bottom + 1,
|
|
"读数区溢出",
|
|
);
|
|
}
|
|
assert.ok((await page.evaluate(pixels)).colors > 40);
|
|
await page.screenshot({
|
|
path: `artifacts/monitor-${width}.png`,
|
|
fullPage: true,
|
|
});
|
|
reports.push(layout);
|
|
if (width >= 1100) {
|
|
for (const mode of ["nozzle", "thermal"]) {
|
|
await page.locator(`[data-technology="${mode}"]`).click();
|
|
await page.waitForTimeout(350);
|
|
const panel = await page
|
|
.locator(`[data-panel="${mode}"]`)
|
|
.evaluate((element) => ({
|
|
bottom: element.getBoundingClientRect().bottom,
|
|
children: [...element.children]
|
|
.filter((child) => child.getBoundingClientRect().height > 0)
|
|
.map((child) => ({
|
|
name: child.className,
|
|
bottom: child.getBoundingClientRect().bottom,
|
|
})),
|
|
}));
|
|
assert.ok(
|
|
panel.children.every((child) => child.bottom <= panel.bottom + 1),
|
|
`${mode} 面板溢出 ${width}: ${JSON.stringify(panel)}`,
|
|
);
|
|
await page.screenshot({
|
|
path: `artifacts/${mode}-${width}.png`,
|
|
fullPage: true,
|
|
});
|
|
}
|
|
await page.locator('[data-technology="model"]').click();
|
|
}
|
|
}
|
|
assert.deepEqual(errors, []);
|
|
const report = { loadMs, initial, moving, pathEvidence, reports, errors };
|
|
await writeFile(
|
|
"artifacts/monitor-report.json",
|
|
JSON.stringify(report, null, 2),
|
|
);
|
|
console.log(JSON.stringify(report, null, 2));
|
|
} finally {
|
|
await browser.close();
|
|
}
|