466 lines
17 KiB
TypeScript
466 lines
17 KiB
TypeScript
import { expect, test, type Locator } from "@playwright/test";
|
|
import { prepareApp } from "./fixtures";
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
await prepareApp(page);
|
|
await page.addInitScript(() => {
|
|
window.localStorage.removeItem(
|
|
"system-simulation-flow:palette-collapsed-sections",
|
|
);
|
|
});
|
|
});
|
|
|
|
async function expectFixedIconCardSize(
|
|
item: Locator,
|
|
) {
|
|
const itemBox = await item.boundingBox();
|
|
const iconBox = await item.locator(".palette-icon").boundingBox();
|
|
|
|
expect(itemBox).not.toBeNull();
|
|
expect(iconBox).not.toBeNull();
|
|
expect(itemBox!.width).toBeGreaterThanOrEqual(75);
|
|
expect(itemBox!.width).toBeLessThanOrEqual(77);
|
|
expect(itemBox!.height).toBeGreaterThanOrEqual(81);
|
|
expect(itemBox!.height).toBeLessThanOrEqual(83);
|
|
expect(iconBox!.width).toBeGreaterThanOrEqual(47);
|
|
expect(iconBox!.width).toBeLessThanOrEqual(49);
|
|
expect(iconBox!.height).toBeGreaterThanOrEqual(47);
|
|
expect(iconBox!.height).toBeLessThanOrEqual(49);
|
|
}
|
|
|
|
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 airItem = palette.getByRole("button", {
|
|
name: "空气介质定义",
|
|
exact: true,
|
|
});
|
|
const heliumItem = palette.getByRole("button", {
|
|
name: "氦气介质定义",
|
|
exact: true,
|
|
});
|
|
|
|
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(
|
|
'[data-component-symbol="amesim_ideal_air_medium"]',
|
|
);
|
|
const heliumSymbol = heliumItem.locator(
|
|
'[data-component-symbol="amesim_helium_medium"]',
|
|
);
|
|
await expect(airSymbol.locator("svg")).toBeVisible();
|
|
await expect(
|
|
airSymbol.locator('[data-symbol-label="Air"]'),
|
|
).toHaveText("Air");
|
|
await expect(airSymbol.locator(".component-symbol-medium-svg path")).toHaveCount(0);
|
|
await expect(heliumSymbol.locator("svg")).toBeVisible();
|
|
await expect(
|
|
heliumSymbol.locator('[data-symbol-label="He"]'),
|
|
).toHaveText("He");
|
|
await expect(heliumSymbol.locator(".component-symbol-medium-svg path")).toHaveCount(0);
|
|
|
|
const unknownSymbol = palette
|
|
.getByRole("button", { name: "通用测试元件", exact: true })
|
|
.locator('[data-component-symbol="generic"]');
|
|
await expect(unknownSymbol.locator("svg")).toBeVisible();
|
|
|
|
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")).toHaveCount(0);
|
|
await expect(airItem).not.toHaveAttribute("title");
|
|
await expectFixedIconCardSize(airItem);
|
|
|
|
await page.reload();
|
|
|
|
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")).toHaveCount(0);
|
|
});
|
|
|
|
test("组件库和分类支持独立折叠并同步可访问状态", async ({ page }) => {
|
|
await page.goto("/");
|
|
|
|
const palette = page.locator("aside.palette");
|
|
const libraryToggle = palette.getByRole("button", {
|
|
name: "自动化测试组件库",
|
|
exact: true,
|
|
});
|
|
const categoryToggle = palette.getByRole("button", {
|
|
name: "介质物性",
|
|
exact: true,
|
|
});
|
|
|
|
await expect(libraryToggle).toHaveAttribute("aria-expanded", "true");
|
|
const libraryContentId = await libraryToggle.getAttribute("aria-controls");
|
|
expect(libraryContentId).toBeTruthy();
|
|
await expect(page.locator(`[id="${libraryContentId}"]`)).toBeVisible();
|
|
|
|
await expect(categoryToggle).toHaveAttribute("aria-expanded", "true");
|
|
const categoryItemsId = await categoryToggle.getAttribute("aria-controls");
|
|
expect(categoryItemsId).toBeTruthy();
|
|
await expect(page.locator(`[id="${categoryItemsId}"]`)).toBeVisible();
|
|
await expect(
|
|
palette.getByRole("button", { name: "空气介质定义", exact: true }),
|
|
).toBeVisible();
|
|
|
|
await categoryToggle.click();
|
|
await expect(categoryToggle).toHaveAttribute("aria-expanded", "false");
|
|
await expect(page.locator(`[id="${categoryItemsId}"]`)).toHaveCount(0);
|
|
await expect(
|
|
palette.getByRole("button", { name: "空气介质定义", exact: true }),
|
|
).toHaveCount(0);
|
|
|
|
await categoryToggle.click();
|
|
await expect(categoryToggle).toHaveAttribute("aria-expanded", "true");
|
|
await expect(page.locator(`[id="${categoryItemsId}"]`)).toBeVisible();
|
|
|
|
await libraryToggle.click();
|
|
await expect(libraryToggle).toHaveAttribute("aria-expanded", "false");
|
|
await expect(page.locator(`[id="${libraryContentId}"]`)).toHaveCount(0);
|
|
await expect(
|
|
palette.getByRole("button", { name: "介质物性", exact: true }),
|
|
).toHaveCount(0);
|
|
|
|
await libraryToggle.click();
|
|
await expect(libraryToggle).toHaveAttribute("aria-expanded", "true");
|
|
await expect(page.locator(`[id="${libraryContentId}"]`)).toBeVisible();
|
|
await expect(
|
|
palette.getByRole("button", { name: "介质物性", exact: true }),
|
|
).toHaveAttribute("aria-expanded", "true");
|
|
});
|
|
|
|
test("图标名称截断且卡片尺寸不随侧栏和视口改变", async ({ page }) => {
|
|
await page.goto("/");
|
|
|
|
const palette = page.locator("aside.palette");
|
|
const longNameItem = palette.getByRole("button", {
|
|
name: "AMESim 气动测试元件",
|
|
exact: true,
|
|
});
|
|
const name = longNameItem.locator(".palette-item-name");
|
|
const meta = longNameItem.locator(".palette-item-meta");
|
|
|
|
await expect(name).toBeVisible();
|
|
await expect(name).toHaveText("AMESim 气动测试元件");
|
|
await expect(meta).toHaveCount(0);
|
|
await expect(longNameItem).not.toHaveAttribute("title");
|
|
const truncation = await name.evaluate((element) => {
|
|
const style = window.getComputedStyle(element);
|
|
return {
|
|
clipped: element.scrollWidth > element.clientWidth,
|
|
overflow: style.overflow,
|
|
textOverflow: style.textOverflow,
|
|
whiteSpace: style.whiteSpace,
|
|
};
|
|
});
|
|
expect(truncation).toEqual({
|
|
clipped: true,
|
|
overflow: "hidden",
|
|
textOverflow: "ellipsis",
|
|
whiteSpace: "nowrap",
|
|
});
|
|
|
|
await expectFixedIconCardSize(longNameItem);
|
|
const initialPaletteBox = await palette.boundingBox();
|
|
expect(initialPaletteBox).not.toBeNull();
|
|
|
|
const paletteSeparator = page.getByRole("separator", {
|
|
name: "调整组件库宽度",
|
|
});
|
|
await paletteSeparator.focus();
|
|
for (let index = 0; index < 5; index += 1) {
|
|
await page.keyboard.press("ArrowRight");
|
|
}
|
|
await expect(paletteSeparator).toHaveAttribute("aria-valuenow", "330");
|
|
const expandedPaletteBox = await palette.boundingBox();
|
|
expect(expandedPaletteBox).not.toBeNull();
|
|
expect(expandedPaletteBox!.width).toBeGreaterThan(initialPaletteBox!.width + 90);
|
|
await expectFixedIconCardSize(longNameItem);
|
|
|
|
await page.setViewportSize({ width: 1000, height: 760 });
|
|
await expect
|
|
.poll(async () => Number(await paletteSeparator.getAttribute("aria-valuenow")))
|
|
.toBeLessThan(330);
|
|
await expectFixedIconCardSize(longNameItem);
|
|
});
|
|
|
|
test("图标悬停后显示完整名称和预览并在移开后关闭", async ({ page }) => {
|
|
await page.goto("/");
|
|
|
|
const item = page.locator("aside.palette").getByRole("button", {
|
|
name: "AMESim 气动测试元件",
|
|
exact: true,
|
|
});
|
|
const preview = page.locator("body > .palette-icon-preview[role='tooltip']");
|
|
|
|
await item.hover();
|
|
await page.waitForTimeout(600);
|
|
await expect(preview).toBeVisible();
|
|
await expect(preview.locator(".palette-icon-preview-label")).toHaveText(
|
|
"AMESim 气动测试元件",
|
|
);
|
|
await expect(
|
|
preview.locator(
|
|
'.palette-icon-preview-symbol [data-component-symbol="generic"] svg',
|
|
),
|
|
).toBeVisible();
|
|
await expect(item).toHaveAttribute("aria-describedby", "palette-icon-preview");
|
|
|
|
await page.locator(".palette-panel-title").hover();
|
|
await expect(preview).toHaveCount(0);
|
|
await expect(item).not.toHaveAttribute("aria-describedby", "palette-icon-preview");
|
|
});
|
|
|
|
test("图标布局仍可拖入氦气介质并在画布显示专用图标", async ({ page }) => {
|
|
await page.goto("/");
|
|
|
|
const palette = page.locator("aside.palette");
|
|
const heliumItem = palette.getByRole("button", {
|
|
name: "氦气介质定义",
|
|
exact: true,
|
|
});
|
|
const canvas = page.locator(".flow-canvas .react-flow__pane");
|
|
await heliumItem.dragTo(canvas, {
|
|
targetPosition: { x: 420, y: 220 },
|
|
});
|
|
|
|
const heliumNode = page.locator(
|
|
'.flow-canvas .react-flow__node[data-id="amesim_helium_medium_1"]',
|
|
);
|
|
await expect(heliumNode).toBeVisible();
|
|
await expect(
|
|
heliumNode.locator(".sim-node.symbol-presentation-bare"),
|
|
).toBeVisible();
|
|
|
|
const heliumCanvasSymbol = heliumNode.locator(
|
|
'.node-symbol [data-component-symbol="amesim_helium_medium"]',
|
|
);
|
|
await expect(heliumCanvasSymbol).toHaveAttribute(
|
|
"data-symbol-presentation",
|
|
"bare",
|
|
);
|
|
await expect(heliumCanvasSymbol.locator("svg")).toBeVisible();
|
|
await expect(
|
|
heliumCanvasSymbol.locator('[data-symbol-label="He"]'),
|
|
).toHaveText("He");
|
|
});
|
|
|
|
test("从组件库拖入时使用工作区图标作为待拖入预览", async ({ page }) => {
|
|
await page.goto("/");
|
|
await page.evaluate(() => {
|
|
const dataTransferPrototype = DataTransfer.prototype;
|
|
const originalSetDragImage = dataTransferPrototype.setDragImage;
|
|
dataTransferPrototype.setDragImage = function (
|
|
this: DataTransfer,
|
|
image: Element,
|
|
x: number,
|
|
y: number,
|
|
) {
|
|
const bounds = image.getBoundingClientRect();
|
|
const style = window.getComputedStyle(image);
|
|
const symbol = image.querySelector<HTMLElement>(".component-symbol");
|
|
const svg = image.querySelector<SVGSVGElement>(".component-symbol-svg");
|
|
(
|
|
window as Window & {
|
|
__componentDragPreview?: {
|
|
backgroundColor: string;
|
|
borderTopWidth: string;
|
|
hasCardLabel: boolean;
|
|
height: number;
|
|
hotspotX: number;
|
|
hotspotY: number;
|
|
rootClassName: string;
|
|
symbolClassName: string;
|
|
viewBox: string | null;
|
|
width: number;
|
|
};
|
|
}
|
|
).__componentDragPreview = {
|
|
backgroundColor: style.backgroundColor,
|
|
borderTopWidth: style.borderTopWidth,
|
|
hasCardLabel: Boolean(
|
|
image.querySelector(".component-drag-preview-label"),
|
|
),
|
|
height: bounds.height,
|
|
hotspotX: x,
|
|
hotspotY: y,
|
|
rootClassName: image.className,
|
|
symbolClassName: symbol?.className ?? "",
|
|
viewBox: svg?.getAttribute("viewBox") ?? null,
|
|
width: bounds.width,
|
|
};
|
|
dataTransferPrototype.setDragImage = originalSetDragImage;
|
|
return originalSetDragImage.call(this, image, x, y);
|
|
};
|
|
});
|
|
|
|
const heliumItem = page
|
|
.locator("aside.palette")
|
|
.getByRole("button", { name: "氦气介质定义", exact: true });
|
|
const canvas = page.locator(".flow-canvas .react-flow__pane");
|
|
await heliumItem.dragTo(canvas, {
|
|
targetPosition: { x: 420, y: 220 },
|
|
});
|
|
|
|
const preview = await page.evaluate(
|
|
() =>
|
|
(
|
|
window as Window & {
|
|
__componentDragPreview?: {
|
|
backgroundColor: string;
|
|
borderTopWidth: string;
|
|
hasCardLabel: boolean;
|
|
height: number;
|
|
hotspotX: number;
|
|
hotspotY: number;
|
|
rootClassName: string;
|
|
symbolClassName: string;
|
|
viewBox: string | null;
|
|
width: number;
|
|
};
|
|
}
|
|
).__componentDragPreview,
|
|
);
|
|
expect(preview).toBeDefined();
|
|
expect(preview!.rootClassName).toContain("component-drag-preview");
|
|
expect(preview!.symbolClassName).toContain("component-symbol-canvas");
|
|
expect(preview!.symbolClassName).not.toContain("component-symbol-palette");
|
|
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);
|
|
expect(Math.abs(preview!.hotspotY - preview!.height / 2)).toBeLessThanOrEqual(0.5);
|
|
|
|
const heliumNode = page.locator(
|
|
'.flow-canvas .react-flow__node[data-id="amesim_helium_medium_1"]',
|
|
);
|
|
const nodeBounds = await heliumNode.boundingBox();
|
|
expect(nodeBounds).not.toBeNull();
|
|
expect(preview!.width).toBeCloseTo(nodeBounds!.width, 1);
|
|
expect(preview!.height).toBeCloseTo(nodeBounds!.height, 1);
|
|
await expect
|
|
.poll(() =>
|
|
heliumNode
|
|
.locator(".component-symbol-canvas .component-symbol-svg")
|
|
.getAttribute("viewBox"),
|
|
)
|
|
.toBe(preview!.viewBox);
|
|
await expect(page.locator("body > .component-drag-preview")).toHaveCount(0);
|
|
});
|