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 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, }); const heliumItem = palette.getByRole("button", { name: "氦气介质定义", 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(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 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).not.toHaveAttribute("title"); await page.reload(); await expect(palette).toHaveAttribute("data-view-mode", "icons"); await expect(iconsButton).toHaveAttribute("aria-pressed", "true"); await expect(airItem.locator(".palette-item-name")).toBeVisible(); await expect(airItem.locator(".palette-item-meta")).toBeHidden(); }); 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("/"); await page .getByRole("button", { name: "仅图标显示", exact: true }) .click(); 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).toBeHidden(); 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("/"); await page .getByRole("button", { name: "仅图标显示", exact: true }) .click(); 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("/"); await page .getByRole("button", { name: "仅图标显示", exact: true }) .click(); 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(".component-symbol"); const svg = image.querySelector(".component-symbol-svg"); ( window as Window & { __componentDragPreview?: { backgroundColor: string; borderTopWidth: string; hasLegacyCardLabel: boolean; height: number; hotspotX: number; hotspotY: number; rootClassName: string; symbolClassName: string; viewBox: string | null; width: number; }; } ).__componentDragPreview = { backgroundColor: style.backgroundColor, borderTopWidth: style.borderTopWidth, hasLegacyCardLabel: 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; hasLegacyCardLabel: 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!.hasLegacyCardLabel).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); });