Files
SystemSimulationApp/frontend/tests/e2e/project-initial-viewport.spec.ts
T

73 lines
4.0 KiB
TypeScript

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<HTMLElement>(".flow-canvas")!;
const viewport = canvas.querySelector<HTMLElement>(".react-flow__viewport")!;
const nodes = [...canvas.querySelectorAll<HTMLElement>(".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<void>(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);
});
}