|
|
|
@@ -8,6 +8,133 @@ const chart = (index: number) => ({
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test.setTimeout(60_000);
|
|
|
|
|
// Keep unrelated avatar animation out of chart geometry and drag measurements.
|
|
|
|
|
test.use({ reducedMotion: "reduce" });
|
|
|
|
|
|
|
|
|
|
test("cancelled drags release capture and preserve restore, resizing and keyboard controls", async ({ page }) => {
|
|
|
|
|
const task = investigationFixture(1, "图窗拖动取消核查");
|
|
|
|
|
const mock = await mockInvestigationPresentation(page, [task]);
|
|
|
|
|
await selectInvestigation(page, task.title);
|
|
|
|
|
await mock.emit(task, chart(1));
|
|
|
|
|
const popup = page.getByRole("dialog", { name: "合成压力分析 1", exact: true });
|
|
|
|
|
await expect(popup).toBeVisible();
|
|
|
|
|
await page.getByRole("button", { name: "关闭通知", exact: true }).evaluateAll((buttons) => buttons.forEach((button) => (button as HTMLButtonElement).click()));
|
|
|
|
|
const heading = popup.locator(".floating-window-heading");
|
|
|
|
|
await heading.click({ trial: true, position: { x: 200, y: 24 } });
|
|
|
|
|
const original = (await popup.boundingBox())!;
|
|
|
|
|
await heading.evaluate((node) => node.addEventListener("pointerdown", (event) => {
|
|
|
|
|
(node as HTMLElement).dataset.testPointerId = String((event as PointerEvent).pointerId);
|
|
|
|
|
}, { once: true }));
|
|
|
|
|
await page.mouse.move(original.x + 200, original.y + 24);
|
|
|
|
|
await page.mouse.down();
|
|
|
|
|
await page.mouse.move(original.x + 100, original.y + 64, { steps: 4 });
|
|
|
|
|
await expect.poll(async () => Math.round((await popup.boundingBox())!.x)).toBe(original.x - 100);
|
|
|
|
|
await heading.evaluate((node) => node.dispatchEvent(new PointerEvent("pointercancel", {
|
|
|
|
|
pointerId: Number((node as HTMLElement).dataset.testPointerId)
|
|
|
|
|
})));
|
|
|
|
|
await page.mouse.up();
|
|
|
|
|
expect(await popup.boundingBox()).toEqual(original);
|
|
|
|
|
expect(await popup.evaluate((node) => (node as HTMLElement).style.willChange)).toBe("");
|
|
|
|
|
await popup.getByRole("button", { name: "最大化窗口", exact: true }).click();
|
|
|
|
|
expect((await popup.boundingBox())!.x).toBe(8);
|
|
|
|
|
await popup.getByRole("button", { name: "还原窗口", exact: true }).click();
|
|
|
|
|
expect(await popup.boundingBox()).toEqual(original);
|
|
|
|
|
await heading.focus();
|
|
|
|
|
await page.keyboard.press("ArrowRight");
|
|
|
|
|
expect((await popup.boundingBox())!.x).toBe(original.x + 16);
|
|
|
|
|
await page.mouse.move(original.x + 16 + original.width - 3, original.y + original.height - 3);
|
|
|
|
|
await page.mouse.down();
|
|
|
|
|
await page.mouse.move(original.x + 16 + original.width + 37, original.y + original.height + 27, { steps: 4 });
|
|
|
|
|
await page.mouse.up();
|
|
|
|
|
expect((await popup.boundingBox())!.width).toBe(original.width + 40);
|
|
|
|
|
expect((await popup.boundingBox())!.height).toBe(original.height + 30);
|
|
|
|
|
await heading.focus();
|
|
|
|
|
await page.keyboard.press("Escape");
|
|
|
|
|
await expect(popup).toHaveCount(0);
|
|
|
|
|
expect(mock.mutations).toEqual([]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("profiles a single drag with eight comparison chart windows", async ({ page }, testInfo) => {
|
|
|
|
|
await page.setViewportSize({ width: 1600, height: 1000 });
|
|
|
|
|
const task = investigationFixture(1, "多图拖动性能核查");
|
|
|
|
|
const mock = await mockInvestigationPresentation(page, [task]);
|
|
|
|
|
await selectInvestigation(page, task.title);
|
|
|
|
|
const slots = Array.from({ length: 13 }, (_, index) => `${String(5 + Math.floor(index / 4)).padStart(2, "0")}:${String(index % 4 * 15).padStart(2, "0")}`);
|
|
|
|
|
for (let index = 1; index <= 8; index++) {
|
|
|
|
|
await mock.emit(task, { action: "open_chart", chart_id: `comparison-${index}`, evidence_refs: ["synthetic:pressure"], chart: {
|
|
|
|
|
title: `合成七日同期曲线 ${index}`, chart_type: "time_comparison", x_data: slots,
|
|
|
|
|
series: [{ name: "当前", type: "line", data: slots.map((_, point) => 30 - index + point / 10),
|
|
|
|
|
sample_times: slots.map((time) => `2026-07-16T${time}:00+08:00`) },
|
|
|
|
|
...Array.from({ length: 7 }, (_, day) => ({ name: `前${day + 1}天`, type: "scatter",
|
|
|
|
|
data: slots.map((_, point) => point < 10 ? null : 30 + day / 10),
|
|
|
|
|
sample_times: slots.map((time, point) => point < 10 ? null : `2026-07-${String(15 - day).padStart(2, "0")}T${time}:00+08:00`) }))]
|
|
|
|
|
} });
|
|
|
|
|
await expect(page.getByRole("dialog", { name: `合成七日同期曲线 ${index}`, exact: true })).toBeVisible();
|
|
|
|
|
}
|
|
|
|
|
await expect(page.locator("[data-floating-window]")).toHaveCount(8);
|
|
|
|
|
await page.getByRole("button", { name: "关闭通知", exact: true }).evaluateAll((buttons) => buttons.forEach((button) => (button as HTMLButtonElement).click()));
|
|
|
|
|
await page.evaluate(() => document.fonts.ready);
|
|
|
|
|
const popup = page.getByRole("dialog", { name: "合成七日同期曲线 8", exact: true });
|
|
|
|
|
const otherWindows = page.locator('[data-floating-window]').filter({ hasNot: page.getByRole("heading", { name: "合成七日同期曲线 8", exact: true }) });
|
|
|
|
|
const otherPositions = await otherWindows.evaluateAll((nodes) => nodes.map((node) => {
|
|
|
|
|
const { x, y } = node.getBoundingClientRect();
|
|
|
|
|
return { x, y };
|
|
|
|
|
}));
|
|
|
|
|
const heading = (await popup.locator(".floating-window-heading").boundingBox())!;
|
|
|
|
|
const before = (await popup.boundingBox())!;
|
|
|
|
|
const cdp = await page.context().newCDPSession(page);
|
|
|
|
|
await cdp.send("Emulation.setCPUThrottlingRate", { rate: 4 });
|
|
|
|
|
await cdp.send("Performance.enable");
|
|
|
|
|
const first = await cdp.send("Performance.getMetrics");
|
|
|
|
|
const events: Array<{ name: string; dur?: number }> = [];
|
|
|
|
|
cdp.on("Tracing.dataCollected", ({ value }) => events.push(...value));
|
|
|
|
|
await cdp.send("Tracing.start", { categories: "devtools.timeline", transferMode: "ReportEvents" });
|
|
|
|
|
await page.evaluate(() => {
|
|
|
|
|
Object.assign(window, { dragSvgMutations: 0, dragTransformWrites: 0 });
|
|
|
|
|
const observer = new MutationObserver((records) => {
|
|
|
|
|
(window as unknown as { dragSvgMutations: number }).dragSvgMutations += records.length;
|
|
|
|
|
});
|
|
|
|
|
document.querySelectorAll("[data-floating-window] svg").forEach((node) => observer.observe(node, { subtree: true, attributes: true, childList: true }));
|
|
|
|
|
const windowObserver = new MutationObserver((records) => {
|
|
|
|
|
for (const record of records) {
|
|
|
|
|
const previous = document.createElement("div");
|
|
|
|
|
previous.setAttribute("style", record.oldValue ?? "");
|
|
|
|
|
if (previous.style.transform !== (record.target as HTMLElement).style.transform)
|
|
|
|
|
(window as unknown as { dragTransformWrites: number }).dragTransformWrites++;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
document.querySelectorAll("[data-floating-window]").forEach((node) => windowObserver.observe(node, { attributes: true, attributeFilter: ["style"], attributeOldValue: true }));
|
|
|
|
|
});
|
|
|
|
|
const x = heading.x + heading.width - 110;
|
|
|
|
|
const y = heading.y + heading.height / 2;
|
|
|
|
|
await page.mouse.move(x, y);
|
|
|
|
|
await page.mouse.down();
|
|
|
|
|
await page.mouse.move(x - 100, y - 60, { steps: 20 });
|
|
|
|
|
await expect.poll(async () => Math.round((await popup.boundingBox())!.x - before.x)).toBe(-100);
|
|
|
|
|
await page.mouse.move(x - 200, y - 120, { steps: 20 });
|
|
|
|
|
await page.mouse.up();
|
|
|
|
|
await expect.poll(async () => Math.round((await popup.boundingBox())!.x - before.x)).toBe(-200);
|
|
|
|
|
await expect.poll(async () => Math.round((await popup.boundingBox())!.y - before.y)).toBe(-120);
|
|
|
|
|
const last = await cdp.send("Performance.getMetrics");
|
|
|
|
|
const finished = new Promise<void>((resolve) => cdp.once("Tracing.tracingComplete", () => resolve()));
|
|
|
|
|
await cdp.send("Tracing.end");
|
|
|
|
|
await finished;
|
|
|
|
|
const metric = (name: string) => (last.metrics.find((item) => item.name === name)?.value ?? 0) - (first.metrics.find((item) => item.name === name)?.value ?? 0);
|
|
|
|
|
const summary = { metrics: Object.fromEntries(["ScriptDuration", "LayoutDuration", "RecalcStyleDuration", "TaskDuration", "LayoutCount", "RecalcStyleCount"].map((name) => [name, metric(name)])),
|
|
|
|
|
trace: Object.fromEntries(["Paint", "Layout", "UpdateLayoutTree", "FunctionCall"].map((name) => [name, { count: events.filter((event) => event.name === name).length,
|
|
|
|
|
ms: events.filter((event) => event.name === name).reduce((total, event) => total + (event.dur ?? 0), 0) / 1000 }])),
|
|
|
|
|
svgMutations: await page.evaluate(() => (window as unknown as { dragSvgMutations: number }).dragSvgMutations),
|
|
|
|
|
transformWrites: await page.evaluate(() => (window as unknown as { dragTransformWrites: number }).dragTransformWrites) };
|
|
|
|
|
await testInfo.attach("drag-profile", { body: JSON.stringify(summary, null, 2), contentType: "application/json" });
|
|
|
|
|
console.info("Window drag metrics", JSON.stringify(summary));
|
|
|
|
|
expect(summary.transformWrites).toBeLessThanOrEqual(2);
|
|
|
|
|
expect(summary.svgMutations).toBe(0);
|
|
|
|
|
expect(await otherWindows.evaluateAll((nodes) => nodes.map((node) => {
|
|
|
|
|
const { x, y } = node.getBoundingClientRect();
|
|
|
|
|
return { x, y };
|
|
|
|
|
}))).toEqual(otherPositions);
|
|
|
|
|
await testInfo.attach("multiple-charts-after-drag", { body: await page.screenshot({ path: testInfo.outputPath("multiple-charts-after-drag.png") }), contentType: "image/png" });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
for (const height of [900, 720]) {
|
|
|
|
|
test(`multiple chart headings stay accessible and drag on first press at ${height}px height`, async ({ page }, testInfo) => {
|
|
|
|
@@ -19,15 +146,18 @@ for (const height of [900, 720]) {
|
|
|
|
|
await mock.emit(task, { ...chart(index), chart_id: `pressure-${index}` });
|
|
|
|
|
await expect(page.getByRole("dialog", { name: `合成压力分析 ${index}`, exact: true })).toBeVisible();
|
|
|
|
|
}
|
|
|
|
|
await page.getByRole("button", { name: "关闭通知", exact: true }).evaluateAll((buttons) => buttons.forEach((button) => (button as HTMLButtonElement).click()));
|
|
|
|
|
if (height === 900) await page.setViewportSize({ width: 1440, height: 720 });
|
|
|
|
|
for (let index = 1; index <= 4; index++) {
|
|
|
|
|
const popup = page.getByRole("dialog", { name: `合成压力分析 ${index}`, exact: true });
|
|
|
|
|
const heading = popup.locator(".floating-window-heading");
|
|
|
|
|
const box = (await heading.boundingBox())!;
|
|
|
|
|
expect((await popup.boundingBox())!.y).toBe(64 + (index - 1) * (box.height + 8));
|
|
|
|
|
const target = { x: box.x + box.width / 2, y: box.y + box.height / 2 };
|
|
|
|
|
expect(await page.evaluate(({ x, y }) => document.elementFromPoint(x, y)?.closest("[data-floating-window]")?.getAttribute("aria-labelledby"), target))
|
|
|
|
|
.toBe(await popup.getAttribute("aria-labelledby"));
|
|
|
|
|
await expect.poll(async () => {
|
|
|
|
|
const current = (await heading.boundingBox())!;
|
|
|
|
|
return page.evaluate(({ x, y }) => document.elementFromPoint(x, y)?.closest("[data-floating-window]")?.getAttribute("aria-labelledby"),
|
|
|
|
|
{ x: current.x + current.width / 2, y: current.y + current.height / 2 });
|
|
|
|
|
}).toBe(await popup.getAttribute("aria-labelledby"));
|
|
|
|
|
}
|
|
|
|
|
await testInfo.attach(`multiple-charts-before-drag-${height}`, {
|
|
|
|
|
body: await page.screenshot({ path: testInfo.outputPath(`multiple-charts-before-drag-${height}.png`) }), contentType: "image/png"
|
|
|
|
|