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,174 @@
import { expect, test, type Locator, type Page } from "@playwright/test";
import { basename, resolve } from "node:path";
import { investigationFixture, mockInvestigationPresentation, selectInvestigation, type SavedPresentationEvent } from "./support/mock-investigation-presentation";
const pressureIds = ["170486", "175618", "159673", "135160", "174754", "123781", "142493"];
const flowIds = ["541061", "542017", "486033"];
const times = Array.from({ length: 13 }, (_, index) => `${String(5 + Math.floor(index / 4)).padStart(2, "0")}:${String(index % 4 * 15).padStart(2, "0")}`);
const pressureTitle = "压力当前曲线与前七天同期对比(m)";
const flowTitle = "流量当前曲线与前七天同期对比(m³/h)";
// These synthetic fixtures preserve the naming shape found in read-only persisted
// SCADA task charts. Values, timestamps and references are self-contained test data.
function comparison(id: string, flow = false, dateOnlyHistory = false) {
const metric = flow ? "流量" : "压力";
const unit = flow ? "m³/h" : "m";
const offset = (flow ? flowIds : pressureIds).indexOf(id);
const base = (flow ? 100 : 32) + offset;
return { action: "open_chart", chart_id: `legacy-${flow ? "flow" : "pressure"}-${id}`, evidence_refs: [`synthetic:legacy-shape:${id}`],
chart: { title: dateOnlyHistory ? `测点 ${id} ${metric}三小时与历史对照(${unit})` : `${metric}测点 ${id} 当前曲线与前七天同期对比(${unit})`,
chart_type: dateOnlyHistory ? "line" : "time_comparison", x_axis_name: "时间", y_axis_name: `${metric}(${unit})`, x_data: times,
series: [{ name: `${id} 当日`, type: "line", data: times.map((_, index) => base - index / 4),
sample_times: times.map((time) => `2026-07-16T${time}:00+08:00`) },
...Array.from({ length: 7 }, (_, day) => ({ name: `${dateOnlyHistory ? "" : `${id} `}07-${String(9 + day).padStart(2, "0")}`, type: "scatter",
data: times.map((_, index) => index < 10 ? null : base + day / 2),
sample_times: times.map((time, index) => index < 10 ? null : `2026-07-${String(9 + day).padStart(2, "0")}T${time}:00+08:00`) }))]
} };
}
const trend = (flow: boolean) => ({ action: "open_chart", chart_id: `trend-${flow ? "flow" : "pressure"}`, evidence_refs: ["synthetic:legacy-trend"],
chart: { title: `${flow ? "流量" : "压力"}测点当前三小时总览`, chart_type: "line", x_data: times,
series: [{ name: "合成当前趋势", data: times.map((_, index) => (flow ? 100 : 32) - index / 4) }] } });
function savedTask(index: number, title: string, commands: unknown[]) {
const task = investigationFixture(index, title);
const run = task.active_run!;
task.status = "waiting_human";
task.lifecycle_status = "waiting_human";
task.latest_run = { ...run, status: "succeeded", finished_at: task.updated_at };
task.active_run = null;
const events: SavedPresentationEvent[] = commands.map((command, sequence) => ({ task_id: task.task_id,
run_id: run.run_id, event_seq: sequence + 1, execution_generation: 1, occurred_at: task.created_at, command }));
return { task, events };
}
const managed = (manager: Locator, title: string) => manager.locator(`[data-managed-chart][data-chart-title="${title}"]`);
const plot = (article: Locator) => article.locator('[data-chart-view] div[role="img"]:not(.echarts-for-react)');
async function locate(page: Page, manager: Locator, title: string) {
await manager.getByRole("combobox", { name: "快速定位图表", exact: true }).click();
await page.getByRole("option").filter({ hasText: title }).click();
await expect(managed(manager, title)).toBeVisible();
}
async function expectBottomLegend(article: Locator) {
const drawing = (await plot(article).boundingBox())!;
const legend = (await article.getByLabel("图表图例", { exact: true }).boundingBox())!;
const bounds = (await article.boundingBox())!;
expect(legend.y).toBeGreaterThanOrEqual(drawing.y + drawing.height - 1);
expect(legend.y + legend.height).toBeLessThanOrEqual(bounds.y + bounds.height + 1);
expect(drawing.height).toBeGreaterThanOrEqual(100);
}
test.use({ reducedMotion: "reduce", hasTouch: true, screenshot: "only-on-failure" });
test.setTimeout(90_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("saved waiting-human SCADA charts restore silently into two trends and two object groups", async ({ page }, testInfo) => {
const { task, events } = savedTask(21, "合成历史 SCADA 旧标题恢复", [trend(false), trend(true),
...pressureIds.map((id) => comparison(id)), ...flowIds.map((id) => comparison(id, true))]);
const mock = await mockInvestigationPresentation(page, [task], events);
await selectInvestigation(page, task.title);
const panel = page.getByLabel("调查实时展示", { exact: true });
await expect(panel.locator("[data-investigation-chart]")).toHaveCount(12);
await expect(page.locator("[data-floating-window]")).toHaveCount(0);
await panel.getByRole("button", { name: "打开图表管理", exact: true }).click();
const manager = page.getByRole("dialog", { name: "图表管理", exact: true });
await expect(manager.locator("[data-managed-chart]")).toHaveCount(4);
await manager.getByRole("button", { name: "关闭窗口", exact: true }).click();
await page.reload({ waitUntil: "domcontentloaded" });
await selectInvestigation(page, task.title);
await expect(panel.locator("[data-investigation-chart]")).toHaveCount(12);
await expect(page.locator("[data-floating-window]")).toHaveCount(0);
await panel.getByRole("button", { name: "打开图表管理", exact: true }).click();
await expect(manager.locator("[data-managed-chart]")).toHaveCount(4);
await expect(manager).toContainText("4 张图表");
const pressure = managed(manager, pressureTitle);
const flow = managed(manager, flowTitle);
await expect(pressure).toBeVisible();
await expect(flow).toBeVisible();
await expect(managed(manager, "压力测点当前三小时总览")).toBeVisible();
await expect(managed(manager, "流量测点当前三小时总览")).toBeVisible();
const pressureObjects = pressure.locator('aside[aria-label="选择图表对象"]');
const flowObjects = flow.locator('aside[aria-label="选择图表对象"]');
await expect(pressureObjects.getByRole("button")).toHaveCount(7);
await expect(flowObjects.getByRole("button")).toHaveCount(3);
await pressureObjects.getByRole("button", { name: "切换对象 175618", exact: true }).click();
await expect(plot(pressure)).toHaveAttribute("aria-label", "压力测点 175618 当前曲线与前七天同期对比(m)");
const legend = pressure.getByLabel("图表图例", { exact: true });
await expect(legend.getByRole("button")).toHaveCount(8);
await expect(legend.getByRole("button", { name: "175618 当日", exact: true })).toBeVisible();
await expectBottomLegend(pressure);
await flowObjects.getByRole("button", { name: "切换对象 486033", exact: true }).click();
await expect(plot(flow)).toHaveAttribute("aria-label", "流量测点 486033 当前曲线与前七天同期对比(m³/h)");
await expectBottomLegend(flow);
await pressure.getByRole("tab", { name: "数据", exact: true }).click();
const table = pressure.getByRole("table");
await expect(table.getByRole("row")).toHaveCount(14);
await expect(table.getByRole("row").last()).toContainText("2026-07-16 08:00:00+08:00");
await expect(table.getByRole("row").last()).toContainText("2026-07-09 08:00:00+08:00");
await expect(table.getByRole("row").last()).toContainText("30 (2026-07-16");
await pressure.getByRole("tab", { name: "复合", exact: true }).click();
await pressure.getByRole("button", { name: `放大查看 ${pressureTitle}`, exact: true }).click();
const child = page.getByRole("dialog", { name: pressureTitle, exact: true });
await expect(child).toBeVisible();
await expect(manager).toBeVisible();
await expect(page.locator("[data-floating-window]")).toHaveCount(2);
await child.getByRole("button", { name: "切换对象 142493", exact: true }).click();
await expect(child.getByRole("img", { name: "压力测点 142493 当前曲线与前七天同期对比(m)", exact: true })).toBeVisible();
await child.getByRole("button", { name: "关闭窗口", exact: true }).click();
await expect(page.locator("[data-floating-window]")).toHaveCount(1);
await page.evaluate(() => document.fonts.ready);
await testInfo.attach("legacy-scada-groups-desktop", { body: await manager.screenshot({ path: testInfo.outputPath("legacy-scada-groups-desktop.png") }), contentType: "image/png" });
for (const width of [375, 320]) {
await page.setViewportSize({ width, height: 812 });
await expect(manager.locator("[data-chart-layout]")).toHaveAttribute("data-chart-layout", "one");
await locate(page, manager, pressureTitle);
const id = width === 375 ? "175618" : "159673";
await pressure.getByRole("combobox", { name: "选择图表对象", exact: true }).tap();
await page.getByRole("option", { name: id, exact: true }).tap();
await expect(plot(pressure)).toHaveAttribute("aria-label", `压力测点 ${id} 当前曲线与前七天同期对比(m)`);
await expectBottomLegend(pressure);
expect(await manager.evaluate((node) => node.scrollWidth - node.clientWidth)).toBeLessThanOrEqual(1);
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(width);
await testInfo.attach(`legacy-scada-groups-${width}`, { body: await manager.screenshot({ path: testInfo.outputPath(`legacy-scada-groups-${width}.png`) }), contentType: "image/png" });
await locate(page, manager, flowTitle);
await flow.getByRole("combobox", { name: "选择图表对象", exact: true }).tap();
await page.getByRole("option", { name: "541061", exact: true }).tap();
await expect(plot(flow)).toHaveAttribute("aria-label", "流量测点 541061 当前曲线与前七天同期对比(m³/h)");
expect(await manager.evaluate((node) => node.scrollWidth - node.clientWidth)).toBeLessThanOrEqual(1);
}
expect(mock.mutations).toEqual([]);
});
test("saved generic point titles group date-only historical scatter labels without changing sampling times", async ({ page }, testInfo) => {
const { task, events } = savedTask(22, "合成早期测点图恢复", [comparison("170486", false, true), comparison("175618", false, true)]);
const mock = await mockInvestigationPresentation(page, [task], events);
await selectInvestigation(page, task.title);
const panel = page.getByLabel("调查实时展示", { exact: true });
await expect(panel.locator("[data-investigation-chart]")).toHaveCount(2);
await expect(page.locator("[data-floating-window]")).toHaveCount(0);
await panel.getByRole("button", { name: "打开图表管理", exact: true }).click();
const manager = page.getByRole("dialog", { name: "图表管理", exact: true });
await expect(manager.locator("[data-managed-chart]")).toHaveCount(1);
const group = managed(manager, "压力三小时与历史对照(m)");
await expect(group.locator('aside[aria-label="选择图表对象"]').getByRole("button")).toHaveCount(2);
await group.getByRole("button", { name: "切换对象 175618", exact: true }).click();
await expect(plot(group)).toHaveAttribute("aria-label", "测点 175618 压力三小时与历史对照(m)");
const legend = group.getByLabel("图表图例", { exact: true });
await expect(legend.getByRole("button")).toHaveCount(8);
await expect(legend.getByRole("button", { name: "07-09", exact: true })).toBeVisible();
await expectBottomLegend(group);
await group.getByRole("tab", { name: "数据", exact: true }).click();
const row = group.getByRole("table").getByRole("row").filter({ hasText: /^07:30/ });
await expect(row).toContainText("2026-07-16 07:30:00+08:00");
await expect(row).toContainText("2026-07-09 07:30:00+08:00");
await expect(row).toContainText("33 (2026-07-09");
await page.evaluate(() => document.fonts.ready);
await testInfo.attach("legacy-date-only-history", { body: await manager.screenshot({ path: testInfo.outputPath("legacy-date-only-history.png") }), contentType: "image/png" });
expect(mock.mutations).toEqual([]);
});