804 lines
26 KiB
TypeScript
804 lines
26 KiB
TypeScript
import { expect, test, type Locator, type Page } 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" },
|
|
{ category: "mechanical", domain: "mechanical", modelType: "amesim_pnrp17" },
|
|
];
|
|
|
|
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 mechanicalPort(
|
|
name: string,
|
|
side: "left" | "right",
|
|
order: number,
|
|
) {
|
|
return {
|
|
name,
|
|
kind: "physical",
|
|
domain: "mechanical",
|
|
nominalRole: "bidirectional",
|
|
positiveFlowDirection: "intoComponent",
|
|
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)];
|
|
}
|
|
if (modelType === "amesim_pnrp17") {
|
|
return [
|
|
physicalPort("port_1", "left", 10),
|
|
mechanicalPort("port_3", "left", 20),
|
|
mechanicalPort("port_4", "left", 30),
|
|
mechanicalPort("port_2", "right", 40),
|
|
mechanicalPort("port_5", "right", 50),
|
|
];
|
|
}
|
|
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: "流动元件",
|
|
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: portsForSymbol(item.modelType),
|
|
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("PNRP17 图标匹配参考结构并将五个端口放在对应接口", async ({ page }) => {
|
|
await page.goto("/");
|
|
|
|
const paletteSymbol = page.locator(
|
|
'aside.palette [data-component-symbol="amesim_pnrp17"]',
|
|
);
|
|
await expect(paletteSymbol).toHaveCount(1);
|
|
await expect(
|
|
paletteSymbol.locator('[stroke="#00af00"], [fill="#00af00"]'),
|
|
).toHaveCount(0);
|
|
|
|
const piston = await dragSymbolToCanvas(page, "amesim_pnrp17", {
|
|
x: 430,
|
|
y: 300,
|
|
});
|
|
await expectSymbolAnchors(piston, [
|
|
{ portName: "port_1", x: 25.5, y: 48 },
|
|
{ portName: "port_3", x: 2, y: 4.4 },
|
|
{ portName: "port_4", x: 2, y: 24.1 },
|
|
{ portName: "port_2", x: 62, y: 4.4 },
|
|
{ portName: "port_5", x: 62, y: 24.1 },
|
|
]);
|
|
await expect(
|
|
piston.locator('.port-handle[data-port-name="port_1"]'),
|
|
).toHaveClass(/react-flow__handle-bottom/);
|
|
});
|
|
|
|
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: 26, y: 46 },
|
|
{ portName: "port_2", x: 2, y: 24 },
|
|
{ portName: "port_3", x: 62, y: 24 },
|
|
]);
|
|
|
|
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"]',
|
|
);
|
|
await expect(signalInput).toHaveClass(/react-flow__handle-bottom/);
|
|
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(() => {
|
|
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);
|
|
});
|