// Real browser/HTTP/native execution. No API routes or result data are mocked. import { chromium } from '../../frontend/node_modules/playwright/index.mjs'; import fs from 'node:fs/promises'; import path from 'node:path'; import { performance } from 'node:perf_hooks'; import assert from 'node:assert/strict'; import { createHash } from 'node:crypto'; const [input, output, baseURL = 'http://127.0.0.1:8011', runCount = '3'] = process.argv.slice(2); if (!input || !output) throw new Error('Usage: node browser_native_simulation.mjs MODEL.json OUTPUT_DIR [URL] [RUNS]'); await fs.mkdir(output, { recursive: true }); const inputText = await fs.readFile(input, 'utf8'); const project = JSON.parse(inputText); const curveNodeId = project.nodes.find(node => node.id === 'amesim_pnl0002_10')?.id ?? project.nodes.find(node => node.data.modelType === 'amesim_pnl0002')?.id; if (!curveNodeId) throw new Error('Curve verification requires a PNL0002 component.'); await fs.writeFile(path.join(output, 'input.json'), inputText); const browser = await chromium.launch({ headless: true }); const context = await browser.newContext({ viewport: { width: 1600, height: 1000 }, acceptDownloads: true }); const page = await context.newPage(); page.setDefaultTimeout(30000); const errors = []; page.on('pageerror', error => errors.push(String(error))); const rows = []; try { await page.goto(baseURL); await page.locator('input[type="file"][accept*=".json"]').setInputFiles(path.resolve(input)); await page.getByRole('textbox', { name: '工程', exact: true }).waitFor(); await page.waitForFunction(name => document.querySelector('input[aria-label="工程"]')?.value === name || [...document.querySelectorAll('input')].some(input => input.value === name), path.basename(input, path.extname(input))); await page.screenshot({ path: path.join(output, 'model.png'), fullPage: true }); for (let i = 0; i <= Number(runCount); i++) { const oldMarker = await page.evaluate(() => sessionStorage.getItem('system-simulation-flow:latest-result')); const responsePromise = page.waitForResponse(response => response.url().endsWith('/api/system-xml/simulate-stream'), { timeout: 180000 }); const started = performance.now(); await page.getByRole('button', { name: '运行仿真', exact: true }).click(); const response = await responsePromise; await response.finished(); if (!response.ok()) throw new Error(`Simulation HTTP ${response.status()}`); const receivedMs = performance.now() - started; await page.waitForFunction(old => { const marker = sessionStorage.getItem('system-simulation-flow:latest-result'); return marker && marker !== old && JSON.parse(marker).storage === 'indexeddb'; }, oldMarker, { timeout: 180000 }); await page.getByRole('button', { name: '运行仿真', exact: true }).waitFor({ state: 'visible' }); await page.waitForFunction(() => !document.querySelector('button[aria-label="运行仿真"]')?.disabled); await page.getByRole('tab', { name: /^结果/ }).click(); await page.getByRole('button', { name: '下载结果文件', exact: true }).waitFor(); const readyMs = performance.now() - started; await page.waitForFunction(old => { const marker = sessionStorage.getItem('system-simulation-flow:latest-result'); return marker && marker !== old && JSON.parse(marker).storage === 'indexeddb'; }, oldMarker); const downloadPromise = page.waitForEvent('download'); await page.getByRole('button', { name: '下载结果文件', exact: true }).click(); await (await downloadPromise).saveAs(path.join(output, `run-${i}.simresult`)); const exported = JSON.parse(await fs.readFile(path.join(output, `run-${i}.simresult`), 'utf8')); const result = exported.snapshot.result; if (!result.success) throw new Error(result.message); assert.equal(result.diagnostics.integration.rtol, 1e-8); assert.equal(result.simulatedUntil, project.simulation.t_stop); const row = { run: i, receivedMs, readyMs, sampleCount: result.series.time.length, ...result.diagnostics.native }; rows.push(row); console.log(JSON.stringify(row)); if (i < Number(runCount)) 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=${JSON.stringify(curveNodeId)}]`).click(); await page.locator('.results-variable-list button').filter({ has: page.locator('small', { hasText: /^K$/ }) }).first().click(); await page.locator('.results-chart-panel .result-chart-window svg').first().waitFor(); const csvDownload = page.waitForEvent('download'); await page.getByRole('button', { name: '下载结果 CSV', exact: true }).click(); await (await csvDownload).saveAs(path.join(output, 'result.csv')); await page.screenshot({ path: path.join(output, 'result.png'), fullPage: true }); await page.reload(); await page.getByRole('tab', { name: /^结果/ }).click(); await page.getByRole('button', { name: '下载结果文件', exact: true }).waitFor(); const restored = page.waitForEvent('download'); await page.getByRole('button', { name: '下载结果文件', exact: true }).click(); await (await restored).saveAs(path.join(output, 'restored.simresult')); const restoredData = JSON.parse(await fs.readFile(path.join(output, 'restored.simresult'), 'utf8')); const lastData = JSON.parse(await fs.readFile(path.join(output, `run-${runCount}.simresult`), 'utf8')); assert.deepEqual(restoredData.snapshot.result, lastData.snapshot.result); await fs.writeFile(path.join(output, 'summary.json'), JSON.stringify({ input: path.resolve(input), inputSha256: createHash('sha256').update(inputText).digest('hex'), baseURL, browser: browser.version(), rows, errors, restored: true, restoredResultIdentical: true, csvExported: true }, null, 2)); if (errors.length) throw new Error(`Browser errors: ${errors.join('\n')}`); } catch (error) { await page.screenshot({ path: path.join(output, 'failure.png'), fullPage: true }); await fs.writeFile(path.join(output, 'failure.txt'), `${error.stack}\nPage errors: ${JSON.stringify(errors)}\n${await page.locator('body').innerText()}`); throw error; } finally { await context.close(); await browser.close(); }