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
@@ -168,7 +168,7 @@ test("调查报告展示 SVG 脉络、处置进展和报告版本,缺数正常
expect(second).not.toBeNull();
return [first!, second!].map((box) => ({ x: Math.round(box.x), y: Math.round(box.y), width: Math.round(box.width), height: Math.round(box.height) }));
};
await expect(panel).toHaveCSS("width", "880px");
await expect(panel).toHaveAttribute("data-presentation", "workspace");
const scheduledColumns = await captureColumns();
await testInfo.attach("scheduled-tab-alignment", { body: await panel.screenshot({ path: testInfo.outputPath("scheduled-tab-alignment.png") }), contentType: "image/png" });
await panel.getByRole("button", { name: /预约任务/ }).click();
@@ -378,7 +378,7 @@ test("调查报告展示 SVG 脉络、处置进展和报告版本,缺数正常
await expect(panel.getByRole("button", { name: "确认并创建预约工单" })).toHaveCount(0);
await page.getByRole("button", { name: "折叠 Agent 面板", exact: true }).click();
await expect(page.getByRole("button", { name: /展开 Agent 助手面板/ })).toBeVisible();
await expect(panel).toHaveCSS("width", "880px");
await expect(panel).toHaveAttribute("data-presentation", "workspace");
await panel.getByRole("heading", { name: "当前结果", exact: true }).scrollIntoViewIfNeeded();
await testInfo.attach("insufficient-report-desktop", { body: await panel.screenshot({
path: testInfo.outputPath("insufficient-report-desktop.png") }), contentType: "image/png" });
@@ -401,6 +401,7 @@ test("调查报告展示 SVG 脉络、处置进展和报告版本,缺数正常
const mobileExpand = mobilePanel.getByRole("button", { name: "展开工况任务" });
if (await mobileExpand.isVisible()) await mobileExpand.click();
await mobilePanel.getByRole("button", { name: /调查任务/ }).click();
await mobilePanel.getByText("水质投诉动态调查", { exact: true }).first().click();
await mobilePanel.getByLabel("调查脉络", { exact: true }).scrollIntoViewIfNeeded();
await expect(mobilePanel.getByLabel("调查脉络", { exact: true }).locator("svg[id]")).toHaveCount(1);
const mobileDiagram = mobilePanel.getByLabel("调查脉络", { exact: true });
@@ -0,0 +1,118 @@
import { expect, test } from "@playwright/test";
import { investigationFixture, mockInvestigationPresentation, selectInvestigation, showInvestigationMap, showInvestigationTasks } from "./support/mock-investigation-presentation";
import { responsesFixture } from "./support/responses-fixture";
test.use({ reducedMotion: "reduce" });
const chart = (id: string) => ({ action: "open_chart", chart_id: id, evidence_refs: ["synthetic:pressure"],
chart: { title: `停靠监测 ${id}`, chart_type: "line", x_data: ["07:45", "08:00"], series: [{ name: "压力(m)", data: [32, 28] }] } });
test("chart manager occupies the right workspace and preserves charts when switching to tasks", async ({ page }, testInfo) => {
await page.setViewportSize({ width: 1920, height: 1000 });
const task = investigationFixture(1, "右侧图表工作区验收");
const mock = await mockInvestigationPresentation(page, [task]);
await selectInvestigation(page, task.title);
await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click();
const presentation = page.getByLabel("调查实时展示", { exact: true });
await expect(presentation).toContainText("实时跟随");
await mock.emit(task, chart("P1"));
const manager = page.getByRole("dialog", { name: "图表管理", exact: true });
await expect(manager).toBeVisible();
await expect(manager).toHaveAttribute("data-window-presentation", "docked");
await expect(page.getByRole("button", { name: /窗口管理,当前/ })).toHaveCount(0);
await expect(manager.getByRole("button", { name: /最大化窗口|还原窗口/ })).toHaveCount(0);
await expect(manager.locator("[data-window-resize-handle]")).toHaveCount(0);
await expect(page.getByRole("region", { name: "工况任务", exact: true })).toBeHidden();
const box = (await manager.boundingBox())!;
const map = (await page.locator("#workspace-map").boundingBox())!;
expect(box.y).toBe(56);
expect(box.x).toBeGreaterThanOrEqual(map.x + map.width);
expect(Math.round(box.x + box.width)).toBe(1920);
await expect(manager.getByRole("img", { name: "停靠监测 P1", exact: true })).toBeVisible();
const separator = page.getByRole("separator", { name: "调整地图与报告宽度" });
await separator.focus();
await page.keyboard.press("End");
await expect.poll(async () => Math.round((await manager.boundingBox())!.width)).toBe(600);
await page.screenshot({ path: testInfo.outputPath("chart-workspace-minimum.png") });
const instance = await manager.locator(".echarts-for-react").getAttribute("_echarts_instance_");
await page.getByRole("button", { name: "工况任务", exact: true }).click();
await expect(manager).toBeHidden();
await expect(page.getByRole("region", { name: "工况任务", exact: true })).toBeVisible();
await showInvestigationMap(page);
await presentation.getByRole("button", { name: "打开图表管理", exact: true }).press("Enter");
await expect(manager).toBeVisible();
await expect(manager.getByLabel("图表管理标题栏", { exact: true })).toBeFocused();
await expect(manager.locator(".echarts-for-react")).toHaveAttribute("_echarts_instance_", instance!);
for (const width of [1200, 1024, 390, 320]) {
await page.setViewportSize({ width, height: 800 });
await expect(page.locator("#main-workspace")).toHaveAttribute("data-layout-mode", "focus");
const bounds = (await manager.boundingBox())!;
expect(bounds.x + bounds.width).toBeLessThanOrEqual(width);
expect(await manager.evaluate((node) => node.scrollWidth - node.clientWidth)).toBeLessThanOrEqual(1);
await page.screenshot({ path: testInfo.outputPath(`chart-workspace-${width}.png`) });
}
await page.setViewportSize({ width: 1920, height: 1000 });
await manager.getByRole("button", { name: "关闭窗口", exact: true }).click();
await expect(manager).toHaveCount(0);
await expect(page.locator("#workspace-map")).toBeVisible();
await showInvestigationTasks(page);
await expect(page.getByRole("region", { name: "工况任务", exact: true })).toBeVisible();
expect(mock.mutations).toEqual([]);
});
test("multiple chart sources share the visible dock without resetting their charts", async ({ page }) => {
await page.setViewportSize({ width: 1920, height: 1000 });
const task = investigationFixture(1, "多来源停靠验收");
const mock = await mockInvestigationPresentation(page, [task]);
await selectInvestigation(page, task.title);
await mock.emit(task, chart("P1"));
const first = page.getByRole("dialog", { name: "图表管理", exact: true }).filter({ hasText: task.title });
await expect(first).toBeVisible();
await expect(first.locator(".echarts-for-react svg")).toBeVisible();
const instance = await first.locator(".echarts-for-react").getAttribute("_echarts_instance_");
await page.getByRole("button", { name: "切换地图 Dev Panel" }).click();
const tools = page.getByTestId("investigation-dev-tools");
await tools.getByLabel("调查展示 JSONL", { exact: true }).fill(JSON.stringify({ record_type: "presentation", command: chart("preview") }));
await expect(tools.getByRole("combobox", { name: "JSONL 测试案例", exact: true })).toBeEnabled();
await tools.getByRole("button", { name: "执行本地预览", exact: true }).click();
await expect(tools.getByRole("status")).toContainText("本地预览完成");
await page.getByRole("button", { name: "切换地图 Dev Panel" }).click();
await page.getByLabel("本地预览展示", { exact: true }).getByRole("button", { name: "打开图表管理", exact: true }).press("Enter");
const second = page.getByRole("dialog", { name: "图表管理", exact: true }).filter({ hasText: "本地预览展示" });
await expect(second).toBeVisible();
expect((await second.boundingBox())!.y).toBe(56);
await expect(first).toBeHidden();
await page.getByLabel("调查实时展示", { exact: true }).getByRole("button", { name: "打开图表管理", exact: true }).press("Enter");
await expect(first).toBeVisible();
expect((await first.boundingBox())!.y).toBe(56);
await expect(second).toBeHidden();
await expect(first.locator(".echarts-for-react")).toHaveAttribute("_echarts_instance_", instance!);
await expect(page.getByRole("button", { name: /窗口管理,当前/ })).toHaveCount(0);
});
test("Agent map location reveals the map while the chart workspace is focused", async ({ page }) => {
await page.setViewportSize({ width: 1200, height: 900 });
const task = investigationFixture(1, "图表专注地图定位验收");
const mock = await mockInvestigationPresentation(page, [task]);
const block = { version: "answer-ui@1", block_id: "dock-location", fallback_text: "合成地图定位", spec: {
root: "location", elements: { location: { type: "MapLocation", props: { label: "定位合成监测点", layer: "junctions", feature_ids: ["P1"], source: "浏览器验收" }, children: [] } }
} };
await page.route("**/api/v1/responses", (route) => route.request().method() === "OPTIONS" ? route.fallback() : route.fulfill({
headers: { "Access-Control-Allow-Origin": "*", "Content-Type": "text/event-stream" }, body: responsesFixture([
{ type: "start", messageId: "dock-location-answer" }, { type: "data-answer_ui", id: block.block_id, data: block },
{ type: "data-final_answer", data: { content: "合成地图定位", ui_block_ids: [block.block_id] } }, { type: "finish", finishReason: "stop" }
])
}));
await selectInvestigation(page, task.title);
await mock.emit(task, chart("P1"));
await expect(page.getByRole("dialog", { name: "图表管理", exact: true })).toBeVisible();
await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click();
const agent = page.getByRole("complementary", { name: "Agent 命令面板" });
await agent.locator(".agent-panel-prompt-textarea").fill("定位合成监测点");
await agent.getByRole("button", { name: "发送 Agent 指令", exact: true }).click();
await expect(page.locator("#workspace-map")).toBeHidden();
await agent.getByRole("button", { name: "定位合成监测点", exact: true }).click();
await expect(page.locator("#workspace-map")).toBeVisible();
await expect(page.locator("#main-workspace")).toHaveAttribute("data-layout-mode", "map_only");
await expect(page.getByRole("button", { name: "返回图表管理", exact: true })).toBeVisible();
});
@@ -77,6 +77,7 @@ async function resolvedColor(element: Locator, token: string, property: string)
}
async function setup(page: Page, count: number) {
await page.setViewportSize({ width: 1920, height: 1000 });
await page.route("**/@fs/**/noto-sans-sc-*.woff2", (route) => route.fulfill({
path: resolve("node_modules/@fontsource-variable/noto-sans-sc/files", basename(new URL(route.request().url()).pathname)), contentType: "font/woff2"
}));
@@ -301,10 +302,8 @@ test("scrolling layouts adapt charts and retain empty places without turning int
}
expect(dimensions.one.width).toBeGreaterThan(dimensions.two.width);
expect(dimensions.one.height).toBeGreaterThan(dimensions.four.height);
const heading = manager.getByLabel("窗口标题栏,可用方向键移动,Shift 加方向键调整大小", { exact: true });
await heading.focus();
for (let index = 0; index < 20; index++) await page.keyboard.press("Shift+ArrowUp");
expect((await manager.boundingBox())!.height).toBeLessThanOrEqual(441);
await page.setViewportSize({ width: 1920, height: 480 });
await expect.poll(async () => (await manager.boundingBox())!.height).toBe(424);
await manager.getByRole("combobox", { name: "快速定位图表", exact: true }).click();
await page.getByRole("option").filter({ hasText: "合成监测 P3" }).click();
await expect(card(manager, "P3")).toBeFocused();
@@ -369,7 +368,7 @@ test("default keyboard dragging swaps fixed slots and narrow scrolling grids ret
await expect.poll(() => manager.locator("[data-chart-manager-scroll]").evaluate((node) => node.scrollTop)).toBeLessThanOrEqual(2);
}
await page.setViewportSize({ width: 1440, height: 900 });
await manager.getByRole("button", { name: "最大化窗口", exact: true }).click();
await page.getByRole("button", { name: "专注图表", exact: true }).click();
await expect(manager.locator("[data-chart-layout]")).toHaveAttribute("data-chart-layout", "four");
await expect(slots(manager)).toHaveCount(6);
await manager.getByRole("combobox", { name: "快速定位图表", exact: true }).click();
@@ -1,6 +1,6 @@
import { expect, test } from "@playwright/test";
import { basename, resolve } from "node:path";
import { investigationFixture, mockInvestigationPresentation, selectInvestigation } from "./support/mock-investigation-presentation";
import { investigationFixture, mockInvestigationPresentation, selectInvestigation, showInvestigationMap, showInvestigationTasks } from "./support/mock-investigation-presentation";
const chart = (id: string, value = 28) => ({ action: "open_chart", chart_id: id, evidence_refs: ["synthetic:pressure"],
chart: { title: `合成监测 ${id}`, chart_type: "line", x_data: ["07:45", "08:00"], series: [{ name: "压力(m)", data: [32, value] }] } });
@@ -17,10 +17,12 @@ test("one manager receives live charts and keeps independent enlarged children",
const task = investigationFixture(1, "多图统一窗口验收");
const mock = await mockInvestigationPresentation(page, [task]);
await selectInvestigation(page, task.title);
await expect(page.getByLabel("调查实时展示", { exact: true })).toContainText("实时跟随");
await mock.emit(task, chart("P1"));
const manager = page.getByRole("dialog", { name: "图表管理", exact: true });
await expect(manager).toBeVisible();
await mock.emit(task, chart("P2"));
await manager.locator("[data-managed-chart]").filter({ hasText: "合成监测 P2" }).scrollIntoViewIfNeeded();
await expect(manager.getByRole("img", { name: "合成监测 P2", exact: true })).toBeVisible();
await expect(page.locator("[data-floating-window]")).toHaveCount(1);
await expect(manager).toContainText("2 张图表");
@@ -45,7 +47,7 @@ test("one manager receives live charts and keeps independent enlarged children",
expect((await first.boundingBox())!.width).toBe(1424);
await first.getByRole("button", { name: "关闭窗口", exact: true }).click();
await manager.locator(".floating-window-heading").focus();
await manager.getByRole("button", { name: "最大化窗口", exact: true }).click();
await page.getByRole("button", { name: "专注图表", exact: true }).click();
await page.evaluate(() => document.fonts.ready);
await testInfo.attach("chart-manager-desktop", { body: await manager.screenshot({ path: testInfo.outputPath("chart-manager-desktop.png") }), contentType: "image/png" });
for (const width of [375, 320]) {
@@ -61,6 +63,7 @@ test("one manager receives live charts and keeps independent enlarged children",
const panel = page.getByLabel("调查实时展示", { exact: true });
await expect(panel.locator('[data-investigation-chart="P3"]')).toHaveCount(1);
await expect(page.locator("[data-floating-window]")).toHaveCount(0);
if (page.viewportSize()!.width >= 1024) await showInvestigationMap(page);
await panel.getByRole("button", { name: "打开图表管理", exact: true }).click();
await expect(manager).toContainText("3 张图表");
expect(mock.mutations).toEqual([]);
@@ -87,21 +90,27 @@ test("opening, switching, refreshing and reconnecting tasks restores charts sile
await expect(panel.locator('[data-investigation-chart="P1"]')).toHaveCount(1);
await expect(page.locator("[data-floating-window]")).toHaveCount(0);
// Create a live request before returning to the same task later.
await expect(panel).toContainText("实时跟随");
await mock.emit(running, chart("P1", 23));
await expect(manager).toBeVisible();
await manager.getByRole("button", { name: "关闭窗口", exact: true }).click();
const tasks = page.getByRole("region", { name: "工况任务", exact: true });
await showInvestigationTasks(page);
await tasks.getByText(completed.title, { exact: true }).first().click();
await expect(panel.locator('[data-investigation-chart="P2"]')).toHaveCount(1);
await showInvestigationMap(page);
await expect(panel.getByRole("img", { name: "合成监测 报告", exact: true })).toBeVisible();
await expect(page.locator("[data-floating-window]")).toHaveCount(0);
await showInvestigationMap(page);
await panel.getByRole("button", { name: "打开图表管理", exact: true }).click();
await expect(manager).toContainText("2 张图表");
await expect(manager.getByRole("img", { name: "合成监测 报告", exact: true })).toBeVisible();
await manager.getByRole("button", { name: "关闭窗口", exact: true }).click();
await showInvestigationTasks(page);
await tasks.getByText(running.title, { exact: true }).first().click();
await expect(panel.locator('[data-investigation-chart="P1"]')).toHaveCount(1);
await expect(page.locator("[data-floating-window]")).toHaveCount(0);
await showInvestigationMap(page);
await panel.getByRole("button", { name: "打开图表管理", exact: true }).click();
await expect(manager).toBeVisible();
await page.reload({ waitUntil: "domcontentloaded" });
+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);
+6 -2
View File
@@ -192,13 +192,17 @@ for (const width of [1440, 375]) {
await expect(tools.getByRole("combobox", { name: "JSONL 测试案例", exact: true })).toBeEnabled({ timeout: 15_000 });
await tools.getByRole("button", { name: "执行本地预览", exact: true }).click();
await expect(tools.getByRole("status")).toContainText("本地预览完成");
await page.getByRole("button", { name: "切换地图 Dev Panel", exact: true }).click();
await page.getByLabel("本地预览展示", { exact: true }).getByRole("button", { name: "打开图表管理", exact: true }).click();
await expect(page.getByRole("dialog", { name: "图表管理", exact: true })).toBeVisible();
const manager = page.getByRole("dialog", { name: "图表管理", exact: true });
await expect(manager).toBeVisible();
await expect(header.getByRole("button", { name: /窗口管理,当前/ })).toHaveCount(0);
await manager.getByRole("button", { name: "放大查看 交互验证压力趋势", exact: true }).click();
await page.setViewportSize({ width, height: 900 });
const navigator = header.getByRole("button", { name: /窗口管理,当前/ });
await navigator.click();
const windowMenu = page.getByRole("menu", { name: "已打开的窗口", exact: true });
const currentWindow = windowMenu.getByRole("menuitem", { name: /定位窗口 .*图表管理/ });
const currentWindow = windowMenu.getByRole("menuitem", { name: "定位窗口 交互验证压力趋势", exact: true });
await expect(currentWindow).toHaveAttribute("aria-current", "true");
await expectPreservedOnHover(page, currentWindow, true);
await expect(currentWindow).toHaveCSS("background-color", await themeColor(currentWindow, "--action-selection-soft", "background-color"));
@@ -1,8 +1,12 @@
import { expect, test } from "@playwright/test";
import { mockFlatWorkOrder } from "./support/mock-hierarchical-work-order";
import { mockWorkbenchContext } from "./support/mock-workbench-context";
test.describe("mobile workbench sheet", () => {
test.use({ viewport: { width: 390, height: 844 } });
test.beforeEach(async ({ page }) => {
await mockWorkbenchContext(page);
});
test("keeps the bottom launcher controls at one height", async ({ page }) => {
await page.goto("/", { waitUntil: "domcontentloaded" });
@@ -72,7 +72,7 @@ test("keeps the open scheduled report when the rolling history refreshes", async
await expect(rows.filter({ hasText: reportTitle ?? "" })).toHaveCount(0);
});
test("scheduled conditions keep acrylic blur outside transformed ancestors", async ({ page }) => {
test("collapsed conditions use acrylic and expand into the analysis workspace", async ({ page }) => {
await page.goto("/", { waitUntil: "domcontentloaded" });
await openConditionFeed(page);
@@ -84,15 +84,14 @@ test("scheduled conditions keep acrylic blur outside transformed ancestors", asy
const timeline = panel.getByTestId("scheduled-condition-timeline");
const detailViewport = panel.locator(".scheduled-feed-detail-viewport");
const detailScroll = panel.getByTestId("scheduled-condition-detail-scroll");
const collapsedPanelBox = await panel.boundingBox();
const collapsedTimelineBox = await timeline.boundingBox();
expect(collapsedPanelBox).not.toBeNull();
expect(collapsedTimelineBox).not.toBeNull();
await panel.getByRole("button", { name: "展开工况任务" }).click();
await expectAcrylicComposition(panel);
await expect(panel).toHaveCSS("width", "880px");
await expect(panel).toHaveAttribute("data-presentation", "workspace");
await expect(panel).toHaveCSS("backdrop-filter", "none");
await timeline.locator("button[data-condition-id]").first().click();
const expandedPanelBox = await panel.boundingBox();
const expandedTimelineBox = await timeline.boundingBox();
const expandedDetailViewportBox = await detailViewport.boundingBox();
@@ -101,15 +100,9 @@ test("scheduled conditions keep acrylic blur outside transformed ancestors", asy
expect(expandedTimelineBox).not.toBeNull();
expect(expandedDetailViewportBox).not.toBeNull();
expect(expandedDetailScrollBox).not.toBeNull();
expect(Math.round(expandedPanelBox!.x + expandedPanelBox!.width)).toBe(
Math.round(collapsedPanelBox!.x + collapsedPanelBox!.width)
);
expect(Math.round(expandedTimelineBox!.width)).toBe(
Math.round(collapsedTimelineBox!.width)
);
expect(Math.round(expandedTimelineBox!.x + expandedTimelineBox!.width)).toBe(
Math.round(collapsedTimelineBox!.x + collapsedTimelineBox!.width)
);
expect(expandedTimelineBox!.y + expandedTimelineBox!.height).toBeLessThanOrEqual(
expandedPanelBox!.y + expandedPanelBox!.height - 12
);
@@ -124,6 +117,7 @@ test("scheduled conditions keep acrylic blur outside transformed ancestors", asy
);
await expect(detailViewport).toHaveCSS("border-radius", "12px");
const timelineScroll = timeline.locator(".scheduled-feed-scroll.flex-1");
await page.mouse.move(0, 0);
expect(await readScrollbarPresentation(detailScroll)).toEqual(
await readScrollbarPresentation(timelineScroll)
);
@@ -165,8 +159,8 @@ test("narrow desktop keeps Agent and expanded conditions from overlapping", asyn
await panel.getByRole("button", { name: "展开工况任务" }).focus();
await page.keyboard.press("Enter");
await expect(page.locator('aside[aria-label="Agent 折叠栏"]')).toBeVisible();
await expect.poll(async () => (await panel.boundingBox())?.width ?? 0).toBeGreaterThan(800);
await expect.poll(async () => (await panel.boundingBox())?.width ?? Infinity).toBeLessThanOrEqual(880);
await expect(panel).toHaveAttribute("data-presentation", "workspace");
await expect(panel).toHaveCSS("width", "1024px");
const expandedConditionBox = await panel.boundingBox();
const collapsedAgentBox = await page
@@ -175,8 +169,9 @@ test("narrow desktop keeps Agent and expanded conditions from overlapping", asyn
expect(expandedConditionBox).not.toBeNull();
expect(collapsedAgentBox).not.toBeNull();
expect(
expandedConditionBox!.x - (collapsedAgentBox!.x + collapsedAgentBox!.width)
).toBeGreaterThanOrEqual(24);
(await panel.getByRole("heading", { name: "工况任务", exact: true }).boundingBox())!.x
- (collapsedAgentBox!.x + collapsedAgentBox!.width)
).toBeGreaterThanOrEqual(0);
await panel.getByRole("button", { name: "收起工况任务" }).click();
await expect(agentPanel).toBeVisible();
@@ -188,7 +183,8 @@ test("switching conditions keeps the detail viewport fully expanded", async ({ p
const panel = page.getByRole("region", { name: "工况任务", exact: true });
await panel.getByRole("button", { name: "展开工况任务" }).click();
await expect(panel).toHaveCSS("width", "880px");
await expect(panel).toHaveAttribute("data-presentation", "workspace");
await panel.getByTestId("scheduled-condition-timeline").getByRole("button", { name: /SCADA 数据诊断/ }).first().click();
const detailScroll = panel.getByTestId("scheduled-condition-detail-scroll");
const baselineHeight = await detailScroll.evaluate(
@@ -0,0 +1,125 @@
import { expect, test } from "@playwright/test";
import { mockWorkbenchContext } from "./support/mock-workbench-context";
import { createAnalysisRuntimeSnapshot, MOCK_SPATIAL_ANALYSIS } from "./support/mock-analysis-runtime";
test.beforeEach(async ({ page }) => {
await page.emulateMedia({ reducedMotion: "reduce" });
await mockWorkbenchContext(page, createAnalysisRuntimeSnapshot(MOCK_SPATIAL_ANALYSIS));
});
test("docks the live task panel into analysis, enforces both minimum widths and restores the analysis", async ({ page }) => {
await page.setViewportSize({ width: 1920, height: 1000 });
const errors: string[] = [];
page.on("pageerror", (error) => errors.push(error.message));
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.getByRole("button", { name: "打开最新后端分析成果" }).click();
await expect(page.locator("#artifact-workspace")).toBeVisible();
await page.getByRole("button", { name: "工况任务", exact: true }).click();
const panel = page.getByRole("region", { name: "工况任务", exact: true });
await expect(panel.getByRole("button", { name: /停靠工况任务/ })).toHaveCount(0);
await expect(panel.locator("[data-condition-dock-toggle]")).toHaveCount(1);
await expect(panel.getByRole("button", { name: "展开工况任务" }).locator(".lucide-maximize2")).toBeVisible();
await expect(panel.locator('button[data-condition-id][aria-pressed="true"]')).toHaveCount(0);
await panel.getByRole("button", { name: "展开工况任务" }).click();
await expect(panel).toHaveAttribute("data-presentation", "workspace");
await expect(panel.getByRole("button", { name: "收起工况任务" }).locator(".lucide-minimize2")).toBeVisible();
await expect(panel.getByText("请选择工况任务", { exact: true })).toBeVisible();
const rows = panel.getByTestId("scheduled-condition-timeline").locator("button[aria-pressed]");
await rows.nth(1).click();
const selectedId = await rows.nth(1).getAttribute("data-condition-id");
await panel.evaluate((element) => Object.assign(window, { originalDockPanel: element }));
await expect(page.locator("#condition-workspace").getByRole("region", { name: "工况任务", exact: true })).toBeVisible();
await expect(page.locator("#artifact-workspace")).toBeHidden();
await expect(panel).toHaveAttribute("data-presentation", "workspace");
await expect(panel.locator(`[data-condition-id="${selectedId}"]`)).toHaveAttribute("aria-pressed", "true");
await expect(panel).toHaveCSS("backdrop-filter", "none");
const separator = page.getByRole("separator", { name: "调整地图与报告宽度" });
await expect(separator).toBeVisible();
await separator.focus();
await page.keyboard.press("Home");
await expect.poll(async () => Math.round((await page.locator("#workspace-map").boundingBox())!.width)).toBe(600);
await page.keyboard.press("End");
await expect.poll(async () => Math.round((await panel.boundingBox())!.width)).toBe(880);
const bounds = await panel.boundingBox();
expect(bounds!.y).toBe(56);
expect(Math.round(bounds!.height)).toBe(944);
const detail = panel.getByTestId("scheduled-condition-detail-scroll");
expect(await detail.evaluate((element) => element.scrollWidth - element.clientWidth)).toBeLessThanOrEqual(1);
const timeline = page.getByRole("region", { name: "运行时间轴", exact: true });
const timelineBox = (await timeline.boundingBox())!;
expect(timelineBox.x + timelineBox.width).toBeLessThanOrEqual(bounds!.x);
await page.screenshot({ path: "test-results/condition-dock-minimum-width.png" });
await page.setViewportSize({ width: 1480, height: 900 });
await expect(page.locator("#main-workspace")).toHaveAttribute("data-layout-mode", "focus");
await expect(panel).toHaveCSS("width", "1480px");
await page.setViewportSize({ width: 1481, height: 900 });
await expect(separator).toBeVisible();
await expect.poll(async () => Math.round((await page.locator("#workspace-map").boundingBox())!.width)).toBe(600);
await expect(panel).toHaveCSS("width", "880px");
await panel.getByRole("button", { name: "收起工况任务" }).click();
await expect(panel).toHaveAttribute("data-presentation", "desktop-floating");
await expect(page.locator("#artifact-workspace")).toBeVisible();
expect(await panel.evaluate((element) => element === (window as unknown as { originalDockPanel: Element }).originalDockPanel)).toBe(true);
await expect(panel.locator(`[data-condition-id="${selectedId}"]`)).toHaveAttribute("aria-pressed", "false");
await panel.getByRole("button", { name: "展开工况任务" }).click();
await expect(panel.locator(`[data-condition-id="${selectedId}"]`)).toHaveAttribute("aria-pressed", "true");
expect(errors).toEqual([]);
});
test("opens and reopens without selecting a task, and selecting a row docks its detail", async ({ page }) => {
await page.setViewportSize({ width: 1920, height: 1000 });
await page.goto("/", { waitUntil: "domcontentloaded" });
const toggle = page.getByRole("button", { name: "工况任务", exact: true });
const panel = page.getByRole("region", { name: "工况任务", exact: true });
await toggle.click();
await expect(panel.locator('button[data-condition-id][aria-pressed="true"]')).toHaveCount(0);
await panel.getByTestId("scheduled-condition-timeline").locator("button[data-condition-id]").nth(1).click();
await expect(panel).toHaveAttribute("data-presentation", "workspace");
await expect(panel.locator('button[data-condition-id][aria-pressed="true"]')).toHaveCount(1);
await expect(panel.getByTestId("scheduled-condition-detail-scroll")).toBeVisible();
await toggle.click();
await expect(panel).toBeHidden();
await toggle.click();
await expect(panel.locator('button[data-condition-id][aria-pressed="true"]')).toHaveCount(0);
await panel.getByRole("button", { name: "展开工况任务" }).click();
await expect(panel.getByText("请选择工况任务", { exact: true })).toBeVisible();
await page.screenshot({ path: "test-results/condition-dock-no-selection.png" });
});
test("uses focus mode when space is limited and adapts through Agent and mobile layouts", async ({ page }) => {
await page.setViewportSize({ width: 1440, height: 900 });
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click();
await page.getByRole("button", { name: "工况任务", exact: true }).click();
const panel = page.getByRole("region", { name: "工况任务", exact: true });
await panel.getByRole("button", { name: "展开工况任务", exact: true }).click();
await expect(page.locator("#main-workspace")).toHaveAttribute("data-layout-mode", "focus");
await expect.poll(async () => (await panel.boundingBox())!.width).toBeGreaterThanOrEqual(880);
await page.setViewportSize({ width: 1024, height: 768 });
await expect(page.getByRole("complementary", { name: "Agent 命令面板" })).toBeHidden();
await expect(panel).toHaveCSS("width", "1024px");
await expect(page.getByRole("separator", { name: "调整地图与报告宽度" })).toBeHidden();
await page.getByRole("button", { name: "显示地图", exact: true }).click();
await expect(panel).toBeHidden();
await expect(page.locator("#workspace-map")).toBeVisible();
await page.getByRole("button", { name: "返回工况任务" }).click();
await expect(panel).toBeVisible();
const rail = (await page.getByRole("complementary", { name: "Agent 折叠栏" }).boundingBox())!;
const title = (await panel.getByRole("heading", { name: "工况任务", exact: true }).boundingBox())!;
expect(title.x).toBeGreaterThanOrEqual(rail.x + rail.width);
await page.screenshot({ path: "test-results/condition-dock-narrow-desktop.png" });
await page.setViewportSize({ width: 390, height: 844 });
await expect(panel).toBeHidden();
await page.getByRole("button", { name: "打开工况任务", exact: true }).click();
await expect(panel).toHaveAttribute("data-presentation", "mobile-sheet");
await expect(panel.locator('button[data-condition-id][aria-pressed="true"]')).toHaveCount(0);
await panel.getByTestId("scheduled-condition-timeline").locator("button[data-condition-id]").first().click();
await expect(panel.getByTestId("scheduled-condition-detail-scroll")).toBeVisible();
await expect(panel.getByRole("button", { name: "收起工况任务" })).toBeVisible();
await expect(panel.getByRole("button", { name: /停靠工况任务/ })).toHaveCount(0);
await page.setViewportSize({ width: 1920, height: 1000 });
await expect(panel).toHaveAttribute("data-presentation", "workspace");
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBe(1920);
});
@@ -144,3 +144,15 @@ export async function selectInvestigation(page: Page, title: string) {
}
await task.click();
}
export async function showInvestigationMap(page: Page) {
const showMap = page.getByRole("button", { name: "显示地图", exact: true });
if (await showMap.isVisible()) await showMap.click();
}
export async function showInvestigationTasks(page: Page) {
if (await page.getByRole("region", { name: "工况任务", exact: true }).isVisible()) return;
const returnToTasks = page.getByRole("button", { name: "返回工况任务", exact: true });
if (await returnToTasks.isVisible()) await returnToTasks.click();
else await page.getByRole("button", { name: /^(?:打开)?工况任务$/ }).click();
}
+13 -9
View File
@@ -4,7 +4,7 @@ import { mockInvestigationPresentation } from "./support/mock-investigation-pres
test.use({ reducedMotion: "reduce" });
for (const width of [1440, 375, 320]) {
test(`header locates managed, chart and table windows without changing their state at ${width}px`, async ({ page }, testInfo) => {
test(`header lists only floating chart and table windows and preserves the docked manager at ${width}px`, async ({ page }, testInfo) => {
test.setTimeout(60_000);
await page.setViewportSize({ width: 1440, height: 900 });
await page.route("**/@fs/**/noto-sans-sc-*.woff2", (route) => route.fulfill({
@@ -28,11 +28,12 @@ for (const width of [1440, 375, 320]) {
await expect(tools.getByRole("combobox", { name: "JSONL 测试案例", exact: true })).toBeEnabled({ timeout: 15_000 });
await tools.getByRole("button", { name: "执行本地预览", exact: true }).click();
await expect(tools.getByRole("status")).toContainText("本地预览完成");
await page.getByRole("button", { name: "切换地图 Dev Panel" }).click();
const panel = page.getByLabel("本地预览展示", { exact: true });
await panel.getByRole("button", { name: "放大查看表格 窗口定位核查表", exact: true }).click();
const table = page.getByRole("dialog", { name: "窗口定位核查表 · 表格", exact: true });
await expect(table).toBeVisible();
await panel.getByRole("button", { name: "打开图表管理", exact: true }).click();
await panel.getByRole("button", { name: "打开图表管理", exact: true }).press("Enter");
const manager = page.getByRole("dialog", { name: "图表管理", exact: true });
await manager.getByRole("button", { name: "放大查看 窗口定位压力趋势", exact: true }).click();
const child = page.getByRole("dialog", { name: "窗口定位压力趋势", exact: true });
@@ -42,7 +43,7 @@ for (const width of [1440, 375, 320]) {
await table.locator(".floating-window-heading").focus();
await search.fill("P2");
await page.setViewportSize({ width, height: 900 });
await expect(navigator).toHaveAttribute("aria-label", "窗口管理,当前 3 个窗口");
await expect(navigator).toHaveAttribute("aria-label", "窗口管理,当前 2 个窗口");
if (width === 1440) {
const alignment = await navigator.evaluate((node) => {
const label = node.children[1].getBoundingClientRect();
@@ -55,19 +56,21 @@ for (const width of [1440, 375, 320]) {
const tableBounds = await table.boundingBox();
await navigator.click();
const menu = page.getByRole("menu", { name: "已打开的窗口", exact: true });
await expect(menu.getByRole("menuitem")).toHaveCount(4);
await expect(menu.getByRole("menuitem")).toHaveCount(3);
await expect(menu.getByRole("menuitem", { name: /图表管理/ })).toHaveCount(0);
await menu.getByRole("menuitem", { name: "定位窗口 窗口定位核查表 · 表格", exact: true }).click();
await expect(table.locator(".floating-window-heading")).toBeFocused();
await expect(navigator).toHaveAttribute("aria-expanded", "false");
expect(await table.boundingBox()).toEqual(tableBounds);
await expect(search).toHaveValue("P2");
const managerBounds = await manager.boundingBox();
const childBounds = await child.boundingBox();
await navigator.focus();
await page.keyboard.press("Enter");
await menu.getByRole("menuitem", { name: /定位窗口 .*图表管理/ }).focus();
await menu.getByRole("menuitem", { name: "定位窗口 窗口定位压力趋势", exact: true }).focus();
await page.keyboard.press("Enter");
await expect(manager.locator(".floating-window-heading")).toBeFocused();
expect(await manager.boundingBox()).toEqual(managerBounds);
await expect(child.locator(".floating-window-heading")).toBeFocused();
expect(await child.boundingBox()).toEqual(childBounds);
await expect(child.getByRole("tab", { name: "数据", exact: true })).toHaveAttribute("aria-selected", "true");
await navigator.click();
await header.getByRole("button", { name: "打开用户菜单", exact: true }).click();
await expect(menu).toHaveCount(0);
@@ -85,7 +88,8 @@ for (const width of [1440, 375, 320]) {
await page.evaluate(() => document.fonts.ready);
await testInfo.attach(`window-menu-${width}`, { body: await page.screenshot({ path: testInfo.outputPath(`window-menu-${width}.png`) }), contentType: "image/png" });
await menu.getByRole("menuitem", { name: "关闭全部窗口", exact: true }).click();
await expect(page.locator("[data-floating-window]")).toHaveCount(0);
await expect(page.locator('[data-window-presentation="floating"]')).toHaveCount(0);
await expect(manager).toBeVisible();
await expect(navigator).toHaveCount(0);
await expect(header.getByRole("button", { name: "打开用户菜单", exact: true })).toBeFocused();
expect(mock.mutations).toEqual([]);