完善AMESim组件界面与仿真求解稳定性
This commit is contained in:
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
Reference in new issue
Block a user