feat(charts): manage investigation charts in a scrolling workspace
Generic Container CI/CD / test-build-publish (push) Successful in 2m7s
Frontend CI/CD / build-test-publish-and-deploy (push) Successful in 2m8s

This commit is contained in:
2026-11-10 17:56:23 +08:00
parent 865491cb23
commit 0ae0a41e61
80 changed files with 4672 additions and 470 deletions
@@ -0,0 +1,118 @@
import { expect, test } from "@playwright/test";
import { basename, resolve } from "node:path";
import { investigationFixture, mockInvestigationPresentation, selectInvestigation } 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] }] } });
test.use({ reducedMotion: "reduce" });
test.setTimeout(60_000);
test.beforeEach(async ({ page }) => {
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"
}));
});
test("one manager receives live charts and keeps independent enlarged children", async ({ page }, testInfo) => {
const task = investigationFixture(1, "多图统一窗口验收");
const mock = await mockInvestigationPresentation(page, [task]);
await selectInvestigation(page, task.title);
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 expect(manager.getByRole("img", { name: "合成监测 P2", exact: true })).toBeVisible();
await expect(page.locator("[data-floating-window]")).toHaveCount(1);
await expect(manager).toContainText("2 张图表");
await manager.getByRole("button", { name: "放大查看 合成监测 P1", exact: true }).click();
const first = page.getByRole("dialog", { name: "合成监测 P1", exact: true });
await expect(first).toBeVisible();
await manager.locator(".floating-window-heading").focus();
await manager.getByRole("button", { name: "放大查看 合成监测 P2", exact: true }).click();
const second = page.getByRole("dialog", { name: "合成监测 P2", exact: true });
await expect(first).toBeVisible();
await expect(second).toBeVisible();
await expect(page.locator("[data-floating-window]")).toHaveCount(3);
const bounds = await second.boundingBox();
await mock.emit(task, chart("P2", 17));
await second.getByRole("tab", { name: "数据", exact: true }).click();
await expect(second.getByRole("table")).toContainText("17");
expect(await second.boundingBox()).toEqual(bounds);
await expect(page.locator("[data-floating-window]")).toHaveCount(3);
await second.getByRole("button", { name: "关闭窗口", exact: true }).click();
await first.locator(".floating-window-heading").focus();
await first.getByRole("button", { name: "最大化窗口", exact: true }).click();
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.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]) {
await page.setViewportSize({ width, height: 720 });
const box = (await manager.boundingBox())!;
expect(box.x).toBeGreaterThanOrEqual(0);
expect(box.x + box.width).toBeLessThanOrEqual(width);
expect(await manager.evaluate((node) => node.scrollWidth - node.clientWidth)).toBeLessThanOrEqual(1);
await testInfo.attach(`chart-manager-${width}`, { body: await manager.screenshot({ path: testInfo.outputPath(`chart-manager-${width}.png`) }), contentType: "image/png" });
}
await manager.getByRole("button", { name: "关闭窗口", exact: true }).click();
await mock.emit(task, chart("P3"));
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);
await panel.getByRole("button", { name: "打开图表管理", exact: true }).click();
await expect(manager).toContainText("3 张图表");
expect(mock.mutations).toEqual([]);
});
test("opening, switching, refreshing and reconnecting tasks restores charts silently, including completed reports", async ({ page }) => {
const running = investigationFixture(1, "运行中历史图表");
const completed = investigationFixture(2, "已完成历史图表");
completed.status = "completed";
completed.lifecycle_status = "completed";
const completedRun = completed.active_run!.run_id;
completed.active_run = null;
completed.latest_report = { revision: 1, execution_generation: 1, source_session_id: "investigation-dev-test", published_at: completed.created_at,
published_by: "fixture", report: { title: "已完成报告", current_conclusion: "合成监测核查完成", conclusion_boundary: "仅供浏览器验证",
stage: "final", outcome: "concluded", stop_reason: null, data_gaps: [], evidence_refs: ["synthetic:pressure"], recommendations: [], pending_items: [],
ui_blocks: [{ evidence_refs: ["synthetic:pressure"], block: { version: "answer-ui@1", block_id: "saved-report-chart", phase: "final", fallback_text: "报告图表",
spec: { root: "chart", elements: { chart: { type: "Chart", props: { ...chart("报告").chart, source: "合成保存证据" }, children: [] } } } } }] } };
const saved = [running, completed].map((task, index) => ({ task_id: task.task_id, event_seq: index + 1,
run_id: task.active_run?.run_id ?? completedRun, execution_generation: 1, occurred_at: task.created_at, command: chart(`P${index + 1}`) }));
const mock = await mockInvestigationPresentation(page, [running, completed], saved);
await selectInvestigation(page, running.title);
const panel = page.getByLabel("调查实时展示", { exact: true });
const manager = page.getByRole("dialog", { name: "图表管理", exact: true });
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 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 tasks.getByText(completed.title, { exact: true }).first().click();
await expect(panel.locator('[data-investigation-chart="P2"]')).toHaveCount(1);
await expect(panel.getByRole("img", { name: "合成监测 报告", exact: true })).toBeVisible();
await expect(page.locator("[data-floating-window]")).toHaveCount(0);
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 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 panel.getByRole("button", { name: "打开图表管理", exact: true }).click();
await expect(manager).toBeVisible();
await page.reload({ waitUntil: "domcontentloaded" });
await expect(page.locator("[data-floating-window]")).toHaveCount(0);
await selectInvestigation(page, completed.title);
await expect(panel.locator('[data-investigation-chart="P2"]')).toHaveCount(1);
await expect(page.locator("[data-floating-window]")).toHaveCount(0);
await page.evaluate(() => (window as unknown as { setInvestigationTestConnection: (value: boolean) => void }).setInvestigationTestConnection(false));
await expect(panel).toContainText("连接中断");
await page.evaluate(() => (window as unknown as { setInvestigationTestConnection: (value: boolean) => void }).setInvestigationTestConnection(true));
await expect(panel).toContainText("实时跟随", { timeout: 15_000 });
await expect(page.locator("[data-floating-window]")).toHaveCount(0);
expect(mock.mutations).toEqual([]);
});