// 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(); }