feat(charts): manage investigation charts in a scrolling workspace
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { mockInvestigationPresentation } from "./support/mock-investigation-presentation";
|
||||
|
||||
test.use({ reducedMotion: "reduce", viewport: { width: 1600, height: 1000 } });
|
||||
for (const target of ["manager", "child"] as const) {
|
||||
test(`resizing ${target} reflows charts at their actual dimensions without stretching text`, async ({ page }, testInfo) => {
|
||||
test.setTimeout(90_000);
|
||||
await mockInvestigationPresentation(page, []);
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
await page.getByRole("button", { name: "切换地图 Dev Panel" }).click();
|
||||
const tools = page.getByTestId("investigation-dev-tools");
|
||||
await tools.getByRole("combobox", { name: "JSONL 测试案例", exact: true }).click();
|
||||
await page.getByRole("option", { name: "复杂图表:多测点与长时序", exact: true }).click();
|
||||
await tools.getByRole("button", { name: "执行本地预览", exact: true }).click();
|
||||
await expect(tools.getByRole("status")).toContainText("本地预览完成", { timeout: 20_000 });
|
||||
const panel = page.getByLabel("本地预览展示", { exact: true });
|
||||
await panel.getByRole("button", { name: "打开图表管理", exact: true }).click();
|
||||
const manager = page.getByRole("dialog", { name: "图表管理", exact: true });
|
||||
if (target === "child") await manager.getByRole("button", { name: "放大查看 合成全天压力对比", exact: true }).click();
|
||||
const popup = target === "manager" ? manager : page.getByRole("dialog", { name: "合成全天压力对比", exact: true });
|
||||
await expect(popup.locator(".echarts-for-react svg").first()).toBeVisible();
|
||||
await popup.locator(".floating-window-heading").hover();
|
||||
await page.evaluate(() => document.fonts.ready);
|
||||
const instances = await popup.locator(".echarts-for-react").evaluateAll((nodes) => nodes.map((node) => node.getAttribute("_echarts_instance_")));
|
||||
const drawingSize = await popup.locator(".echarts-for-react").first().evaluate((node) => ({ width: node.clientWidth, height: node.clientHeight }));
|
||||
const font = await popup.locator(".echarts-for-react svg text").first().evaluate((node) => ({
|
||||
size: getComputedStyle(node).fontSize, height: node.getBoundingClientRect().height
|
||||
}));
|
||||
const before = (await popup.boundingBox())!;
|
||||
const handle = (await popup.locator(".floating-window-resize").boundingBox())!;
|
||||
const session = await page.context().newCDPSession(page);
|
||||
await session.send("Emulation.setCPUThrottlingRate", { rate: 4 });
|
||||
await session.send("Performance.enable");
|
||||
await popup.evaluate(async (node) => {
|
||||
await new Promise<void>((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
|
||||
Object.assign(window, { chartResizeMutations: 0 });
|
||||
const observer = new MutationObserver((records) => {
|
||||
(window as unknown as { chartResizeMutations: number }).chartResizeMutations += records.length;
|
||||
});
|
||||
node.querySelectorAll(".echarts-for-react svg").forEach((svg) => observer.observe(svg, { subtree: true, attributes: true, childList: true }));
|
||||
});
|
||||
const first = await session.send("Performance.getMetrics");
|
||||
const started = Date.now();
|
||||
await page.mouse.move(handle.x + handle.width - 3, handle.y + handle.height - 3);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(handle.x + handle.width - 3 + 128, handle.y + handle.height - 3 + 64, { steps: 32 });
|
||||
// While the pointer is still held, layout and the chart drawing must adapt together.
|
||||
await expect.poll(() => popup.locator(".echarts-for-react").first().evaluate((node) => Math.abs(node.clientWidth - Number(node.querySelector("svg")?.getAttribute("width"))))).toBeLessThan(1);
|
||||
await expect.poll(() => popup.locator(".echarts-for-react").first().evaluate((node) => Math.abs(node.clientHeight - Number(node.querySelector("svg")?.getAttribute("height"))))).toBeLessThan(1);
|
||||
const during = await page.evaluate(() => (window as unknown as { chartResizeMutations: number }).chartResizeMutations);
|
||||
const preview = (await popup.boundingBox())!;
|
||||
await expect(popup.locator("[data-floating-window-document]")).toHaveCSS("transform", "none");
|
||||
expect(await popup.locator("[data-floating-window-document]").evaluate((node) => ({ width: (node as HTMLElement).style.width, height: (node as HTMLElement).style.height }))).toEqual({ width: "", height: "" });
|
||||
const resizedFont = await popup.locator(".echarts-for-react svg text").first().evaluate((node) => ({ size: getComputedStyle(node).fontSize, height: node.getBoundingClientRect().height }));
|
||||
expect(resizedFont.size).toBe(font.size);
|
||||
expect(Math.abs(resizedFont.height - font.height)).toBeLessThan(1);
|
||||
const actualDrawing = await popup.locator(".echarts-for-react").first().evaluate((node) => ({ width: node.clientWidth, height: node.clientHeight }));
|
||||
expect(actualDrawing.width).toBeGreaterThan(drawingSize.width);
|
||||
expect(actualDrawing.height).toBeGreaterThanOrEqual(drawingSize.height);
|
||||
await page.mouse.up();
|
||||
const last = await session.send("Performance.getMetrics");
|
||||
const metric = (name: string) => (last.metrics.find((item) => item.name === name)?.value ?? 0) - (first.metrics.find((item) => item.name === name)?.value ?? 0);
|
||||
const summary = { target, svgMutationsDuringResize: during, elapsedMs: Date.now() - started,
|
||||
scriptMs: metric("ScriptDuration") * 1000, layoutMs: metric("LayoutDuration") * 1000, taskMs: metric("TaskDuration") * 1000 };
|
||||
console.info("Chart resize profile", JSON.stringify(summary));
|
||||
await testInfo.attach("resize-profile", { body: JSON.stringify(summary, null, 2), contentType: "application/json" });
|
||||
expect(Math.round(preview.width - before.width)).toBe(128);
|
||||
expect(Math.round(preview.height - before.height)).toBe(64);
|
||||
expect(during).toBeGreaterThan(0);
|
||||
// Bound the expensive work on the same 4x CPU fixture used to reproduce the lag.
|
||||
expect(summary.elapsedMs).toBeLessThan(12_000);
|
||||
expect(summary.scriptMs).toBeLessThan(6_000);
|
||||
expect(await popup.locator(".echarts-for-react").evaluateAll((nodes) => nodes.map((node) => node.getAttribute("_echarts_instance_")))).toEqual(instances);
|
||||
await expect.poll(() => popup.locator(".echarts-for-react").first().evaluate((node) => Math.abs(node.clientWidth - Number(node.querySelector("svg")?.getAttribute("width"))))).toBeLessThan(1);
|
||||
const layout = await popup.evaluate((node) => ({
|
||||
plots: Array.from(node.querySelectorAll<HTMLElement>(".echarts-for-react")).filter((plot) => plot.getBoundingClientRect().height > 0).map((plot) => plot.clientHeight),
|
||||
legends: Array.from(node.querySelectorAll<HTMLElement>('[data-chart-group-tile="true"]')).map((card) => {
|
||||
const legend = card.querySelector<HTMLElement>('[aria-label="图表图例"]')!;
|
||||
const first = legend.querySelector("button")!;
|
||||
return { height: legend.clientHeight, firstRowHeight: first.getBoundingClientRect().height,
|
||||
bottomOverflow: legend.getBoundingClientRect().bottom - card.getBoundingClientRect().bottom };
|
||||
})
|
||||
}));
|
||||
for (const height of layout.plots) expect(height).toBeGreaterThanOrEqual(100);
|
||||
for (const legend of layout.legends) {
|
||||
expect(legend.height).toBeGreaterThanOrEqual(legend.firstRowHeight - 1);
|
||||
expect(legend.bottomOverflow).toBeLessThanOrEqual(1);
|
||||
}
|
||||
await testInfo.attach("resize-layout", { body: JSON.stringify(layout, null, 2), contentType: "application/json" });
|
||||
await session.send("Emulation.setCPUThrottlingRate", { rate: 1 });
|
||||
const chartSurface = target === "manager" ? manager.getByRole("group", { name: "合成全天压力对比", exact: true }) : popup;
|
||||
const selectedObject = chartSurface.getByRole("button", { name: /^切换对象 / }).nth(1);
|
||||
await selectedObject.click();
|
||||
const hiddenSeries = chartSurface.getByLabel("显示的数据序列").getByRole("button").last();
|
||||
await hiddenSeries.click();
|
||||
await expect(selectedObject).toHaveAttribute("aria-pressed", "true");
|
||||
await expect(hiddenSeries).toHaveAttribute("aria-pressed", "false");
|
||||
const selectedInstance = await chartSurface.locator(".echarts-for-react").first().getAttribute("_echarts_instance_");
|
||||
const beforeCancel = (await popup.boundingBox())!;
|
||||
const cancelHandle = popup.locator(".floating-window-resize");
|
||||
const cancelBox = (await cancelHandle.boundingBox())!;
|
||||
await cancelHandle.evaluate((node) => node.addEventListener("pointerdown", (event) => {
|
||||
(node as HTMLElement).dataset.testPointerId = String((event as PointerEvent).pointerId);
|
||||
}, { once: true }));
|
||||
await page.mouse.move(cancelBox.x + cancelBox.width - 3, cancelBox.y + cancelBox.height - 3);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(cancelBox.x + cancelBox.width - 3 + 48, cancelBox.y + cancelBox.height - 3 + 32, { steps: 4 });
|
||||
await expect.poll(async () => Math.round((await popup.boundingBox())!.width - beforeCancel.width)).toBe(48);
|
||||
await cancelHandle.evaluate((node) => node.dispatchEvent(new PointerEvent("pointercancel", {
|
||||
pointerId: Number((node as HTMLElement).dataset.testPointerId)
|
||||
})));
|
||||
await page.mouse.up();
|
||||
await expect.poll(() => popup.boundingBox()).toEqual(beforeCancel);
|
||||
await expect(popup.locator("[data-floating-window-document]")).toHaveCSS("transform", "none");
|
||||
await expect(selectedObject).toHaveAttribute("aria-pressed", "true");
|
||||
await expect(hiddenSeries).toHaveAttribute("aria-pressed", "false");
|
||||
const boundaryBox = (await cancelHandle.boundingBox())!;
|
||||
await page.mouse.move(boundaryBox.x + boundaryBox.width - 3, boundaryBox.y + boundaryBox.height - 3);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(1599, 999, { steps: 8 });
|
||||
await page.mouse.up();
|
||||
const final = (await popup.boundingBox())!;
|
||||
expect(Math.round(final.x + final.width)).toBe(1592);
|
||||
expect(Math.round(final.y + final.height)).toBe(992);
|
||||
await expect(selectedObject).toHaveAttribute("aria-pressed", "true");
|
||||
await expect(hiddenSeries).toHaveAttribute("aria-pressed", "false");
|
||||
await expect(chartSurface.locator(".echarts-for-react").first()).toHaveAttribute("_echarts_instance_", selectedInstance!);
|
||||
await expect.poll(() => chartSurface.locator(".echarts-for-react").first().evaluate((node) => Math.abs(node.clientWidth - Number(node.querySelector("svg")?.getAttribute("width"))))).toBeLessThan(1);
|
||||
await testInfo.attach(`resize-${target}`, { body: await popup.screenshot({ path: testInfo.outputPath(`resize-${target}.png`) }), contentType: "image/png" });
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user