// Run against a separately started backend serving the current frontend build. // No mocked catalog or simulation; a fresh browser context protects user state. 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 [baseURL, projectPath, output] = process.argv.slice(2); assert(baseURL && projectPath && output, 'Usage: node tests/manual/system_simulation_skill.mjs BASE_URL PROJECT OUTPUT'); const project = JSON.parse(await fs.readFile(projectPath, 'utf8')); await fs.mkdir(output, { recursive: true }); const browser = await chromium.launch({ headless: true, ...(process.env.PLAYWRIGHT_CHANNEL ? { channel: process.env.PLAYWRIGHT_CHANNEL } : {}), ...(process.env.PLAYWRIGHT_EXECUTABLE_PATH ? { executablePath: process.env.PLAYWRIGHT_EXECUTABLE_PATH } : {}), }); const page = await browser.newPage({ viewport: { width: 1440, height: 1000 }, acceptDownloads: true }); const errors = []; page.on('pageerror', error => errors.push(String(error))); page.setDefaultTimeout(15000); try { await page.goto(baseURL); await page.getByRole('button', { name: '运行仿真', exact: true }).waitFor(); await page.locator('input[type="file"][accept*=".json"]').setInputFiles(path.resolve(projectPath)); const canvasNodes = page.locator('.flow-canvas .react-flow__node'); await page.waitForFunction(count => document.querySelectorAll('.flow-canvas .react-flow__node').length === count, project.nodes.length); assert.equal(await canvasNodes.count(), project.nodes.length); for (const node of project.nodes) { assert.equal(await page.locator(`.flow-canvas .react-flow__node[data-id="${node.id}"]`).count(), 1); } await page.getByRole('button', { name: '检查模型', exact: true }).click(); const expand = page.getByRole('button', { name: '展开仿真控制台', exact: true }); if (await expand.count()) await expand.click(); await page.getByRole('button', { name: '生成系统 XML', exact: true }).click(); const xml = await page.getByLabel('生成的系统 XML', { exact: true }).last().innerText(); assert.match(xml, /schemaVersion="3"/); assert.match(xml, /name="p0" value="250000"/); assert.match(xml, /name="T0" value="293.15"/); await fs.writeFile(path.join(output, 'browser.xml'), xml); const responsePromise = page.waitForResponse(response => response.url().endsWith('/api/system-xml/simulate-stream'), { timeout: 120000 }); await page.getByRole('button', { name: '运行仿真', exact: true }).click(); const response = await responsePromise; assert(response.ok(), await response.text()); const events = (await response.text()).trim().split('\n').map(line => JSON.parse(line)); const resultEvent = events.find(event => event.event === 'result'); assert(resultEvent?.result?.success, JSON.stringify(events.slice(-3))); const result = resultEvent.result; assert.equal(result.simulatedUntil, project.simulation.t_stop); assert(Object.values(result.series).every(values => values.every(Number.isFinite))); const pressure = result.variables.find(variable => variable.componentId === 'tank' && variable.quantity === 'pressure'); assert(pressure && result.series[pressure.key]?.length > 1); assert(result.series[pressure.key].every(value => Math.abs(value - 250000) < 1e-6)); await fs.writeFile(path.join(output, 'result.json'), JSON.stringify(result)); const downloadPromise = page.waitForEvent('download'); await page.getByRole('button', { name: '导出工程 JSON', exact: true }).click(); await (await downloadPromise).saveAs(path.join(output, 'roundtrip.json')); const exported = JSON.parse(await fs.readFile(path.join(output, 'roundtrip.json'), 'utf8')); assert.equal(exported.projectSchemaVersion, 2); assert.equal(exported.nodes.length, project.nodes.length); assert.equal(exported.edges.length, project.edges.length); assert.deepEqual(errors, []); await page.screenshot({ path: path.join(output, 'acceptance.png'), fullPage: true }); const summary = { browser: browser.version(), components: exported.nodes.length, connections: exported.edges.length, simulatedUntil: result.simulatedUntil, sampleCount: result.series.time.length, pressureKey: pressure.key, browserImport: 'passed', browserExport: 'passed', simulation: 'passed', errors }; await fs.writeFile(path.join(output, 'summary.json'), JSON.stringify(summary, null, 2)); console.log(JSON.stringify(summary)); } catch (error) { await fs.writeFile(path.join(output, 'failure.txt'), `${error.stack}\n${await page.locator('body').innerText()}`); await page.screenshot({ path: path.join(output, 'failure.png'), fullPage: true }); throw error; } finally { await browser.close(); }