import { expect, test, type Locator, type Page } from "@playwright/test"; type SymbolCase = { category: string; domain: "medium" | "pneumatic" | "signal" | "mechanical"; modelType: string; }; const symbolCases: SymbolCase[] = [ { category: "media", domain: "medium", modelType: "amesim_ideal_air_medium" }, { category: "media", domain: "medium", modelType: "amesim_helium_medium" }, { category: "storage", domain: "pneumatic", modelType: "amesim_pnch023" }, { category: "storage", domain: "pneumatic", modelType: "amesim_pnch012" }, { category: "flow", domain: "pneumatic", modelType: "amesim_pnor001" }, { category: "flow", domain: "pneumatic", modelType: "amesim_pnvo001_fixed" }, { category: "flow", domain: "pneumatic", modelType: "amesim_pnvo001" }, { category: "flow", domain: "pneumatic", modelType: "amesim_pnl00r" }, { category: "flow", domain: "pneumatic", modelType: "amesim_pnl0001" }, { category: "flow", domain: "pneumatic", modelType: "amesim_pnl0002" }, { category: "flow", domain: "pneumatic", modelType: "amesim_pnl0003" }, { category: "junctions", domain: "pneumatic", modelType: "amesim_pn3node2" }, { category: "junctions", domain: "pneumatic", modelType: "amesim_p4node2" }, { category: "boundary", domain: "pneumatic", modelType: "amesim_pnpl01" }, { category: "signals", domain: "signal", modelType: "amesim_step0" }, { category: "signals", domain: "signal", modelType: "amesim_ud00" }, { category: "mechanical", domain: "mechanical", modelType: "amesim_f000" }, { category: "mechanical", domain: "mechanical", modelType: "amesim_forc" }, { category: "mechanical", domain: "mechanical", modelType: "amesim_mecmas21" }, { category: "mechanical", domain: "mechanical", modelType: "amesim_lstp00a" }, { category: "mechanical", domain: "mechanical", modelType: "amesim_lmechn1" }, { category: "mechanical", domain: "mechanical", modelType: "amesim_pnrp17" }, ]; function physicalPort( name: string, side: "left" | "right", order: number, ) { return { name, kind: "physical", domain: "pneumatic", nominalRole: "bidirectional", positiveFlowDirection: "intoComponent", side, order, }; } function signalPort( name: string, side: "left" | "right", nominalRole: "input" | "output", order: number, ) { return { name, kind: "signal", domain: "signal", nominalRole, side, order, }; } function mechanicalPort( name: string, side: "left" | "right", order: number, ) { return { name, kind: "physical", domain: "mechanical", nominalRole: "bidirectional", positiveFlowDirection: "intoComponent", side, order, }; } function portsForSymbol(modelType: string) { if (modelType === "amesim_pnor001") { return [ physicalPort("port_1", "left", 10), physicalPort("port_2", "right", 20), ]; } if (modelType === "amesim_pnvo001") { return [ signalPort("res", "left", "input", 5), physicalPort("port_2", "left", 10), physicalPort("port_3", "right", 20), ]; } if (modelType === "amesim_step0") { return [signalPort("out", "right", "output", 10)]; } if (modelType === "amesim_pnrp17") { return [ physicalPort("port_1", "left", 10), mechanicalPort("port_3", "left", 20), mechanicalPort("port_4", "left", 30), mechanicalPort("port_2", "right", 40), mechanicalPort("port_5", "right", 50), ]; } return []; } async function dragSymbolToCanvas( page: Page, modelType: string, targetPosition: { x: number; y: number }, ) { const pane = page.locator(".flow-canvas .react-flow__pane"); const paletteItem = page .locator(".palette-item") .filter({ has: page.locator(`[data-component-symbol="${modelType}"]`), }) .first(); await paletteItem.dragTo(pane, { targetPosition }); const node = page .locator( `.flow-canvas .react-flow__node[data-id^="${modelType}_"]`, ) .last(); await expect(node).toBeVisible(); const nodeId = await node.getAttribute("data-id"); expect(nodeId).not.toBeNull(); return page.locator( `.flow-canvas .react-flow__node[data-id="${nodeId}"]`, ); } async function expectSymbolAnchors( node: Locator, anchors: Array<{ portName: string; x: number; y: number }>, ) { for (const anchor of anchors) { const error = await node.evaluate((element, expected) => { const svg = element.querySelector(".component-symbol-svg"); const handle = element.querySelector( `.port-handle[data-port-name="${expected.portName}"]`, ); const matrix = svg?.getScreenCTM(); if (!svg || !handle || !matrix) { return Number.POSITIVE_INFINITY; } const point = svg.createSVGPoint(); point.x = expected.x; point.y = expected.y; const screenPoint = point.matrixTransform(matrix); const handleBounds = handle.getBoundingClientRect(); return Math.hypot( screenPoint.x - (handleBounds.left + handleBounds.width / 2), screenPoint.y - (handleBounds.top + handleBounds.height / 2), ); }, anchor); expect(error).toBeLessThan(2.5); } } async function locatorCenter(locator: Locator) { const bounds = await locator.boundingBox(); expect(bounds).not.toBeNull(); return { x: bounds!.x + bounds!.width / 2, y: bounds!.y + bounds!.height / 2, }; } async function distanceBetween(first: Locator, second: Locator) { const [firstCenter, secondCenter] = await Promise.all([ locatorCenter(first), locatorCenter(second), ]); return Math.hypot( secondCenter.x - firstCenter.x, secondCenter.y - firstCenter.y, ); } async function dragNodePortNearTarget( page: Page, movingNode: Locator, movingPort: Locator, targetPort: Locator, remainingGap = 8, ) { const [dragStart, movingPortCenter, targetPortCenter] = await Promise.all([ locatorCenter(movingNode.locator(".node-symbol")), locatorCenter(movingPort), locatorCenter(targetPort), ]); const deltaX = targetPortCenter.x - movingPortCenter.x; const deltaY = targetPortCenter.y - movingPortCenter.y; const distance = Math.hypot(deltaX, deltaY); expect(distance).toBeGreaterThan(remainingGap); const movementRatio = (distance - remainingGap) / distance; await page.mouse.move(dragStart.x, dragStart.y); await page.mouse.down(); let pointerX = dragStart.x + deltaX * movementRatio; let pointerY = dragStart.y + deltaY * movementRatio; await page.mouse.move(pointerX, pointerY, { steps: 12 }); // 旋转会改变节点外框尺寸,React Flow 可能同时刷新 viewport;根据实际 // Handle 中心再校正两次,确保测试模拟的是用户真正把端口拖到附近。 for (let attempt = 0; attempt < 2; attempt += 1) { const [actualMovingCenter, actualTargetCenter] = await Promise.all([ locatorCenter(movingPort), locatorCenter(targetPort), ]); const correctionX = actualTargetCenter.x - actualMovingCenter.x; const correctionY = actualTargetCenter.y - actualMovingCenter.y; const actualDistance = Math.hypot(correctionX, correctionY); if (actualDistance <= remainingGap + 1) { break; } const correctionRatio = (actualDistance - remainingGap) / actualDistance; pointerX += correctionX * correctionRatio; pointerY += correctionY * correctionRatio; await page.mouse.move(pointerX, pointerY, { steps: 4 }); } } const categoryLabels: Record = { boundary: "边界元件", flow: "流动元件", junctions: "连接元件", mechanical: "机械元件", media: "介质物性", signals: "信号元件", storage: "储能元件", }; test.beforeEach(async ({ page }) => { await page.addInitScript(() => { const preparedKey = "system-simulation-flow:component-symbols-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({ schemaVersion: 1, libraries: [ { id: "amesim", label: "AMESim 组件库", version: "0.2.0", sourcePackage: "tests.e2e.amesim-symbols", temporary: true, order: 1, components: symbolCases.map((item, index) => ({ type: item.modelType, label: item.modelType, modelType: item.modelType, modelVersion: "0.1.0", symbol: item.modelType, order: index + 1, category: { id: item.category, label: categoryLabels[item.category], order: Object.keys(categoryLabels).indexOf(item.category) + 1, }, ports: portsForSymbol(item.modelType), parameters: [], })), }, ], }), }); }); }); test("全部已移植 AMESim 模型使用专属图标及正确领域配色", async ({ page }) => { await page.goto("/"); const palette = page.locator("aside.palette"); await expect(palette.locator('[data-component-symbol^="amesim_"]')).toHaveCount( symbolCases.length, ); await expect(palette.locator('[data-component-symbol="generic"]')).toHaveCount(0); for (const item of symbolCases) { const symbol = palette.locator(`[data-component-symbol="${item.modelType}"]`); await expect(symbol).toHaveCount(1); await expect(symbol.locator("svg")).toHaveAttribute("data-symbol-domain", item.domain); await expect(symbol).toHaveAttribute( "data-symbol-presentation", item.domain === "medium" ? "bare" : "framed", ); } }); test("PNRP17 图标匹配参考结构并将五个端口放在对应接口", async ({ page }) => { await page.goto("/"); const paletteSymbol = page.locator( 'aside.palette [data-component-symbol="amesim_pnrp17"]', ); await expect(paletteSymbol).toHaveCount(1); await expect( paletteSymbol.locator('[stroke="#00af00"], [fill="#00af00"]'), ).toHaveCount(0); const piston = await dragSymbolToCanvas(page, "amesim_pnrp17", { x: 430, y: 300, }); await expectSymbolAnchors(piston, [ { portName: "port_1", x: 25.5, y: 48 }, { portName: "port_3", x: 2, y: 4.4 }, { portName: "port_4", x: 2, y: 24.1 }, { portName: "port_2", x: 62, y: 4.4 }, { portName: "port_5", x: 62, y: 24.1 }, ]); await expect( piston.locator('.port-handle[data-port-name="port_1"]'), ).toHaveClass(/react-flow__handle-bottom/); }); test("AMESim canvas nodes use icon anchors and highlight only compatible free ports", async ({ page, }) => { await page.goto("/"); const firstOrifice = await dragSymbolToCanvas(page, "amesim_pnor001", { x: 250, y: 210, }); const secondOrifice = await dragSymbolToCanvas(page, "amesim_pnor001", { x: 610, y: 210, }); const stepSource = await dragSymbolToCanvas(page, "amesim_step0", { x: 250, y: 450, }); const signalOrifice = await dragSymbolToCanvas(page, "amesim_pnvo001", { x: 610, y: 450, }); await expect(firstOrifice.locator(".sim-node")).toHaveClass( /component-icon-only/, ); await expect(firstOrifice.locator(".node-label")).toBeHidden(); const frameStyle = await firstOrifice.locator(".sim-node-frame").evaluate( (element) => ({ borderWidth: getComputedStyle(element).borderTopWidth, background: getComputedStyle(element).backgroundColor, }), ); expect(frameStyle).toEqual({ borderWidth: "0px", background: "rgba(0, 0, 0, 0)", }); const firstOrificeAnchors = [ { portName: "port_1", x: 2, y: 24 }, { portName: "port_2", x: 62, y: 24 }, ]; await expectSymbolAnchors(firstOrifice, firstOrificeAnchors); await expectSymbolAnchors(signalOrifice, [ { portName: "res", x: 26, y: 46 }, { portName: "port_2", x: 2, y: 24 }, { portName: "port_3", x: 62, y: 24 }, ]); await firstOrifice.locator(".node-symbol").click({ position: { x: 56, y: 42 } }); await page .getByRole("button", { name: "顺时针旋转 90° (Ctrl+R)", exact: true }) .click(); await expectSymbolAnchors(firstOrifice, firstOrificeAnchors); await page .getByRole("button", { name: "水平镜像 (Shift+H)", exact: true }) .click(); await expectSymbolAnchors(firstOrifice, firstOrificeAnchors); const firstPhysicalOutput = firstOrifice.locator( '.port-handle[data-port-name="port_2"]', ); const sameNodePort = firstOrifice.locator( '.port-handle[data-port-name="port_1"]', ); const compatiblePhysicalPort = secondOrifice.locator( '.port-handle[data-port-name="port_1"]', ); const signalOutput = stepSource.locator( '.port-handle[data-port-name="out"]', ); const signalInput = signalOrifice.locator( '.port-handle[data-port-name="res"]', ); await expect(signalInput).toHaveClass(/react-flow__handle-bottom/); const pneumaticInput = signalOrifice.locator( '.port-handle[data-port-name="port_2"]', ); await firstPhysicalOutput.click(); await expect(firstPhysicalOutput).toHaveAttribute( "data-connection-state", "origin", ); await expect(compatiblePhysicalPort).toHaveAttribute( "data-connection-state", "compatible", ); await expect(pneumaticInput).toHaveAttribute( "data-connection-state", "compatible", ); await expect(sameNodePort).toHaveAttribute( "data-connection-state", "incompatible", ); await expect(signalInput).toHaveAttribute( "data-connection-state", "incompatible", ); await page.locator(".flow-canvas .react-flow__pane").click({ position: { x: 20, y: 20 }, }); await expect(firstPhysicalOutput).toHaveAttribute( "data-connection-state", "idle", ); await compatiblePhysicalPort.click(); await expect(page.locator(".flow-canvas .react-flow__edge")).toHaveCount(0); await expect(compatiblePhysicalPort).toHaveAttribute( "data-connection-state", "origin", ); await page.keyboard.press("Escape"); await expect(compatiblePhysicalPort).toHaveAttribute( "data-connection-state", "idle", ); await firstPhysicalOutput.click(); await compatiblePhysicalPort.click(); await expect(page.locator(".flow-canvas .react-flow__edge")).toHaveCount(1); await expect(firstPhysicalOutput).toHaveAttribute("data-connected", "true"); await expect(compatiblePhysicalPort).toHaveAttribute("data-connected", "true"); await expect(firstPhysicalOutput).toHaveCSS("opacity", "0"); await expect(compatiblePhysicalPort).toHaveCSS("opacity", "0"); const physicalEdge = page.locator(".flow-canvas .react-flow__edge").first(); await physicalEdge.locator(".react-flow__edge-interaction").click({ force: true }); await page.getByRole("button", { name: "删除 (Delete)", exact: true }).click(); await expect(page.locator(".flow-canvas .react-flow__edge")).toHaveCount(0); await expect(firstPhysicalOutput).toHaveAttribute("data-connected", "false"); await expect(compatiblePhysicalPort).toHaveAttribute("data-connected", "false"); await expect(firstPhysicalOutput).toHaveCSS("opacity", "1"); await expect(compatiblePhysicalPort).toHaveCSS("opacity", "1"); await expect(firstPhysicalOutput).not.toHaveCSS("pointer-events", "none"); await expect(compatiblePhysicalPort).not.toHaveCSS("pointer-events", "none"); await firstPhysicalOutput.click(); await compatiblePhysicalPort.click(); await expect(page.locator(".flow-canvas .react-flow__edge")).toHaveCount(1); const signalOutputBounds = await signalOutput.boundingBox(); const signalInputBounds = await signalInput.boundingBox(); expect(signalOutputBounds).not.toBeNull(); expect(signalInputBounds).not.toBeNull(); await page.mouse.move( signalOutputBounds!.x + signalOutputBounds!.width / 2, signalOutputBounds!.y + signalOutputBounds!.height / 2, ); await page.mouse.down(); await expect(signalOutput).toHaveAttribute( "data-connection-state", "origin", ); await expect(signalInput).toHaveAttribute( "data-connection-state", "compatible", ); const paneBounds = await page .locator(".flow-canvas .react-flow__pane") .boundingBox(); expect(paneBounds).not.toBeNull(); await page.mouse.move(paneBounds!.x + 20, paneBounds!.y + 20, { steps: 4 }); await page.mouse.up(); await expect(page.locator(".flow-canvas .react-flow__edge")).toHaveCount(1); await expect(page.locator(".flow-canvas .react-flow__connection")).toHaveCount(0); await expect(signalOutput).toHaveAttribute( "data-connection-state", "idle", ); await expect(signalInput).toHaveAttribute( "data-connection-state", "idle", ); await expect(pneumaticInput).toHaveAttribute( "data-connection-state", "idle", ); const retriedSignalOutputBounds = await signalOutput.boundingBox(); const retriedSignalInputBounds = await signalInput.boundingBox(); expect(retriedSignalOutputBounds).not.toBeNull(); expect(retriedSignalInputBounds).not.toBeNull(); await page.mouse.move( retriedSignalOutputBounds!.x + retriedSignalOutputBounds!.width / 2, retriedSignalOutputBounds!.y + retriedSignalOutputBounds!.height / 2, ); await page.mouse.down(); await expect(signalOutput).toHaveAttribute( "data-connection-state", "origin", ); await page.mouse.move( retriedSignalOutputBounds!.x + retriedSignalOutputBounds!.width / 2 + 12, retriedSignalOutputBounds!.y + retriedSignalOutputBounds!.height / 2, ); await expect(signalInput).toHaveAttribute( "data-connection-state", "compatible", ); await expect(pneumaticInput).toHaveAttribute( "data-connection-state", "incompatible", ); await page.mouse.move( retriedSignalInputBounds!.x + retriedSignalInputBounds!.width / 2, retriedSignalInputBounds!.y + retriedSignalInputBounds!.height / 2, { steps: 5 }, ); await page.mouse.up(); await expect(page.locator(".flow-canvas .react-flow__edge")).toHaveCount(2); await expect(signalOutput).toHaveAttribute("data-connected", "true"); await expect(signalInput).toHaveAttribute("data-connected", "true"); }); test("兼容接口接近时吸附为无连线接触连接,拖开后自动断开", async ({ page, }) => { await page.goto("/"); await page.getByRole("button", { name: "关闭网格吸附", exact: true }).click(); const firstOrifice = await dragSymbolToCanvas(page, "amesim_pnor001", { x: 280, y: 300, }); const secondOrifice = await dragSymbolToCanvas(page, "amesim_pnor001", { x: 650, y: 300, }); const firstPort = firstOrifice.locator( '.port-handle[data-port-name="port_2"]', ); const secondPort = secondOrifice.locator( '.port-handle[data-port-name="port_1"]', ); await dragNodePortNearTarget( page, secondOrifice, secondPort, firstPort, ); await expect(firstPort).toHaveAttribute( "data-connection-state", "compatible", ); await expect(secondPort).toHaveAttribute( "data-connection-state", "origin", ); expect(await distanceBetween(firstPort, secondPort)).toBeGreaterThan(4); await page.mouse.up(); await expect .poll(() => distanceBetween(firstPort, secondPort)) .toBeLessThan(2); await expect(page.locator(".flow-canvas .react-flow__edge")).toHaveCount(1); await expect(page.locator(".flow-canvas .react-flow__edge-path")).toHaveCount(0); const contactHit = page.locator(".flow-canvas .contact-edge-hit"); await expect(contactHit).toHaveCount(1); await expect(firstPort).toHaveAttribute("data-connected", "true"); await expect(secondPort).toHaveAttribute("data-connected", "true"); await expect(firstPort).toHaveCSS("opacity", "0"); await expect(secondPort).toHaveCSS("opacity", "0"); await page.getByRole("button", { name: "生成系统 XML", exact: true }).click(); const xmlBlock = page.getByLabel("生成的系统 XML"); await expect(xmlBlock).toContainText(' { const raw = window.localStorage.getItem( "system-simulation-flow:project:demo-system", ); const project = raw ? JSON.parse(raw) : null; return project?.edges?.[0] ?? null; }); expect(savedEdge).toMatchObject({ source: "amesim_pnor001_2", sourceHandle: "port_1", target: "amesim_pnor001_1", targetHandle: "port_2", }); await contactHit.click(); await expect(page.locator(".flow-canvas .react-flow__edge")).toHaveClass( /editor-edge-selected/, ); await page.getByRole("button", { name: "删除 (Delete)", exact: true }).click(); await expect(page.locator(".flow-canvas .react-flow__edge")).toHaveCount(0); await expect(firstPort).toHaveAttribute("data-connected", "false"); await expect(secondPort).toHaveAttribute("data-connected", "false"); expect(await distanceBetween(firstPort, secondPort)).toBeGreaterThan(8); await dragNodePortNearTarget( page, secondOrifice, secondPort, firstPort, 5, ); await expect(firstPort).toHaveAttribute( "data-connection-state", "compatible", ); await page.mouse.up(); await expect(page.locator(".flow-canvas .react-flow__edge")).toHaveCount(1); await expect(page.locator(".flow-canvas .react-flow__edge-path")).toHaveCount(0); await page .getByRole("button", { name: "顺时针旋转 90° (Ctrl+R)", exact: true }) .click(); await expect(page.locator(".flow-canvas .react-flow__edge")).toHaveCount(0); await page.getByRole("button", { name: "撤销 (Ctrl+Z)", exact: true }).click(); await expect(page.locator(".flow-canvas .react-flow__edge")).toHaveCount(1); await expect(page.locator(".flow-canvas .react-flow__edge-path")).toHaveCount(0); const secondSymbolCenter = await locatorCenter( secondOrifice.locator(".node-symbol"), ); await page.mouse.move(secondSymbolCenter.x, secondSymbolCenter.y); await page.mouse.down(); await page.mouse.move(secondSymbolCenter.x + 90, secondSymbolCenter.y + 35, { steps: 10, }); await page.mouse.up(); await expect(page.locator(".flow-canvas .react-flow__edge")).toHaveCount(0); await expect(firstPort).toHaveAttribute("data-connected", "false"); await expect(secondPort).toHaveAttribute("data-connected", "false"); await expect(firstPort).toHaveCSS("opacity", "1"); await expect(secondPort).toHaveCSS("opacity", "1"); expect(await distanceBetween(firstPort, secondPort)).toBeGreaterThan(50); }); test("不同类型接口接近时不会吸附或建立逻辑连接", async ({ page }) => { await page.goto("/"); await page.getByRole("button", { name: "关闭网格吸附", exact: true }).click(); const orifice = await dragSymbolToCanvas(page, "amesim_pnor001", { x: 280, y: 320, }); const stepSource = await dragSymbolToCanvas(page, "amesim_step0", { x: 650, y: 320, }); const physicalPort = orifice.locator( '.port-handle[data-port-name="port_2"]', ); const signalPort = stepSource.locator( '.port-handle[data-port-name="out"]', ); await dragNodePortNearTarget( page, stepSource, signalPort, physicalPort, ); await expect(physicalPort).not.toHaveAttribute( "data-connection-state", "compatible", ); await page.mouse.up(); await expect(page.locator(".flow-canvas .react-flow__edge")).toHaveCount(0); expect(await distanceBetween(physicalPort, signalPort)).toBeGreaterThan(4); await expect(physicalPort).toHaveAttribute("data-connected", "false"); await expect(signalPort).toHaveAttribute("data-connected", "false"); }); test("旋转后的接口可以与方向不同的兼容接口接触连接", async ({ page }) => { await page.goto("/"); await page.getByRole("button", { name: "关闭网格吸附", exact: true }).click(); const horizontalOrifice = await dragSymbolToCanvas(page, "amesim_pnor001", { x: 300, y: 300, }); const rotatedOrifice = await dragSymbolToCanvas(page, "amesim_pnor001", { x: 650, y: 500, }); await rotatedOrifice.locator(".node-symbol").click(); await page .getByRole("button", { name: "顺时针旋转 90° (Ctrl+R)", exact: true }) .click(); const horizontalPort = horizontalOrifice.locator( '.port-handle[data-port-name="port_2"]', ); const rotatedPort = rotatedOrifice.locator( '.port-handle[data-port-name="port_1"]', ); await dragNodePortNearTarget( page, rotatedOrifice, rotatedPort, horizontalPort, ); await expect(horizontalPort).toHaveAttribute( "data-connection-state", "compatible", ); await expect(rotatedPort).toHaveAttribute( "data-connection-state", "origin", ); await page.mouse.up(); await expect .poll(() => distanceBetween(horizontalPort, rotatedPort)) .toBeLessThan(2); await expect(page.locator(".flow-canvas .react-flow__edge")).toHaveCount(1); await expect(page.locator(".flow-canvas .react-flow__edge-path")).toHaveCount(0); await expect(horizontalPort).toHaveAttribute("data-connected", "true"); await expect(rotatedPort).toHaveAttribute("data-connected", "true"); }); test("旧工程保存的通用图标会按当前目录定义升级", async ({ page }) => { await page.goto("/"); await page.evaluate(() => { window.localStorage.setItem( "system-simulation-flow:autosave", JSON.stringify({ name: "legacy-symbol-project", nodes: [ { id: "amesim_pnl0001_1", type: "simulationComponent", position: { x: 200, y: 160 }, data: { label: "amesim_pnl0001_1", componentType: "amesim_pnl0001", modelType: "amesim_pnl0001", symbol: "pipe", ports: [], parameters: {}, parameterUnits: {}, rotation: 0, mirrored: false, }, }, ], edges: [], simulation: { t_start: 0, t_stop: 1, step: 0.1, max_step: 0.05, method: "BDF", }, }), ); }); await page.reload(); await page.getByRole("button", { name: "恢复", exact: true }).click(); const restoredNode = page.locator( '.flow-canvas .react-flow__node[data-id="amesim_pnl0001_1"]', ); await expect(restoredNode).toBeVisible(); await expect( restoredNode.locator('[data-component-symbol="amesim_pnl0001"]'), ).toBeVisible(); await expect(restoredNode.locator('[data-component-symbol="pipe"]')).toHaveCount(0); });