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(); }