446 lines
33 KiB
JavaScript
446 lines
33 KiB
JavaScript
import assert from "node:assert/strict";
|
||
import fs from "node:fs";
|
||
import os from "node:os";
|
||
import path from "node:path";
|
||
import { spawn } from "node:child_process";
|
||
import { once } from "node:events";
|
||
|
||
const origin = process.env.FREECAD_BROWSER_ORIGIN ?? "http://127.0.0.1:5188";
|
||
const profile = fs.mkdtempSync(path.join(os.tmpdir(), "freecad-techdraw-browser-"));
|
||
const artifacts = path.resolve("docs/current/techdraw-browser");
|
||
fs.mkdirSync(artifacts, { recursive: true });
|
||
const chrome = spawn("/usr/bin/google-chrome", ["--headless=new", "--no-sandbox", "--disable-dev-shm-usage", "--remote-debugging-port=0", `--user-data-dir=${profile}`, "about:blank"], { stdio: "ignore" });
|
||
const closed = once(chrome, "exit");
|
||
let socket;
|
||
const pending = new Map();
|
||
let sequence = 0;
|
||
async function until(label, action, predicate, timeout = 30000) {
|
||
const deadline = Date.now() + timeout;
|
||
let value;
|
||
while (Date.now() < deadline) {
|
||
value = await action();
|
||
if (predicate(value)) return value;
|
||
await new Promise(resolve => setTimeout(resolve, 100));
|
||
}
|
||
throw new Error(`${label}: ${JSON.stringify(value)}`);
|
||
}
|
||
async function send(method, params = {}) {
|
||
const id = ++sequence;
|
||
const detail = method === "Runtime.evaluate" ? String(params.expression).replace(/\s+/g, " ").slice(0, 240) : params.url ?? "";
|
||
const started = performance.now();
|
||
if (process.env.FREECAD_TECHDRAW_TRACE === "1") fs.writeSync(2, `TechDraw browser start ${id} ${method}: ${detail}\n`);
|
||
return new Promise((resolve, reject) => {
|
||
const timer = setTimeout(() => { pending.delete(id); reject(new Error(`${method} timed out at ${detail}`)); }, 45000);
|
||
pending.set(id, { resolve: result => {
|
||
if (process.env.FREECAD_TECHDRAW_TRACE === "1") fs.writeSync(2, `TechDraw browser completed ${id}: ${(performance.now() - started).toFixed(1)} ms\n`);
|
||
resolve(result);
|
||
}, reject, timer });
|
||
socket.send(JSON.stringify({ id, method, params }));
|
||
});
|
||
}
|
||
async function evaluate(expression) {
|
||
const result = await send("Runtime.evaluate", { expression, awaitPromise: true, returnByValue: true });
|
||
if (result.exceptionDetails) throw new Error(JSON.stringify(result.exceptionDetails));
|
||
return result.result?.value;
|
||
}
|
||
try {
|
||
const port = await until("Chrome startup", () => {
|
||
try { return fs.readFileSync(path.join(profile, "DevToolsActivePort"), "utf8").split("\n")[0]; } catch { return null; }
|
||
}, Boolean);
|
||
const targets = await fetch(`http://127.0.0.1:${port}/json/list`).then(r => r.json());
|
||
socket = new WebSocket(targets.find(t => t.type === "page").webSocketDebuggerUrl);
|
||
socket.addEventListener("message", event => {
|
||
const message = JSON.parse(event.data);
|
||
if (process.env.FREECAD_TECHDRAW_TRACE === "1" && message.method === "Runtime.consoleAPICalled") {
|
||
fs.writeSync(2, `TechDraw browser console: ${JSON.stringify(message.params.args.map(value => value.value ?? value.description))}\n`);
|
||
}
|
||
const request = pending.get(message.id);
|
||
if (!request) return;
|
||
pending.delete(message.id); clearTimeout(request.timer);
|
||
if (message.error) request.reject(new Error(JSON.stringify(message.error)));
|
||
else request.resolve(message.result);
|
||
});
|
||
await new Promise((resolve, reject) => { socket.addEventListener("open", resolve, { once: true }); socket.addEventListener("error", reject, { once: true }); });
|
||
await send("Page.enable");
|
||
await send("Runtime.enable");
|
||
await send("Emulation.setDeviceMetricsOverride", { width: 1440, height: 900, deviceScaleFactor: 1, mobile: false });
|
||
if (["balloon", "active-view", "clip-group"].includes(process.env.FREECAD_TECHDRAW_BROWSER_CASE)) {
|
||
const activeView = process.env.FREECAD_TECHDRAW_BROWSER_CASE === "active-view";
|
||
const clipGroup = process.env.FREECAD_TECHDRAW_BROWSER_CASE === "clip-group";
|
||
const capabilityId = clipGroup ? "TechDraw:desktop:TechDraw_ClipGroup" : activeView ? "TechDraw:desktop:TechDraw_ActiveView" : "TechDraw:desktop:TechDraw_Balloon";
|
||
const cases = [];
|
||
await send("Page.navigate", { url: `${origin}/workbench.html?module=TechDraw&command=techdraw.createRecompute` });
|
||
await until("Workbench ready", () => evaluate("!!document.querySelector('#payload-editor')"), Boolean);
|
||
const run = async (command, args, desktop = false) => {
|
||
const selector = desktop ? ".matrix-list button" : "button.command";
|
||
await until(command, () => evaluate(`Array.from(document.querySelectorAll(${JSON.stringify(selector)})).some(button=>button.querySelector('span')?.textContent===${JSON.stringify(command)})`), Boolean);
|
||
await evaluate(`Array.from(document.querySelectorAll(${JSON.stringify(selector)})).find(button=>button.querySelector('span')?.textContent===${JSON.stringify(command)}).click()`);
|
||
await evaluate(`{const input=document.querySelector('#payload-editor');Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype,'value').set.call(input,${JSON.stringify(JSON.stringify(args))});input.dispatchEvent(new Event('input',{bubbles:true}));}`);
|
||
const before = await evaluate("window.__freecadWorkbenchState?.response?.requestId");
|
||
await evaluate("document.querySelector('#payload-editor + button.run').click()");
|
||
const completed = await until(command, () => evaluate("window.__freecadWorkbenchState"),
|
||
value => !!value?.response && value.response.requestId !== before);
|
||
return completed.response;
|
||
};
|
||
if (clipGroup) {
|
||
const documentId = "ClipGroupBrowser";
|
||
const isolatedId = "ClipGroupBrowserIsolation";
|
||
const drawing = { schema: "freecad-web-techdraw/frontend-drawing/v1", module: "TechDraw", action: "create/recompute", documentId };
|
||
for (const id of [isolatedId, documentId])
|
||
assert.equal((await run("techdraw.createRecompute", { ...drawing, documentId: id, box: { length: 18, width: 9, height: 6 } })).kind, "result");
|
||
const created = [];
|
||
for (const [index, dimensions] of [{ width: 48, height: 36, x: 12, y: -4, showFrame: true },
|
||
{ width: 72, height: 24, x: -8, y: 6, showFrame: false }].entries()) {
|
||
const input = { documentId, selection: [{ documentId, object: "View" }], ...dimensions };
|
||
const response = await run("TechDraw_ClipGroup", input, true);
|
||
assert.equal(response.kind, "result", JSON.stringify(response));
|
||
const actual = response.result.records.find(record => record.apiId === capabilityId);
|
||
assert.equal(actual.status, "exposed", JSON.stringify(actual));
|
||
assert.equal(actual.nativeApplied, true);
|
||
const state = actual.result;
|
||
assert.equal(state.document, documentId);
|
||
assert.equal(state.page, "Page");
|
||
assert.equal(state.type, "TechDraw::DrawViewClip");
|
||
for (const [key, value] of Object.entries(dimensions)) assert.equal(state[key], value);
|
||
assert.deepEqual(state.members, []);
|
||
assert.ok(state.views.includes("View") && state.views.includes(state.object));
|
||
assert.ok(!created.includes(state.object));
|
||
created.push(state.object);
|
||
const displayed = await until("Clip group state displayed", () => evaluate("document.querySelector('[aria-label=\"Clip group\"]')?.textContent"),
|
||
text => text?.includes(state.object) && text.includes(`${dimensions.width} × ${dimensions.height} mm`));
|
||
const read = await run("TechDraw_ClipGroup", { documentId, page: "Page", object: state.object, action: "read", selection: [] }, true);
|
||
assert.equal(read.kind, "result", JSON.stringify(read));
|
||
assert.equal(read.result.records[0].documentChanged, false);
|
||
assert.deepEqual(read.result.records[0].result, state);
|
||
const retained = await run("techdraw.createRecompute", drawing);
|
||
assert.equal(retained.result.document.objectCount, 5 + index);
|
||
cases.push({ input, actual: { ...actual, read, displayed, objectCount: retained.result.document.objectCount },
|
||
assertions: [{ name: "Workbench creates distinct native clip groups and displays persisted dimensions and membership", passed: true }] });
|
||
}
|
||
for (const args of [{ action: "cancel" }, { page: "Missing" }, { width: 0 },
|
||
{ selection: [{ documentId: isolatedId, object: "View" }] }]) {
|
||
const input = { documentId, selection: [], ...args };
|
||
const actual = await run("TechDraw_ClipGroup", input, true);
|
||
if (args.action === "cancel") {
|
||
assert.equal(actual.kind, "result", JSON.stringify(actual));
|
||
assert.equal(actual.result.records[0].status, "cancelled");
|
||
} else assert.equal(actual.kind, "error", JSON.stringify(actual));
|
||
assert.equal((await run("techdraw.createRecompute", drawing)).result.document.objectCount, 6);
|
||
assert.equal((await run("techdraw.createRecompute", { ...drawing, documentId: isolatedId })).result.document.objectCount, 4);
|
||
cases.push({ input, actual, assertions: [{ name: "Browser rejection and cancellation preserve clip groups and the isolated document", passed: true }] });
|
||
}
|
||
const recovered = await run("TechDraw_ClipGroup", { documentId, page: "Page", width: 21, height: 17, selection: [] }, true);
|
||
assert.equal(recovered.kind, "result", JSON.stringify(recovered));
|
||
assert.equal(recovered.result.records[0].result.width, 21);
|
||
assert.equal((await run("techdraw.createRecompute", drawing)).result.document.objectCount, 7);
|
||
cases.push({ input: { documentId, width: 21, height: 17 }, actual: recovered,
|
||
assertions: [{ name: "Browser clip creation recovers after invalid requests without resetting the document", passed: true }] });
|
||
} else if (activeView) {
|
||
const documentId = "ActiveViewBrowser";
|
||
const drawing = { schema: "freecad-web-techdraw/frontend-drawing/v1", module: "TechDraw", action: "create/recompute", documentId };
|
||
const images = [];
|
||
for (const [index, box] of [{ length: 18, width: 9, height: 6 }, { length: 7, width: 12, height: 4 }].entries()) {
|
||
assert.equal((await run("techdraw.createRecompute", { ...drawing, box })).kind, "result");
|
||
const loaded = await run("TechDraw_ActiveView", { documentId, action: "read", selection: [] }, true);
|
||
assert.equal(loaded.kind, "result", JSON.stringify(loaded));
|
||
const read = loaded.result.records.find(record => record.apiId === capabilityId);
|
||
assert.ok(["success", "exposed"].includes(read.status), JSON.stringify(read));
|
||
assert.equal(read.result.document, documentId);
|
||
assert.equal(read.result.displayObjects[0].name, "Box");
|
||
const capture = await until("Current-document viewport capture", () => evaluate(`(async()=>{
|
||
try { const {captureTechDrawActiveView}=await import('/src/techdraw-desktop-adapter.ts');
|
||
return captureTechDrawActiveView(${JSON.stringify(documentId)}); } catch { return null; }
|
||
})()`), value => typeof value?.image === "string");
|
||
const pixels = await evaluate(`(async()=>{
|
||
const bitmap=new Image();bitmap.src=${JSON.stringify(capture.image)};await bitmap.decode();
|
||
const canvas=document.createElement('canvas');canvas.width=bitmap.width;canvas.height=bitmap.height;
|
||
const context=canvas.getContext('2d');context.drawImage(bitmap,0,0);
|
||
const data=context.getImageData(0,0,canvas.width,canvas.height).data;
|
||
const colors=new Set();for(let i=0;i<data.length;i+=4) colors.add(data[i]+','+data[i+1]+','+data[i+2]);
|
||
return {width:canvas.width,height:canvas.height,colors:colors.size};
|
||
})()`);
|
||
assert.ok(pixels.width > 100 && pixels.height > 100 && pixels.colors > 8, JSON.stringify(pixels));
|
||
images.push(capture.image);
|
||
const foreignCapture = await evaluate(`(async()=>{
|
||
const {captureTechDrawActiveView}=await import('/src/techdraw-desktop-adapter.ts');
|
||
try {captureTechDrawActiveView('ForeignDocument');return {rejected:false};}
|
||
catch(error){return {rejected:true,message:error.message};}
|
||
})()`);
|
||
assert.equal(foreignCapture.rejected, true, JSON.stringify(foreignCapture));
|
||
const input = { documentId, page: "Page", scale: index + 1, x: 30 + index * 20, y: 40, selection: [] };
|
||
const response = await run("TechDraw_ActiveView", input, true);
|
||
assert.equal(response.kind, "result", JSON.stringify(response));
|
||
const actual = response.result.records.find(record => record.apiId === capabilityId);
|
||
assert.ok(["success", "exposed"].includes(actual.status), JSON.stringify(actual));
|
||
assert.equal(actual.nativeApplied, true);
|
||
assert.equal(actual.documentChanged, true);
|
||
assert.equal(actual.result.document, documentId);
|
||
assert.equal(actual.result.page, "Page");
|
||
assert.ok(actual.result.svg.includes(capture.image));
|
||
const stored = await until("Active view OPFS SVG", () => evaluate(`(async()=>{
|
||
try {const root=await navigator.storage.getDirectory();const td=await root.getDirectoryHandle('TechDraw');
|
||
const doc=await td.getDirectoryHandle(${JSON.stringify(documentId)});
|
||
return await (await (await doc.getFileHandle(${JSON.stringify(actual.result.view + ".svg")})).getFile()).text();
|
||
}catch{return null;}
|
||
})()`), value => value === actual.result.svg);
|
||
assert.equal(stored, actual.result.svg);
|
||
const retained = await run("techdraw.createRecompute", drawing);
|
||
assert.equal(retained.result.document.objectCount, 5 + index);
|
||
cases.push({ input, actual: { ...actual, pixels, foreignCapture, objectCount: retained.result.document.objectCount },
|
||
assertions: [{ name: "Workbench loads native geometry, captures nonblank viewport and persists the exact embedded drawing in OPFS", passed: true },
|
||
{ name: "Viewport capture rejects a different document instead of reusing current pixels", passed: true }] });
|
||
}
|
||
assert.notEqual(images[0], images[1], "Changed geometry must change the captured drawing");
|
||
const cancelled = await run("TechDraw_ActiveView", { documentId, action: "cancel" }, true);
|
||
assert.equal(cancelled.kind, "result", JSON.stringify(cancelled));
|
||
assert.equal(cancelled.result.records[0].status, "cancelled");
|
||
const rejected = await run("TechDraw_ActiveView", { documentId, action: "read", selection: [{ documentId: "ForeignDocument", object: "Box" }] }, true);
|
||
assert.equal(rejected.kind, "error", JSON.stringify(rejected));
|
||
const retained = await run("techdraw.createRecompute", drawing);
|
||
assert.equal(retained.result.document.objectCount, 6);
|
||
cases.push({ input: { documentId, actions: ["cancel", "foreign-document read"] }, actual: { cancelled, rejected, objectCount: retained.result.document.objectCount },
|
||
assertions: [{ name: "Workbench cancellation and foreign selection preserve both captured views", passed: true }] });
|
||
const loaded = await run("TechDraw_ActiveView", { documentId, action: "read", selection: [] }, true);
|
||
assert.equal(loaded.kind, "result", JSON.stringify(loaded));
|
||
for (const [width, height] of [[1440, 900], [390, 844]]) {
|
||
await send("Emulation.setDeviceMetricsOverride", { width, height, deviceScaleFactor: 1, mobile: width < 700 });
|
||
const screenshot = await send("Page.captureScreenshot", { format: "png", captureBeyondViewport: true });
|
||
fs.writeFileSync(path.join(artifacts, `active-view-${width}.png`), Buffer.from(screenshot.data, "base64"));
|
||
}
|
||
} else {
|
||
const documentId = "BalloonBrowser";
|
||
const drawing = { schema: "freecad-web-techdraw/frontend-drawing/v1", module: "TechDraw", action: "create/recompute", documentId };
|
||
assert.equal((await run("techdraw.createRecompute", { ...drawing, box: { length: 18, width: 9, height: 6 } })).kind, "result");
|
||
for (const [index, origin] of [[1, [4, -3]], [2, [-6, 5]]]) {
|
||
const input = { documentId, page: "Page", origin,
|
||
selection: [{ documentId, object: "View" }] };
|
||
const response = await run("TechDraw_Balloon", input, true);
|
||
assert.equal(response.kind, "result", JSON.stringify(response));
|
||
const actual = response.result.records.find(record => record.apiId === capabilityId);
|
||
assert.equal(actual.status, "exposed", JSON.stringify(actual));
|
||
assert.equal(actual.result.owner, "View");
|
||
assert.equal(actual.result.document, documentId);
|
||
assert.equal(actual.result.text, String(index));
|
||
assert.deepEqual(actual.result.origin, origin);
|
||
assert.deepEqual(actual.result.position, origin.map(value => value + 20));
|
||
assert.ok(actual.result.views.includes(actual.result.object));
|
||
cases.push({ input, actual, assertions: [{ name: "Workbench selection reaches native balloon owner, numbering and placement", passed: true }] });
|
||
}
|
||
for (const input of [{ documentId, action: "cancel" }, { documentId, page: "Page", origin: [1, 2],
|
||
selection: [{ documentId: "AnotherDocument", object: "View" }] }]) {
|
||
const actual = await run("TechDraw_Balloon", input, true);
|
||
if (input.action === "cancel") {
|
||
assert.equal(actual.kind, "result", JSON.stringify(actual));
|
||
assert.equal(actual.result.records[0].status, "cancelled");
|
||
} else assert.equal(actual.kind, "error", JSON.stringify(actual));
|
||
const retained = await run("techdraw.createRecompute", drawing);
|
||
assert.equal(retained.kind, "result", JSON.stringify(retained));
|
||
assert.equal(retained.result.document.objectCount, 6);
|
||
cases.push({ input, actual: { response: actual, objectCount: retained.result.document.objectCount },
|
||
assertions: [{ name: "Workbench cancellation and foreign selection preserve the document", passed: true }] });
|
||
}
|
||
}
|
||
console.log(JSON.stringify({ schema: "freecad-web-business-cases/v1", results: [{ capabilityId, status: "passed", cases }] }));
|
||
} else {
|
||
await send("Page.navigate", { url: `${origin}/techdraw.html` });
|
||
const state = () => evaluate("({isolated:crossOriginIsolated,state:window.__freecadTechDrawState,text:document.querySelector('[role=status]')?.textContent})");
|
||
const initial = await until("Native drawing", state, value => ["success", "error"].includes(value?.state?.stage));
|
||
assert.equal(initial.state.stage, "success", JSON.stringify(initial));
|
||
assert.equal(initial.state.response.result.volume, 17280);
|
||
await evaluate(`{ const set = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set;
|
||
for (const [id, value] of [['length','11'],['width','6'],['height','3']]) {
|
||
const input = document.getElementById(id); set.call(input, value); input.dispatchEvent(new Event('input', {bubbles:true}));
|
||
} }`);
|
||
await evaluate("document.getElementById('run').click()");
|
||
await until("Edited drawing", state, value => Math.abs((value?.state?.response?.result?.volume ?? 0) - 198) < 1e-8);
|
||
await send("Page.setDownloadBehavior", { behavior: "allow", downloadPath: profile });
|
||
await evaluate("document.getElementById('export').click()");
|
||
const download = path.join(profile, "TechDrawDrawing.svg");
|
||
await until("SVG download", () => fs.existsSync(download), Boolean);
|
||
assert.ok(fs.readFileSync(download, "utf8").includes("<path"));
|
||
const checks = [];
|
||
for (const [width, height] of [[1440, 900], [390, 844]]) {
|
||
await send("Emulation.setDeviceMetricsOverride", { width, height, deviceScaleFactor: 1, mobile: width < 700 });
|
||
const layout = await evaluate(`(async () => {
|
||
const image = document.querySelector('img.drawing'); await image.decode();
|
||
const canvas = document.createElement('canvas'); canvas.width=320;canvas.height=240;
|
||
const context = canvas.getContext('2d');context.drawImage(image,0,0,320,240);
|
||
const pixels=context.getImageData(0,0,320,240).data;let painted=0;
|
||
for(let i=0;i<pixels.length;i+=4) if(pixels[i+3]>20) painted++;
|
||
return {painted,overflow:document.documentElement.scrollWidth>innerWidth,
|
||
bounds:[image.getBoundingClientRect().width,image.getBoundingClientRect().height]};
|
||
})()`);
|
||
assert.equal(layout.overflow, false);
|
||
assert.ok(layout.painted > 100, JSON.stringify(layout));
|
||
const screenshot = await send("Page.captureScreenshot", { format: "png", captureBeyondViewport: true });
|
||
fs.writeFileSync(path.join(artifacts, `${width}.png`), Buffer.from(screenshot.data, "base64"));
|
||
checks.push({ width, height, ...layout });
|
||
}
|
||
const routed = await evaluate(`(async()=>{
|
||
const {WebModuleRouter}=await import('/src/module-router.ts'); const router=new WebModuleRouter();
|
||
try {await router.loadManifest('/modules/manifest.json');const result=router.request('TechDraw','techdraw.createRecompute',
|
||
{schema:'freecad-web-techdraw/frontend-drawing/v1',module:'TechDraw',action:'create/recompute',documentId:'RouterDrawing',box:{length:5,width:7,height:2}});
|
||
const drawing=await result;
|
||
const saved=await router.request('TechDraw','techdraw.nativeSaveReopen',
|
||
{schema:'freecad-web-techdraw/save-reopen/v1',module:'TechDraw',action:'save/reopen',sourceEntry:'src/Mod/TechDraw/App/AppTechDraw.cpp',documentId:'RouterDrawing'});
|
||
if(saved.kind!=='result') throw new Error(JSON.stringify(saved));
|
||
return {drawing,saved:{documentId:saved.result.documentId,objectCount:saved.result.objectCount,byteLength:saved.result.bytes.byteLength,size:saved.result.size},loaded:router.listLoaded()};
|
||
}finally{router.dispose();}
|
||
})()`);
|
||
assert.equal(routed.drawing.kind, "result", JSON.stringify(routed));
|
||
assert.ok(Math.abs(routed.drawing.result.volume - 70) < 1e-8);
|
||
assert.equal(routed.saved.documentId, "RouterDrawing");
|
||
assert.equal(routed.saved.objectCount, routed.drawing.result.document.objectCount);
|
||
assert.ok(routed.saved.byteLength > 0);
|
||
assert.equal(routed.saved.byteLength, routed.saved.size);
|
||
assert.equal(routed.loaded.length, 1);
|
||
assert.equal(routed.loaded[0].worker, "techdraw-python.worker.ts");
|
||
await evaluate("document.getElementById('reset').click()");
|
||
await until("Reset drawing", state, value => value?.state?.response?.result?.volume === 17280);
|
||
await send("Emulation.setDeviceMetricsOverride", { width: 1440, height: 900, deviceScaleFactor: 1, mobile: false });
|
||
await send("Page.navigate", { url: `${origin}/workbench.html?module=TechDraw&command=techdraw.createRecompute` });
|
||
await until("Workbench ready", () => evaluate("!!document.querySelector('#payload-editor')"), Boolean);
|
||
const workbench = () => evaluate("window.__freecadWorkbenchState");
|
||
// Route an explicitly named drawing while the shell still has its initial document selected.
|
||
await evaluate(`{
|
||
const input = document.querySelector('#payload-editor');
|
||
const payload = JSON.parse(input.value);
|
||
payload.documentId = 'TechDrawDrawing';
|
||
Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value').set.call(input, JSON.stringify(payload));
|
||
input.dispatchEvent(new Event('input', {bubbles:true}));
|
||
}`);
|
||
await evaluate("document.querySelector('button.run').click()");
|
||
const created = await until("Workbench drawing", workbench, value => !!value?.response);
|
||
assert.equal(created.response.kind, "result", JSON.stringify(created));
|
||
assert.equal(created.response.result.volume, 17280);
|
||
assert.equal(created.response.result.document.name, "TechDrawDrawing");
|
||
assert.equal(await evaluate("document.querySelector('figure figcaption')?.textContent"), "TechDrawDrawing / Page / View");
|
||
assert.ok(await evaluate("Array.from(document.querySelectorAll('.stat')).some(item=>item.textContent.includes('TechDrawDrawing.View'))"));
|
||
let detailRecord;
|
||
if (process.env.FREECAD_TECHDRAW_BROWSER_CASE !== "image") {
|
||
await until("Detail command available", () => evaluate("Array.from(document.querySelectorAll('.matrix-list button')).some(button=>button.querySelector('span')?.textContent==='TechDraw_DetailView')"), Boolean);
|
||
await evaluate("Array.from(document.querySelectorAll('.matrix-list button')).find(button=>button.querySelector('span')?.textContent==='TechDraw_DetailView').click()");
|
||
// Deliberately omit document and baseView: the next command must use the live selection.
|
||
const detailInput = { anchorX: 0, anchorY: 0, radius: 12, scale: 2, x: 180, y: 100, reference: "A" };
|
||
await evaluate(`{const input=document.querySelector('#payload-editor');Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype,'value').set.call(input,${JSON.stringify(JSON.stringify(detailInput))});input.dispatchEvent(new Event('input',{bubbles:true}));}`);
|
||
await evaluate("document.querySelector('button.run').click()");
|
||
const detail = await until("Workbench detail", workbench, value => !!value?.response);
|
||
assert.equal(detail.response.kind, "result", JSON.stringify(detail));
|
||
detailRecord = detail.response.result.records.find(item => item.apiId === "TechDraw:desktop:TechDraw_DetailView");
|
||
assert.ok(["partial", "exposed"].includes(detailRecord?.status), JSON.stringify(detail));
|
||
assert.equal(detailRecord.result.document, "TechDrawDrawing");
|
||
assert.equal(detailRecord.result.baseView, "View");
|
||
assert.ok(detailRecord.result.edgeCount > 0);
|
||
assert.ok(detailRecord.result.svg.includes("<path"));
|
||
assert.equal(await evaluate("document.querySelector('figure figcaption')?.textContent"), `TechDrawDrawing / Page / ${detailRecord.result.view}`);
|
||
}
|
||
await until("Image command available", () => evaluate("Array.from(document.querySelectorAll('.matrix-list button')).some(button=>button.querySelector('span')?.textContent==='TechDraw_Image')"), Boolean);
|
||
await evaluate("Array.from(document.querySelectorAll('.matrix-list button')).find(button=>button.querySelector('span')?.textContent==='TechDraw_Image').click()");
|
||
await evaluate(`{
|
||
const input=document.querySelector('#payload-editor');
|
||
Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype,'value').set.call(input,JSON.stringify({page:'Page',owner:'View'}));
|
||
input.dispatchEvent(new Event('input',{bubbles:true}));
|
||
}`);
|
||
await evaluate(`{
|
||
const bytes=new Uint8Array(58), data=new DataView(bytes.buffer);
|
||
bytes.set([66,77]);data.setUint32(2,58,true);data.setUint32(10,54,true);data.setUint32(14,40,true);
|
||
data.setInt32(18,1,true);data.setInt32(22,1,true);data.setUint16(26,1,true);data.setUint16(28,24,true);
|
||
data.setUint32(34,4,true);bytes.set([0,0,255],54);
|
||
const transfer=new DataTransfer();transfer.items.add(new File([bytes],'red.bmp',{type:'image/bmp'}));
|
||
const input=document.querySelector('input[type=file]');input.files=transfer.files;input.dispatchEvent(new Event('change',{bubbles:true}));
|
||
}`);
|
||
await until("Image upload read", () => evaluate("JSON.parse(document.querySelector('#payload-editor').value).imageBase64"), Boolean);
|
||
await evaluate("document.querySelector('button.run').click()");
|
||
const inserted = await until("Workbench image", workbench, value => !!value?.response);
|
||
assert.equal(inserted.response.kind, "result", JSON.stringify(inserted));
|
||
const imageRecord = inserted.response.result.records.find(item => item.apiId === "TechDraw:desktop:TechDraw_Image");
|
||
assert.ok(["exposed", "success"].includes(imageRecord.status), JSON.stringify(imageRecord));
|
||
assert.equal(imageRecord.code, "native_image_created");
|
||
assert.equal(imageRecord.result.document, "TechDrawDrawing");
|
||
const imagePixels = await evaluate(`(async()=>{
|
||
const image=document.querySelector('img[src^="data:image/bmp"]');await image.decode();
|
||
const canvas=document.createElement('canvas');canvas.width=canvas.height=1;
|
||
const context=canvas.getContext('2d');context.drawImage(image,0,0);
|
||
return Array.from(context.getImageData(0,0,1,1).data);
|
||
})()`);
|
||
assert.deepEqual(imagePixels, [255,0,0,255]);
|
||
const cancelImage = async () => {
|
||
await evaluate("Array.from(document.querySelectorAll('button')).find(button=>button.textContent==='取消').click()");
|
||
const cancelled = await until("Workbench image cancellation", workbench,
|
||
value => value?.response?.kind === "error"
|
||
|| value?.response?.result?.records?.some(record => record.status === "cancelled"));
|
||
assert.equal(cancelled.response.kind, "result", JSON.stringify(cancelled));
|
||
const record = cancelled.response.result.records.find(item => item.apiId === "TechDraw:desktop:TechDraw_Image");
|
||
assert.equal(record?.status, "cancelled", JSON.stringify(cancelled));
|
||
assert.equal(record.code, "image_insertion_cancelled");
|
||
const imageCount = await evaluate("document.querySelectorAll('img[src^=\"data:image/bmp\"]').length");
|
||
assert.equal(imageCount, 1);
|
||
return { record, imageCount };
|
||
};
|
||
const cancelledImage = await cancelImage();
|
||
const executeWorkbench = async (command, args, desktop = false) => {
|
||
await evaluate(`Array.from(document.querySelectorAll(${JSON.stringify(desktop ? '.matrix-list button' : 'button.command')})).find(button=>button.querySelector('span')?.textContent===${JSON.stringify(command)}).click()`);
|
||
await evaluate(`{const input=document.querySelector('#payload-editor');Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype,'value').set.call(input,${JSON.stringify(JSON.stringify(args))});input.dispatchEvent(new Event('input',{bubbles:true}));}`);
|
||
await evaluate("document.querySelector('button.run').click()");
|
||
const result = await until(`Workbench ${command}`, workbench, value => !!value?.response);
|
||
assert.equal(result.response.kind, "result", JSON.stringify(result));
|
||
return result.response.result;
|
||
};
|
||
const drawingArgs = documentId => ({schema:'freecad-web-techdraw/frontend-drawing/v1',module:'TechDraw',action:'create/recompute',documentId});
|
||
await executeWorkbench('techdraw.createRecompute', {...drawingArgs('ImageIsolation'), box:{length:9,width:7,height:5}});
|
||
const greenBytes = Buffer.from(imageRecord.result.imageBase64, 'base64');
|
||
greenBytes.set([0,255,0],54);
|
||
const green = await executeWorkbench('TechDraw_Image', {documentId:'ImageIsolation',page:'Page',owner:'View',imageBase64:greenBytes.toString('base64')}, true);
|
||
assert.equal(green.records[0].code, 'native_image_created', JSON.stringify(green));
|
||
const retainedImages = [];
|
||
for (const [documentId, expected] of [['TechDrawDrawing',[255,0,0,255]],['ImageIsolation',[0,255,0,255]]]) {
|
||
const retained = await executeWorkbench('techdraw.createRecompute', drawingArgs(documentId));
|
||
assert.ok(retained.document.objectCount >= 5);
|
||
const actual = await evaluate(`(async()=>{
|
||
const images=Array.from(document.querySelectorAll('img[src^="data:image/bmp"]'));
|
||
if(images.length!==1) return {count:images.length};
|
||
await images[0].decode();const canvas=document.createElement('canvas');canvas.width=canvas.height=1;
|
||
const context=canvas.getContext('2d');context.drawImage(images[0],0,0);
|
||
return {count:images.length,pixel:Array.from(context.getImageData(0,0,1,1).data),caption:images[0].parentElement.querySelector('figcaption').textContent};
|
||
})()`);
|
||
assert.equal(actual.count,1, `Retained image missing after returning to ${documentId}`);
|
||
assert.deepEqual(actual.pixel,expected);
|
||
assert.ok(actual.caption.startsWith(`${documentId} / Page / `));
|
||
retainedImages.push({documentId,expected,actual,objectCount:retained.document.objectCount});
|
||
}
|
||
for (const [width,height] of [[1440,900],[390,844]]) {
|
||
await send("Emulation.setDeviceMetricsOverride", {width,height,deviceScaleFactor:1,mobile:width<700});
|
||
const screenshot=await send("Page.captureScreenshot",{format:"png",captureBeyondViewport:true});
|
||
fs.writeFileSync(path.join(artifacts,`image-${width}.png`),Buffer.from(screenshot.data,"base64"));
|
||
}
|
||
console.log(JSON.stringify({ schema: "freecad-web-business-cases/v1", results: [{
|
||
capabilityId: "TechDraw:desktop:TechDraw_Image", status: "passed", cases: [{
|
||
input: { file: "red.bmp", pixel: [255,0,0,255], page: "Page", owner: "View", document: "TechDrawDrawing" },
|
||
actual: { imageRecord, imagePixels, checks, retainedImages, routed }, assertions: [
|
||
{ name: "Manifest routing retains the same native drawing for creation and FCStd save", passed: routed.loaded.length === 1 && routed.saved.byteLength > 0 },
|
||
{ name: "Switching documents and recomputing retains each native image and displays only its document pixels",
|
||
passed: retainedImages.length === 2 && retainedImages.every(({actual, expected, documentId}) =>
|
||
actual.count === 1 && actual.pixel.every((value, index) => value === expected[index])
|
||
&& actual.caption.startsWith(`${documentId} / Page / `)) },
|
||
{ name: "Uploaded image inserted through workbench Worker in current document", passed: imageRecord.result.document === "TechDrawDrawing" && imageRecord.code === "native_image_created" },
|
||
{ name: "Browser decodes uploaded native image to expected RGBA pixel", passed: imagePixels.every((value, index) => value === [255,0,0,255][index]) },
|
||
],
|
||
}, {
|
||
input: { documentId: "TechDrawDrawing", page: "Page", owner: "View", action: "cancel", imageBase64: imageRecord.result.imageBase64 },
|
||
actual: cancelledImage,
|
||
assertions: [
|
||
{ name: "Workbench cancellation reaches native image command", passed: cancelledImage.record.status === "cancelled"
|
||
&& cancelledImage.record.code === "image_insertion_cancelled" },
|
||
{ name: "Workbench cancellation preserves the displayed image", passed: cancelledImage.imageCount === 1 },
|
||
],
|
||
}],
|
||
}] }));
|
||
}
|
||
} finally {
|
||
for (const request of pending.values()) clearTimeout(request.timer);
|
||
socket?.close();
|
||
chrome.kill("SIGTERM");
|
||
await closed;
|
||
fs.rmSync(profile, { recursive: true, force: true });
|
||
}
|