import { expect, test, type Locator, type Page } from "@playwright/test"; import { expandSimulationConsole, prepareApp } from "./fixtures"; test.beforeEach(async ({ page }) => { await prepareApp(page); }); function parameterRow(table: Locator, name: string) { return parameterNameCell(table, name).locator(".."); } function parameterNameCell(table: Locator, name: string) { return table.getByRole("rowheader", { name, exact: true }); } async function addPneumaticTestComponent(page: Page) { const canvas = page.locator(".flow-canvas .react-flow__pane"); await page .getByRole("button", { name: "AMESim 气动测试元件", exact: true }) .dragTo(canvas, { targetPosition: { x: 620, y: 330 } }); const node = page.locator( '.flow-canvas .react-flow__node[data-id="amesim_pneumatic_test_1"]', ); await expect(node).toBeVisible(); await node.click(); } async function addChoiceVisibilityTestComponent(page: Page) { const canvas = page.locator(".flow-canvas .react-flow__pane"); await page .getByRole("button", { name: "离散参数测试元件", exact: true }) .dragTo(canvas, { targetPosition: { x: 620, y: 330 } }); const node = page.locator( '.flow-canvas .react-flow__node[data-id="choice_visibility_test_1"]', ); await expect(node).toBeVisible(); await node.click(); } async function addMecmasEncodingTestComponent(page: Page) { const canvas = page.locator(".flow-canvas .react-flow__pane"); await page .getByRole("button", { name: "MECMAS21 编码测试元件", exact: true }) .dragTo(canvas, { targetPosition: { x: 620, y: 330 } }); const node = page.locator( '.flow-canvas .react-flow__node[data-id="amesim_mecmas21_1"]', ); await expect(node).toBeVisible(); await node.click(); } async function addParameterMetadataTestComponent( page: Page, label: string, type: string, ) { const canvas = page.locator(".flow-canvas .react-flow__pane"); await page .getByRole("button", { name: label, exact: true }) .dragTo(canvas, { targetPosition: { x: 620, y: 330 } }); const node = page.locator( `.flow-canvas .react-flow__node[data-id="${type}_1"]`, ); await expect(node).toBeVisible(); await node.click(); } test("组件参数和仿真设置使用共享的三列表格", async ({ page }) => { await page.goto("/"); await addPneumaticTestComponent(page); const componentTable = page.getByRole("table", { name: "组件参数" }); const simulationTable = page.getByRole("table", { name: "仿真设置" }); await expect(componentTable).toBeVisible(); await expect(simulationTable).toBeVisible(); await expect(componentTable.getByRole("columnheader")).toHaveText([ "参数", "值", "单位", ]); await expect(componentTable.getByRole("rowheader")).toHaveText([ "组件名称", "节点 ID", "介质物性索引", "参考压力", "参考温度", ]); await expect(simulationTable.getByRole("rowheader")).toHaveText([ "起始时间", "结束时间", "采样步长", "最大积分步长", "求解器", ]); await expect(parameterRow(simulationTable, "起始时间").locator("td").last()).toHaveText( "s", ); await expect(parameterRow(simulationTable, "求解器").locator("td").last()).toHaveText( "—", ); const componentNameWidth = await componentTable .locator("thead .parameter-table-name-cell") .evaluate((element) => element.getBoundingClientRect().width); const simulationNameWidth = await simulationTable .locator("thead .parameter-table-name-cell") .evaluate((element) => element.getBoundingClientRect().width); expect(Math.abs(componentNameWidth - simulationNameWidth)).toBeLessThanOrEqual(1); const panelTitleHeights = await page .locator(".properties .panel-title") .evaluateAll((elements) => elements.map((element) => element.getBoundingClientRect().height), ); expect(panelTitleHeights).toHaveLength(2); panelTitleHeights.forEach((height) => expect(height).toBeLessThanOrEqual(32)); const compactTableMetrics = await page .locator(".properties .parameter-table") .evaluateAll((tables) => tables.map((table) => { const content = table.closest(".properties-subpane-content"); const headerCell = table.querySelector("thead th"); const bodyRows = Array.from( table.querySelectorAll("tbody .parameter-table-row"), ); const control = table.querySelector("input, select"); if (!content || !headerCell || !control) { throw new Error("参数表缺少布局测量所需的元素"); } const tableBounds = table.getBoundingClientRect(); const contentBounds = content.getBoundingClientRect(); const tableStyle = getComputedStyle(table); const contentLeft = contentBounds.left + content.clientLeft; const contentRight = contentLeft + content.clientWidth; const controlBounds = control.getBoundingClientRect(); const controlCell = control.closest("td"); if (!controlCell) { throw new Error("参数控件不在数据单元格中"); } const controlCellBounds = controlCell.getBoundingClientRect(); return { leftInset: tableBounds.left - contentLeft, rightInset: contentRight - tableBounds.right, borderRadius: tableStyle.borderRadius, borderTopWidth: tableStyle.borderTopWidth, headerHeight: headerCell.getBoundingClientRect().height, headerPosition: getComputedStyle(headerCell).position, maximumRowHeight: Math.max( ...bodyRows.map((row) => row.getBoundingClientRect().height), ), controlHeight: controlBounds.height, controlContained: controlBounds.left >= controlCellBounds.left - 0.5 && controlBounds.right <= controlCellBounds.right + 0.5 && controlBounds.top >= controlCellBounds.top - 0.5 && controlBounds.bottom <= controlCellBounds.bottom + 0.5, }; }), ); expect(compactTableMetrics).toHaveLength(2); compactTableMetrics.forEach((metrics) => { expect(Math.abs(metrics.leftInset)).toBeLessThanOrEqual(1); expect(Math.abs(metrics.rightInset)).toBeLessThanOrEqual(1); expect(metrics.borderRadius).toBe("0px"); expect(metrics.borderTopWidth).toBe("0px"); expect(metrics.headerHeight).toBeLessThanOrEqual(28); expect(metrics.headerPosition).toBe("sticky"); expect(metrics.maximumRowHeight).toBeLessThanOrEqual(32); expect(metrics.controlHeight).toBeGreaterThanOrEqual(22); expect(metrics.controlHeight).toBeLessThanOrEqual(28); expect(metrics.controlContained).toBe(true); }); await expect( componentTable.getByRole("rowheader", { name: "组件名称", exact: true, }), ).toHaveCSS("text-align", "left"); await expect(page.getByLabel("参考压力", { exact: true })).toHaveCSS( "text-align", "right", ); const valueColumnAlignment = await page .locator(".properties .parameter-table tbody .parameter-table-value-cell") .evaluateAll((cells) => cells.every((cell) => { const controls = Array.from( cell.querySelectorAll("input, select"), ); return ( getComputedStyle(cell).textAlign === "right" && controls.every( (control) => getComputedStyle(control).textAlign === "right", ) ); }), ); expect(valueColumnAlignment).toBe(true); await expect( componentTable.locator(".parameter-table-read-only-value").first(), ).toHaveCSS("justify-content", "flex-end"); await expect(page.getByLabel("介质物性索引", { exact: true })).toHaveCSS( "text-align-last", "right", ); const solver = page.getByLabel("求解器", { exact: true }); await expect(solver).toHaveCSS("text-align", "right"); await expect(solver).toHaveCSS("text-align-last", "right"); await solver.selectOption("RK45"); await expect(solver).toHaveValue("RK45"); }); test("通用离散选项按多条件控制参数行且保留隐藏值和未知旧值", async ({ page, }) => { await page.goto("/"); await addChoiceVisibilityTestComponent(page); const componentTable = page.getByRole("table", { name: "组件参数" }); const mode = page.getByLabel("工作模式", { exact: true }); const variant = page.getByLabel("高级变体", { exact: true }); await expect(mode).toHaveValue("1"); await expect(mode.locator("option:checked")).toHaveText("基础模式"); await expect(variant).toHaveValue("10"); await expect(parameterRow(componentTable, "基础参数")).toBeVisible(); await expect(parameterRow(componentTable, "高级参数")).toHaveCount(0); await expect(parameterRow(componentTable, "组合参数")).toHaveCount(0); await mode.selectOption("2"); await expect(parameterRow(componentTable, "基础参数")).toHaveCount(0); await expect(parameterRow(componentTable, "高级参数")).toBeVisible(); await expect(parameterRow(componentTable, "组合参数")).toHaveCount(0); const advancedValue = page.getByLabel("高级参数", { exact: true }); await advancedValue.fill("42"); await advancedValue.press("Enter"); await variant.selectOption("20"); await expect(parameterRow(componentTable, "组合参数")).toBeVisible(); await mode.selectOption("3"); await expect(parameterRow(componentTable, "高级参数")).toBeVisible(); await expect(parameterRow(componentTable, "组合参数")).toBeVisible(); await mode.selectOption("1"); await expect(parameterRow(componentTable, "高级参数")).toHaveCount(0); await expect(parameterRow(componentTable, "组合参数")).toHaveCount(0); await mode.selectOption("2"); await expect(page.getByLabel("高级参数", { exact: true })).toHaveValue("42"); await page.getByRole("button", { name: "保存工程", exact: true }).click(); const changedProjectCount = await page.evaluate(() => { let changed = 0; Object.keys(window.localStorage) .filter((key) => [ "system-simulation-flow:autosave", "system-simulation-flow:project:", ].some((prefix) => key.startsWith(prefix)), ) .forEach((key) => { const raw = window.localStorage.getItem(key); if (!raw) { return; } const project = JSON.parse(raw) as { nodes?: Array<{ id?: string; data?: { parameters?: Record }; }>; }; const node = project.nodes?.find( (candidate) => candidate.id === "choice_visibility_test_1", ); if (!node?.data?.parameters) { return; } node.data.parameters.mode = 99; window.localStorage.setItem(key, JSON.stringify(project)); changed += 1; }); return changed; }); expect(changedProjectCount).toBeGreaterThanOrEqual(2); await page.getByRole("button", { name: "加载工程", exact: true }).click(); await page .locator('.flow-canvas .react-flow__node[data-id="choice_visibility_test_1"]') .click(); await expect(mode).toHaveValue(""); await expect(mode).toHaveAttribute("aria-invalid", "true"); await expect(mode.locator("option:checked")).toHaveText( "当前选项无效,请重新选择", ); await expect(parameterRow(componentTable, "基础参数")).toHaveCount(0); await expect(parameterRow(componentTable, "高级参数")).toHaveCount(0); await expect(parameterRow(componentTable, "组合参数")).toHaveCount(0); await page.getByRole("button", { name: "保存工程", exact: true }).click(); const persistedModes = await page.evaluate(() => Object.keys(window.localStorage) .filter((key) => key.startsWith("system-simulation-flow:project:")) .flatMap((key) => { const raw = window.localStorage.getItem(key); if (!raw) { return []; } const project = JSON.parse(raw) as { nodes?: Array<{ id?: string; data?: { parameters?: Record }; }>; }; const node = project.nodes?.find( (candidate) => candidate.id === "choice_visibility_test_1", ); return node?.data?.parameters ? [node.data.parameters.mode] : []; }), ); expect(persistedModes).toContain(99); await mode.selectOption("2"); await expect(mode).toHaveValue("2"); await expect(mode).not.toHaveAttribute("aria-invalid", "true"); await expect(page.getByLabel("高级参数", { exact: true })).toHaveValue("42"); }); test("PNOR001 和 PNL0001 控制项前置并切换折叠组中的条件参数", async ({ page, }) => { await page.goto("/"); await addParameterMetadataTestComponent( page, "PNOR001 常系数气动孔口", "amesim_pnor001", ); const componentTable = page.getByRole("table", { name: "组件参数" }); const flowset = page.getByLabel("流量系数设置", { exact: true }); const flowGroup = componentTable.getByRole("button", { name: /流量系数/ }); await expect(flowset.locator("option")).toHaveText(["Cq", "Cv", "Kv"]); await expect(flowset).toHaveValue("1"); await expect(flowGroup).toHaveAttribute("aria-expanded", "false"); await expect(page.getByLabel("流量系数 Cq", { exact: true })).toBeHidden(); await expect(page.getByLabel("孔口面积", { exact: true })).toBeHidden(); await expect(parameterRow(componentTable, "流量系数 Cv")).toHaveCount(0); const flowsetBox = await parameterRow( componentTable, "流量系数设置", ).boundingBox(); const flowGroupBox = await flowGroup.boundingBox(); expect(flowsetBox).not.toBeNull(); expect(flowGroupBox).not.toBeNull(); expect(flowsetBox!.y).toBeLessThan(flowGroupBox!.y); await flowGroup.click(); const area = page.getByLabel("孔口面积", { exact: true }); await expect(page.getByLabel("流量系数 Cq", { exact: true })).toBeVisible(); await expect(area).toBeVisible(); await area.fill("7.5e-6"); await area.press("Enter"); await expect(area).toHaveValue("7.5e-6"); await flowset.selectOption("2"); await expect(parameterRow(componentTable, "流量系数 Cq")).toHaveCount(0); await expect(parameterRow(componentTable, "孔口面积")).toHaveCount(0); await expect(page.getByLabel("流量系数 Cv", { exact: true })).toBeVisible(); await expect(parameterRow(componentTable, "流量系数 Kv")).toHaveCount(0); await flowset.selectOption("1"); await expect(area).toHaveValue("7.5e-6"); await addParameterMetadataTestComponent( page, "PNL0001 C-R 动态管路", "amesim_pnl0001", ); const mode = page.getByLabel("热模型", { exact: true }); const thermalGroup = componentTable.getByRole("button", { name: /热力学/ }); await expect(mode.locator("option")).toHaveText(["多方过程", "带换热"]); await expect(mode).toHaveValue("2"); const pnlParameterOrder = ( await componentTable.getByRole("rowheader").allTextContents() ).map((text) => text.trim()); expect(pnlParameterOrder.indexOf("热模型")).toBeLessThan( pnlParameterOrder.indexOf("管径"), ); await expect(thermalGroup).toHaveAttribute("aria-expanded", "false"); await thermalGroup.click(); await expect(parameterRow(componentTable, "多方指数")).toHaveCount(0); await expect(page.getByLabel("换热系数", { exact: true })).toBeVisible(); await expect(page.getByLabel("外部温度", { exact: true })).toBeVisible(); await mode.selectOption("1"); await expect(page.getByLabel("多方指数", { exact: true })).toBeVisible(); await expect(parameterRow(componentTable, "换热系数")).toHaveCount(0); await expect(parameterRow(componentTable, "外部温度")).toHaveCount(0); await expect(thermalGroup).toHaveAttribute("aria-expanded", "true"); }); test("UD00 段数前置并逐段显示第 2 至第 4 段参数", async ({ page }) => { await page.goto("/"); await addParameterMetadataTestComponent( page, "UD00 分段线性信号", "amesim_ud00", ); const componentTable = page.getByRole("table", { name: "组件参数" }); const stageCount = page.getByLabel("段数", { exact: true }); const cyclic = page.getByLabel("循环", { exact: true }); const stageGroup = componentTable.getByRole("button", { name: /分段参数/ }); await expect(stageCount.locator("option")).toHaveText([ "1", "2", "3", "4", "5", "6", "7", "8", ]); await expect(cyclic.locator("option")).toHaveText(["否", "是"]); const parameterOrder = ( await componentTable.getByRole("rowheader").allTextContents() ).map((text) => text.trim()); expect(parameterOrder.indexOf("段数")).toBeLessThan( parameterOrder.indexOf("启动时间"), ); expect(parameterOrder.indexOf("循环")).toBeLessThan( parameterOrder.indexOf("启动时间"), ); await expect(stageGroup).toHaveAttribute("aria-expanded", "false"); await expect(page.getByLabel("第 1 段起点", { exact: true })).toBeHidden(); await stageGroup.click(); await expect(page.getByLabel("第 1 段起点", { exact: true })).toBeVisible(); const expectStageVisibility = async (stage: number, visible: boolean) => { for (const suffix of ["起点", "终点", "时长"]) { const row = parameterRow(componentTable, `第 ${stage} 段${suffix}`); if (visible) { await expect(row).toBeVisible(); } else { await expect(row).toHaveCount(0); } } }; await expectStageVisibility(2, false); await expectStageVisibility(3, false); await expectStageVisibility(4, false); await stageCount.selectOption("2"); await expectStageVisibility(2, true); await expectStageVisibility(3, false); await expectStageVisibility(4, false); await stageCount.selectOption("3"); await expectStageVisibility(2, true); await expectStageVisibility(3, true); await expectStageVisibility(4, false); await stageCount.selectOption("4"); await expectStageVisibility(2, true); await expectStageVisibility(3, true); await expectStageVisibility(4, true); await expect(stageGroup).toHaveAttribute("aria-expanded", "true"); }); test("LSTP00A 刚度模式前置并切换数值与几何刚度参数", async ({ page }) => { await page.goto("/"); await addParameterMetadataTestComponent( page, "LSTP00A 弹性接触", "amesim_lstp00a", ); const componentTable = page.getByRole("table", { name: "组件参数" }); const stiffnessMode = page.getByLabel("刚度模式", { exact: true }); const contactOption = page.getByLabel("允许负接触力", { exact: true }); const stiffnessGroup = componentTable.getByRole("button", { name: /刚度/ }); const contactGroup = componentTable.getByRole("button", { name: /接触/ }); await expect(stiffnessMode.locator("option")).toHaveText([ "数值刚度", "几何参数", ]); await expect(contactOption.locator("option")).toHaveText([ "允许负接触力", "不允许负接触力", ]); await expect(stiffnessGroup).toHaveAttribute("aria-expanded", "false"); await expect(contactGroup).toHaveAttribute("aria-expanded", "false"); const stiffnessModeBox = await parameterRow( componentTable, "刚度模式", ).boundingBox(); const stiffnessGroupBox = await stiffnessGroup.boundingBox(); expect(stiffnessModeBox).not.toBeNull(); expect(stiffnessGroupBox).not.toBeNull(); expect(stiffnessModeBox!.y).toBeLessThan(stiffnessGroupBox!.y); await stiffnessGroup.click(); await expect(page.getByLabel("接触刚度", { exact: true })).toBeVisible(); for (const label of ["有效圈数", "剪切模量", "弹簧直径", "线径"]) { await expect(parameterRow(componentTable, label)).toHaveCount(0); } await stiffnessMode.selectOption("2"); await expect(parameterRow(componentTable, "接触刚度")).toHaveCount(0); for (const label of ["有效圈数", "剪切模量", "弹簧直径", "线径"]) { await expect(page.getByLabel(label, { exact: true })).toBeVisible(); } await expect(stiffnessGroup).toHaveAttribute("aria-expanded", "true"); await stiffnessMode.selectOption("1"); await expect(page.getByLabel("接触刚度", { exact: true })).toBeVisible(); await expect(parameterRow(componentTable, "剪切模量")).toHaveCount(0); }); test("MECMAS21 可配置参数前置并按条件显示可折叠参数组", async ({ page, }) => { await page.goto("/"); await addMecmasEncodingTestComponent(page); const componentTable = page.getByRole("table", { name: "组件参数" }); const useFriction = page.getByLabel("启用摩擦", { exact: true }); const stoptype = page.getByLabel("限位类型", { exact: true }); const frictionGroup = componentTable.getByRole("button", { name: /摩擦/ }); const endstopsGroup = componentTable.getByRole("button", { name: /限位/ }); const parameterOrder = ( await componentTable.getByRole("rowheader").allTextContents() ).map((text) => text.trim()); expect(parameterOrder.indexOf("启用摩擦")).toBeGreaterThanOrEqual(0); expect(parameterOrder.indexOf("限位类型")).toBeGreaterThanOrEqual(0); expect(parameterOrder.indexOf("质量")).toBeGreaterThanOrEqual(0); expect(parameterOrder.indexOf("启用摩擦")).toBeLessThan( parameterOrder.indexOf("质量"), ); expect(parameterOrder.indexOf("限位类型")).toBeLessThan( parameterOrder.indexOf("质量"), ); await expect(useFriction).toHaveValue("1"); await expect(stoptype).toHaveValue("4"); await expect(frictionGroup).toHaveCount(0); await expect(endstopsGroup).toHaveCount(0); await useFriction.selectOption("2"); await expect(frictionGroup).toBeVisible(); await expect(frictionGroup).toHaveAttribute("aria-expanded", "false"); await expect(frictionGroup.locator("xpath=ancestor::th")).toHaveAttribute( "colspan", "3", ); await expect(page.getByLabel("摩擦类型", { exact: true })).toBeHidden(); const frictionGroupBox = await frictionGroup.boundingBox(); const unrelatedRowBox = await parameterRow( componentTable, "无关编码", ).boundingBox(); expect(frictionGroupBox).not.toBeNull(); expect(unrelatedRowBox).not.toBeNull(); expect(frictionGroupBox!.y).toBeGreaterThan(unrelatedRowBox!.y); await frictionGroup.focus(); await expect(frictionGroup).toBeFocused(); await page.keyboard.press("Enter"); await expect(frictionGroup).toHaveAttribute("aria-expanded", "true"); const frictionType = page.getByLabel("摩擦类型", { exact: true }); const strib = page.getByLabel("Stribeck 选项", { exact: true }); const viscousFriction = page.getByLabel("黏性摩擦系数", { exact: true }); await expect(frictionType).toBeVisible(); await expect(viscousFriction).toBeVisible(); await expect(strib).toHaveCount(0); await frictionType.selectOption("2"); await expect(strib).toBeVisible(); const expandedOrder = ( await componentTable.getByRole("rowheader").allTextContents() ).map((text) => text.trim()); expect(expandedOrder.indexOf("摩擦类型")).toBeLessThan( expandedOrder.indexOf("Stribeck 选项"), ); expect(expandedOrder.indexOf("Stribeck 选项")).toBeLessThan( expandedOrder.indexOf("黏性摩擦系数"), ); await frictionGroup.focus(); await expect(frictionGroup).toBeFocused(); await page.keyboard.press("Space"); await expect(frictionGroup).toHaveAttribute("aria-expanded", "false"); await expect(frictionType).toBeHidden(); await expect(strib).toBeHidden(); await expect(viscousFriction).toBeHidden(); await page.keyboard.press("Space"); await expect(frictionGroup).toHaveAttribute("aria-expanded", "true"); await expect(frictionType).toHaveValue("2"); await stoptype.selectOption("2"); await expect(endstopsGroup).toBeVisible(); await expect(endstopsGroup).toHaveAttribute("aria-expanded", "false"); await expect(endstopsGroup.locator("xpath=ancestor::th")).toHaveAttribute( "colspan", "3", ); await endstopsGroup.focus(); await page.keyboard.press("Enter"); await expect(endstopsGroup).toHaveAttribute("aria-expanded", "true"); await expect(page.getByLabel("允许负接触力", { exact: true })).toBeVisible(); await expect(page.getByLabel("下位移限位", { exact: true })).toBeVisible(); const endstopOrder = ( await componentTable.getByRole("rowheader").allTextContents() ).map((text) => text.trim()); expect(endstopOrder.indexOf("允许负接触力")).toBeLessThan( endstopOrder.indexOf("下位移限位"), ); const endstopsGroupBox = await endstopsGroup.boundingBox(); expect(endstopsGroupBox).not.toBeNull(); expect(frictionGroupBox!.y).toBeLessThan(endstopsGroupBox!.y); await stoptype.selectOption("3"); await expect(frictionGroup).toBeVisible(); await expect(endstopsGroup).toBeVisible(); await expect(frictionType).toHaveCount(0); await expect(strib).toHaveCount(0); await expect(viscousFriction).toBeVisible(); await expect(page.getByLabel("允许负接触力", { exact: true })).toHaveCount(0); await expect(page.getByLabel("下位移限位", { exact: true })).toBeVisible(); await stoptype.selectOption("4"); await expect(endstopsGroup).toHaveCount(0); await expect(frictionType).toBeVisible(); }); test("MECMAS21 参数组折叠和条件切换保留值并清理详情浮层", async ({ page, }) => { await page.goto("/"); await addMecmasEncodingTestComponent(page); const componentTable = page.getByRole("table", { name: "组件参数" }); const useFriction = page.getByLabel("启用摩擦", { exact: true }); const frictionGroup = componentTable.getByRole("button", { name: /摩擦/ }); await useFriction.selectOption("2"); await frictionGroup.click(); const frictionType = page.getByLabel("摩擦类型", { exact: true }); await frictionType.selectOption("2"); const strib = page.getByLabel("Stribeck 选项", { exact: true }); const viscousFriction = page.getByLabel("黏性摩擦系数", { exact: true }); await strib.selectOption("2"); await viscousFriction.fill("12.5"); await viscousFriction.press("Enter"); await expect(viscousFriction).toHaveValue("12.5"); await frictionGroup.focus(); await parameterNameCell(componentTable, "黏性摩擦系数").hover(); await page.waitForTimeout(600); const preview = page.locator("body > #parameter-help-preview"); await expect(preview.locator(".parameter-help-preview-title")).toHaveText( "黏性摩擦系数", ); await page.keyboard.press("Enter"); await expect(frictionGroup).toHaveAttribute("aria-expanded", "false"); await expect(preview).toHaveCount(0); await frictionGroup.click(); await expect(frictionType).toHaveValue("2"); await expect(strib).toHaveValue("2"); await expect(viscousFriction).toHaveValue("12.5"); await useFriction.selectOption("1"); await expect(frictionGroup).toHaveCount(0); await useFriction.selectOption("2"); await expect(frictionGroup).toBeVisible(); if ((await frictionGroup.getAttribute("aria-expanded")) !== "true") { await frictionGroup.click(); } await expect(frictionType).toHaveValue("2"); await expect(strib).toHaveValue("2"); await expect(viscousFriction).toHaveValue("12.5"); await viscousFriction.fill("-1"); await viscousFriction.press("Enter"); await expect(componentTable.getByText("不能小于 0", { exact: true })).toBeVisible(); await frictionGroup.click(); await expect(frictionGroup).toHaveAttribute("aria-expanded", "false"); await expect(frictionGroup).toContainText("1 个错误"); await useFriction.selectOption("1"); await expect(frictionGroup).toBeVisible(); await expect(frictionGroup).toContainText("1 个错误"); await expect(frictionGroup).toHaveAttribute("aria-describedby"); await frictionGroup.click(); await expect(componentTable).toContainText( "1 个错误参数被当前配置选项隐藏,请调整上方配置后修改", ); }); test("MECMAS21 当前参数编码由工程版本统一并写入 XML", async ({ page, }) => { await page.goto("/"); await addMecmasEncodingTestComponent(page); const useFriction = page.getByLabel("启用摩擦", { exact: true }); const frictionGroup = page .getByRole("table", { name: "组件参数" }) .getByRole("button", { name: /摩擦/ }); await expect(useFriction).toHaveValue("1"); await expect(frictionGroup).toHaveCount(0); await page.getByRole("button", { name: "保存工程", exact: true }).click(); const savedContracts = await page.evaluate(() => Object.keys(window.localStorage) .filter((key) => key.startsWith("system-simulation-flow:project:")) .flatMap((key) => { const raw = window.localStorage.getItem(key); if (!raw) { return []; } const project = JSON.parse(raw) as { projectSchemaVersion?: number; nodes?: Array<{ id?: string; data?: { modelVersion?: string } }>; }; const node = project.nodes?.find( (node) => node.id === "amesim_mecmas21_1", ); return node ? [{ projectSchemaVersion: project.projectSchemaVersion, modelVersion: node.data?.modelVersion, }] : []; }), ); expect(savedContracts).toContainEqual({ projectSchemaVersion: 1, modelVersion: "0.2.0", }); await page.getByRole("button", { name: "生成系统 XML", exact: true }).click(); await expandSimulationConsole(page); await expect(page.getByLabel("生成的系统 XML")).toContainText( 'type="amesim_mecmas21" modelVersion="0.2.0"', ); const changedProjectCount = await page.evaluate(() => { let changed = 0; Object.keys(window.localStorage) .filter((key) => [ "system-simulation-flow:autosave", "system-simulation-flow:project:", ].some((prefix) => key.startsWith(prefix)), ) .forEach((key) => { const raw = window.localStorage.getItem(key); if (!raw) { return; } const project = JSON.parse(raw) as { nodes?: Array<{ id?: string; data?: { modelType?: string; parameters?: Record; }; }>; }; const node = project.nodes?.find( (candidate) => candidate.id === "amesim_mecmas21_1", ); if (!node?.data?.parameters) { return; } node.data.parameters.unrelatedCode = 9; window.localStorage.setItem(key, JSON.stringify(project)); changed += 1; }); return changed; }); expect(changedProjectCount).toBeGreaterThanOrEqual(2); await page.getByRole("button", { name: "加载工程", exact: true }).click(); await page .locator('.flow-canvas .react-flow__node[data-id="amesim_mecmas21_1"]') .click(); await expect(useFriction).toHaveValue("1"); await expect(useFriction.locator("option:checked")).toHaveText("禁用"); await useFriction.selectOption("2"); await expect(frictionGroup).toHaveAttribute("aria-expanded", "false"); await frictionGroup.click(); const frictionType = page.getByLabel("摩擦类型", { exact: true }); await frictionType.selectOption("2"); const strib = page.getByLabel("Stribeck 选项", { exact: true }); await strib.selectOption("2"); await expect(strib).toHaveValue("2"); await expect(strib.locator("option:checked")).toHaveText("启用"); await expect(page.getByLabel("无关编码", { exact: true })).toHaveValue("9"); await useFriction.selectOption("1"); await expect(frictionGroup).toHaveCount(0); await page.getByRole("button", { name: "保存工程", exact: true }).click(); await page.getByRole("button", { name: "加载工程", exact: true }).click(); await page .locator('.flow-canvas .react-flow__node[data-id="amesim_mecmas21_1"]') .click(); await expect(useFriction).toHaveValue("1"); await expect(page.getByLabel("无关编码", { exact: true })).toHaveValue("9"); await useFriction.selectOption("2"); await expect(frictionGroup).toBeVisible(); if ((await frictionGroup.getAttribute("aria-expanded")) !== "true") { await frictionGroup.click(); } await expect(frictionType).toHaveValue("2"); await expect(strib).toHaveValue("2"); }); test("MECMAS21 旧的 0/1 编码不转换并阻止执行", async ({ page, }) => { await page.goto("/"); await addMecmasEncodingTestComponent(page); await page.getByRole("button", { name: "保存工程", exact: true }).click(); const changed = await page.evaluate(() => { const key = Object.keys(window.localStorage).find((candidate) => candidate.startsWith("system-simulation-flow:project:"), ); const raw = key ? window.localStorage.getItem(key) : null; if (!key || !raw) { return false; } const project = JSON.parse(raw) as { nodes?: Array<{ id?: string; data?: { parameters?: Record; }; }>; }; const node = project.nodes?.find( (candidate) => candidate.id === "amesim_mecmas21_1", ); if (!node?.data?.parameters) { return false; } node.data.parameters.useFriction = 0; window.localStorage.setItem(key, JSON.stringify(project)); return true; }); expect(changed).toBe(true); await page.getByRole("button", { name: "加载工程", exact: true }).click(); const node = page.locator( '.flow-canvas .react-flow__node[data-id="amesim_mecmas21_1"]', ); await expect(node.locator('[data-model-contract-compatible="true"]')).toBeVisible(); await node.click(); const useFriction = page.getByLabel("启用摩擦", { exact: true }); await expect(useFriction).toHaveValue(""); await expect(useFriction.locator("option:checked")).toHaveText( "当前选项无效,请重新选择", ); const consolePanel = page.getByRole("complementary", { name: "仿真控制台", exact: true, }); await page.getByRole("button", { name: "检查模型", exact: true }).click(); await expect(consolePanel).toHaveClass(/minimized/); await expect( consolePanel.locator(".simulation-console-dock-summary"), ).toContainText("模型检查发现"); await expandSimulationConsole(page); await expect(consolePanel.locator(".simulation-console-dock-log")).toContainText( "useFriction=0", ); await page.getByRole("button", { name: "生成系统 XML", exact: true }).click(); await expandSimulationConsole(page); await expect(consolePanel).toContainText("不能生成执行 XML"); await page.getByRole("button", { name: "运行仿真", exact: true }).click(); await expect(consolePanel).toContainText("模型检查发现"); }); test("缺失模型版本的工程可查看但不补默认参数且不能执行", async ({ page, }) => { await page.goto("/"); await addMecmasEncodingTestComponent(page); await page.getByRole("button", { name: "保存工程", exact: true }).click(); const changed = await page.evaluate(() => { const key = Object.keys(window.localStorage).find((candidate) => candidate.startsWith("system-simulation-flow:project:"), ); const raw = key ? window.localStorage.getItem(key) : null; if (!key || !raw) { return false; } const project = JSON.parse(raw) as { nodes?: Array<{ id?: string; data?: { modelVersion?: string; parameters?: Record; }; }>; }; const node = project.nodes?.find( (candidate) => candidate.id === "amesim_mecmas21_1", ); if (!node?.data?.parameters) { return false; } delete node.data.modelVersion; delete node.data.parameters.strib; window.localStorage.setItem(key, JSON.stringify(project)); return true; }); expect(changed).toBe(true); await page.getByRole("button", { name: "加载工程", exact: true }).click(); const node = page.locator( '.flow-canvas .react-flow__node[data-id="amesim_mecmas21_1"]', ); await expect(node.locator('[data-model-contract-compatible="false"]')).toBeVisible(); await expect(node.getByLabel(/模型合同不兼容/)).toBeVisible(); await page.getByRole("button", { name: "保存工程", exact: true }).click(); const persistedWithoutDefaults = await page.evaluate(() => { const key = Object.keys(window.localStorage).find((candidate) => candidate.startsWith("system-simulation-flow:project:"), ); const raw = key ? window.localStorage.getItem(key) : null; if (!raw) { return false; } const project = JSON.parse(raw) as { nodes?: Array<{ id?: string; data?: { modelVersion?: string; parameters?: Record; }; }>; }; const data = project.nodes?.find( (candidate) => candidate.id === "amesim_mecmas21_1", )?.data; return Boolean( data && data.modelVersion === undefined && data.parameters && !("strib" in data.parameters), ); }); expect(persistedWithoutDefaults).toBe(true); const consolePanel = page.getByRole("complementary", { name: "仿真控制台", exact: true, }); await page.getByRole("button", { name: "检查模型", exact: true }).click(); await expect(consolePanel).toHaveClass(/minimized/); await expect( consolePanel.locator(".simulation-console-dock-summary"), ).toContainText("模型检查发现"); await expandSimulationConsole(page); await expect(consolePanel.locator(".simulation-console-dock-log")).toContainText( "缺少 modelVersion", ); await page.getByRole("button", { name: "生成系统 XML", exact: true }).click(); await expandSimulationConsole(page); await expect(consolePanel).toContainText("不能生成执行 XML"); await page.getByRole("button", { name: "运行仿真", exact: true }).click(); await expect(consolePanel).toContainText("模型检查发现"); }); test("详情浮层只在参数列悬停且下拉选择期间不触发", async ({ page }) => { await page.goto("/"); await addMecmasEncodingTestComponent(page); const componentTable = page.getByRole("table", { name: "组件参数" }); const choice = page.getByLabel("启用摩擦", { exact: true }); const otherNameCell = parameterNameCell(componentTable, "无关编码"); const preview = page.locator("body > #parameter-help-preview"); await otherNameCell.hover(); await page.waitForTimeout(600); await expect(preview).toBeVisible(); await expect(preview.locator(".parameter-help-preview-title")).toHaveText( "无关编码", ); await choice.hover(); await expect(preview).toHaveCount(0); await choice.focus(); await expect(choice).toBeFocused(); await otherNameCell.hover(); await page.waitForTimeout(600); await expect(preview).toHaveCount(0); await choice.selectOption("2"); await choice.focus(); await otherNameCell.hover(); await page.waitForTimeout(600); await expect(preview).toHaveCount(0); await choice.evaluate((element) => element.blur()); await page.locator(".panel-title").filter({ hasText: "组件参数" }).hover(); await otherNameCell.hover(); await page.waitForTimeout(600); await expect(preview.locator(".parameter-help-preview-title")).toHaveText( "无关编码", ); }); test("最窄参数面板滚动时表头固定且值列保持可用宽度", async ({ page }) => { await page.goto("/"); await addPneumaticTestComponent(page); const widthSeparator = page.getByRole("separator", { name: "调整参数面板宽度", exact: true, }); await widthSeparator.focus(); await page.keyboard.press("ArrowRight"); await page.keyboard.press("ArrowRight"); await page.keyboard.press("ArrowRight"); await expect(widthSeparator).toHaveAttribute("aria-valuenow", "280"); const heightSeparator = page.getByRole("separator", { name: "调整组件参数和仿真设置区域高度", exact: true, }); await heightSeparator.focus(); for (let step = 0; step < 11; step += 1) { await page.keyboard.press("ArrowUp"); } await expect(heightSeparator).toHaveAttribute("aria-valuenow", "120"); const componentContent = page .getByRole("region", { name: "组件参数子栏", exact: true }) .locator(".properties-subpane-content"); await expect .poll(() => componentContent.evaluate( (element) => element.scrollHeight - element.clientHeight, ), ) .toBeGreaterThan(0); await componentContent.evaluate((element) => { element.scrollTop = 48; }); await expect .poll(() => componentContent.evaluate((element) => element.scrollTop)) .toBeGreaterThan(0); const narrowLayout = await componentContent.evaluate((content) => { const header = content.querySelector( "thead .parameter-table-name-cell", ); const valueHeader = content.querySelector( "thead .parameter-table-value-cell", ); const wrap = content.querySelector(".parameter-table-wrap"); if (!header || !valueHeader || !wrap) { throw new Error("无法测量最窄参数表布局"); } return { stickyTopDelta: header.getBoundingClientRect().top - content.getBoundingClientRect().top, valueWidth: valueHeader.getBoundingClientRect().width, horizontalOverflow: wrap.scrollWidth - wrap.clientWidth, }; }); expect(Math.abs(narrowLayout.stickyTopDelta)).toBeLessThanOrEqual(1); expect(narrowLayout.valueWidth).toBeGreaterThanOrEqual(95); expect(narrowLayout.horizontalOverflow).toBeLessThanOrEqual(1); }); test("参数说明只在参数列悬停显示,单位切换不改变 XML 基础值", async ({ page, }) => { await page.goto("/"); await addPneumaticTestComponent(page); const componentTable = page.getByRole("table", { name: "组件参数" }); const pressureRow = parameterRow(componentTable, "参考压力"); const pressureNameCell = parameterNameCell(componentTable, "参考压力"); const pressureValueCell = pressureRow.locator(".parameter-table-value-cell"); const pressureUnitCell = pressureRow.locator(".parameter-table-unit-cell"); const preview = page.locator("body > #parameter-help-preview"); await pressureValueCell.hover(); await page.waitForTimeout(600); await expect(preview).toHaveCount(0); await pressureUnitCell.hover(); await page.waitForTimeout(600); await expect(preview).toHaveCount(0); const pressureNameCellBox = await pressureNameCell.boundingBox(); expect(pressureNameCellBox).not.toBeNull(); const pointer = { x: pressureNameCellBox!.x + pressureNameCellBox!.width / 2, y: pressureNameCellBox!.y + pressureNameCellBox!.height / 2, }; await page.mouse.move(pointer.x, pointer.y); await page.waitForTimeout(600); await expect(preview).toBeVisible(); await expect(preview.locator(".parameter-help-preview-title")).toHaveText("参考压力"); await expect(preview.locator(".parameter-help-preview-description")).toContainText( "单位换算和 XML 基础单位输出", ); await expect(preview.locator(".parameter-help-preview-meta")).toContainText( "默认值:100000", ); await expect(preview.locator(".parameter-help-preview-meta")).toContainText( "当前单位:Pa", ); const previewBox = await preview.boundingBox(); expect(previewBox).not.toBeNull(); const horizontalGap = pointer.x < previewBox!.x ? previewBox!.x - pointer.x : pointer.x > previewBox!.x + previewBox!.width ? pointer.x - (previewBox!.x + previewBox!.width) : 0; const verticalGap = pointer.y < previewBox!.y ? previewBox!.y - pointer.y : pointer.y > previewBox!.y + previewBox!.height ? pointer.y - (previewBox!.y + previewBox!.height) : 0; expect(Math.hypot(horizontalGap, verticalGap)).toBeLessThanOrEqual(24); await page.mouse.move(pointer.x + 24, pointer.y); await page.waitForTimeout(100); const stationaryPreviewBox = await preview.boundingBox(); expect(stationaryPreviewBox).not.toBeNull(); expect(Math.abs(stationaryPreviewBox!.x - previewBox!.x)).toBeLessThanOrEqual(1); expect(Math.abs(stationaryPreviewBox!.y - previewBox!.y)).toBeLessThanOrEqual(1); await pressureValueCell.hover(); await expect(preview).toHaveCount(0); await page.locator(".panel-title").filter({ hasText: "组件参数" }).hover(); await expect(preview).toHaveCount(0); await page.getByLabel("参考压力单位").selectOption("kPa"); await expect(page.getByLabel("参考压力", { exact: true })).toHaveValue("100"); await pressureNameCell.hover(); await page.waitForTimeout(600); await expect(preview.locator(".parameter-help-preview-meta")).toContainText( "默认值:100", ); await expect(preview.locator(".parameter-help-preview-meta")).toContainText( "当前单位:kPa", ); await page.getByRole("button", { name: "生成系统 XML", exact: true }).click(); await expandSimulationConsole(page); const consolePanel = page.getByRole("complementary", { name: "仿真控制台", exact: true, }); await expect(consolePanel.getByLabel("生成的系统 XML")).toContainText( ' { await page.goto("/"); await addPneumaticTestComponent(page); const pressureInput = page.getByLabel("参考压力", { exact: true }); const pressureUnit = page.getByLabel("参考压力单位", { exact: true }); await pressureInput.fill("1e-5"); await pressureInput.press("Enter"); await pressureUnit.selectOption("kPa"); await expect(pressureInput).toHaveValue("1e-8"); await pressureUnit.selectOption("Pa"); await expect(pressureInput).toHaveValue("1e-5"); await pressureUnit.selectOption("kPa"); await pressureInput.fill("1.234567890123E+3"); await pressureInput.press("Enter"); await pressureUnit.selectOption("Pa"); await expect(pressureInput).toHaveValue("1.234567890123E+6"); await pressureUnit.selectOption("kPa"); await expect(pressureInput).toHaveValue("1.234567890123E+3"); await pressureInput.fill("1.20E+3"); await pressureInput.press("Enter"); await pressureUnit.selectOption("Pa"); await expect(pressureInput).toHaveValue("1.20E+6"); await pressureUnit.selectOption("kPa"); await expect(pressureInput).toHaveValue("1.20E+3"); const temperatureInput = page.getByLabel("参考温度", { exact: true }); const temperatureUnit = page.getByLabel("参考温度单位", { exact: true }); await temperatureUnit.selectOption("degC"); await temperatureInput.fill("1e-10"); await temperatureInput.press("Enter"); await temperatureUnit.selectOption("K"); await expect(temperatureInput).toHaveValue("2.731500000001e2"); await temperatureUnit.selectOption("degC"); await expect(temperatureInput).toHaveValue("1e-10"); await page.getByRole("button", { name: "保存工程", exact: true }).click(); await pressureInput.fill("2"); await pressureInput.press("Enter"); await page.getByRole("button", { name: "加载工程", exact: true }).click(); await page .locator('.flow-canvas .react-flow__node[data-id="amesim_pneumatic_test_1"]') .click(); await expect(pressureUnit).toHaveValue("kPa"); await expect(pressureInput).toHaveValue("1.20E+3"); await expect(temperatureUnit).toHaveValue("degC"); await expect(temperatureInput).toHaveValue("1e-10"); await page.getByRole("button", { name: "生成系统 XML", exact: true }).click(); await expandSimulationConsole(page); await expect(page.getByLabel("生成的系统 XML")).toContainText( ' { await page.goto("/"); const simulationTable = page.getByRole("table", { name: "仿真设置" }); const nameHeader = simulationTable.locator("thead .parameter-table-name-cell"); const separator = simulationTable.getByRole("separator", { name: "调整参数名列宽", exact: true, }); const initialWidth = await nameHeader.evaluate( (element) => element.getBoundingClientRect().width, ); const initialValue = Number(await separator.getAttribute("aria-valuenow")); const separatorBox = await separator.boundingBox(); expect(separatorBox).not.toBeNull(); await page.mouse.move( separatorBox!.x + separatorBox!.width / 2, separatorBox!.y + separatorBox!.height / 2, ); await page.mouse.down(); await page.mouse.move( separatorBox!.x + separatorBox!.width / 2 + 16, separatorBox!.y + separatorBox!.height / 2, { steps: 3 }, ); await page.mouse.up(); await expect(separator).toHaveAttribute( "aria-valuenow", String(initialValue + 16), ); const resizedWidth = await nameHeader.evaluate( (element) => element.getBoundingClientRect().width, ); expect(resizedWidth).toBeGreaterThan(initialWidth + 14); await page.reload(); const reloadedTable = page.getByRole("table", { name: "仿真设置" }); await expect( reloadedTable.getByRole("separator", { name: "调整参数名列宽", exact: true, }), ).toHaveAttribute("aria-valuenow", String(initialValue + 16)); const wrapOverflow = await reloadedTable.evaluate((table) => { const wrap = table.closest(".parameter-table-wrap"); return wrap ? wrap.scrollWidth - wrap.clientWidth : Number.POSITIVE_INFINITY; }); expect(wrapOverflow).toBeLessThanOrEqual(1); const reloadedSeparator = reloadedTable.getByRole("separator", { name: "调整参数名列宽", exact: true, }); await reloadedSeparator.dblclick(); await expect(reloadedSeparator).toHaveAttribute("aria-valuenow", "112"); await reloadedSeparator.focus(); await page.keyboard.press("ArrowRight"); await expect(reloadedSeparator).toHaveAttribute("aria-valuenow", "120"); }); test("组件参数和仿真设置是可独立滚动并可调高度的子栏", async ({ page }) => { await page.goto("/"); await addPneumaticTestComponent(page); const componentPane = page.getByRole("region", { name: "组件参数子栏", exact: true, }); const simulationPane = page.getByRole("region", { name: "仿真设置子栏", exact: true, }); const separator = page.getByRole("separator", { name: "调整组件参数和仿真设置区域高度", exact: true, }); await expect(componentPane).toBeVisible(); await expect(simulationPane).toBeVisible(); await expect(separator).toBeVisible(); const initialValue = Number(await separator.getAttribute("aria-valuenow")); const initialMaximum = Number(await separator.getAttribute("aria-valuemax")); expect(initialMaximum).toBeGreaterThanOrEqual(initialValue); const initialComponentBox = await componentPane.boundingBox(); const initialSimulationBox = await simulationPane.boundingBox(); const separatorBox = await separator.boundingBox(); expect(initialComponentBox).not.toBeNull(); expect(initialSimulationBox).not.toBeNull(); expect(separatorBox).not.toBeNull(); expect(initialSimulationBox!.height).toBeGreaterThanOrEqual(120); await page.mouse.move( separatorBox!.x + separatorBox!.width / 2, separatorBox!.y + separatorBox!.height / 2, ); await page.mouse.down(); await page.mouse.move( separatorBox!.x + separatorBox!.width / 2, separatorBox!.y + separatorBox!.height / 2 + 40, { steps: 4 }, ); await page.mouse.up(); await expect(separator).toHaveAttribute( "aria-valuenow", String(initialValue + 40), ); const resizedComponentBox = await componentPane.boundingBox(); const resizedSimulationBox = await simulationPane.boundingBox(); expect(resizedComponentBox!.height).toBeGreaterThan( initialComponentBox!.height + 38, ); expect(resizedSimulationBox!.height).toBeLessThan( initialSimulationBox!.height - 38, ); const overflowModes = await page .locator(".properties-subpane-content") .evaluateAll((elements) => elements.map((element) => getComputedStyle(element).overflowY), ); expect(overflowModes).toEqual(["auto", "auto"]); await page.reload(); const reloadedSeparator = page.getByRole("separator", { name: "调整组件参数和仿真设置区域高度", exact: true, }); await expect(reloadedSeparator).toHaveAttribute( "aria-valuenow", String(initialValue + 40), ); await reloadedSeparator.focus(); await page.keyboard.press("ArrowUp"); await expect(reloadedSeparator).toHaveAttribute( "aria-valuenow", String(initialValue + 20), ); await reloadedSeparator.dblclick(); await expect(reloadedSeparator).toHaveAttribute("aria-valuenow", "340"); }); test("组件参数公式保留原式,并在说明中显示计算值且 XML 只写数值", async ({ page, }) => { await page.goto("/"); await addPneumaticTestComponent(page); const expression = "3.14*10^2/4"; const componentTable = page.getByRole("table", { name: "组件参数" }); const pressureRow = parameterRow(componentTable, "参考压力"); const pressureInput = page.getByLabel("参考压力", { exact: true }); await page.getByLabel("参考压力单位").selectOption("kPa"); await pressureInput.fill(expression); await pressureInput.press("Enter"); await expect(pressureInput).toHaveValue(expression); await expect(pressureRow).not.toContainText("78.5"); await parameterNameCell(componentTable, "参考压力").hover(); await page.waitForTimeout(600); const preview = page.locator("body > #parameter-help-preview"); await expect(preview).toBeVisible(); await expect(preview.getByTestId("parameter-calculated-value")).toContainText( "78.5", ); await expect(preview.getByTestId("parameter-calculated-value")).toContainText( "kPa", ); await page.getByRole("button", { name: "生成系统 XML", exact: true }).click(); await expandSimulationConsole(page); const consolePanel = page.getByRole("complementary", { name: "仿真控制台", exact: true, }); const xmlBlock = consolePanel.getByLabel("生成的系统 XML"); await expect(xmlBlock).toContainText( ' { await page.goto("/"); await addPneumaticTestComponent(page); const invalidExpression = "3.14*/4"; const componentTable = page.getByRole("table", { name: "组件参数" }); const pressureInput = page.getByLabel("参考压力", { exact: true }); await pressureInput.fill(invalidExpression); await pressureInput.press("Enter"); await expect(pressureInput).toHaveValue(invalidExpression); await expect(pressureInput).toHaveAttribute("aria-invalid", "true"); await expect( componentTable.locator(".parameter-table-message-row .field-error"), ).toBeVisible(); await page.getByRole("button", { name: "生成系统 XML", exact: true }).click(); await expandSimulationConsole(page); const consolePanel = page.getByRole("complementary", { name: "仿真控制台", exact: true, }); await expect(consolePanel).toContainText("XML 生成失败"); await expect(consolePanel).toContainText("参考压力"); await expect(consolePanel.getByLabel("生成的系统 XML")).toHaveCount(0); }); test("仿真数值设置同样保留公式,并在执行协议中使用计算值", async ({ page, }) => { await page.goto("/"); const expression = "1+2*3"; const simulationTable = page.getByRole("table", { name: "仿真设置" }); const stopRow = parameterRow(simulationTable, "结束时间"); const stopInput = page.getByLabel("结束时间", { exact: true }); await stopInput.fill(expression); await stopInput.press("Enter"); await expect(stopInput).toHaveValue(expression); await expect(stopRow).not.toContainText("7"); await parameterNameCell(simulationTable, "结束时间").hover(); await page.waitForTimeout(600); await expect( page.getByTestId("parameter-calculated-value"), ).toContainText("7 s"); await page.getByRole("button", { name: "生成系统 XML", exact: true }).click(); await expandSimulationConsole(page); const xmlBlock = page.getByLabel("生成的系统 XML"); await expect(xmlBlock).toContainText('tStop="7"'); await expect(xmlBlock).not.toContainText(expression); }); test("仿真数值设置中的科学计数法在保存加载后保持", async ({ page }) => { await page.goto("/"); const stepInput = page.getByLabel("采样步长", { exact: true }); const maxStepInput = page.getByLabel("最大积分步长", { exact: true }); await stepInput.fill("1e-5"); await stepInput.press("Enter"); await maxStepInput.fill("1.20E-4"); await maxStepInput.press("Enter"); await page.getByRole("button", { name: "保存工程", exact: true }).click(); await stepInput.fill("0.1"); await stepInput.press("Enter"); await maxStepInput.fill("0.01"); await maxStepInput.press("Enter"); await page.getByRole("button", { name: "加载工程", exact: true }).click(); await expect(stepInput).toHaveValue("1e-5"); await expect(maxStepInput).toHaveValue("1.20E-4"); await page.getByRole("button", { name: "生成系统 XML", exact: true }).click(); await expandSimulationConsole(page); const xmlBlock = page.getByLabel("生成的系统 XML"); await expect(xmlBlock).toContainText('sampleStep="0.00001"'); await expect(xmlBlock).toContainText('maxStep="0.00012"'); });