Files
FreeCADWasmDev/tools/part-make-solid-ui-business-cases.mjs

126 lines
9.2 KiB
JavaScript

import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import { createHash } from 'node:crypto';
import { fileURLToPath } from 'node:url';
import { playwright } from './part-primitive-business-cases.mjs';
import { makeSolidId, runMakeSolidCases as runNativeCases } from './part-make-solid-business-cases.mjs';
export { makeSolidId };
export async function runMakeSolidWorkflowCases(ids, placementCase) {
const results = await runNativeCases(ids, placementCase);
if (!ids.includes(makeSolidId)) return results;
const result = results.find(item => item.capabilityId === makeSolidId);
if (!result) throw Error('Make Solid native case returned no result');
if (result.status !== 'passed') return results;
result.status = 'failed';
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const directory = path.join(root, 'docs/current/part-make-solid-ui-business', new Date().toISOString().replace(/[:.]/g, '-'));
fs.mkdirSync(directory, { recursive: true });
result.evidenceFiles ??= {};
let browser;
try {
browser = await playwright().chromium.launch({ executablePath: process.env.FREECAD_CHROME ?? '/usr/bin/google-chrome',
headless: true, args: ['--no-sandbox', '--disable-dev-shm-usage'] });
const page = await browser.newPage({ viewport: { width: 1440, height: 900 }, deviceScaleFactor: 1 });
const errors = []; page.on('pageerror', error => errors.push(String(error)));
const document = 'SolidUIBusiness';
await page.goto(`${process.env.FREECAD_BROWSER_ORIGIN ?? 'http://127.0.0.1:5180'}/workbench.html?module=Part&command=part.document&document=${document}`);
const run = async payload => {
await page.locator('#payload-editor').fill(JSON.stringify(payload));
await page.evaluate(() => { if (window.__freecadWorkbenchState) window.__freecadWorkbenchState.response = null; });
await page.getByRole('button', { name: '执行选中命令', exact: true }).click();
await page.waitForFunction(() => !!window.__freecadWorkbenchState?.response && !document.querySelector('button.run')?.disabled);
const reply = await page.evaluate(() => window.__freecadWorkbenchState.response);
assert.equal(reply.kind, 'result', JSON.stringify(reply)); return reply.result;
};
await run({ schema: 'freecad-web-part/document/v1', module: 'Part', document, action: 'create' });
await page.locator('aside:nth-child(2) > button.command').filter({ has: page.getByText('part.documentViewCommand', { exact: true }) }).click();
const source = `import FreeCAD as App, Part
d=App.getDocument('${document}')
box=Part.makeBox(2,3,4)
d.addObject('Part::Feature','Shell').Shape=box.Shells[0]
d.addObject('Part::Feature','ExistingSolid').Shape=box
d.addObject('Part::Feature','Wire').Shape=Part.makePolygon([App.Vector(0,0,0),App.Vector(5,0,0)])
d.recompute()`;
const seeded = await run({ schema: 'freecad-web/document-view/v1', owner: 'DocumentProbe', document,
command: 'Std_CommandLine', parameters: { operation: 'submit', source: `exec(${JSON.stringify(source)})` } });
assert.equal(seeded.stderr, '');
const selectTask = async () => {
await page.locator('.matrix-list button').filter({ has: page.locator('span', { hasText: /^Part_MakeSolid$/ }) }).click();
await page.getByLabel('Make solid document', { exact: true }).fill(document);
};
await selectTask();
await page.getByRole('button', { name: 'Read solid document', exact: true }).click();
await page.getByRole('checkbox', { name: 'Solid source Shell', exact: true }).waitFor();
assert.equal(await page.getByRole('button', { name: 'Convert to solid', exact: true }).isDisabled(), true);
for (const name of ['Shell', 'ExistingSolid', 'Wire']) await page.getByRole('checkbox', { name: `Solid source ${name}`, exact: true }).check();
await page.getByRole('button', { name: 'Convert to solid', exact: true }).click();
await page.locator('[data-made-solid="Shell_solid"]').filter({ hasText: '24.000' }).waitFor();
const reply = await page.evaluate(() => window.__freecadWorkbenchState.response);
assert.equal(reply.kind, 'result');
const outcomes = reply.result.makeSolidResults;
assert.equal(outcomes.find(item => item.source === 'Shell').status, 'created');
for (const name of ['ExistingSolid', 'Wire']) assert.equal(outcomes.find(item => item.source === name).status, 'skipped');
const solid = reply.result.objects.find(item => item.name === 'Shell_solid');
assert.equal(solid.type, 'Part::Feature'); assert.equal(solid.topology.Solid, 1); assert.equal(solid.topology.valid, true);
assert.ok(Math.abs(solid.volume - 24) < 1e-7); assert.ok(solid.mesh.triangleCount > 0);
assert.equal(reply.result.objects.length, 4);
for (const name of ['Shell', 'ExistingSolid', 'Wire']) assert.equal(await page.getByRole('checkbox', { name: `Solid source ${name}`, exact: true }).isChecked(), true);
await page.getByRole('button', { name: 'Solid undo', exact: true }).click();
await page.locator('[data-made-solid="Shell_solid"]').waitFor({ state: 'detached' });
await page.getByRole('button', { name: 'Solid redo', exact: true }).click();
await page.locator('[data-made-solid="Shell_solid"]').filter({ hasText: '24.000' }).waitFor();
await page.getByRole('button', { name: 'Save solid document', exact: true }).click();
await page.getByRole('link', { name: 'Open solid in project', exact: true }).waitFor();
const bytes = await page.evaluate(async name => {
const { readDocumentFile } = await import('/src/storage.ts');
return [...new Uint8Array(await readDocumentFile(`Solid-${name}.FCStd`))];
}, document);
assert.ok(bytes.length > 100);
const capture = async name => {
const canvas = page.locator('[aria-label="Solid geometry"] canvas'); await canvas.waitFor();
const png = await canvas.screenshot({ path: path.join(directory, name) });
const colors = await page.evaluate(async base64 => {
const image = new Image(); image.src = `data:image/png;base64,${base64}`; await image.decode();
const canvas = document.createElement('canvas'); canvas.width = image.width; canvas.height = image.height;
const context = canvas.getContext('2d'); context.drawImage(image, 0, 0);
const pixels = context.getImageData(0, 0, canvas.width, canvas.height).data;
const colors = new Set(); for (let i = 0; i < pixels.length; i += 4) colors.add(`${pixels[i]},${pixels[i + 1]},${pixels[i + 2]}`);
return colors.size;
}, png.toString('base64'));
assert.ok(colors > 8, `Canvas contains only ${colors} colors`);
return createHash('sha256').update(png).digest('hex');
};
const initial = await capture('desktop-geometry.png');
const bounds = await page.locator('[aria-label="Solid geometry"] canvas').boundingBox();
await page.mouse.move(bounds.x + bounds.width / 2, bounds.y + bounds.height / 2); await page.mouse.down();
await page.mouse.move(bounds.x + bounds.width / 2 + 70, bounds.y + bounds.height / 2 + 30, { steps: 8 }); await page.mouse.up();
assert.notEqual(await capture('rotated-geometry.png'), initial);
await page.screenshot({ path: path.join(directory, 'desktop.png'), fullPage: true });
await page.reload(); await selectTask();
await page.getByLabel('Solid FCStd file', { exact: true }).setInputFiles({ name: `${document}.FCStd`, mimeType: 'application/octet-stream', buffer: Buffer.from(bytes) });
await page.locator('[data-made-solid="Shell_solid"]').filter({ hasText: '24.000' }).waitFor();
await page.getByRole('checkbox', { name: 'Solid source Shell', exact: true }).check();
await page.getByRole('button', { name: 'Convert to solid', exact: true }).click();
await page.locator('[data-made-solid="Shell_solid001"]').filter({ hasText: '24.000' }).waitFor();
await page.setViewportSize({ width: 390, height: 844 });
await capture('mobile-geometry.png'); await page.screenshot({ path: path.join(directory, 'mobile.png'), fullPage: true });
const layout = await page.getByRole('region', { name: 'Convert to solid', exact: true }).evaluate(element => ({ width: element.clientWidth, scroll: element.scrollWidth }));
assert.ok(layout.scroll <= layout.width + 1, JSON.stringify(layout)); assert.deepEqual(errors, []);
for (const name of ['desktop.png', 'desktop-geometry.png', 'rotated-geometry.png', 'mobile.png', 'mobile-geometry.png']) {
const file = path.join(directory, name); result.evidenceFiles[path.relative(root, file)] = createHash('sha256').update(fs.readFileSync(file)).digest('hex');
}
result.cases.push({ input: { document, selection: ['Shell', 'ExistingSolid', 'Wire'], operations: ['convert', 'undo', 'redo', 'save', 'reload', 'open', 'convert-again'] },
actual: { solid, outcomes, savedBytes: bytes.length, layout }, assertions: [{ name: 'Mounted solid conversion preserves selection, skips ineligible geometry, restores native solids from OPFS and renders interactive desktop/mobile geometry', passed: true }] });
result.status = 'passed';
} catch (error) {
result.status = 'failed'; result.error = `${result.error ?? ''}\nMounted Make Solid workflow: ${error.stack ?? error}`;
} finally {
await browser?.close();
fs.writeFileSync(path.join(directory, 'result.json'), JSON.stringify(result, null, 2));
}
return results;
}