126 lines
7.7 KiB
JavaScript
126 lines
7.7 KiB
JavaScript
// 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(); }
|