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

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

+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 });
}
});