完善建模交互与结果曲线功能

支持元件接口接触吸附、旋转后兼容连接及无连线逻辑边。

完善 AMESim 图标显示、参数双栏、结果曲线待定窗口及相关端到端测试。
This commit is contained in:
ljz committed 2026-08-02 17:31:28 +08:00
1 parent 410ef535e8
commit 503394f3ed
10 files changed
+2542 -389

No files matched your search

+595 -2
View File
@@ -1,4 +1,4 @@
import { expect, test } from "@playwright/test";
import { expect, test, type Locator, type Page } from "@playwright/test";
type SymbolCase = {
category: string;
@@ -30,6 +30,176 @@ const symbolCases: SymbolCase[] = [
{ category: "mechanical", domain: "mechanical", modelType: "amesim_lmechn1" },
];
function physicalPort(
name: string,
side: "left" | "right",
order: number,
) {
return {
name,
kind: "physical",
domain: "pneumatic",
nominalRole: "bidirectional",
positiveFlowDirection: "intoComponent",
side,
order,
};
}
function signalPort(
name: string,
side: "left" | "right",
nominalRole: "input" | "output",
order: number,
) {
return {
name,
kind: "signal",
domain: "signal",
nominalRole,
side,
order,
};
}
function portsForSymbol(modelType: string) {
if (modelType === "amesim_pnor001") {
return [
physicalPort("port_1", "left", 10),
physicalPort("port_2", "right", 20),
];
}
if (modelType === "amesim_pnvo001") {
return [
signalPort("res", "left", "input", 5),
physicalPort("port_2", "left", 10),
physicalPort("port_3", "right", 20),
];
}
if (modelType === "amesim_step0") {
return [signalPort("out", "right", "output", 10)];
}
return [];
}
async function dragSymbolToCanvas(
page: Page,
modelType: string,
targetPosition: { x: number; y: number },
) {
const pane = page.locator(".flow-canvas .react-flow__pane");
const paletteItem = page
.locator(".palette-item")
.filter({
has: page.locator(`[data-component-symbol="${modelType}"]`),
})
.first();
await paletteItem.dragTo(pane, { targetPosition });
const node = page
.locator(
`.flow-canvas .react-flow__node[data-id^="${modelType}_"]`,
)
.last();
await expect(node).toBeVisible();
const nodeId = await node.getAttribute("data-id");
expect(nodeId).not.toBeNull();
return page.locator(
`.flow-canvas .react-flow__node[data-id="${nodeId}"]`,
);
}
async function expectSymbolAnchors(
node: Locator,
anchors: Array<{ portName: string; x: number; y: number }>,
) {
for (const anchor of anchors) {
const error = await node.evaluate((element, expected) => {
const svg = element.querySelector<SVGSVGElement>(".component-symbol-svg");
const handle = element.querySelector<HTMLElement>(
`.port-handle[data-port-name="${expected.portName}"]`,
);
const matrix = svg?.getScreenCTM();
if (!svg || !handle || !matrix) {
return Number.POSITIVE_INFINITY;
}
const point = svg.createSVGPoint();
point.x = expected.x;
point.y = expected.y;
const screenPoint = point.matrixTransform(matrix);
const handleBounds = handle.getBoundingClientRect();
return Math.hypot(
screenPoint.x - (handleBounds.left + handleBounds.width / 2),
screenPoint.y - (handleBounds.top + handleBounds.height / 2),
);
}, anchor);
expect(error).toBeLessThan(2.5);
}
}
async function locatorCenter(locator: Locator) {
const bounds = await locator.boundingBox();
expect(bounds).not.toBeNull();
return {
x: bounds!.x + bounds!.width / 2,
y: bounds!.y + bounds!.height / 2,
};
}
async function distanceBetween(first: Locator, second: Locator) {
const [firstCenter, secondCenter] = await Promise.all([
locatorCenter(first),
locatorCenter(second),
]);
return Math.hypot(
secondCenter.x - firstCenter.x,
secondCenter.y - firstCenter.y,
);
}
async function dragNodePortNearTarget(
page: Page,
movingNode: Locator,
movingPort: Locator,
targetPort: Locator,
remainingGap = 8,
) {
const [dragStart, movingPortCenter, targetPortCenter] = await Promise.all([
locatorCenter(movingNode.locator(".node-symbol")),
locatorCenter(movingPort),
locatorCenter(targetPort),
]);
const deltaX = targetPortCenter.x - movingPortCenter.x;
const deltaY = targetPortCenter.y - movingPortCenter.y;
const distance = Math.hypot(deltaX, deltaY);
expect(distance).toBeGreaterThan(remainingGap);
const movementRatio = (distance - remainingGap) / distance;
await page.mouse.move(dragStart.x, dragStart.y);
await page.mouse.down();
let pointerX = dragStart.x + deltaX * movementRatio;
let pointerY = dragStart.y + deltaY * movementRatio;
await page.mouse.move(pointerX, pointerY, { steps: 12 });
// 旋转会改变节点外框尺寸,React Flow 可能同时刷新 viewport;根据实际
// Handle 中心再校正两次,确保测试模拟的是用户真正把端口拖到附近。
for (let attempt = 0; attempt < 2; attempt += 1) {
const [actualMovingCenter, actualTargetCenter] = await Promise.all([
locatorCenter(movingPort),
locatorCenter(targetPort),
]);
const correctionX = actualTargetCenter.x - actualMovingCenter.x;
const correctionY = actualTargetCenter.y - actualMovingCenter.y;
const actualDistance = Math.hypot(correctionX, correctionY);
if (actualDistance <= remainingGap + 1) {
break;
}
const correctionRatio = (actualDistance - remainingGap) / actualDistance;
pointerX += correctionX * correctionRatio;
pointerY += correctionY * correctionRatio;
await page.mouse.move(pointerX, pointerY, { steps: 4 });
}
}
const categoryLabels: Record<string, string> = {
boundary: "边界元件",
flow: "流动元件",
@@ -76,7 +246,7 @@ test.beforeEach(async ({ page }) => {
label: categoryLabels[item.category],
order: Object.keys(categoryLabels).indexOf(item.category) + 1,
},
ports: [],
ports: portsForSymbol(item.modelType),
parameters: [],
})),
},
@@ -106,6 +276,429 @@ test("全部已移植 AMESim 模型使用专属图标及正确领域配色", asy
}
});
test("AMESim canvas nodes use icon anchors and highlight only compatible free ports", async ({
page,
}) => {
await page.goto("/");
const firstOrifice = await dragSymbolToCanvas(page, "amesim_pnor001", {
x: 250,
y: 210,
});
const secondOrifice = await dragSymbolToCanvas(page, "amesim_pnor001", {
x: 610,
y: 210,
});
const stepSource = await dragSymbolToCanvas(page, "amesim_step0", {
x: 250,
y: 450,
});
const signalOrifice = await dragSymbolToCanvas(page, "amesim_pnvo001", {
x: 610,
y: 450,
});
await expect(firstOrifice.locator(".sim-node")).toHaveClass(
/component-icon-only/,
);
await expect(firstOrifice.locator(".node-label")).toBeHidden();
const frameStyle = await firstOrifice.locator(".sim-node-frame").evaluate(
(element) => ({
borderWidth: getComputedStyle(element).borderTopWidth,
background: getComputedStyle(element).backgroundColor,
}),
);
expect(frameStyle).toEqual({
borderWidth: "0px",
background: "rgba(0, 0, 0, 0)",
});
const firstOrificeAnchors = [
{ portName: "port_1", x: 2, y: 24 },
{ portName: "port_2", x: 62, y: 24 },
];
await expectSymbolAnchors(firstOrifice, firstOrificeAnchors);
await expectSymbolAnchors(signalOrifice, [
{ portName: "res", x: 2, y: 16 },
{ portName: "port_2", x: 2, y: 32 },
{ portName: "port_3", x: 62, y: 25 },
]);
await firstOrifice.locator(".node-symbol").click({ position: { x: 56, y: 42 } });
await page
.getByRole("button", { name: "顺时针旋转 90° (Ctrl+R)", exact: true })
.click();
await expectSymbolAnchors(firstOrifice, firstOrificeAnchors);
await page
.getByRole("button", { name: "水平镜像 (Shift+H)", exact: true })
.click();
await expectSymbolAnchors(firstOrifice, firstOrificeAnchors);
const firstPhysicalOutput = firstOrifice.locator(
'.port-handle[data-port-name="port_2"]',
);
const sameNodePort = firstOrifice.locator(
'.port-handle[data-port-name="port_1"]',
);
const compatiblePhysicalPort = secondOrifice.locator(
'.port-handle[data-port-name="port_1"]',
);
const signalOutput = stepSource.locator(
'.port-handle[data-port-name="out"]',
);
const signalInput = signalOrifice.locator(
'.port-handle[data-port-name="res"]',
);
const pneumaticInput = signalOrifice.locator(
'.port-handle[data-port-name="port_2"]',
);
await firstPhysicalOutput.click();
await expect(firstPhysicalOutput).toHaveAttribute(
"data-connection-state",
"origin",
);
await expect(compatiblePhysicalPort).toHaveAttribute(
"data-connection-state",
"compatible",
);
await expect(pneumaticInput).toHaveAttribute(
"data-connection-state",
"compatible",
);
await expect(sameNodePort).toHaveAttribute(
"data-connection-state",
"incompatible",
);
await expect(signalInput).toHaveAttribute(
"data-connection-state",
"incompatible",
);
await page.locator(".flow-canvas .react-flow__pane").click({
position: { x: 20, y: 20 },
});
await expect(firstPhysicalOutput).toHaveAttribute(
"data-connection-state",
"idle",
);
await compatiblePhysicalPort.click();
await expect(page.locator(".flow-canvas .react-flow__edge")).toHaveCount(0);
await expect(compatiblePhysicalPort).toHaveAttribute(
"data-connection-state",
"origin",
);
await page.keyboard.press("Escape");
await expect(compatiblePhysicalPort).toHaveAttribute(
"data-connection-state",
"idle",
);
await firstPhysicalOutput.click();
await compatiblePhysicalPort.click();
await expect(page.locator(".flow-canvas .react-flow__edge")).toHaveCount(1);
await expect(firstPhysicalOutput).toHaveAttribute("data-connected", "true");
await expect(compatiblePhysicalPort).toHaveAttribute("data-connected", "true");
await expect(firstPhysicalOutput).toHaveCSS("opacity", "0");
await expect(compatiblePhysicalPort).toHaveCSS("opacity", "0");
const physicalEdge = page.locator(".flow-canvas .react-flow__edge").first();
await physicalEdge.locator(".react-flow__edge-interaction").click({ force: true });
await page.getByRole("button", { name: "删除 (Delete)", exact: true }).click();
await expect(page.locator(".flow-canvas .react-flow__edge")).toHaveCount(0);
await expect(firstPhysicalOutput).toHaveAttribute("data-connected", "false");
await expect(compatiblePhysicalPort).toHaveAttribute("data-connected", "false");
await expect(firstPhysicalOutput).toHaveCSS("opacity", "1");
await expect(compatiblePhysicalPort).toHaveCSS("opacity", "1");
await expect(firstPhysicalOutput).not.toHaveCSS("pointer-events", "none");
await expect(compatiblePhysicalPort).not.toHaveCSS("pointer-events", "none");
await firstPhysicalOutput.click();
await compatiblePhysicalPort.click();
await expect(page.locator(".flow-canvas .react-flow__edge")).toHaveCount(1);
const signalOutputBounds = await signalOutput.boundingBox();
const signalInputBounds = await signalInput.boundingBox();
expect(signalOutputBounds).not.toBeNull();
expect(signalInputBounds).not.toBeNull();
await page.mouse.move(
signalOutputBounds!.x + signalOutputBounds!.width / 2,
signalOutputBounds!.y + signalOutputBounds!.height / 2,
);
await page.mouse.down();
await expect(signalOutput).toHaveAttribute(
"data-connection-state",
"origin",
);
await expect(signalInput).toHaveAttribute(
"data-connection-state",
"compatible",
);
const paneBounds = await page
.locator(".flow-canvas .react-flow__pane")
.boundingBox();
expect(paneBounds).not.toBeNull();
await page.mouse.move(paneBounds!.x + 20, paneBounds!.y + 20, { steps: 4 });
await page.mouse.up();
await expect(page.locator(".flow-canvas .react-flow__edge")).toHaveCount(1);
await expect(page.locator(".flow-canvas .react-flow__connection")).toHaveCount(0);
await expect(signalOutput).toHaveAttribute(
"data-connection-state",
"idle",
);
await expect(signalInput).toHaveAttribute(
"data-connection-state",
"idle",
);
await expect(pneumaticInput).toHaveAttribute(
"data-connection-state",
"idle",
);
const retriedSignalOutputBounds = await signalOutput.boundingBox();
const retriedSignalInputBounds = await signalInput.boundingBox();
expect(retriedSignalOutputBounds).not.toBeNull();
expect(retriedSignalInputBounds).not.toBeNull();
await page.mouse.move(
retriedSignalOutputBounds!.x + retriedSignalOutputBounds!.width / 2,
retriedSignalOutputBounds!.y + retriedSignalOutputBounds!.height / 2,
);
await page.mouse.down();
await expect(signalOutput).toHaveAttribute(
"data-connection-state",
"origin",
);
await page.mouse.move(
retriedSignalOutputBounds!.x + retriedSignalOutputBounds!.width / 2 + 12,
retriedSignalOutputBounds!.y + retriedSignalOutputBounds!.height / 2,
);
await expect(signalInput).toHaveAttribute(
"data-connection-state",
"compatible",
);
await expect(pneumaticInput).toHaveAttribute(
"data-connection-state",
"incompatible",
);
await page.mouse.move(
retriedSignalInputBounds!.x + retriedSignalInputBounds!.width / 2,
retriedSignalInputBounds!.y + retriedSignalInputBounds!.height / 2,
{ steps: 5 },
);
await page.mouse.up();
await expect(page.locator(".flow-canvas .react-flow__edge")).toHaveCount(2);
await expect(signalOutput).toHaveAttribute("data-connected", "true");
await expect(signalInput).toHaveAttribute("data-connected", "true");
});
test("兼容接口接近时吸附为无连线接触连接,拖开后自动断开", async ({
page,
}) => {
await page.goto("/");
await page.getByRole("button", { name: "关闭网格吸附", exact: true }).click();
const firstOrifice = await dragSymbolToCanvas(page, "amesim_pnor001", {
x: 280,
y: 300,
});
const secondOrifice = await dragSymbolToCanvas(page, "amesim_pnor001", {
x: 650,
y: 300,
});
const firstPort = firstOrifice.locator(
'.port-handle[data-port-name="port_2"]',
);
const secondPort = secondOrifice.locator(
'.port-handle[data-port-name="port_1"]',
);
await dragNodePortNearTarget(
page,
secondOrifice,
secondPort,
firstPort,
);
await expect(firstPort).toHaveAttribute(
"data-connection-state",
"compatible",
);
await expect(secondPort).toHaveAttribute(
"data-connection-state",
"origin",
);
expect(await distanceBetween(firstPort, secondPort)).toBeGreaterThan(4);
await page.mouse.up();
await expect
.poll(() => distanceBetween(firstPort, secondPort))
.toBeLessThan(2);
await expect(page.locator(".flow-canvas .react-flow__edge")).toHaveCount(1);
await expect(page.locator(".flow-canvas .react-flow__edge-path")).toHaveCount(0);
const contactHit = page.locator(".flow-canvas .contact-edge-hit");
await expect(contactHit).toHaveCount(1);
await expect(firstPort).toHaveAttribute("data-connected", "true");
await expect(secondPort).toHaveAttribute("data-connected", "true");
await expect(firstPort).toHaveCSS("opacity", "0");
await expect(secondPort).toHaveCSS("opacity", "0");
await page.getByRole("button", { name: "生成系统 XML", exact: true }).click();
const xmlBlock = page.getByLabel("生成的系统 XML");
await expect(xmlBlock).toContainText('<Connection id="edge-contact-');
await expect(xmlBlock).toContainText('component="amesim_pnor001_1" port="port_2"');
await expect(xmlBlock).toContainText('component="amesim_pnor001_2" port="port_1"');
await page.getByRole("button", { name: "保存工程", exact: true }).click();
const savedEdge = await page.evaluate(() => {
const raw = window.localStorage.getItem(
"system-simulation-flow:project:demo-system",
);
const project = raw ? JSON.parse(raw) : null;
return project?.edges?.[0] ?? null;
});
expect(savedEdge).toMatchObject({
source: "amesim_pnor001_2",
sourceHandle: "port_1",
target: "amesim_pnor001_1",
targetHandle: "port_2",
});
await contactHit.click();
await expect(page.locator(".flow-canvas .react-flow__edge")).toHaveClass(
/editor-edge-selected/,
);
await page.getByRole("button", { name: "删除 (Delete)", exact: true }).click();
await expect(page.locator(".flow-canvas .react-flow__edge")).toHaveCount(0);
await expect(firstPort).toHaveAttribute("data-connected", "false");
await expect(secondPort).toHaveAttribute("data-connected", "false");
expect(await distanceBetween(firstPort, secondPort)).toBeGreaterThan(8);
await dragNodePortNearTarget(
page,
secondOrifice,
secondPort,
firstPort,
5,
);
await expect(firstPort).toHaveAttribute(
"data-connection-state",
"compatible",
);
await page.mouse.up();
await expect(page.locator(".flow-canvas .react-flow__edge")).toHaveCount(1);
await expect(page.locator(".flow-canvas .react-flow__edge-path")).toHaveCount(0);
await page
.getByRole("button", { name: "顺时针旋转 90° (Ctrl+R)", exact: true })
.click();
await expect(page.locator(".flow-canvas .react-flow__edge")).toHaveCount(0);
await page.getByRole("button", { name: "撤销 (Ctrl+Z)", exact: true }).click();
await expect(page.locator(".flow-canvas .react-flow__edge")).toHaveCount(1);
await expect(page.locator(".flow-canvas .react-flow__edge-path")).toHaveCount(0);
const secondSymbolCenter = await locatorCenter(
secondOrifice.locator(".node-symbol"),
);
await page.mouse.move(secondSymbolCenter.x, secondSymbolCenter.y);
await page.mouse.down();
await page.mouse.move(secondSymbolCenter.x + 90, secondSymbolCenter.y + 35, {
steps: 10,
});
await page.mouse.up();
await expect(page.locator(".flow-canvas .react-flow__edge")).toHaveCount(0);
await expect(firstPort).toHaveAttribute("data-connected", "false");
await expect(secondPort).toHaveAttribute("data-connected", "false");
await expect(firstPort).toHaveCSS("opacity", "1");
await expect(secondPort).toHaveCSS("opacity", "1");
expect(await distanceBetween(firstPort, secondPort)).toBeGreaterThan(50);
});
test("不同类型接口接近时不会吸附或建立逻辑连接", async ({ page }) => {
await page.goto("/");
await page.getByRole("button", { name: "关闭网格吸附", exact: true }).click();
const orifice = await dragSymbolToCanvas(page, "amesim_pnor001", {
x: 280,
y: 320,
});
const stepSource = await dragSymbolToCanvas(page, "amesim_step0", {
x: 650,
y: 320,
});
const physicalPort = orifice.locator(
'.port-handle[data-port-name="port_2"]',
);
const signalPort = stepSource.locator(
'.port-handle[data-port-name="out"]',
);
await dragNodePortNearTarget(
page,
stepSource,
signalPort,
physicalPort,
);
await expect(physicalPort).not.toHaveAttribute(
"data-connection-state",
"compatible",
);
await page.mouse.up();
await expect(page.locator(".flow-canvas .react-flow__edge")).toHaveCount(0);
expect(await distanceBetween(physicalPort, signalPort)).toBeGreaterThan(4);
await expect(physicalPort).toHaveAttribute("data-connected", "false");
await expect(signalPort).toHaveAttribute("data-connected", "false");
});
test("旋转后的接口可以与方向不同的兼容接口接触连接", async ({ page }) => {
await page.goto("/");
await page.getByRole("button", { name: "关闭网格吸附", exact: true }).click();
const horizontalOrifice = await dragSymbolToCanvas(page, "amesim_pnor001", {
x: 300,
y: 300,
});
const rotatedOrifice = await dragSymbolToCanvas(page, "amesim_pnor001", {
x: 650,
y: 500,
});
await rotatedOrifice.locator(".node-symbol").click();
await page
.getByRole("button", { name: "顺时针旋转 90° (Ctrl+R)", exact: true })
.click();
const horizontalPort = horizontalOrifice.locator(
'.port-handle[data-port-name="port_2"]',
);
const rotatedPort = rotatedOrifice.locator(
'.port-handle[data-port-name="port_1"]',
);
await dragNodePortNearTarget(
page,
rotatedOrifice,
rotatedPort,
horizontalPort,
);
await expect(horizontalPort).toHaveAttribute(
"data-connection-state",
"compatible",
);
await expect(rotatedPort).toHaveAttribute(
"data-connection-state",
"origin",
);
await page.mouse.up();
await expect
.poll(() => distanceBetween(horizontalPort, rotatedPort))
.toBeLessThan(2);
await expect(page.locator(".flow-canvas .react-flow__edge")).toHaveCount(1);
await expect(page.locator(".flow-canvas .react-flow__edge-path")).toHaveCount(0);
await expect(horizontalPort).toHaveAttribute("data-connected", "true");
await expect(rotatedPort).toHaveAttribute("data-connected", "true");
});
test("旧工程保存的通用图标会按当前目录定义升级", async ({ page }) => {
await page.goto("/");
await page.evaluate(() => {
+30 -2
View File
@@ -52,14 +52,27 @@ test("恢复自动保存工程后自动适配建模画布", async ({ page }) =>
test("切换到结果页时自动适配只读系统图", async ({ page }) => {
await prepareApp(page);
await page.addInitScript((snapshot) => {
await page.addInitScript(({ project, snapshot }) => {
window.sessionStorage.setItem(
"system-simulation-flow:latest-result",
JSON.stringify(snapshot),
);
}, resultSnapshot);
window.localStorage.setItem(
"system-simulation-flow:project:demo-system",
JSON.stringify(project),
);
}, { project: wideProject, snapshot: resultSnapshot });
await page.goto("/");
await page.getByRole("button", { name: "加载工程" }).click();
const freeModelingHandle = page.locator(
'.flow-canvas .react-flow__node[data-id="generic_sensor_1"] .port-handle[data-port-name="port_a"]',
);
await freeModelingHandle.dispatchEvent("click");
await expect(freeModelingHandle).toHaveAttribute(
"data-connection-state",
"origin",
);
await page.getByRole("tab", { name: /^结果/ }).click();
await expectAllNodesInsideCanvas(page, ".results-system-canvas");
await expect(
@@ -67,6 +80,10 @@ test("切换到结果页时自动适配只读系统图", async ({ page }) => {
).toHaveCount(0);
await page.getByRole("tab", { name: "建模" }).click();
await expect(freeModelingHandle).toHaveAttribute(
"data-connection-state",
"idle",
);
await expect(
page.getByRole("complementary", { name: "仿真控制台", exact: true }),
).toBeVisible();
@@ -78,4 +95,15 @@ test("切换到结果页时自动适配只读系统图", async ({ page }) => {
await expect(page.locator(".results-system-canvas .react-flow__edge")).toHaveCount(
wideProject.edges.length,
);
const connectedResultHandles = page.locator(
'.results-system-canvas .port-handle[data-connected="true"]',
);
const idleResultHandles = page.locator(
'.results-system-canvas .port-handle[data-connected="false"]',
);
await expect(connectedResultHandles).toHaveCount(
wideProject.edges.length * 2,
);
await expect(idleResultHandles).toHaveCount(2);
await expect(connectedResultHandles.first()).toHaveCSS("opacity", "0");
});
@@ -202,6 +202,88 @@ test("参数表列宽可调整并在刷新后保留", async ({ page }) => {
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,
}) => {
@@ -0,0 +1,160 @@
import { expect, test, type Page } from "@playwright/test";
import { prepareApp, resultSnapshot } from "./fixtures";
const chartWindowSnapshot = {
...resultSnapshot,
id: "e2e-pending-chart-windows",
result: {
...resultSnapshot.result,
variables: [
...resultSnapshot.result.variables,
{
key: "generic_sensor_1.pressure",
componentId: "generic_sensor_1",
componentType: "generic_sensor",
scope: "component",
portName: null,
name: "pressure",
label: "压力",
quantity: "pressure",
unit: "Pa",
category: "state",
order: 2,
},
],
final: {
...resultSnapshot.result.final,
"generic_sensor_1.pressure": 120000,
},
series: {
...resultSnapshot.result.series,
"generic_sensor_1.pressure": [100000, 110000, 120000],
},
},
};
async function openResults(page: Page) {
await page.getByRole("tab", { name: "结果", exact: true }).click();
await expect(page.locator(".results-chart-workspace")).toBeVisible();
}
test.beforeEach(async ({ page }) => {
await prepareApp(page);
await page.addInitScript((snapshot) => {
window.sessionStorage.setItem(
"system-simulation-flow:latest-result",
JSON.stringify(snapshot),
);
}, chartWindowSnapshot);
});
test("组合曲线窗口先创建为空窗,并在删除最后一条曲线后恢复待定", async ({
page,
}) => {
await page.goto("/");
await openResults(page);
await page
.getByRole("button", { name: "新建同单位多曲线对比窗口", exact: true })
.click();
let sameUnitWindow = page.locator(
'.result-chart-window[data-chart-kind="multi"]',
);
await expect(sameUnitWindow).toHaveCount(1);
await expect(sameUnitWindow).toHaveAttribute("data-pending", "true");
await expect(sameUnitWindow).toContainText("等待第一条曲线");
const valueVariable = page
.locator(".results-variable-list button")
.filter({ hasText: "数值" })
.first();
await valueVariable.dragTo(sameUnitWindow);
await expect(sameUnitWindow).toHaveAttribute("data-pending", "false");
await expect(sameUnitWindow).toContainText("1/1 条显示");
await expect(sameUnitWindow).toContainText("无量纲");
await sameUnitWindow
.getByRole("button", {
name: "管理多曲线窗口中的曲线",
exact: true,
})
.click();
await sameUnitWindow.locator(".result-chart-multi-menu-row .remove").click();
await expect(sameUnitWindow).toHaveCount(1);
await expect(sameUnitWindow).toHaveAttribute("data-pending", "true");
await expect(sameUnitWindow).toContainText("等待第一条曲线");
await page.reload();
await openResults(page);
sameUnitWindow = page.locator(
'.result-chart-window[data-chart-kind="multi"]',
);
await expect(sameUnitWindow).toHaveCount(1);
await expect(sameUnitWindow).toHaveAttribute("data-pending", "true");
await page
.getByRole("button", { name: "新建异单位上下对比窗口", exact: true })
.click();
const mixedWindow = page.locator(
'.result-chart-window[data-chart-kind="mixed"]',
);
await expect(mixedWindow).toHaveCount(1);
await expect(mixedWindow).toHaveAttribute("data-pending", "true");
const pressureVariable = page
.locator(".results-variable-list button")
.filter({ hasText: "压力" })
.first();
await pressureVariable.dragTo(mixedWindow);
await expect(mixedWindow).toHaveAttribute("data-pending", "false");
await expect(mixedWindow).toContainText("Pa");
await mixedWindow
.getByRole("button", {
name: "管理多曲线窗口中的曲线",
exact: true,
})
.click();
await mixedWindow.locator(".result-chart-multi-menu-row .remove").click();
await expect(mixedWindow).toHaveCount(1);
await expect(mixedWindow).toHaveAttribute("data-pending", "true");
});
test("a pending window recovered from stale variable keys accepts a new first curve", async ({
page,
}) => {
await page.addInitScript((snapshotId) => {
window.sessionStorage.setItem(
`system-simulation-flow:result-layout:${snapshotId}`,
JSON.stringify([
{
id: "chart-1",
kind: "multi",
variableKey: "removed.variable",
variableKeys: ["removed.variable"],
hiddenVariableKeys: [],
variableUnits: { "removed.variable": "Pa" },
x: 40,
y: 40,
width: 620,
height: 330,
unit: "Pa",
cursorEnabled: false,
zIndex: 1,
},
]),
);
}, chartWindowSnapshot.id);
await page.goto("/");
await openResults(page);
const pendingWindow = page.locator(
'.result-chart-window[data-chart-kind="multi"]',
);
await expect(pendingWindow).toHaveAttribute("data-pending", "true");
await page.locator(".results-variable-list button").first().dragTo(pendingWindow);
await expect(pendingWindow).toHaveAttribute("data-pending", "false");
});