126 lines
9.2 KiB
JavaScript
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;
|
|
}
|