import { test, expect } from "@playwright/test"; import { prepareApp, wideProject } from "./fixtures"; for (const action of ["import", "load", "recover"] as const) { test(`${action}: first painted project is already fitted, including replacement with identical node IDs`, async ({ page }) => { await prepareApp(page); await page.goto("/"); await expect(page.getByRole("button", { name: /通用测试元件/ })).toBeVisible(); const old = { ...wideProject, nodes: wideProject.nodes.map((node, index) => ({ ...node, position: { x: index * 70, y: 0 }, })) }; if (action !== "recover") { await page.locator('input[type="file"]').setInputFiles({ name: "old.json", mimeType: "application/json", buffer: Buffer.from(JSON.stringify(old)) }); await expect(page.locator(".flow-canvas")).not.toHaveClass(/is-fitting-project/); } await page.evaluate(({ project, action }) => { const key = "system-simulation-flow:autosave"; localStorage.setItem(key, JSON.stringify(project)); }, { project: wideProject, action }); if (action === "recover") { await page.reload(); await expect(page.getByRole("button", { name: "恢复草稿", exact: true })).toBeEnabled(); } if (action === "load") await page.getByRole("textbox", { name: "工程", exact: true }).fill("target"); // Frame sampling, not just final bounds: detect even a single visible frame // with the incoming wide graph rendered at the previous/default scale. await page.evaluate(() => { const samples: Array<{ opacity: number; fits: boolean; transform: string }> = []; Object.assign(window, { projectPaintSamples: samples, projectPaintDone: false }); const sample = () => { const canvas = document.querySelector(".flow-canvas")!; const viewport = canvas.querySelector(".react-flow__viewport")!; const nodes = [...canvas.querySelectorAll(".react-flow__node")]; // The last incoming node is positioned far away, unlike the old compact graph. const incoming = nodes.find(node => node.dataset.id === "generic_sensor_4"); if (incoming && incoming.style.transform.includes("1860")) { const bounds = canvas.getBoundingClientRect(); samples.push({ opacity: Number(getComputedStyle(viewport).opacity), transform: getComputedStyle(viewport).transform, fits: nodes.every(node => { const rect = node.getBoundingClientRect(); return rect.left >= bounds.left - 1 && rect.right <= bounds.right + 1 && rect.top >= bounds.top - 1 && rect.bottom <= bounds.bottom + 1; }), }); } if (!(window as unknown as { projectPaintDone: boolean }).projectPaintDone) requestAnimationFrame(sample); }; requestAnimationFrame(sample); }); if (action === "import") { await page.locator('input[type="file"]').setInputFiles({ name: "target.json", mimeType: "application/json", buffer: Buffer.from(JSON.stringify(wideProject)) }); } else { await page.getByRole("button", { name: action === "load" ? "加载工程" : "恢复草稿", exact: true }).click(); } await expect(page.locator(".flow-canvas")).not.toHaveClass(/is-fitting-project/); const samples = await page.evaluate(async () => { for (let frame = 0; frame < 12; frame++) await new Promise(resolve => requestAnimationFrame(() => resolve())); const state = window as unknown as { projectPaintDone: boolean; projectPaintSamples: Array<{ opacity: number; fits: boolean; transform: string }> }; state.projectPaintDone = true; return state.projectPaintSamples; }); const visible = samples.filter(sample => sample.opacity > 0); expect(visible.length).toBeGreaterThan(0); expect(visible.every(sample => sample.fits), JSON.stringify(samples)).toBe(true); expect(new Set(visible.map(sample => sample.transform)).size).toBe(1); }); }