完善AMESim组件界面与仿真求解稳定性

This commit is contained in:
ljz committed 2026-08-02 00:57:48 +08:00
1 parent e7177ab03e
commit 410ef535e8
34 files changed
+3340 -251

No files matched your search

@@ -0,0 +1,156 @@
import { expect, test } 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" },
];
const categoryLabels: Record<string, string> = {
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: [],
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("旧工程保存的通用图标会按当前目录定义升级", 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);
});
@@ -0,0 +1,42 @@
import { expect, test } from "@playwright/test";
import { evaluateParameterExpression } from "../../src/parameterExpression";
function expectExpressionValue(expression: string, expected: number) {
const result = evaluateParameterExpression(expression);
expect(result.ok, result.ok ? undefined : result.message).toBe(true);
if (result.ok) {
expect(result.value).toBeCloseTo(expected, 10);
}
}
test("安全解析 AMESim 风格的算术、幂与科学计数等式", () => {
expectExpressionValue("=3.14*10^2/4", 78.5);
expectExpressionValue("(2+3)*4", 20);
expectExpressionValue("2^3^2", 512);
expectExpressionValue("-2^2", -4);
expectExpressionValue("2.5E-3", 0.0025);
});
test("支持有限的数学常量与白名单函数", () => {
expectExpressionValue("sqrt(16)+abs(-2)", 6);
expectExpressionValue("sin(pi/2)+ln(e)", 2);
expectExpressionValue("max(1, 5, 3)+pow(2, 3)", 13);
});
test("拒绝除零、非法定义域、未知变量和任意代码", () => {
for (const expression of [
"1/0",
"sqrt(-1)",
"unknown+1",
"window.alert(1)",
"1+*2",
"1e309",
]) {
const result = evaluateParameterExpression(expression);
expect(result.ok, expression).toBe(false);
if (!result.ok) {
expect(result.message.length).toBeGreaterThan(0);
}
}
});
+105
View File
@@ -201,3 +201,108 @@ test("参数表列宽可调整并在刷新后保留", async ({ page }) => {
await page.keyboard.press("ArrowRight");
await expect(reloadedSeparator).toHaveAttribute("aria-valuenow", "120");
});
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 pressureRow.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();
const consolePanel = page.getByRole("complementary", {
name: "仿真控制台",
exact: true,
});
const xmlBlock = consolePanel.getByLabel("生成的系统 XML");
await expect(xmlBlock).toContainText(
'<Parameter name="reference_pressure" value="78500"',
);
await expect(xmlBlock).not.toContainText(expression);
await page.getByRole("button", { name: "保存工程", exact: true }).click();
await pressureInput.fill("120");
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(page.getByLabel("参考压力", { exact: true })).toHaveValue(
expression,
);
});
test("非法组件参数公式显示错误并阻止 XML 生成", async ({ page }) => {
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();
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 stopRow.hover();
await page.waitForTimeout(600);
await expect(
page.getByTestId("parameter-calculated-value"),
).toContainText("7 s");
await page.getByRole("button", { name: "生成系统 XML", exact: true }).click();
const xmlBlock = page.getByLabel("生成的系统 XML");
await expect(xmlBlock).toContainText('tStop="7"');
await expect(xmlBlock).not.toContainText(expression);
});