Files
FreeCADWasmDev/tools/techdraw-browser-cases.mjs

446 lines
33 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 });
}