feat(workbench): dock task and chart workspaces
Generic Container CI/CD / test-build-publish (push) Successful in 1m13s
Frontend CI/CD / build-test-publish-and-deploy (push) Successful in 1m14s

This commit is contained in:
2026-11-12 11:42:25 +08:00
parent 33f0b90a7d
commit 7a304a91a8
27 changed files with 766 additions and 201 deletions
+65 -35
View File
@@ -1,6 +1,25 @@
import { expect, test } from "@playwright/test";
import { expect, test, type Locator, type Page } from "@playwright/test";
import { mockInvestigationPresentation } from "./support/mock-investigation-presentation";
async function selectSecondObject(page: Page, surface: Locator) {
const select = surface.getByRole("combobox", { name: "选择图表对象", exact: true });
let label: string;
if (await select.isVisible()) {
await select.click();
const option = page.getByRole("option").nth(1);
label = (await option.innerText()).trim();
await option.click();
} else {
const button = surface.getByRole("button", { name: /^切换对象 / }).nth(1);
label = (await button.getAttribute("aria-label"))!.replace("切换对象 ", "");
await button.click();
}
return async () => {
if (await select.isVisible()) await expect(select).toContainText(label);
else await expect(surface.getByRole("button", { name: `切换对象 ${label}`, exact: true })).toHaveAttribute("aria-pressed", "true");
};
}
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) => {
@@ -13,6 +32,7 @@ for (const target of ["manager", "child"] as const) {
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 });
await page.getByRole("button", { name: "切换地图 Dev Panel" }).click();
const panel = page.getByLabel("本地预览展示", { exact: true });
await panel.getByRole("button", { name: "打开图表管理", exact: true }).click();
const manager = page.getByRole("dialog", { name: "图表管理", exact: true });
@@ -27,7 +47,8 @@ for (const target of ["manager", "child"] as const) {
size: getComputedStyle(node).fontSize, height: node.getBoundingClientRect().height
}));
const before = (await popup.boundingBox())!;
const handle = (await popup.locator(".floating-window-resize").boundingBox())!;
const resizeControl = target === "manager" ? page.getByRole("separator", { name: "调整地图与报告宽度" }) : popup.locator(".floating-window-resize");
const handle = (await resizeControl.boundingBox())!;
const session = await page.context().newCDPSession(page);
await session.send("Emulation.setCPUThrottlingRate", { rate: 4 });
await session.send("Performance.enable");
@@ -41,9 +62,12 @@ for (const target of ["manager", "child"] as const) {
});
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);
const pointer = target === "manager"
? { x: handle.x + handle.width / 2, y: handle.y + handle.height / 2, dx: -128, dy: 0 }
: { x: handle.x + handle.width - 3, y: handle.y + handle.height - 3, dx: 128, dy: 64 };
await page.mouse.move(pointer.x, pointer.y);
await page.mouse.down();
await page.mouse.move(handle.x + handle.width - 3 + 128, handle.y + handle.height - 3 + 64, { steps: 32 });
await page.mouse.move(pointer.x + pointer.dx, pointer.y + pointer.dy, { 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);
@@ -65,7 +89,7 @@ for (const target of ["manager", "child"] as const) {
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(Math.round(preview.height - before.height)).toBe(target === "manager" ? 0 : 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);
@@ -89,40 +113,46 @@ for (const target of ["manager", "child"] as const) {
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 expectSelectedObject = await selectSecondObject(page, chartSurface);
const hiddenSeries = chartSurface.getByLabel("显示的数据序列").getByRole("button").last();
await hiddenSeries.click();
await expect(selectedObject).toHaveAttribute("aria-pressed", "true");
await expectSelectedObject();
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");
if (target === "child") {
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 expectSelectedObject();
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);
} else {
await expect(popup.locator("[data-window-resize-handle]")).toHaveCount(0);
await resizeControl.focus();
await page.keyboard.press("ArrowRight");
await page.keyboard.press("ArrowLeft");
}
await expectSelectedObject();
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);