Files
FreeCADWasmDev/tools/partdesign-datum-clone-business-cases.mjs
wangdequan 9a7c4f0335 Save FreeCAD Web workspace sources and user validation evidence
IFC native binding remains incomplete; preserve accurate test results.
2026-09-15 22:01:58 +08:00

121 lines
7.0 KiB
JavaScript

import { loadWasmProbe } from './wasm-probe-runtime.mjs';
export const datumCloneIds = ['Clone', 'CompDatums', 'CompSketches', 'CoordinateSystem', 'Line']
.map(name => `PartDesign:desktop:PartDesign_${name}`);
export async function runDatumCloneCases(ids, loadWorker) {
if (!ids.length) return [];
const { kernel } = await loadWasmProbe('web/public/kernel', 'FreeCADWebPartDesignRuntimeProbe');
const worker = await loadWorker(kernel);
let sequence = 0;
const router = { request: (_module, command, payload, document) =>
worker.send(worker.request(`datum-clone-${++sequence}`, command, payload, document)) };
const results = [];
for (const capabilityId of ids) {
const document = `DatumClone${process.pid}_${sequence}`;
const cases = [];
const record = (input, actual, checks) => {
const assertions = Object.entries(checks).map(([name, passed]) => ({ name, passed: Boolean(passed) }));
cases.push({ input, actual, assertions });
if (assertions.some(check => !check.passed)) throw new Error('native business assertion failed');
};
const native = async (action, args = {}) => {
const response = await router.request('PartDesign', 'partdesign.document', {
schema: 'freecad-web-partdesign/document/v1', module: 'PartDesign', document, action, ...args,
}, document);
if (response.kind !== 'result') throw new Error(JSON.stringify(response));
return response.result;
};
try {
await native('create');
await native('createBody', { object: 'SourceBody' });
let selected;
const execute = args => worker.desktop(router, capabilityId.split(':').at(-1), args, document,
(_document, selection) => { selected = selection; });
if (capabilityId.endsWith('_Clone')) {
await native('setSketch', { object: 'Profile', body: 'SourceBody', profileType: 'Rectangle',
parameters: { Width: 3, Height: 4 }, position: [0, 0, 0] });
await native('setFeature', { object: 'Pad', body: 'SourceBody', profile: 'Profile', featureType: 'Pad', length: 5 });
const input = { object: 'Clone', body: 'CloneBody', selection: [{ documentId: document, object: 'Pad' }] };
const actual = await execute(input);
const clone = actual.result?.objects?.find(object => object.name === 'Clone');
record(input, actual, { 'native linked clone': clone?.type === 'PartDesign::FeatureBase' && clone.base === 'Pad'
&& clone.body === 'CloneBody' && Math.abs(clone.volume - 60) < 1e-7,
'selection updated': selected?.[0]?.object === 'Clone' });
const changed = await native('setFeature', { object: 'Pad', body: 'SourceBody', profile: 'Profile', featureType: 'Pad', length: 7 });
record({ length: 7 }, changed, { 'clone recomputes from source': Math.abs(changed.objects.find(o => o.name === 'Clone').volume - 84) < 1e-7 });
} else {
const sketch = capabilityId.endsWith('_CompSketches');
const input = sketch ? { object: 'Profile', body: 'SourceBody', profileType: 'Rectangle',
parameters: { Width: 7, Height: 2 }, position: [1, 2, 3] }
: { object: 'Datum', body: 'SourceBody', origin: [2, -3, 5] };
const actual = await execute(input);
const object = actual.result?.objects?.find(o => o.name === input.object);
const type = sketch ? 'Sketcher::SketchObject' : capabilityId.endsWith('_CompDatums') ? 'PartDesign::Plane'
: capabilityId.endsWith('_Line') ? 'PartDesign::Line' : 'PartDesign::CoordinateSystem';
record(input, actual, { 'default child creates native type': object?.type === type,
'native placement': JSON.stringify(object?.position) === JSON.stringify(input.position ?? input.origin),
'native body ownership': object?.body === 'SourceBody',
'selection updated': selected?.[0]?.object === input.object });
if (capabilityId.endsWith('_Line')) {
const moved = { ...input, origin: [-4, 7, 11] };
const updated = await execute(moved);
const line = updated.result?.objects?.find(o => o.name === input.object);
record(moved, updated, {
'same native datum updated': line?.type === 'PartDesign::Line' && line.body === 'SourceBody'
&& updated.result.objects.filter(o => o.name === input.object).length === 1,
'updated native placement': JSON.stringify(line?.position) === JSON.stringify(moved.origin),
});
const undone = await native('undo');
record({ action: 'undo' }, undone, {
'undo restores original placement': JSON.stringify(undone.objects.find(o => o.name === input.object)?.position)
=== JSON.stringify(input.origin),
});
const redone = await native('redo');
record({ action: 'redo' }, redone, {
'redo restores edited placement': JSON.stringify(redone.objects.find(o => o.name === input.object)?.position)
=== JSON.stringify(moved.origin),
});
await native('createBody', { object: 'OtherBody' });
for (const invalid of [
{ ...moved, body: 'OtherBody' },
{ ...moved, object: 'SourceBody' },
{ ...moved, origin: [1, 2] },
]) {
const beforeInvalid = await native('snapshot');
const rejected = await execute(invalid);
const afterInvalid = await native('snapshot');
record(invalid, rejected, {
'invalid datum edit rejected without mutation': rejected.kind === 'error'
&& JSON.stringify(beforeInvalid) === JSON.stringify(afterInvalid),
});
}
const beforeAttachment = await native('snapshot');
const attached = { ...input, selection: [{ documentId: document, object: 'Datum' }] };
const rejectedAttachment = await execute(attached);
const afterAttachment = await native('snapshot');
record(attached, rejectedAttachment, {
'unsupported attachment leaves document unchanged': rejectedAttachment.kind === 'error'
&& JSON.stringify(beforeAttachment) === JSON.stringify(afterAttachment),
});
}
}
const before = await native('snapshot');
const rejected = await execute({ object: 'Rejected', activeTask: true });
const after = await native('snapshot');
record({ activeTask: true }, rejected, { 'active task rejected without mutation': rejected.kind === 'error'
&& JSON.stringify(before) === JSON.stringify(after) });
await native('undo');
await native('redo');
} catch (error) {
cases.push({ error: String(error), assertions: [{ name: 'business workflow completed', passed: false }] });
} finally {
try { await native('close'); } catch (error) {
cases.push({ error: String(error), assertions: [{ name: 'document closed', passed: false }] });
}
}
results.push({ capabilityId, status: cases.some(c => c.assertions.some(a => !a.passed)) ? 'failed' : 'passed', cases });
}
return results;
}