完善建模交互、组件图标与系统协议

This commit is contained in:
ljz committed 2026-08-15 17:40:18 +08:00
1 parent 456c29b3b6
commit 6572defaa4
66 files changed
+10067 -4163

No files matched your search

+12 -4
View File
@@ -1,5 +1,5 @@
import { expect, test } from "@playwright/test";
import { prepareApp } from "./fixtures";
import { expandSimulationConsole, prepareApp } from "./fixtures";
test.beforeEach(async ({ page }) => {
await prepareApp(page);
@@ -62,8 +62,9 @@ test("拖入介质定义后可供气动元件选择,删除定义会回退到
);
await page.getByRole("button", { name: "生成系统 XML" }).click();
await expandSimulationConsole(page);
const xmlBlock = consolePanel.getByLabel("生成的系统 XML");
await expect(xmlBlock).toContainText('mediumReferenceVersion="1"');
await expect(xmlBlock).toContainText('schemaVersion="3"');
await expect(xmlBlock).toContainText('<Parameter name="gi" value="0"');
});
@@ -82,6 +83,7 @@ test("介质定义的物性计算模型会写入 XML", async ({ page }) => {
await expect(page.getByLabel("物性计算模型")).toHaveValue("0");
await page.getByRole("button", { name: "生成系统 XML" }).click();
await expandSimulationConsole(page);
const consolePanel = page.getByRole("complementary", {
name: "仿真控制台",
exact: true,
@@ -133,6 +135,7 @@ test("氦气介质自动分配 gi,并以 Peng–Robinson 模型写入 XML", as
await expect(mediumSelect).toHaveValue("2");
await page.getByRole("button", { name: "生成系统 XML" }).click();
await expandSimulationConsole(page);
const consolePanel = page.getByRole("complementary", {
name: "仿真控制台",
exact: true,
@@ -156,8 +159,8 @@ test("氦气介质自动分配 gi,并以 Peng–Robinson 模型写入 XML", as
test("导入工程中的未知物性计算模型会被检查出来", async ({ page }) => {
await page.goto("/");
const project = {
projectSchemaVersion: 1,
name: "invalid-property-model",
mediumReferenceVersion: 1,
nodes: [
{
id: "amesim_ideal_air_medium_1",
@@ -196,7 +199,12 @@ test("导入工程中的未知物性计算模型会被检查出来", async ({ pa
name: "仿真控制台",
exact: true,
});
await expect(consolePanel).toContainText(
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(
"空气介质_1:物性计算模型不在支持的可选项中",
);
});
File diff suppressed because it is too large. Load diff
+214 -6
View File
@@ -1,4 +1,4 @@
import { expect, test } from "@playwright/test";
import { expect, test, type Page } from "@playwright/test";
import {
expectAllNodesInsideCanvas,
prepareApp,
@@ -6,14 +6,39 @@ import {
wideProject,
} from "./fixtures";
async function readModelingViewport(page: Page) {
return page
.locator(".flow-canvas .react-flow__viewport")
.evaluate((viewport) => {
const values =
getComputedStyle(viewport)
.transform.match(/-?\d*\.?\d+(?:e[-+]?\d+)?/gi)
?.map(Number) ?? [];
return {
x: values[4] ?? 0,
y: values[5] ?? 0,
zoom: values[0] ?? 1,
};
});
}
test("拖入组件不自动适配画布,手动适配按钮仍然生效", async ({ page }) => {
await prepareApp(page);
await page.goto("/");
const viewport = page.locator(".flow-canvas .react-flow__viewport");
const zoomOut = page.locator(".flow-canvas .react-flow__controls-zoomout");
await zoomOut.click();
await zoomOut.click();
await expect(page.locator(".flow-canvas .react-flow__controls")).toHaveCount(0);
await expect(page.locator(".flow-canvas .react-flow__attribution")).toHaveCount(0);
const pane = page.locator(".flow-canvas .react-flow__pane");
const paneBox = await pane.boundingBox();
expect(paneBox).not.toBeNull();
await page.mouse.move(
paneBox!.x + paneBox!.width / 2,
paneBox!.y + paneBox!.height / 2,
);
await page.keyboard.down("Control");
await page.mouse.wheel(0, 240);
await page.keyboard.up("Control");
await page.waitForTimeout(250);
const viewportBeforeDrop = await viewport.getAttribute("style");
expect(viewportBeforeDrop).toBeTruthy();
@@ -31,6 +56,133 @@ test("拖入组件不自动适配画布,手动适配按钮仍然生效", async
.not.toBe(viewportBeforeDrop);
});
test("普通滚轮纵向平移,Ctrl 滚轮缩放且滚动条和中键拖动保持同步", async ({
page,
}) => {
await prepareApp(page);
await page.goto("/");
const canvas = page.locator(".flow-canvas");
const pane = canvas.locator(".react-flow__pane");
const horizontalScrollbar = page.getByRole("scrollbar", {
name: "水平滚动画布",
});
const verticalScrollbar = page.getByRole("scrollbar", {
name: "垂直滚动画布",
});
await expect(horizontalScrollbar).toBeVisible();
await expect(verticalScrollbar).toBeVisible();
const horizontalBox = await horizontalScrollbar.boundingBox();
const verticalBox = await verticalScrollbar.boundingBox();
expect(horizontalBox).not.toBeNull();
expect(verticalBox).not.toBeNull();
expect(horizontalBox!.height).toBeGreaterThanOrEqual(8);
expect(horizontalBox!.height).toBeLessThanOrEqual(12);
expect(verticalBox!.width).toBeGreaterThanOrEqual(8);
expect(verticalBox!.width).toBeLessThanOrEqual(12);
const paneBox = await pane.boundingBox();
expect(paneBox).not.toBeNull();
const pointer = {
x: paneBox!.x + paneBox!.width * 0.55,
y: paneBox!.y + paneBox!.height * 0.45,
};
await page.mouse.move(pointer.x, pointer.y);
const beforeWheel = await readModelingViewport(page);
const beforeVerticalScroll = await verticalScrollbar.evaluate(
(element) => element.scrollTop,
);
await page.mouse.wheel(0, 180);
await expect
.poll(async () => (await readModelingViewport(page)).y)
.not.toBeCloseTo(beforeWheel.y, 1);
const afterWheel = await readModelingViewport(page);
expect(afterWheel.x).toBeCloseTo(beforeWheel.x, 1);
expect(afterWheel.zoom).toBeCloseTo(beforeWheel.zoom, 3);
await expect
.poll(() => verticalScrollbar.evaluate((element) => element.scrollTop))
.not.toBeCloseTo(beforeVerticalScroll, 1);
const beforeZoom = await readModelingViewport(page);
const horizontalBeforeZoom = await horizontalScrollbar.evaluate(
(element) => element.scrollLeft,
);
const verticalBeforeZoom = await verticalScrollbar.evaluate(
(element) => element.scrollTop,
);
await page.keyboard.down("Control");
await page.mouse.wheel(0, -180);
await page.keyboard.up("Control");
await expect
.poll(async () => (await readModelingViewport(page)).zoom)
.not.toBeCloseTo(beforeZoom.zoom, 2);
const horizontalAfterZoom = await horizontalScrollbar.evaluate(
(element) => element.scrollLeft,
);
const verticalAfterZoom = await verticalScrollbar.evaluate(
(element) => element.scrollTop,
);
expect(
Math.hypot(
horizontalAfterZoom - horizontalBeforeZoom,
verticalAfterZoom - verticalBeforeZoom,
),
).toBeGreaterThan(1);
const beforeMiddleDrag = await readModelingViewport(page);
await page.mouse.move(pointer.x, pointer.y);
await page.mouse.down({ button: "middle" });
await page.mouse.move(pointer.x + 70, pointer.y + 45, { steps: 4 });
await page.mouse.up({ button: "middle" });
await expect
.poll(async () => {
const next = await readModelingViewport(page);
return Math.hypot(next.x - beforeMiddleDrag.x, next.y - beforeMiddleDrag.y);
})
.toBeGreaterThan(20);
const afterMiddleDrag = await readModelingViewport(page);
expect(afterMiddleDrag.zoom).toBeCloseTo(beforeMiddleDrag.zoom, 3);
await expect(canvas.locator(".react-flow__selection")).toHaveCount(0);
await verticalScrollbar.focus();
const beforeKeyboardScroll = await verticalScrollbar.evaluate(
(element) => element.scrollTop,
);
await page.keyboard.press("ArrowDown");
await expect
.poll(() => verticalScrollbar.evaluate((element) => element.scrollTop))
.toBeGreaterThan(beforeKeyboardScroll);
const beforeScrollbarDrag = await readModelingViewport(page);
await horizontalScrollbar.evaluate((element) => {
element.scrollLeft += 120;
element.dispatchEvent(new Event("scroll", { bubbles: true }));
});
await expect
.poll(async () => (await readModelingViewport(page)).x)
.not.toBeCloseTo(beforeScrollbarDrag.x, 1);
const afterScrollbarDrag = await readModelingViewport(page);
expect(afterScrollbarDrag.y).toBeCloseTo(beforeScrollbarDrag.y, 1);
expect(afterScrollbarDrag.zoom).toBeCloseTo(beforeScrollbarDrag.zoom, 3);
const horizontalMaximum = await horizontalScrollbar.evaluate(
(element) => element.scrollWidth - element.clientWidth,
);
const verticalMaximum = await verticalScrollbar.evaluate(
(element) => element.scrollHeight - element.clientHeight,
);
await expect(horizontalScrollbar).toHaveAttribute(
"aria-valuemax",
String(Math.round(horizontalMaximum)),
);
await expect(verticalScrollbar).toHaveAttribute(
"aria-valuemax",
String(Math.round(verticalMaximum)),
);
});
test("加载本地工程后自动适配建模画布", async ({ page }) => {
await prepareApp(page);
await page.addInitScript((project) => {
@@ -48,7 +200,10 @@ test("加载本地工程后自动适配建模画布", async ({ page }) => {
name: "仿真控制台",
exact: true,
});
await expect(consolePanel).not.toContainText("工程已加载");
await expect(consolePanel.locator(".simulation-console-dock-summary")).toContainText(
"工程已加载",
);
await expect(consolePanel.locator(".simulation-console-dock-log")).toHaveCount(0);
await page.getByRole("button", { name: "展开仿真控制台" }).click();
await expect(consolePanel).toContainText("工程已加载");
});
@@ -83,7 +238,10 @@ test("恢复自动保存工程后自动适配建模画布", async ({ page }) =>
name: "仿真控制台",
exact: true,
});
await expect(consolePanel).not.toContainText("已恢复上次自动保存的工程");
await expect(consolePanel.locator(".simulation-console-dock-summary")).toContainText(
"已恢复上次自动保存的工程",
);
await expect(consolePanel.locator(".simulation-console-dock-log")).toHaveCount(0);
await page.getByRole("button", { name: "展开仿真控制台" }).click();
await expect(consolePanel).toContainText("已恢复上次自动保存的工程");
});
@@ -113,6 +271,9 @@ test("切换到结果页时自动适配只读系统图", async ({ page }) => {
);
await page.getByRole("tab", { name: /^结果/ }).click();
await expectAllNodesInsideCanvas(page, ".results-system-canvas");
await expect(
page.locator(".results-system-canvas .react-flow__attribution"),
).toHaveCount(0);
await expect(
page.getByRole("complementary", { name: "仿真控制台", exact: true }),
).toHaveCount(0);
@@ -145,4 +306,51 @@ test("切换到结果页时自动适配只读系统图", async ({ page }) => {
);
await expect(idleResultHandles).toHaveCount(2);
await expect(connectedResultHandles.first()).toHaveCSS("opacity", "0");
await expect(
idleResultHandles.locator('[data-port-glyph-shape="circle"]'),
).toHaveCount(2);
const resultViewportZoom = await page
.locator(".results-system-canvas .react-flow__viewport")
.evaluate(
(element) =>
new DOMMatrixReadOnly(getComputedStyle(element).transform).a,
);
const idleResultPortShapes = await idleResultHandles.evaluateAll((elements) =>
elements.map((element) => {
const bounds = element.getBoundingClientRect();
const glyphBounds = element
.querySelector<SVGSVGElement>(".port-handle-glyph")
?.getBoundingClientRect();
const circle = element.querySelector<SVGCircleElement>(
'[data-port-glyph-shape="circle"]',
);
return {
circle: circle
? {
cx: circle.cx.baseVal.value,
cy: circle.cy.baseVal.value,
r: circle.r.baseVal.value,
}
: null,
glyphHeight: glyphBounds?.height ?? null,
glyphWidth: glyphBounds?.width ?? null,
height: bounds.height,
width: bounds.width,
};
}),
);
for (const shape of idleResultPortShapes) {
expect(Math.abs(shape.width - 6)).toBeLessThan(0.05);
expect(Math.abs(shape.height - 6)).toBeLessThan(0.05);
expect(shape.glyphWidth).not.toBeNull();
expect(shape.glyphHeight).not.toBeNull();
expect(
Math.abs(shape.glyphWidth! - 6 * resultViewportZoom),
).toBeLessThan(0.1);
expect(
Math.abs(shape.glyphHeight! - 6 * resultViewportZoom),
).toBeLessThan(0.1);
expect(Math.abs(shape.glyphWidth! - shape.glyphHeight!)).toBeLessThan(0.01);
expect(shape.circle).toEqual({ cx: 3, cy: 3, r: 2.5 });
}
});
+23 -1
View File
@@ -1,5 +1,15 @@
import { expect, type Page } from "@playwright/test";
export async function expandSimulationConsole(page: Page) {
const expandButton = page.getByRole("button", {
name: "展开仿真控制台",
exact: true,
});
if ((await expandButton.count()) > 0) {
await expandButton.click();
}
}
const physicalPort = (
name: string,
side: "left" | "right",
@@ -73,6 +83,15 @@ const ud00StageParameterNames = Array.from(
export const componentCatalog = {
schemaVersion: 1,
libraries: [
{
id: "experimental",
label: "临时测试组件库",
version: "0.1.0",
sourcePackage: "tests.e2e.experimental",
temporary: true,
order: 100,
components: [],
},
{
id: "e2e-library",
label: "自动化测试组件库",
@@ -323,7 +342,7 @@ export const componentCatalog = {
type: "amesim_mecmas21",
label: "MECMAS21 编码测试元件",
modelType: "amesim_mecmas21",
modelVersion: "1.0.0",
modelVersion: "0.2.0",
symbol: "symbol-not-registered",
order: 6,
category: {
@@ -868,6 +887,7 @@ const projectPort = (
});
export const wideProject = {
projectSchemaVersion: 1,
name: "demo-system",
nodes: Array.from({ length: 4 }, (_, index) => ({
id: `generic_sensor_${index + 1}`,
@@ -880,6 +900,7 @@ export const wideProject = {
label: `generic_sensor_${index + 1}`,
componentType: "generic_sensor",
modelType: "generic_sensor",
modelVersion: "1.0.0",
symbol: "symbol-not-registered",
ports: [
projectPort("port_a", "left", "bidirectional"),
@@ -897,6 +918,7 @@ export const wideProject = {
target: `generic_sensor_${index + 2}`,
sourceHandle: "port_b",
targetHandle: "port_a",
data: { isContactEdge: false },
})),
simulation: {
t_start: 0,
+397 -6
View File
@@ -1,11 +1,11 @@
import { expect, test } from "@playwright/test";
import { prepareApp } from "./fixtures";
import { prepareApp, wideProject } from "./fixtures";
test.beforeEach(async ({ page }) => {
await prepareApp(page);
});
test("未知图形元件使用带端口和名称的默认节点,并将动作结果写入控制台", async ({
test("未知图形元件使用带端口和名称的默认节点且拖入动作不污染仿真控制台", async ({
page,
}) => {
await page.goto("/");
@@ -31,26 +31,113 @@ test("未知图形元件使用带端口和名称的默认节点,并将动作
name: "仿真控制台",
exact: true,
});
await expect(consolePanel).toHaveClass(/minimized/);
await expect(consolePanel).toHaveCSS("height", "40px");
await expect(consolePanel).not.toHaveClass(/has-unread/);
await expect(consolePanel.locator(".simulation-console-unread")).toHaveCount(0);
await expect(consolePanel).not.toContainText("已添加组件");
await expect(consolePanel.locator(".simulation-console-dock-summary")).toHaveText(
"等待仿真任务",
);
await expect(consolePanel.locator(".simulation-console-dock-log")).toHaveCount(0);
await page.getByRole("button", { name: "生成系统 XML" }).click();
await expect(consolePanel).toContainText("XML 已生成");
await expect(consolePanel).toHaveClass(/minimized/);
await expect(consolePanel).toHaveCSS("height", "40px");
await expect(consolePanel.locator(".simulation-console-dock-summary")).toHaveText(
"系统 XML 已生成",
);
await expect(consolePanel.locator(".simulation-console-dock-log")).toHaveCount(0);
await page.getByRole("button", { name: "展开仿真控制台" }).click();
const xmlBlock = consolePanel.getByLabel("生成的系统 XML");
await expect(xmlBlock).toBeVisible();
await expect(xmlBlock).toContainText('<?xml version="1.0" encoding="UTF-8"?>');
await expect(xmlBlock).toContainText('<Component id="generic_sensor_1"');
await expect(xmlBlock).toContainText('schemaVersion="3"');
await expect(xmlBlock).toContainText('unitSystem="SI"');
await expect(xmlBlock).toContainText('sampleStep="0.1"');
await expect(xmlBlock).toContainText(
'<Component id="generic_sensor_1" type="generic_sensor" modelVersion="1.0.0"',
);
await expect(xmlBlock).toContainText('<Parameter name="gain" value="1"');
await expect(xmlBlock).not.toContainText("<Port");
await expect(xmlBlock).not.toContainText("componentType=");
await expect(xmlBlock).not.toContainText("rotation=");
await expect(xmlBlock).not.toContainText("mirrored=");
await expect(page.locator(".properties .xml-output")).toHaveCount(0);
await expect(page.locator(".properties")).not.toContainText("系统 XML");
await page.getByRole("button", { name: "收起仿真控制台" }).click();
await page.getByRole("button", { name: "检查模型" }).click();
await expect(consolePanel).toContainText(/模型检查/);
await expect(consolePanel).toHaveClass(/minimized/);
await expect(consolePanel).toHaveCSS("height", "40px");
await expect(consolePanel.locator(".simulation-console-dock-summary")).toContainText(
"模型检查",
);
await expect(consolePanel.locator(".simulation-console-dock-log")).toHaveCount(0);
await expect(page.locator(".status-strip")).toHaveCount(0);
await expect(page.locator(".validation-section")).toHaveCount(0);
await expect(page.locator(".properties")).not.toContainText("仿真结果");
});
test("XML 导出拒绝目录未定义的组件参数", async ({ page }) => {
await page.goto("/");
await page
.getByRole("button", { name: /通用测试元件/ })
.dragTo(page.locator(".flow-canvas .react-flow__pane"));
await page.getByRole("button", { name: "保存工程", exact: true }).click();
await page.evaluate(() => {
const key = "system-simulation-flow:project:demo-system";
const raw = window.localStorage.getItem(key);
if (!raw) {
throw new Error("测试工程未保存");
}
const project = JSON.parse(raw) as {
nodes: Array<{ data: { parameters: Record<string, number> } }>;
};
project.nodes[0].data.parameters.typoGain = 2;
window.localStorage.setItem(key, JSON.stringify(project));
});
await page.getByRole("button", { name: "加载工程", exact: true }).click();
await page.getByRole("button", { name: "展开仿真控制台" }).click();
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("目录未定义的参数:typoGain");
await expect(consolePanel.getByLabel("生成的系统 XML")).toHaveCount(0);
});
test("XML 参数使用可往返的双精度文本", async ({ page }) => {
await page.goto("/");
await page
.getByRole("button", { name: /通用测试元件/ })
.dragTo(page.locator(".flow-canvas .react-flow__pane"));
await page.getByRole("button", { name: "保存工程", exact: true }).click();
await page.evaluate(() => {
const key = "system-simulation-flow:project:demo-system";
const raw = window.localStorage.getItem(key);
if (!raw) {
throw new Error("测试工程未保存");
}
const project = JSON.parse(raw) as {
nodes: Array<{ data: { parameters: Record<string, number> } }>;
};
project.nodes[0].data.parameters.gain = 1.2345678901234567;
window.localStorage.setItem(key, JSON.stringify(project));
});
await page.getByRole("button", { name: "加载工程", exact: true }).click();
await page.getByRole("button", { name: "展开仿真控制台" }).click();
await page.getByRole("button", { name: "生成系统 XML", exact: true }).click();
await expect(page.getByLabel("生成的系统 XML")).toContainText(
'<Parameter name="gain" value="1.2345678901234567"',
);
});
test("拖入组件后仅选择新组件,变换操作不会继续作用于旧选择", async ({
page,
}) => {
@@ -91,6 +178,310 @@ test("拖入组件后仅选择新组件,变换操作不会继续作用于旧
);
});
test("复制后粘贴进入待放置状态,跟随鼠标并在左键确认时写入工程", async ({
page,
}) => {
await page.goto("/");
const pane = page.locator(".flow-canvas .react-flow__pane");
await page
.getByRole("button", { name: /通用测试元件/ })
.dragTo(pane, { targetPosition: { x: 250, y: 220 } });
const original = page.locator(
'.flow-canvas .react-flow__node[data-id="generic_sensor_1"]',
);
await original.click();
await page.keyboard.press("Control+c");
const paneBounds = await pane.boundingBox();
expect(paneBounds).not.toBeNull();
const firstPointer = {
x: paneBounds!.x + paneBounds!.width * 0.62,
y: paneBounds!.y + paneBounds!.height * 0.54,
};
await page.mouse.move(firstPointer.x, firstPointer.y);
await page.keyboard.press("Control+v");
const preview = page.locator(
'.flow-canvas .react-flow__node[data-id="generic_sensor_2"]',
);
await expect(preview).toHaveClass(/pending-paste-node/);
await expect(page.getByText(/待放置:左键放置/)).toBeVisible();
await expect(page.locator(".flow-canvas .react-flow__node")).toHaveCount(2);
await expect(page.getByLabel("组件名称")).toHaveCount(0);
await expect(page.getByRole("button", { name: "撤销 (Ctrl+Z)" })).toBeDisabled();
await expect(page.getByRole("button", { name: "复制 (Ctrl+C)" })).toBeDisabled();
await expect(page.getByRole("button", { name: "粘贴 (Ctrl+V)" })).toBeDisabled();
await expect(page.getByRole("button", { name: /通用测试元件/ })).toBeDisabled();
const previewBeforeMove = await preview.boundingBox();
expect(previewBeforeMove).not.toBeNull();
await page.mouse.move(firstPointer.x + 90, firstPointer.y + 55, { steps: 4 });
await expect
.poll(async () => (await preview.boundingBox())?.x ?? 0)
.toBeGreaterThan(previewBeforeMove!.x + 40);
const viewport = page.locator(".flow-canvas .react-flow__viewport");
const viewportBeforeZoom = await viewport.getAttribute("style");
await page.keyboard.down("Control");
await page.mouse.wheel(0, -160);
await page.keyboard.up("Control");
await expect(viewport).not.toHaveAttribute("style", viewportBeforeZoom ?? "");
await expect(preview).toHaveClass(/pending-paste-node/);
const previewBeforeScroll = await preview.boundingBox();
expect(previewBeforeScroll).not.toBeNull();
const horizontalScrollbar = page.getByRole("scrollbar", {
name: "水平滚动画布",
});
const viewportBeforeScroll = await viewport.getAttribute("style");
await horizontalScrollbar.focus();
await page.keyboard.press("ArrowRight");
await expect(viewport).not.toHaveAttribute("style", viewportBeforeScroll ?? "");
await expect(preview).toHaveClass(/pending-paste-node/);
const previewAfterScroll = await preview.boundingBox();
expect(previewAfterScroll).not.toBeNull();
expect(
Math.abs(
previewAfterScroll!.x +
previewAfterScroll!.width / 2 -
(previewBeforeScroll!.x + previewBeforeScroll!.width / 2),
),
).toBeLessThanOrEqual(10);
expect(
Math.abs(
previewAfterScroll!.y +
previewAfterScroll!.height / 2 -
(previewBeforeScroll!.y + previewBeforeScroll!.height / 2),
),
).toBeLessThanOrEqual(10);
const verticalBefore = await preview.locator(".sim-node").evaluate((element) =>
element.classList.contains("orientation-vertical"),
);
const previewBounds = await preview.boundingBox();
expect(previewBounds).not.toBeNull();
await page.mouse.click(
previewBounds!.x + previewBounds!.width / 2,
previewBounds!.y + previewBounds!.height / 2,
{ button: "middle" },
);
await expect
.poll(() =>
preview.locator(".sim-node").evaluate((element) =>
element.classList.contains("orientation-vertical"),
),
)
.not.toBe(verticalBefore);
const finalBounds = await preview.boundingBox();
expect(finalBounds).not.toBeNull();
await page.mouse.click(
finalBounds!.x + finalBounds!.width / 2,
finalBounds!.y + finalBounds!.height / 2,
);
await expect(preview).not.toHaveClass(/pending-paste-node/);
await expect(preview).toHaveClass(/selected/);
await expect(page.getByLabel("组件名称")).toHaveValue("generic_sensor_1_副本");
});
test("复制已连接组件时副本 ID 避让画布中的同名节点且不会移动原元件", async ({
page,
}) => {
const project = structuredClone(wideProject);
project.nodes = project.nodes.slice(0, 2);
project.edges = project.edges.slice(0, 1);
project.nodes[1] = {
...project.nodes[1],
data: {
...project.nodes[1].data,
label: "amesim_pneumatic_test_1",
componentType: "amesim_pneumatic_test",
modelType: "amesim_pneumatic_test",
},
};
await page.addInitScript((storedProject) => {
window.localStorage.setItem(
"system-simulation-flow:project:demo-system",
JSON.stringify(storedProject),
);
}, project);
await page.goto("/");
await page.getByRole("button", { name: "加载工程" }).click();
const firstNode = page.locator(
'.flow-canvas .react-flow__node[data-id="generic_sensor_1"]',
);
const occupiedIdNode = page.locator(
'.flow-canvas .react-flow__node[data-id="generic_sensor_2"]',
);
await expect(firstNode).toBeVisible();
await expect(occupiedIdNode).toBeVisible();
const firstBoundsBefore = await firstNode.boundingBox();
const occupiedBoundsBefore = await occupiedIdNode.boundingBox();
expect(firstBoundsBefore).not.toBeNull();
expect(occupiedBoundsBefore).not.toBeNull();
await firstNode.click();
await page.keyboard.press("Control+c");
const pane = page.locator(".flow-canvas .react-flow__pane");
const paneBounds = await pane.boundingBox();
expect(paneBounds).not.toBeNull();
await page.mouse.move(
paneBounds!.x + paneBounds!.width * 0.62,
paneBounds!.y + paneBounds!.height * 0.68,
);
await page.keyboard.press("Control+v");
const preview = page.locator(
'.flow-canvas .react-flow__node[data-id="generic_sensor_3"]',
);
await expect(preview).toHaveClass(/pending-paste-node/);
await expect(preview.locator(".node-label")).toHaveText(
"generic_sensor_1_副本",
);
await expect(occupiedIdNode).not.toHaveClass(/pending-paste-node/);
await expect(page.locator(".flow-canvas .react-flow__node")).toHaveCount(3);
const previewBounds = await preview.boundingBox();
expect(previewBounds).not.toBeNull();
await page.mouse.click(
previewBounds!.x + previewBounds!.width / 2,
previewBounds!.y + previewBounds!.height / 2,
);
await expect(preview).not.toHaveClass(/pending-paste-node/);
await expect(preview).toHaveClass(/selected/);
await expect(page.locator(".flow-canvas .react-flow__edge")).toHaveCount(1);
const firstBoundsAfter = await firstNode.boundingBox();
const occupiedBoundsAfter = await occupiedIdNode.boundingBox();
expect(firstBoundsAfter).toEqual(firstBoundsBefore);
expect(occupiedBoundsAfter).toEqual(occupiedBoundsBefore);
const nodeIds = await page
.locator(".flow-canvas .react-flow__node")
.evaluateAll((nodes) => nodes.map((node) => node.getAttribute("data-id")));
expect(new Set(nodeIds).size).toBe(nodeIds.length);
});
test("控制台文本选择使用浏览器原生复制,不触发元件复制", async ({ page }) => {
await page.goto("/");
await page
.getByRole("button", { name: /通用测试元件/ })
.dragTo(page.locator(".flow-canvas .react-flow__pane"));
const node = page.locator(
'.flow-canvas .react-flow__node[data-id="generic_sensor_1"]',
);
await node.click();
await page.getByRole("button", { name: "展开仿真控制台" }).click();
await page.getByRole("button", { name: "生成系统 XML" }).click();
const xml = page.getByLabel("生成的系统 XML");
await expect(xml).toBeVisible();
await xml.evaluate((element) => {
const range = document.createRange();
range.selectNodeContents(element);
const selection = window.getSelection();
selection?.removeAllRanges();
selection?.addRange(range);
});
const copiedModelMessagesBefore = await page
.locator(".simulation-console-dock-entry-message", { hasText: "已复制" })
.count();
await page.keyboard.press("Control+c");
await expect(page.locator(".flow-canvas .pending-paste-node")).toHaveCount(0);
await expect
.poll(() =>
page
.locator(".simulation-console-dock-entry-message", { hasText: "已复制" })
.count(),
)
.toBe(copiedModelMessagesBefore);
});
test("底部仿真控制台可以调整高度并折叠显示摘要", async ({ page }) => {
await page.goto("/");
const workbench = page.locator(".workbench");
const palette = page.locator(".palette");
const canvas = page.locator(".flow-canvas");
const properties = page.locator(".properties");
const consolePanel = page.getByRole("complementary", {
name: "仿真控制台",
exact: true,
});
const expectClose = (actual: number, expected: number, tolerance = 2) => {
expect(Math.abs(actual - expected)).toBeLessThanOrEqual(tolerance);
};
await expect(consolePanel).toHaveClass(/minimized/);
await expect(consolePanel).toHaveCSS("height", "40px");
const initialWorkbench = await workbench.boundingBox();
const initialPalette = await palette.boundingBox();
const initialCanvas = await canvas.boundingBox();
const initialProperties = await properties.boundingBox();
const initialConsole = await consolePanel.boundingBox();
expect(initialWorkbench).not.toBeNull();
expect(initialPalette).not.toBeNull();
expect(initialCanvas).not.toBeNull();
expect(initialProperties).not.toBeNull();
expect(initialConsole).not.toBeNull();
expectClose(initialConsole!.x, initialCanvas!.x);
expectClose(initialConsole!.width, initialCanvas!.width);
expectClose(initialConsole!.y, initialCanvas!.y + initialCanvas!.height);
expectClose(
initialConsole!.y + initialConsole!.height,
initialWorkbench!.y + initialWorkbench!.height,
);
expectClose(initialPalette!.y, initialWorkbench!.y);
expectClose(initialPalette!.height, initialWorkbench!.height);
expectClose(initialProperties!.y, initialWorkbench!.y);
expectClose(initialProperties!.height, initialWorkbench!.height);
await page.getByRole("button", { name: "展开仿真控制台" }).click();
await expect(consolePanel).toHaveClass(/normal/);
const expandedCanvas = await canvas.boundingBox();
const expandedConsole = await consolePanel.boundingBox();
const expandedPalette = await palette.boundingBox();
const expandedProperties = await properties.boundingBox();
expect(expandedCanvas).not.toBeNull();
expect(expandedConsole).not.toBeNull();
expect(expandedPalette).not.toBeNull();
expect(expandedProperties).not.toBeNull();
expect(expandedCanvas!.height).toBeLessThan(initialCanvas!.height - 100);
expectClose(expandedConsole!.x, expandedCanvas!.x);
expectClose(expandedConsole!.width, expandedCanvas!.width);
expectClose(expandedPalette!.y, initialPalette!.y);
expectClose(expandedPalette!.height, initialPalette!.height);
expectClose(expandedProperties!.y, initialProperties!.y);
expectClose(expandedProperties!.height, initialProperties!.height);
const resizer = page.getByRole("separator", {
name: "调整仿真控制台高度",
});
const resizerBox = await resizer.boundingBox();
expect(resizerBox).not.toBeNull();
await page.mouse.move(resizerBox!.x + resizerBox!.width / 2, resizerBox!.y + 3);
await page.mouse.down();
await page.mouse.move(resizerBox!.x + resizerBox!.width / 2, resizerBox!.y - 55, {
steps: 5,
});
await page.mouse.up();
await expect
.poll(async () => (await consolePanel.boundingBox())?.height ?? 0)
.toBeGreaterThan(expandedConsole!.height + 35);
const resizedConsole = await consolePanel.boundingBox();
const resizedCanvas = await canvas.boundingBox();
expect(resizedConsole).not.toBeNull();
expect(resizedCanvas).not.toBeNull();
expectClose(
resizedConsole!.height - expandedConsole!.height,
expandedCanvas!.height - resizedCanvas!.height,
3,
);
await page.getByRole("button", { name: "收起仿真控制台" }).click();
await expect(consolePanel).toHaveCSS("height", "40px");
await expect(consolePanel.locator(".simulation-console-dock-summary")).not.toBeEmpty();
const collapsedCanvas = await canvas.boundingBox();
expect(collapsedCanvas).not.toBeNull();
expectClose(collapsedCanvas!.height, initialCanvas!.height);
});
test("框选结束后隐藏整体包围框并保留每个组件的虚线选中框", async ({
page,
}) => {
+127 -38
View File
@@ -28,18 +28,116 @@ async function expectFixedIconCardSize(
expect(iconBox!.height).toBeLessThanOrEqual(49);
}
test("组件库可切换详细与仅图标显示,并在刷新后保留选择", async ({ page }) => {
test("组件库标题、库分组和类别分组保持清晰的三级字号", async ({ page }) => {
await page.goto("/");
const paletteTitle = page.locator(".palette-panel-title > span");
const libraryTitle = page.locator(".palette-library-heading strong").first();
const categoryTitle = page.locator(".palette-category-title > span").first();
await expect(paletteTitle).toHaveCSS("font-size", "16px");
await expect(paletteTitle).toHaveCSS("font-weight", "700");
await expect(libraryTitle).toHaveCSS("font-size", "14px");
await expect(libraryTitle).toHaveCSS("font-weight", "700");
await expect(categoryTitle).toHaveCSS("font-size", "12px");
await expect(categoryTitle).toHaveCSS("font-weight", "600");
const [paletteSize, librarySize, categorySize] = await Promise.all(
[paletteTitle, libraryTitle, categoryTitle].map((locator) =>
locator.evaluate((element) => parseFloat(getComputedStyle(element).fontSize)),
),
);
expect(paletteSize).toBeGreaterThan(librarySize);
expect(librarySize).toBeGreaterThan(categorySize);
});
test("组件目录成功时显示绿色状态且隐藏临时测试组件库", async ({ page }) => {
await page.goto("/");
const status = page.locator(".catalog-source");
await expect(status).toHaveClass(/ready/);
await expect(status).toHaveText("已加载 1 个组件库");
await expect(status).toHaveAttribute("title", "已加载 1 个组件库");
await expect(status.locator(".catalog-source-dot")).toHaveCSS(
"background-color",
"rgb(25, 135, 84)",
);
await expect(page.getByText("临时测试组件库", { exact: true })).toHaveCount(0);
await expect(
page.getByRole("button", { name: "自动化测试组件库", exact: true }),
).toBeVisible();
});
test("组件目录失败时不显示兜底库并在状态悬停时说明失败范围", async ({
page,
}) => {
await page.unroute("**/api/components/catalog");
await page.route("**/api/components/catalog", async (route) => {
await route.fulfill({
status: 503,
contentType: "application/json",
body: JSON.stringify({ detail: "catalog unavailable" }),
});
});
await page.goto("/");
const status = page.locator(".catalog-source");
await expect(status).toHaveClass(/error/);
await expect(status).toContainText("加载失败");
await expect(status.locator(".catalog-source-dot")).toHaveCSS(
"background-color",
"rgb(197, 48, 48)",
);
await expect(page.locator("aside.palette .palette-library")).toHaveCount(0);
await expect(page.locator("aside.palette .palette-item")).toHaveCount(0);
await expect(page.getByText("临时测试组件库", { exact: true })).toHaveCount(0);
await status.hover();
const detail = page.getByRole("tooltip");
await expect(detail).toBeVisible();
await expect(detail).toContainText("AMESim 组件库");
await expect(detail).toContainText("503");
});
test("组件目录格式错误时在悬停详情中指出失败的组件库", async ({ page }) => {
await page.unroute("**/api/components/catalog");
await page.route("**/api/components/catalog", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
schemaVersion: 1,
libraries: [
{
id: "broken-library",
label: "损坏组件库",
components: [],
},
],
}),
});
});
await page.goto("/");
const status = page.locator(".catalog-source");
await expect(status).toHaveClass(/error/);
await status.hover();
await expect(page.getByRole("tooltip")).toContainText(
"加载失败的组件库:损坏组件库",
);
await expect(page.locator("aside.palette .palette-library")).toHaveCount(0);
});
test("组件库始终使用图标布局并忽略旧的列表模式设置", async ({ page }) => {
await page.addInitScript(() => {
window.localStorage.setItem(
"system-simulation-flow:palette-display-mode",
"detailed",
);
});
await page.goto("/");
const palette = page.locator("aside.palette");
const detailedButton = page.getByRole("button", {
name: "详细显示",
exact: true,
});
const iconsButton = page.getByRole("button", {
name: "仅图标显示",
exact: true,
});
const airItem = palette.getByRole("button", {
name: "空气介质定义",
exact: true,
@@ -49,9 +147,13 @@ test("组件库可切换详细与仅图标显示,并在刷新后保留选择",
exact: true,
});
await expect(palette).toHaveAttribute("data-view-mode", "detailed");
await expect(detailedButton).toHaveAttribute("aria-pressed", "true");
await expect(iconsButton).toHaveAttribute("aria-pressed", "false");
await expect(palette).not.toHaveAttribute("data-view-mode");
await expect(
page.getByRole("group", { name: "组件库显示方式" }),
).toHaveCount(0);
await expect(page.getByRole("button", { name: "详细显示" })).toHaveCount(0);
await expect(page.getByRole("button", { name: "仅图标显示" })).toHaveCount(0);
await expect(page.getByText("临时测试组件库", { exact: true })).toHaveCount(0);
await expect(airItem.locator(".palette-item-copy")).toBeVisible();
const airSymbol = airItem.locator(
@@ -76,22 +178,19 @@ test("组件库可切换详细与仅图标显示,并在刷新后保留选择",
.locator('[data-component-symbol="generic"]');
await expect(unknownSymbol.locator("svg")).toBeVisible();
await iconsButton.click();
await expect(palette).toHaveAttribute("data-view-mode", "icons");
await expect(detailedButton).toHaveAttribute("aria-pressed", "false");
await expect(iconsButton).toHaveAttribute("aria-pressed", "true");
await expect(airItem).toBeVisible();
await expect(airItem.locator(".palette-item-name")).toBeVisible();
await expect(airItem.locator(".palette-item-name")).toHaveText("空气介质定义");
await expect(airItem.locator(".palette-item-meta")).toBeHidden();
await expect(airItem.locator(".palette-item-meta")).toHaveCount(0);
await expect(airItem).not.toHaveAttribute("title");
await expectFixedIconCardSize(airItem);
await page.reload();
await expect(palette).toHaveAttribute("data-view-mode", "icons");
await expect(iconsButton).toHaveAttribute("aria-pressed", "true");
await expect(palette).not.toHaveAttribute("data-view-mode");
await expect(page.getByRole("button", { name: "详细显示" })).toHaveCount(0);
await expect(airItem.locator(".palette-item-name")).toBeVisible();
await expect(airItem.locator(".palette-item-meta")).toBeHidden();
await expect(airItem.locator(".palette-item-meta")).toHaveCount(0);
});
test("组件库和分类支持独立折叠并同步可访问状态", async ({ page }) => {
@@ -146,11 +245,8 @@ test("组件库和分类支持独立折叠并同步可访问状态", async ({ pa
).toHaveAttribute("aria-expanded", "true");
});
test("仅图标模式名称截断且卡片尺寸不随侧栏和视口改变", async ({ page }) => {
test("图标名称截断且卡片尺寸不随侧栏和视口改变", async ({ page }) => {
await page.goto("/");
await page
.getByRole("button", { name: "仅图标显示", exact: true })
.click();
const palette = page.locator("aside.palette");
const longNameItem = palette.getByRole("button", {
@@ -162,7 +258,7 @@ test("仅图标模式名称截断且卡片尺寸不随侧栏和视口改变", as
await expect(name).toBeVisible();
await expect(name).toHaveText("AMESim 气动测试元件");
await expect(meta).toBeHidden();
await expect(meta).toHaveCount(0);
await expect(longNameItem).not.toHaveAttribute("title");
const truncation = await name.evaluate((element) => {
const style = window.getComputedStyle(element);
@@ -204,11 +300,8 @@ test("仅图标模式名称截断且卡片尺寸不随侧栏和视口改变", as
await expectFixedIconCardSize(longNameItem);
});
test("仅图标模式悬停后显示完整名称和图标预览并在移开后关闭", async ({ page }) => {
test("图标悬停后显示完整名称和预览并在移开后关闭", async ({ page }) => {
await page.goto("/");
await page
.getByRole("button", { name: "仅图标显示", exact: true })
.click();
const item = page.locator("aside.palette").getByRole("button", {
name: "AMESim 气动测试元件",
@@ -234,13 +327,9 @@ test("仅图标模式悬停后显示完整名称和图标预览并在移开后
await expect(item).not.toHaveAttribute("aria-describedby", "palette-icon-preview");
});
test("仅图标模式仍可拖入氦气介质并在画布显示专用图标", async ({ page }) => {
test("图标布局仍可拖入氦气介质并在画布显示专用图标", async ({ page }) => {
await page.goto("/");
await page
.getByRole("button", { name: "仅图标显示", exact: true })
.click();
const palette = page.locator("aside.palette");
const heliumItem = palette.getByRole("button", {
name: "氦气介质定义",
@@ -292,7 +381,7 @@ test("从组件库拖入时使用工作区图标作为待拖入预览", async ({
__componentDragPreview?: {
backgroundColor: string;
borderTopWidth: string;
hasLegacyCardLabel: boolean;
hasCardLabel: boolean;
height: number;
hotspotX: number;
hotspotY: number;
@@ -305,7 +394,7 @@ test("从组件库拖入时使用工作区图标作为待拖入预览", async ({
).__componentDragPreview = {
backgroundColor: style.backgroundColor,
borderTopWidth: style.borderTopWidth,
hasLegacyCardLabel: Boolean(
hasCardLabel: Boolean(
image.querySelector(".component-drag-preview-label"),
),
height: bounds.height,
@@ -336,7 +425,7 @@ test("从组件库拖入时使用工作区图标作为待拖入预览", async ({
__componentDragPreview?: {
backgroundColor: string;
borderTopWidth: string;
hasLegacyCardLabel: boolean;
hasCardLabel: boolean;
height: number;
hotspotX: number;
hotspotY: number;
@@ -352,7 +441,7 @@ test("从组件库拖入时使用工作区图标作为待拖入预览", async ({
expect(preview!.rootClassName).toContain("component-drag-preview");
expect(preview!.symbolClassName).toContain("component-symbol-canvas");
expect(preview!.symbolClassName).not.toContain("component-symbol-palette");
expect(preview!.hasLegacyCardLabel).toBe(false);
expect(preview!.hasCardLabel).toBe(false);
expect(preview!.borderTopWidth).toBe("0px");
expect(preview!.backgroundColor).toBe("rgba(0, 0, 0, 0)");
expect(Math.abs(preview!.hotspotX - preview!.width / 2)).toBeLessThanOrEqual(0.5);
+188 -20
View File
@@ -1,6 +1,6 @@
import { expect, test, type Locator, type Page } from "@playwright/test";
import { prepareApp } from "./fixtures";
import { expandSimulationConsole, prepareApp } from "./fixtures";
test.beforeEach(async ({ page }) => {
await prepareApp(page);
@@ -695,7 +695,7 @@ test("MECMAS21 参数组折叠和条件切换保留值并清理详情浮层", as
);
});
test("MECMAS21 旧参数编码只迁移一次且新工程和 XML 写入版本", async ({
test("MECMAS21 当前参数编码由工程版本统一并写入 XML", async ({
page,
}) => {
await page.goto("/");
@@ -709,7 +709,7 @@ test("MECMAS21 旧参数编码只迁移一次且新工程和 XML 写入版本",
await expect(frictionGroup).toHaveCount(0);
await page.getByRole("button", { name: "保存工程", exact: true }).click();
const savedEncodingVersions = await page.evaluate(() =>
const savedContracts = await page.evaluate(() =>
Object.keys(window.localStorage)
.filter((key) => key.startsWith("system-simulation-flow:project:"))
.flatMap((key) => {
@@ -718,24 +718,32 @@ test("MECMAS21 旧参数编码只迁移一次且新工程和 XML 写入版本",
return [];
}
const project = JSON.parse(raw) as {
amesimParameterEncodingVersion?: number | null;
nodes?: Array<{ id?: string }>;
projectSchemaVersion?: number;
nodes?: Array<{ id?: string; data?: { modelVersion?: string } }>;
};
return project.nodes?.some(
const node = project.nodes?.find(
(node) => node.id === "amesim_mecmas21_1",
)
? [project.amesimParameterEncodingVersion]
);
return node
? [{
projectSchemaVersion: project.projectSchemaVersion,
modelVersion: node.data?.modelVersion,
}]
: [];
}),
);
expect(savedEncodingVersions).toContain(1);
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(
'amesimParameterEncodingVersion="1"',
'type="amesim_mecmas21" modelVersion="0.2.0"',
);
const migratedProjectCount = await page.evaluate(() => {
const changedProjectCount = await page.evaluate(() => {
let changed = 0;
Object.keys(window.localStorage)
.filter((key) =>
@@ -750,8 +758,6 @@ test("MECMAS21 旧参数编码只迁移一次且新工程和 XML 写入版本",
return;
}
const project = JSON.parse(raw) as {
mediumReferenceVersion?: number | null;
amesimParameterEncodingVersion?: number | null;
nodes?: Array<{
id?: string;
data?: {
@@ -766,18 +772,13 @@ test("MECMAS21 旧参数编码只迁移一次且新工程和 XML 写入版本",
if (!node?.data?.parameters) {
return;
}
project.amesimParameterEncodingVersion = null;
project.mediumReferenceVersion = null;
delete node.data.modelType;
node.data.parameters.useFriction = 0;
delete node.data.parameters.strib;
node.data.parameters.unrelatedCode = 9;
window.localStorage.setItem(key, JSON.stringify(project));
changed += 1;
});
return changed;
});
expect(migratedProjectCount).toBeGreaterThanOrEqual(2);
expect(changedProjectCount).toBeGreaterThanOrEqual(2);
await page.getByRole("button", { name: "加载工程", exact: true }).click();
await page
@@ -791,6 +792,7 @@ test("MECMAS21 旧参数编码只迁移一次且新工程和 XML 写入版本",
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");
@@ -813,6 +815,166 @@ test("MECMAS21 旧参数编码只迁移一次且新工程和 XML 写入版本",
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<string, number | string>;
};
}>;
};
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<string, number | string>;
};
}>;
};
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<string, number | string>;
};
}>;
};
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);
@@ -995,6 +1157,7 @@ test("参数说明只在参数列悬停显示,单位切换不改变 XML 基础
);
await page.getByRole("button", { name: "生成系统 XML", exact: true }).click();
await expandSimulationConsole(page);
const consolePanel = page.getByRole("complementary", {
name: "仿真控制台",
exact: true,
@@ -1056,6 +1219,7 @@ test("科学计数法在提交、单位换算和工程加载后保持", async ({
await expect(temperatureInput).toHaveValue("1e-10");
await page.getByRole("button", { name: "生成系统 XML", exact: true }).click();
await expandSimulationConsole(page);
await expect(page.getByLabel("生成的系统 XML")).toContainText(
'<Parameter name="reference_pressure" value="1200000"',
);
@@ -1235,6 +1399,7 @@ test("组件参数公式保留原式,并在说明中显示计算值且 XML 只
);
await page.getByRole("button", { name: "生成系统 XML", exact: true }).click();
await expandSimulationConsole(page);
const consolePanel = page.getByRole("complementary", {
name: "仿真控制台",
exact: true,
@@ -1275,6 +1440,7 @@ test("非法组件参数公式显示错误并阻止 XML 生成", async ({ page }
).toBeVisible();
await page.getByRole("button", { name: "生成系统 XML", exact: true }).click();
await expandSimulationConsole(page);
const consolePanel = page.getByRole("complementary", {
name: "仿真控制台",
exact: true,
@@ -1305,6 +1471,7 @@ test("仿真数值设置同样保留公式,并在执行协议中使用计算
).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);
@@ -1330,7 +1497,8 @@ test("仿真数值设置中的科学计数法在保存加载后保持", async ({
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('step="0.00001"');
await expect(xmlBlock).toContainText('sampleStep="0.00001"');
await expect(xmlBlock).toContainText('maxStep="0.00012"');
});