旧版前端工程文件导入时版本对比查验、审阅与仿真时部分阻挡功能实现;前端参数输入格式统一规范

This commit is contained in:
lujingze committed 2026-09-12 14:15:40 +00:00
1 parent 22579e51c9
commit 44b6ea74ab
32 files changed
+2087 -322

No files matched your search

@@ -0,0 +1,125 @@
// Real catalog, project UI, HTTP and native execution in the rehearsal sandbox.
import { chromium } from '../../frontend/node_modules/playwright/index.mjs';
import fs from 'node:fs/promises';
import path from 'node:path';
import assert from 'node:assert/strict';
const [input, output, url = 'http://127.0.0.1:8036'] = process.argv.slice(2);
if (!input || !output) throw new Error('Usage: node browser_component_registration.mjs PROJECT.json OUTPUT_DIR [URL]');
await fs.mkdir(output, { recursive: true });
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1600, height: 1000 }, acceptDownloads: true });
page.setDefaultTimeout(15000);
const errors = [];
page.on('pageerror', e => errors.push(String(e)));
async function download(label, file) {
const promise = page.waitForEvent('download');
await page.getByRole('button', { name: label, exact: true }).click();
await (await promise).saveAs(path.join(output, file));
return path.join(output, file);
}
try {
await page.goto(url);
const catalog = await (await page.request.get(`${url}/api/components/catalog`)).json();
assert(catalog.libraries.some(x => x.id === 'registration_demo'));
const palette = page.getByRole('button', { name: '注册演练斜坡信号', exact: true });
await palette.waitFor();
await palette.dragTo(page.locator('.react-flow__pane').first(), { targetPosition: { x: 500, y: 300 } });
await page.locator('.react-flow__node').first().waitFor();
const paletteProject = JSON.parse(await fs.readFile(await download('导出工程 JSON', 'palette-created.json'), 'utf8'));
assert.equal(paletteProject.nodes.length, 1);
assert.equal(paletteProject.nodes[0].data.modelType, 'registration_demo_ramp');
assert.deepEqual(paletteProject.nodes[0].data.parameters, { offset: 2, amplitude: 3, duration: 1 });
const validProject = JSON.parse(await fs.readFile(input, 'utf8'));
const invalidProject = structuredClone(validProject);
invalidProject.nodes[0].data.ports[0].positiveFlowDirection = null;
await page.locator('input[type="file"][accept*=".json"]').setInputFiles({
name: 'invalid-catalog-port.json', mimeType: 'application/json',
buffer: Buffer.from(JSON.stringify(invalidProject)),
});
await page.getByText('导入失败:文件不是有效的 ReactFlow 工程 JSON', { exact: true }).waitFor();
assert.equal(await page.locator('.react-flow__node').count(), 1);
await page.locator('input[type="file"][accept*=".json"]').setInputFiles(path.resolve(input));
await page.locator('.react-flow__node[data-id="ramp_1"]').click();
const timeUnits = await page.getByRole('combobox', { name: '时长单位', exact: true }).locator('option').evaluateAll(options => options.map(o => o.value));
assert.deepEqual(timeUnits, ['s']);
const duration = page.getByRole('textbox', { name: '时长', exact: true });
assert.equal(await duration.inputValue(), '1');
await duration.fill('2');
await duration.press('Tab');
const projectPath = await download('导出工程 JSON', 'edited-project.json');
const project = JSON.parse(await fs.readFile(projectPath, 'utf8'));
assert.equal(project.nodes[0].data.parameters.duration, 2);
assert.equal(project.nodes[0].data.parameterUnits.duration, 's');
assert.equal(project.nodes[0].data.ports[0].name, 'out');
assert.equal(project.nodes.length, 4);
assert.equal(project.edges.length, 3);
await page.getByRole('button', { name: '生成系统 XML', exact: true }).click();
const xml = await fs.readFile(await download('下载系统 XML', 'browser-input.xml'), 'utf8');
assert.match(xml, /name="duration" value="2"/);
assert.match(xml, /type="registration_demo_ramp"/);
await page.screenshot({ path: path.join(output, 'model.png'), fullPage: true });
const rows = [];
let lastResult;
for (let index = 0; index < 2; index++) {
const marker = await page.evaluate(() => sessionStorage.getItem('system-simulation-flow:latest-result'));
const responsePromise = page.waitForResponse(r => r.url().endsWith('/api/system-xml/simulate-stream'));
await page.getByRole('button', { name: '运行仿真', exact: true }).click();
const response = await responsePromise;
await response.finished();
assert(response.ok());
await page.waitForFunction(old => {
const value = sessionStorage.getItem('system-simulation-flow:latest-result');
return value && value !== old && JSON.parse(value).storage === 'indexeddb';
}, marker);
await page.getByRole('tab', { name: /^结果/ }).click();
const data = JSON.parse(await fs.readFile(await download('下载结果文件', `run-${index}.simresult`), 'utf8'));
const result = data.snapshot.result;
assert(result.success);
assert.equal(result.simulatedUntil, 1);
assert.equal(result.series.time.length, 11);
assert(result.series['force_1.force']);
result.series.time.forEach((t, i) => assert(Math.abs(result.series['force_1.force'][i] - (2 + 1.5*t)) < 1e-12));
result.series.time.forEach((t, i) => {
assert(Math.abs(result.series['ramp_1.y'][i] - (2 + 1.5*t)) < 1e-12);
assert.equal(result.series['ramp_1.y'][i], result.series['ramp_1.out.signal'][i]);
});
const massErrors = { x: 0, v: 0 };
result.series.time.forEach((t, i) => {
// m=10, F=2+1.5t, x0=v0=0; analytic free-mass motion.
massErrors.x = Math.max(massErrors.x, Math.abs(result.series['mass_1.x'][i] - (.1*t*t + .025*t*t*t)));
massErrors.v = Math.max(massErrors.v, Math.abs(result.series['mass_1.v'][i] - (.2*t + .075*t*t)));
});
assert(massErrors.x < 1e-7 && massErrors.v < 1e-7);
if (index === 1) assert(result.diagnostics.native.cacheHit);
rows.push({ run: index, samples: 11, cacheHit: result.diagnostics.native.cacheHit, analyticPassed: true, massErrors });
lastResult = result;
if (!index) await page.getByRole('tab', { name: '建模', exact: true }).click();
}
await page.getByRole('button', { name: '适应系统图窗口', exact: true }).click();
await page.locator('.results-system-panel .react-flow__node[data-id="ramp_1"]').click();
await page.locator('.results-variable-list button').first().click();
await page.locator('.results-chart-panel .result-chart-window svg').first().waitFor();
const csv = await fs.readFile(await download('下载结果 CSV', 'result.csv'), 'utf8');
const records = csv.replace(/^\uFEFF/, '').trim().split(/\r?\n/).map(s => s.split(','));
assert.equal(records.length, 12);
const signalColumns = records[0].flatMap((key, i) => /ramp_1[.](y|out[.]signal)/.test(key) ? [i] : []);
assert.equal(signalColumns.length, 2);
for (let i = 1; i < records.length; i++) {
const values = records[i].map(Number);
for (const j of signalColumns) assert(Math.abs(values[j] - (2 + 1.5*values[0])) < 1e-12);
}
await page.screenshot({ path: path.join(output, 'result.png'), fullPage: true });
await page.reload();
await page.getByRole('tab', { name: /^结果/ }).click();
const restored = JSON.parse(await fs.readFile(await download('下载结果文件', 'restored.simresult'), 'utf8'));
assert.deepEqual(restored.snapshot.result, lastResult);
assert.deepEqual(errors, []);
await fs.writeFile(path.join(output, 'summary.json'), JSON.stringify({ url, browser: browser.version(),
mocked: false, paletteCreation: true, catalogDrivenParameters: true, timeUnits, siParameterEdit: true,
invalidCatalogSnapshotRejected: true, projectExport: true, xmlExport: true, chartVisible: true, rows, csvAnalyticPassed: true, restoredIdentical: true, errors }, null, 2));
console.log('Browser registration rehearsal passed.');
} catch (error) {
await page.screenshot({ path: path.join(output, 'failure.png'), fullPage: true });
await fs.writeFile(path.join(output, 'failure.txt'), `${error.stack}\n${await page.locator('body').innerText()}`);
throw error;
} finally { await browser.close(); }