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