import { expect, type Page } from "@playwright/test"; const physicalPort = ( name: string, side: "left" | "right", nominalRole: "inlet" | "outlet" | "bidirectional", ) => ({ name, kind: "physical", domain: "pneumatic", nominalRole, positiveFlowDirection: "intoComponent", side, order: side === "left" ? 0 : 1, }); export const componentCatalog = { schemaVersion: 1, libraries: [ { id: "e2e-library", label: "自动化测试组件库", version: "1.0.0", sourcePackage: "tests.e2e", temporary: true, order: 1, components: [ { type: "amesim_ideal_air_medium", label: "空气介质定义", modelType: "amesim_ideal_air_medium", modelVersion: "0.2.0", symbol: "amesim_ideal_air_medium", order: 1, category: { id: "media", label: "介质物性", order: 1, }, role: "amesimGasMediumDefinition", ports: [], parameters: [ { name: "gi", label: "介质定义索引(gi)", quantity: "dimensionless", unit: "", default: 1, minimum: 1, maximum: 99, minimumExclusive: false, description: "介质定义索引由画布自动分配;索引 0 保留给内置理想空气。", }, { name: "property_model", label: "物性计算模型", quantity: "dimensionless", unit: "", default: 0, minimum: 0, maximum: 0, minimumExclusive: false, editor: "amesimGasPropertyModel", options: [{ value: 0, label: "理想气体" }], description: "选择空气介质采用的物性计算模型。", }, ], }, { type: "amesim_helium_medium", label: "氦气介质定义", modelType: "amesim_helium_medium", modelVersion: "0.1.0", symbol: "amesim_helium_medium", order: 2, category: { id: "media", label: "介质物性", order: 1, }, role: "amesimGasMediumDefinition", ports: [], parameters: [ { name: "gi", label: "介质定义索引(gi)", quantity: "dimensionless", unit: "", default: 1, minimum: 1, maximum: 99, minimumExclusive: false, description: "介质定义索引由画布自动分配;索引 0 保留给内置理想空气。", }, { name: "property_model", label: "物性计算模型", quantity: "dimensionless", unit: "", default: 0, minimum: 0, maximum: 0, minimumExclusive: false, editor: "amesimGasPropertyModel", options: [{ value: 0, label: "Peng–Robinson" }], description: "氦气当前使用 Peng–Robinson 状态方程计算物性。", }, ], }, { type: "amesim_pneumatic_test", label: "AMESim 气动测试元件", modelType: "amesim_pneumatic_test", modelVersion: "1.0.0", symbol: "symbol-not-registered", order: 3, category: { id: "pneumatic", label: "气动元件", order: 2, }, ports: [ physicalPort("port_a", "left", "bidirectional"), physicalPort("port_b", "right", "bidirectional"), ], parameters: [ { name: "gi", label: "介质物性模型(gi)", quantity: "dimensionless", unit: "", default: 0, minimum: 0, maximum: 99, minimumExclusive: false, editor: "amesimGasReference", description: "选择该气动元件引用的画布介质定义索引;0 表示内置理想空气。", }, { name: "reference_pressure", label: "参考压力", quantity: "pressure", unit: "Pa", default: 100000, minimum: 0, minimumExclusive: true, description: "用于验证参数表单位换算和 XML 基础单位输出的参考压力。", }, { name: "reference_temperature", label: "参考温度", quantity: "temperature", unit: "K", default: 300, minimum: 0, minimumExclusive: false, description: "用于验证带偏移量单位的数值换算和显示精度。", }, ], }, { type: "generic_sensor", label: "通用测试元件", modelType: "generic_sensor", modelVersion: "1.0.0", symbol: "symbol-not-registered", order: 4, category: { id: "generic", label: "通用组件", order: 1, }, ports: [ physicalPort("port_a", "left", "bidirectional"), physicalPort("port_b", "right", "bidirectional"), ], parameters: [ { name: "gain", label: "增益", quantity: "dimensionless", unit: "", default: 1, minimumExclusive: false, description: "传感器输入值的无量纲放大倍数。", }, ], }, ], }, ], }; const projectPort = ( name: string, side: "left" | "right", nominalRole: "inlet" | "outlet" | "bidirectional", ) => ({ name, kind: "physical", domain: "pneumatic", nominalRole, positiveFlowDirection: "intoComponent", side, }); export const wideProject = { name: "demo-system", nodes: Array.from({ length: 4 }, (_, index) => ({ id: `generic_sensor_${index + 1}`, type: "simulationComponent", position: { x: index * 620, y: index % 2 === 0 ? 0 : 260, }, data: { label: `generic_sensor_${index + 1}`, componentType: "generic_sensor", modelType: "generic_sensor", symbol: "symbol-not-registered", ports: [ projectPort("port_a", "left", "bidirectional"), projectPort("port_b", "right", "bidirectional"), ], parameters: { gain: 1 }, parameterUnits: { gain: "" }, rotation: 0, mirrored: false, }, })), edges: Array.from({ length: 3 }, (_, index) => ({ id: `edge-${index + 1}`, source: `generic_sensor_${index + 1}`, target: `generic_sensor_${index + 2}`, sourceHandle: "port_b", targetHandle: "port_a", })), simulation: { t_start: 0, t_stop: 10, step: 0.1, max_step: 0.05, method: "RK45", }, }; export const resultSnapshot = { id: "e2e-result", createdAt: "2026-07-28T10:00:00.000Z", project: wideProject, result: { success: true, status: "completed", partial: false, message: "Simulation completed", simulatedUntil: 10, requestedStopTime: 10, variables: [ { key: "generic_sensor_1.value", componentId: "generic_sensor_1", componentType: "generic_sensor", scope: "component", portName: null, name: "value", label: "数值", quantity: "dimensionless", unit: "", category: "state", order: 1, }, ], final: { "generic_sensor_1.value": 1 }, series: { time: [0, 5, 10], "generic_sensor_1.value": [0, 0.5, 1], }, diagnostics: { pressureFlow: { solveCount: 3, maxScaledResidual: 0, maxEvaluationsPerSolve: 1, }, stream: { maxIterationsPerSolve: 1, }, stateCount: 1, sampleCount: 3, }, }, }; export async function prepareApp(page: Page) { await page.addInitScript(() => { const preparedKey = "system-simulation-flow:e2e-storage-prepared"; if (window.sessionStorage.getItem(preparedKey) !== "1") { window.localStorage.clear(); window.sessionStorage.clear(); window.sessionStorage.setItem(preparedKey, "1"); } window.location.hash = "#/modeling"; }); await page.route("**/api/components/catalog", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(componentCatalog), }); }); } export async function expectAllNodesInsideCanvas( page: Page, canvasSelector: string, expectedCount = wideProject.nodes.length, ) { const canvas = page.locator(canvasSelector); await expect(canvas).toBeVisible(); const nodes = canvas.locator(".react-flow__node"); await expect(nodes).toHaveCount(expectedCount); await expect .poll( async () => page.evaluate( ({ selector, count }) => { const canvasElement = document.querySelector(selector); const nodeElements = Array.from( canvasElement?.querySelectorAll(".react-flow__node") ?? [], ); if (!canvasElement || nodeElements.length !== count) { return false; } const canvasBounds = canvasElement.getBoundingClientRect(); const tolerance = 3; return nodeElements.every((node) => { const bounds = node.getBoundingClientRect(); return ( bounds.left >= canvasBounds.left - tolerance && bounds.top >= canvasBounds.top - tolerance && bounds.right <= canvasBounds.right + tolerance && bounds.bottom <= canvasBounds.bottom + tolerance ); }); }, { selector: canvasSelector, count: expectedCount }, ), { message: `所有节点应完整显示在 ${canvasSelector} 内` }, ) .toBe(true); }