feat(charts): manage investigation charts in a scrolling workspace
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { expect, test, type Locator, type TestInfo } from "@playwright/test";
|
||||
import { mockAgentApi } from "./support/mock-agent-api";
|
||||
import { mockWorkbenchContext } from "./support/mock-workbench-context";
|
||||
import { responsesFixture } from "./support/responses-fixture";
|
||||
@@ -7,6 +7,22 @@ const title = "空间候选管段(距离近,非故障定位)";
|
||||
const ids = ["19025", "19026", "19022", "19027"];
|
||||
const distances = [283.85, 283.86, 291.45, 292.44];
|
||||
|
||||
async function checkHeaderTarget(ui: Locator, testInfo: TestInfo, phase: string) {
|
||||
const header = ui.getByRole("columnheader", { name: "距离(米)" });
|
||||
const button = header.getByRole("button");
|
||||
const cell = (await header.boundingBox())!;
|
||||
const target = (await button.boundingBox())!;
|
||||
await testInfo.attach(`${phase}-header-target`, { body: JSON.stringify({ cell, target }), contentType: "application/json" });
|
||||
expect(Math.abs(target.x - cell.x)).toBeLessThanOrEqual(1);
|
||||
expect(Math.abs(target.y - cell.y)).toBeLessThanOrEqual(1);
|
||||
expect(Math.abs(target.width - cell.width)).toBeLessThanOrEqual(1);
|
||||
expect(Math.abs(target.height - cell.height)).toBeLessThanOrEqual(1);
|
||||
const background = await button.evaluate((element) => getComputedStyle(element).backgroundColor);
|
||||
await header.hover({ position: { x: cell.width - 6, y: cell.height / 2 } });
|
||||
await expect.poll(() => button.evaluate((element) => getComputedStyle(element).backgroundColor)).not.toBe(background);
|
||||
await expect(ui.getByRole("columnheader").first().getByRole("button")).toHaveCSS("background-color", background);
|
||||
}
|
||||
|
||||
for (const width of [1440, 375]) for (const kind of ["Table", "Chart"] as const) {
|
||||
test(`${kind} data stays display-only inline and expanded at ${width}px`, async ({ page }, testInfo) => {
|
||||
await page.setViewportSize({ width, height: 1000 });
|
||||
@@ -38,16 +54,25 @@ for (const width of [1440, 375]) for (const kind of ["Table", "Chart"] as const)
|
||||
await expect(ui.getByRole("table")).toContainText("19025");
|
||||
await expect(ui.getByRole("table")).toContainText("283.85");
|
||||
await expect(ui.getByRole("columnheader")).toHaveCount(2);
|
||||
await expect(ui.locator("thead svg")).toHaveCount(0);
|
||||
await expect(ui.getByRole("checkbox")).toHaveCount(0);
|
||||
await expect(ui.getByRole("button", { name: /加入分析|针对.*提问|取消选行/ })).toHaveCount(0);
|
||||
await expect(ui.getByRole("region", { name: "分析内容篮" })).toHaveCount(0);
|
||||
await page.getByRole("button", { name: "关闭通知", exact: true }).evaluateAll((buttons) => buttons.forEach((button) => (button as HTMLButtonElement).click()));
|
||||
await page.evaluate(() => document.fonts.ready);
|
||||
await checkHeaderTarget(ui, testInfo, "inline");
|
||||
await ui.locator("table").screenshot({ path: testInfo.outputPath("inline-display.png") });
|
||||
const sort = ui.getByRole("button", { name: "按距离(米)排序", exact: true });
|
||||
await sort.click();
|
||||
const header = ui.getByRole("columnheader", { name: "距离(米)" });
|
||||
const column = (await header.boundingBox())!;
|
||||
await header.click({ position: { x: column.width - 6, y: column.height / 2 } });
|
||||
await expect(ui.getByRole("columnheader", { name: "距离(米)" })).toHaveAttribute("aria-sort", "ascending");
|
||||
await expect(sort.locator("svg.lucide-arrow-up")).toHaveCount(1);
|
||||
await expect(ui.locator("thead svg")).toHaveCount(1);
|
||||
await sort.click();
|
||||
await expect(ui.getByRole("columnheader", { name: "距离(米)" })).toHaveAttribute("aria-sort", "descending");
|
||||
await expect(sort.locator("svg.lucide-arrow-down")).toHaveCount(1);
|
||||
await expect(ui.locator("thead svg")).toHaveCount(1);
|
||||
await expect(ui.locator("tbody tr").first()).toContainText("19027");
|
||||
await ui.getByRole("textbox", { name: `搜索${title}` }).fill("19025");
|
||||
await expect(ui.locator("tbody tr")).toHaveCount(1);
|
||||
@@ -56,15 +81,19 @@ for (const width of [1440, 375]) for (const kind of ["Table", "Chart"] as const)
|
||||
ui = workspace.locator('[data-answer-ui="selection-style"]');
|
||||
await expect(ui.getByRole("textbox", { name: `搜索${title}` })).toHaveValue("19025");
|
||||
await expect(ui.getByRole("columnheader", { name: "距离(米)" })).toHaveAttribute("aria-sort", "descending");
|
||||
await expect(ui.getByRole("button", { name: "按距离(米)排序", exact: true }).locator("svg.lucide-arrow-down")).toHaveCount(1);
|
||||
await expect(ui.locator("thead svg")).toHaveCount(1);
|
||||
await expect(ui.getByRole("checkbox")).toHaveCount(0);
|
||||
await expect(ui.locator('tbody tr[data-state="selected"]')).toHaveCount(0);
|
||||
await expect(ui.getByRole("button", { name: /加入分析|针对.*提问|取消选行/ })).toHaveCount(0);
|
||||
await ui.getByRole("textbox", { name: `搜索${title}` }).fill("");
|
||||
await page.getByRole("button", { name: "关闭通知", exact: true }).evaluateAll((buttons) => buttons.forEach((button) => (button as HTMLButtonElement).click()));
|
||||
await checkHeaderTarget(ui, testInfo, "expanded");
|
||||
await ui.locator("[data-interactive-table]").screenshot({ path: testInfo.outputPath("expanded-display.png") });
|
||||
await ui.getByRole("button", { name: "按距离(米)排序", exact: true }).focus();
|
||||
await page.keyboard.press("Enter");
|
||||
await expect(ui.getByRole("columnheader", { name: "距离(米)" })).toHaveAttribute("aria-sort", "none");
|
||||
await expect(ui.locator("thead svg")).toHaveCount(0);
|
||||
await ui.getByRole("textbox", { name: `搜索${title}` }).fill("没有匹配的管段");
|
||||
await expect(ui.getByRole("cell", { name: "当前筛选没有匹配的数据。" })).toHaveAttribute("colspan", "2");
|
||||
await expect(ui.getByRole("checkbox")).toHaveCount(0);
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import { expect, test, type Page, type TestInfo } from "@playwright/test";
|
||||
import { mockInvestigationPresentation } from "./support/mock-investigation-presentation";
|
||||
|
||||
const labels = ["点标注", "线标注", "范围标注"];
|
||||
type ButtonFrame = {
|
||||
label: string;
|
||||
sameNode: boolean;
|
||||
isConnected: boolean;
|
||||
hovered: boolean;
|
||||
pressed: string | null;
|
||||
disabled: boolean;
|
||||
background: string;
|
||||
color: string;
|
||||
opacity: string;
|
||||
rect: { x: number; y: number; width: number; height: number };
|
||||
icon: { background: string; color: string; transitions: string[] };
|
||||
svgColor: string;
|
||||
};
|
||||
type Frame = { elapsed: number; buttons: ButtonFrame[] };
|
||||
type SamplingProbe = { frames: Frame[]; done: Promise<void>; finishAction: () => number };
|
||||
|
||||
async function startSampling(page: Page) {
|
||||
await page.evaluate((labels) => {
|
||||
const toolbar = [...document.querySelectorAll<HTMLElement>('[role="toolbar"][aria-label="绘制工具"]')]
|
||||
.find((node) => node.getBoundingClientRect().width > 0)!;
|
||||
const find = (label: string) => [...toolbar.querySelectorAll<HTMLButtonElement>("button")].find((button) => button.textContent?.trim() === label)!;
|
||||
const initial = labels.map(find);
|
||||
const frames: Frame[] = [];
|
||||
const started = performance.now();
|
||||
let actionFinishedAt: number | null = null;
|
||||
let finish: () => void;
|
||||
const done = new Promise<void>((resolve) => { finish = resolve; });
|
||||
const read = () => {
|
||||
const elapsed = performance.now() - started;
|
||||
frames.push({ elapsed, buttons: labels.map((label, index) => {
|
||||
const button = find(label);
|
||||
const icon = button.querySelector<HTMLElement>(":scope > span")!;
|
||||
const svg = icon.querySelector("svg")!;
|
||||
const style = getComputedStyle(button);
|
||||
const iconStyle = getComputedStyle(icon);
|
||||
const bounds = button.getBoundingClientRect();
|
||||
return { label, sameNode: button === initial[index], isConnected: button.isConnected,
|
||||
hovered: button.matches(":hover"), pressed: button.getAttribute("aria-pressed"), disabled: button.disabled,
|
||||
background: style.backgroundColor, color: style.color, opacity: style.opacity,
|
||||
rect: { x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height },
|
||||
icon: { background: iconStyle.backgroundColor, color: iconStyle.color,
|
||||
transitions: icon.getAnimations().filter((animation) => animation instanceof CSSTransition).map((animation) => (animation as CSSTransition).transitionProperty) },
|
||||
svgColor: getComputedStyle(svg).color
|
||||
};
|
||||
}) });
|
||||
// Actionability waits can outlast 500ms; keep every frame through the action
|
||||
// and at least one full 150ms color transition after it has completed.
|
||||
if (elapsed < 500 || actionFinishedAt === null || elapsed < actionFinishedAt + 200) requestAnimationFrame(read);
|
||||
else finish();
|
||||
};
|
||||
Object.assign(window, { annotationHoverProbe: { frames, done,
|
||||
finishAction: () => { actionFinishedAt = performance.now() - started; return actionFinishedAt; }
|
||||
} });
|
||||
read();
|
||||
}, labels);
|
||||
}
|
||||
|
||||
async function collectSampling(page: Page): Promise<{ frames: Frame[]; actionFinishedAt: number }> {
|
||||
return page.evaluate(async () => {
|
||||
const probe = (window as unknown as { annotationHoverProbe: SamplingProbe }).annotationHoverProbe;
|
||||
const actionFinishedAt = probe.finishAction();
|
||||
await probe.done;
|
||||
return { frames: probe.frames, actionFinishedAt };
|
||||
});
|
||||
}
|
||||
|
||||
async function screenshot(page: Page, testInfo: TestInfo, name: string) {
|
||||
const panel = page.getByRole("region", { name: "标注", exact: true });
|
||||
await testInfo.attach(name, { body: await panel.screenshot({ path: testInfo.outputPath(`${name}.png`), animations: "allow" }), contentType: "image/png" });
|
||||
}
|
||||
|
||||
test.use({ reducedMotion: "no-preference", hasTouch: false });
|
||||
for (const width of [1440, 375]) {
|
||||
test(`annotation icons track button colors through hover, selection, cancellation and rapid reselection at ${width}px`, async ({ page }, testInfo) => {
|
||||
test.setTimeout(60_000);
|
||||
await page.setViewportSize({ width, height: 900 });
|
||||
await mockInvestigationPresentation(page, []);
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => Boolean(window.__waterNetworkMap?.getLayer("scada-pressure-hit")));
|
||||
await page.getByRole("button", { name: "标注:管理地图标注", exact: true }).click();
|
||||
const panel = page.getByRole("region", { name: "标注", exact: true });
|
||||
const point = panel.getByRole("button", { name: "点标注", exact: true });
|
||||
const line = panel.getByRole("button", { name: "线标注", exact: true });
|
||||
const polygon = panel.getByRole("button", { name: "范围标注", exact: true });
|
||||
await expect(point).toBeEnabled();
|
||||
await point.click({ trial: true });
|
||||
await page.evaluate(() => document.fonts.ready);
|
||||
await page.mouse.move(1, 1);
|
||||
const sample = async (name: string, action: () => Promise<unknown>, selected: string | null, capture = false) => {
|
||||
if (capture) await screenshot(page, testInfo, `${name}-before`);
|
||||
await startSampling(page);
|
||||
await action();
|
||||
if (capture) await screenshot(page, testInfo, `${name}-during`);
|
||||
const { frames, actionFinishedAt } = await collectSampling(page);
|
||||
expect(frames.at(-1)!.elapsed - actionFinishedAt, `${name}: sampling must cover at least 200ms after the completed action`).toBeGreaterThanOrEqual(200);
|
||||
const failures = frames.flatMap((frame) => frame.buttons.flatMap((button, index) => {
|
||||
const initial = frames[0].buttons[index];
|
||||
const shifted = (Object.keys(button.rect) as Array<keyof ButtonFrame["rect"]>).some((key) => Math.abs(button.rect[key] - initial.rect[key]) > 0.5);
|
||||
return !button.sameNode || !button.isConnected || shifted || button.disabled !== initial.disabled || button.opacity !== "1"
|
||||
|| button.svgColor !== button.color || button.icon.color !== button.color
|
||||
? [{ elapsed: frame.elapsed, ...button }] : [];
|
||||
}));
|
||||
if (failures.length) await testInfo.attach(`${name}-failed-frames`, { body: JSON.stringify(failures.slice(0, 12), null, 2), contentType: "application/json" });
|
||||
expect(failures, `${name}: icon colors, DOM identity, geometry and enabled state must remain in sync on every animation frame`).toEqual([]);
|
||||
for (const button of frames.at(-1)!.buttons) expect(button.pressed).toBe(String(button.label === selected));
|
||||
if (name.startsWith("hover-")) for (let index = 0; index < labels.length; index++) {
|
||||
const changes = frames.slice(1).filter((frame, frameIndex) => frame.buttons[index].hovered !== frames[frameIndex].buttons[index].hovered).length;
|
||||
expect(changes, "a single pointer entry or exit must not make hover blink repeatedly").toBeLessThanOrEqual(1);
|
||||
}
|
||||
if (capture) await screenshot(page, testInfo, `${name}-after`);
|
||||
};
|
||||
await sample("hover-point-first", () => point.hover(), null);
|
||||
await sample("hover-line-first", () => line.hover(), null);
|
||||
await sample("hover-point-repeat", () => point.hover(), null);
|
||||
await sample("hover-line-repeat", () => line.hover(), null);
|
||||
await sample("select-point", () => point.click(), "点标注");
|
||||
await sample("select-line-stay", () => line.click(), "线标注", true);
|
||||
await sample("select-polygon-stay", () => polygon.click(), "范围标注");
|
||||
await sample("cancel-polygon-stay", () => polygon.click(), null);
|
||||
await sample("select-point-again", () => point.click(), "点标注");
|
||||
await sample("select-line-return-to-point", async () => { await line.click(); await point.hover(); }, "线标注");
|
||||
await sample("cancel-line-return-to-point", async () => { await line.click(); await point.hover(); }, null);
|
||||
await sample("rapid-reselection", async () => { await point.click(); await line.click(); await point.click(); }, "点标注", true);
|
||||
});
|
||||
}
|
||||
@@ -63,7 +63,11 @@ test("standalone basemap controls share the same pointer and keyboard boundary",
|
||||
|
||||
async function renderControls(page: Page) {
|
||||
await page.evaluate(async () => {
|
||||
const paths = ["/node_modules/.vite/deps/react.js", "/node_modules/.vite/deps/react-dom_client.js", "/src/features/map/core/components/base-layers-control.tsx"];
|
||||
const entry = await fetch("/src/main.tsx").then((response) => response.text());
|
||||
const reactPath = entry.match(/["']([^"']*\/react\.js(?:\?[^"']*)?)["']/)?.[1];
|
||||
const domPath = entry.match(/["']([^"']*\/react-dom_client\.js(?:\?[^"']*)?)["']/)?.[1];
|
||||
if (!reactPath || !domPath) throw new Error("App React dependency URLs not found");
|
||||
const paths = [reactPath, domPath, "/src/features/map/core/components/base-layers-control.tsx"];
|
||||
const [react, dom, controls] = await Promise.all(paths.map((path) => import(path)));
|
||||
const h = react.default.createElement;
|
||||
function Examples() {
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { basename, resolve } from "node:path";
|
||||
import { investigationFixture, mockInvestigationPresentation, selectInvestigation } from "./support/mock-investigation-presentation";
|
||||
|
||||
const pressureIds = ["170486", "175618", "159673", "135160", "174754"];
|
||||
const flowIds = ["542017", "486033", "524529", "541061", "542121"];
|
||||
const times = Array.from({ length: 13 }, (_, i) => `${String(5 + Math.floor(i / 4)).padStart(2, "0")}:${String(i % 4 * 15).padStart(2, "0")}`);
|
||||
const chart = (id: string, flow: boolean) => ({
|
||||
action: "open_chart", chart_id: `chart_tc_${id}`, evidence_refs: [`synthetic:${id}`],
|
||||
chart: { title: `${id} ${flow ? "管流" : "压力"}七日同时刻对照(${flow ? "m³/h" : "m"})`, chart_type: "time_comparison",
|
||||
x_axis_name: "时间", y_axis_name: `${flow ? "流量(m³/h)" : "压力(m)"}`, x_data: times,
|
||||
series: [{ name: `${id} 当前 2026-07-16`, type: "line", data: times.map((_, i) => 30 - i / 2),
|
||||
sample_times: times.map((time) => `2026-07-16T${time}:00+08:00`) },
|
||||
...Array.from({ length: 7 }, (_, day) => ({ name: `${id} 2026-07-${String(9 + day).padStart(2, "0")}`, type: "scatter",
|
||||
data: times.map((_, i) => i < 10 ? null : 30 + day / 2),
|
||||
sample_times: times.map((time, i) => i < 10 ? null : `2026-07-${String(9 + day).padStart(2, "0")}T${time}:00+08:00`) }))] }
|
||||
});
|
||||
|
||||
test.use({ reducedMotion: "reduce" });
|
||||
for (const width of [1440, 375, 320]) {
|
||||
test(`groups comparisons with object selection and legends below the plot at ${width}px`, async ({ page }, testInfo) => {
|
||||
test.setTimeout(60_000);
|
||||
await page.setViewportSize({ width: 1440, height: 900 });
|
||||
// This workspace links dependencies outside Vite's root. Serve the installed font for visual checks.
|
||||
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"
|
||||
}));
|
||||
const task = investigationFixture(1, "同类图表聚合核查");
|
||||
const mock = await mockInvestigationPresentation(page, [task]);
|
||||
await selectInvestigation(page, task.title);
|
||||
await page.setViewportSize({ width, height: 900 });
|
||||
for (const [ids, flow] of [[pressureIds, false], [flowIds, true]] as const) for (const id of ids) {
|
||||
await mock.emit(task, chart(id, flow));
|
||||
await expect(page.locator(`[data-presentation-scope="investigation"] [data-investigation-chart="chart_tc_${id}"]`)).toHaveCount(1);
|
||||
}
|
||||
const manager = page.getByRole("dialog", { name: "图表管理", exact: true });
|
||||
await expect(manager).toBeVisible();
|
||||
await expect(manager.getByRole("group", { name: "压力七日同时刻对照(m)", exact: true })).toBeVisible();
|
||||
await expect(page.locator("[data-floating-window]")).toHaveCount(1);
|
||||
await manager.getByRole("button", { name: "关闭窗口", exact: true }).click();
|
||||
const panel = page.getByLabel("调查实时展示", { exact: true });
|
||||
await panel.getByRole("button", { name: "放大查看 压力七日同时刻对照(m)", exact: true }).click();
|
||||
const pressure = page.getByRole("dialog", { name: "压力七日同时刻对照(m)", exact: true });
|
||||
const flow = page.getByRole("dialog", { name: "管流七日同时刻对照(m³/h)", exact: true });
|
||||
await expect(pressure).toBeVisible();
|
||||
await expect(flow).toHaveCount(0);
|
||||
await expect(page.locator("[data-floating-window]")).toHaveCount(1);
|
||||
if (width < 640) await pressure.getByRole("button", { name: "最大化窗口", exact: true }).click();
|
||||
await expect(pressure.getByLabel("选择图表对象").getByRole("button")).toHaveCount(5);
|
||||
const objects = pressure.getByLabel("选择图表对象");
|
||||
const select = objects.getByRole("button", { name: "切换对象 175618", exact: true });
|
||||
const bounds = await pressure.boundingBox();
|
||||
await select.click();
|
||||
expect(await pressure.boundingBox()).toEqual(bounds);
|
||||
await expect(select).toHaveAttribute("aria-pressed", "true");
|
||||
await expect(objects.getByRole("button", { pressed: true })).toHaveCount(1);
|
||||
await expect(pressure.getByRole("img", { name: "175618 压力七日同时刻对照(m)", exact: true })).toBeVisible();
|
||||
const legends = pressure.getByLabel("图表图例");
|
||||
await expect(legends.getByRole("button")).toHaveCount(8);
|
||||
const drawing = pressure.getByRole("img", { name: "175618 压力七日同时刻对照(m)", exact: true });
|
||||
const plotBox = (await drawing.boundingBox())!;
|
||||
const legendBox = (await legends.boundingBox())!;
|
||||
const objectBox = (await objects.boundingBox())!;
|
||||
expect(legendBox.y).toBeGreaterThanOrEqual(plotBox.y + plotBox.height - 1);
|
||||
if (width > 640) expect(objectBox.x).toBeGreaterThanOrEqual(plotBox.x + plotBox.width);
|
||||
else expect(objectBox.y).toBeGreaterThanOrEqual(legendBox.y + legendBox.height - 1);
|
||||
expect(plotBox.height).toBeGreaterThanOrEqual(120);
|
||||
expect(await pressure.evaluate((node) => node.scrollWidth - node.clientWidth)).toBeLessThanOrEqual(1);
|
||||
await page.evaluate(() => document.fonts.ready);
|
||||
await testInfo.attach(`grouped-pressure-${width}`, { body: await pressure.screenshot({ path: testInfo.outputPath(`grouped-pressure-${width}.png`) }), contentType: "image/png" });
|
||||
await legends.getByRole("button", { name: "175618 2026-07-09", exact: true }).click();
|
||||
await expect(legends.getByRole("button", { name: "175618 2026-07-09", exact: true })).toHaveAttribute("aria-pressed", "false");
|
||||
await pressure.getByRole("tab", { name: "数据", exact: true }).click();
|
||||
await expect(pressure.getByRole("table").getByRole("row")).toHaveCount(14);
|
||||
await expect(pressure.getByRole("table")).toContainText("2026-07-16 08:00:00+08:00");
|
||||
await objects.getByRole("button", { name: "切换对象 159673", exact: true }).focus();
|
||||
await page.keyboard.press("Enter");
|
||||
await expect(pressure.getByRole("img", { name: "159673 压力七日同时刻对照(m)", exact: true })).toBeVisible();
|
||||
await mock.emit(task, { action: "close_chart", chart_id: "chart_tc_159673" });
|
||||
await expect(objects.getByRole("button")).toHaveCount(4);
|
||||
await expect(objects.getByRole("button", { pressed: true })).toHaveCount(1);
|
||||
await expect(panel.getByText("部分未完成", { exact: true })).toHaveCount(0);
|
||||
await pressure.getByRole("button", { name: "关闭窗口", exact: true }).click();
|
||||
await panel.getByRole("button", { name: "关闭图表 压力七日同时刻对照(m)", exact: true }).click();
|
||||
await expect(panel.locator('[data-investigation-chart^="chart_tc_1"]')).toHaveCount(0);
|
||||
await expect(panel.locator("[data-investigation-chart]")).toHaveCount(5);
|
||||
expect(mock.mutations).toEqual([]);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,480 @@
|
||||
import { expect, test, type Locator, type Page } from "@playwright/test";
|
||||
import { basename, resolve } from "node:path";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { investigationFixture, mockInvestigationPresentation, selectInvestigation } from "./support/mock-investigation-presentation";
|
||||
|
||||
const times = Array.from({ length: 13 }, (_, index) => `${String(5 + Math.floor(index / 4)).padStart(2, "0")}:${String(index % 4 * 15).padStart(2, "0")}`);
|
||||
const allDayTimes = Array.from({ length: 96 }, (_, index) => `${String(Math.floor(index / 4)).padStart(2, "0")}:${String(index % 4 * 15).padStart(2, "0")}`);
|
||||
const chart = (id: string, value = 28) => ({ action: "open_chart", chart_id: id, evidence_refs: ["synthetic:pressure"],
|
||||
chart: { title: `合成监测 ${id}`, chart_type: "line", x_data: times, series: [
|
||||
{ name: "压力(m)", data: times.map((_, index) => index === 12 ? value : 32 - index / 4) },
|
||||
{ name: "参考(m)", data: times.map((_, index) => 31 - index / 8) }
|
||||
] } });
|
||||
const comparison = (id: string, flow = false) => {
|
||||
const labels = flow ? allDayTimes : times;
|
||||
return { action: "open_chart", chart_id: `comparison_${id}`, evidence_refs: [`synthetic:${id}`],
|
||||
chart: { title: `${id} ${flow ? "管流" : "压力"}七日同时刻对照(${flow ? "m³/h" : "m"})`, chart_type: "time_comparison", x_axis_name: "时间", y_axis_name: flow ? "流量(m³/h)" : "压力(m)", x_data: labels,
|
||||
series: [{ name: `${id} 当前 2026-07-16`, type: "line", data: labels.map((_, index) => flow ? 1237 + Math.sin(index / 8) * 245 : 32 - index / 4),
|
||||
sample_times: labels.map((time) => `2026-07-16T${time}:00+08:00`) },
|
||||
...Array.from({ length: 7 }, (_, day) => ({ name: `${id} 2026-07-${String(9 + day).padStart(2, "0")}`, type: "scatter",
|
||||
data: labels.map((_, index) => index < labels.length - 3 ? null : flow ? 1360 + day * 21 : 30 + day / 2),
|
||||
sample_times: labels.map((time, index) => index < labels.length - 3 ? null : `2026-07-${String(9 + day).padStart(2, "0")}T${time}:00+08:00`) }))]
|
||||
} };
|
||||
};
|
||||
|
||||
const card = (manager: Locator, id: string) => manager.locator(`[data-managed-chart][data-chart-title="合成监测 ${id}"]`);
|
||||
const slots = (manager: Locator) => manager.locator("[data-chart-slot]:visible");
|
||||
const positions = (manager: Locator) => manager.locator("[data-managed-chart]").evaluateAll((nodes) => Object.fromEntries(nodes.map((node) => [
|
||||
node.getAttribute("data-chart-title"), Number(node.getAttribute("data-slot-index"))
|
||||
])));
|
||||
|
||||
async function expectCompleteLegendRow(article: Locator) {
|
||||
const legend = article.getByLabel("图表图例", { exact: true });
|
||||
const row = article.getByLabel("显示的数据序列", { exact: true }).getByRole("button").first();
|
||||
const rowHeight = (await row.boundingBox())!.height;
|
||||
const box = await legend.evaluate((node) => ({
|
||||
height: node.clientHeight, padding: Number.parseFloat(getComputedStyle(node).paddingTop)
|
||||
}));
|
||||
expect(box.height, "a full legend button must fit without being clipped by its scrolling container").toBeGreaterThanOrEqual(rowHeight + box.padding - 1);
|
||||
const legendBounds = (await legend.boundingBox())!;
|
||||
const groupBounds = (await article.locator("[data-chart-group-tile]").boundingBox())!;
|
||||
expect(legendBounds.y + legendBounds.height).toBeLessThanOrEqual(groupBounds.y + groupBounds.height + 1);
|
||||
}
|
||||
|
||||
async function coordinateBounds(plot: Locator) {
|
||||
return plot.locator("svg").evaluate((svg) => {
|
||||
const width = svg.getBoundingClientRect().width;
|
||||
const lines = [...svg.querySelectorAll("path")].filter((path) => getComputedStyle(path).stroke === "rgb(226, 232, 240)")
|
||||
.map((path) => path.getBoundingClientRect()).filter((box) => box.width > width / 2 && box.height <= 1);
|
||||
if (lines.length < 2) throw new Error("Expected horizontal value-axis grid lines to measure the actual coordinate area");
|
||||
const x = Math.min(...lines.map((line) => line.x));
|
||||
const y = Math.min(...lines.map((line) => line.y));
|
||||
return { x, y, width: Math.max(...lines.map((line) => line.right)) - x, height: Math.max(...lines.map((line) => line.bottom)) - y, gridLines: lines.length };
|
||||
});
|
||||
}
|
||||
|
||||
async function expectNoHorizontalOverflow(manager: Locator) {
|
||||
const measured = await manager.evaluate((node) => [node, ...node.querySelectorAll<HTMLElement>("[data-chart-manager-scroll], [data-chart-layout], [data-managed-chart]")]
|
||||
.filter((element) => element.clientWidth > 0)
|
||||
.map((element) => ({
|
||||
label: element.getAttribute("data-chart-title") ?? element.getAttribute("aria-label") ?? element.tagName,
|
||||
width: element.clientWidth,
|
||||
scrollWidth: element.scrollWidth
|
||||
})));
|
||||
for (const element of measured) expect(element.scrollWidth - element.width, `${element.label} must not create a horizontal scrollbar`).toBeLessThanOrEqual(1);
|
||||
return measured;
|
||||
}
|
||||
|
||||
async function resolvedColor(element: Locator, token: string, property: string) {
|
||||
return element.evaluate((node, { token, property }) => {
|
||||
const probe = document.createElement("span");
|
||||
probe.style.setProperty(property, `var(${token})`);
|
||||
node.append(probe);
|
||||
const color = getComputedStyle(probe).getPropertyValue(property);
|
||||
probe.remove();
|
||||
return color;
|
||||
}, { token, property });
|
||||
}
|
||||
|
||||
async function setup(page: Page, count: number) {
|
||||
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"
|
||||
}));
|
||||
const task = investigationFixture(1, "固定图表槽位验收");
|
||||
const mock = await mockInvestigationPresentation(page, [task]);
|
||||
await selectInvestigation(page, task.title);
|
||||
const panel = page.getByLabel("调查实时展示", { exact: true });
|
||||
await expect(panel.getByText("实时跟随", { exact: true })).toBeVisible();
|
||||
for (let index = 1; index <= count; index++) await mock.emit(task, chart(`P${index}`));
|
||||
const manager = page.getByRole("dialog", { name: "图表管理", exact: true });
|
||||
await expect(manager.locator("[data-managed-chart]")).toHaveCount(count);
|
||||
return { manager, mock, task, panel };
|
||||
}
|
||||
|
||||
test.use({ reducedMotion: "reduce", hasTouch: false, screenshot: "only-on-failure" });
|
||||
test.setTimeout(90_000);
|
||||
|
||||
test("quick chart location highlights the position bar without an outer frame", async ({ page }, testInfo) => {
|
||||
const { manager } = await setup(page, 4);
|
||||
await page.evaluate(() => document.fonts.ready);
|
||||
for (const width of [1440, 1600, 375]) {
|
||||
await page.setViewportSize({ width, height: 900 });
|
||||
const toolbar = manager.getByRole("toolbar", { name: "图表管理工具", exact: true });
|
||||
const locate = toolbar.getByRole("combobox", { name: "快速定位图表", exact: true });
|
||||
await expect(toolbar).not.toHaveCSS("background-color", "rgba(0, 0, 0, 0)");
|
||||
await expect(locate).toHaveCSS("border-top-color", "rgba(0, 0, 0, 0)");
|
||||
await expect(locate).toHaveCSS("height", "32px");
|
||||
const locateBounds = (await locate.boundingBox())!;
|
||||
expect(locateBounds.width).toBeLessThanOrEqual(288);
|
||||
for (const control of await toolbar.getByRole("button").all()) {
|
||||
const bounds = (await control.boundingBox())!;
|
||||
expect(Math.abs(bounds.height - locateBounds.height)).toBeLessThanOrEqual(1);
|
||||
if (width > 640) expect(Math.abs(bounds.y - locateBounds.y)).toBeLessThanOrEqual(1);
|
||||
}
|
||||
await expect(toolbar.getByRole("group", { name: "图表排列方式", exact: true })).toHaveCSS("background-color", "rgba(0, 0, 0, 0)");
|
||||
expect(await toolbar.evaluate((node) => node.scrollWidth - node.clientWidth)).toBeLessThanOrEqual(1);
|
||||
const target = card(manager, "P4");
|
||||
await manager.getByRole("combobox", { name: "快速定位图表", exact: true }).click();
|
||||
await page.getByRole("option").filter({ hasText: "合成监测 P4" }).click();
|
||||
await expect(target).toBeFocused();
|
||||
await expect(target).toHaveAttribute("data-chart-located", "true");
|
||||
const hasVisibleOutline = () => target.evaluate((node) => {
|
||||
const style = getComputedStyle(node);
|
||||
return style.outlineStyle !== "none" && Number.parseFloat(style.outlineWidth) > 0 && style.outlineColor !== "rgba(0, 0, 0, 0)";
|
||||
});
|
||||
expect(await hasVisibleOutline()).toBe(false);
|
||||
await expect(target).toHaveCSS("box-shadow", "none");
|
||||
const bar = target.locator(":scope > div").first();
|
||||
await expect(bar).not.toHaveCSS("background-color", "rgba(0, 0, 0, 0)");
|
||||
await expect(target.getByText("已定位", { exact: true })).toBeVisible();
|
||||
const bounds = await target.boundingBox();
|
||||
await testInfo.attach(`chart-location-${width}`, { body: await manager.screenshot({ path: testInfo.outputPath(`chart-location-${width}.png`) }), contentType: "image/png" });
|
||||
await expect(target).toHaveAttribute("data-chart-located", "false");
|
||||
await expect(bar).toHaveCSS("background-color", "rgba(0, 0, 0, 0)");
|
||||
expect(await hasVisibleOutline()).toBe(false);
|
||||
expect(await target.boundingBox()).toEqual(bounds);
|
||||
await locate.focus();
|
||||
await locate.press("ArrowDown");
|
||||
await page.getByRole("option").filter({ hasText: "合成监测 P4" }).focus();
|
||||
await page.keyboard.press("Enter");
|
||||
await expect(target).toBeFocused();
|
||||
expect(await hasVisibleOutline()).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
test("fixed chart slots swap without inserting or redrawing during pointer movement and fill vacancies before adding a row", async ({ page }, testInfo) => {
|
||||
const { manager, mock, task } = await setup(page, 4);
|
||||
const source = card(manager, "P1");
|
||||
await manager.getByRole("button", { name: "四宫格布局", exact: true }).click();
|
||||
await expect(slots(manager)).toHaveCount(4);
|
||||
await card(manager, "P1").getByLabel("显示的数据序列").getByRole("button", { name: "参考(m)", exact: true }).click();
|
||||
const instance = await card(manager, "P1").locator(".echarts-for-react").getAttribute("_echarts_instance_");
|
||||
const chartsBefore = await page.locator(".echarts-for-react").count();
|
||||
const handle = card(manager, "P1").getByRole("button", { name: "拖动图表 合成监测 P1", exact: true });
|
||||
const positionBar = handle.locator("..");
|
||||
const start = (await handle.boundingBox())!;
|
||||
const bar = (await positionBar.boundingBox())!;
|
||||
for (const [buttonEdge, barEdge] of [[start.x, bar.x], [start.y, bar.y], [start.x + start.width, bar.x + bar.width], [start.y + start.height, bar.y + bar.height]]) {
|
||||
expect(Math.abs(buttonEdge - barEdge), "the drag button must cover every edge of the position bar").toBeLessThanOrEqual(1);
|
||||
}
|
||||
await expect(manager.getByRole("combobox", { name: /^移动图表 / })).toHaveCount(0);
|
||||
await expect(manager.getByText("移动到", { exact: true })).toHaveCount(0);
|
||||
const rightBlank = { x: start.width - 12, y: start.height / 2 };
|
||||
await page.mouse.move(1, 1);
|
||||
const highlight = source.locator("[data-chart-hover-highlight]");
|
||||
await expect(highlight).toHaveCSS("opacity", "0");
|
||||
await expect(highlight).toHaveCSS("pointer-events", "none");
|
||||
const sourceBounds = (await source.boundingBox())!;
|
||||
const highlightBounds = (await highlight.boundingBox())!;
|
||||
for (const [highlightEdge, sourceEdge] of [[highlightBounds.x, sourceBounds.x], [highlightBounds.y, sourceBounds.y], [highlightBounds.x + highlightBounds.width, sourceBounds.x + sourceBounds.width], [highlightBounds.y + highlightBounds.height, sourceBounds.y + sourceBounds.height]]) {
|
||||
expect(Math.abs(highlightEdge - sourceEdge), "the hover highlight must cover every edge of the whole chart card").toBeLessThanOrEqual(1);
|
||||
}
|
||||
const highlightBackground = await highlight.evaluate((node) => getComputedStyle(node).backgroundColor);
|
||||
const highlightAlpha = Number(highlightBackground.match(/\/\s*([\d.]+)\s*\)$/)?.[1] ?? highlightBackground.match(/^rgba\(.+,\s*([\d.]+)\s*\)$/)?.[1] ?? 1);
|
||||
expect(highlightAlpha, "the whole-card tint must have a real, subtle background").toBeGreaterThan(0);
|
||||
expect(highlightAlpha).toBeLessThanOrEqual(0.06);
|
||||
// Hover the chart body edge, away from both its title bar and ECharts tooltips.
|
||||
await source.hover({ position: { x: sourceBounds.width - 6, y: sourceBounds.height / 2 } });
|
||||
await expect(highlight).toHaveCSS("opacity", "1");
|
||||
await testInfo.attach("whole-card-hover", { body: await source.screenshot({ path: testInfo.outputPath("whole-card-hover.png") }), contentType: "image/png" });
|
||||
await page.mouse.move(1, 1);
|
||||
await expect(highlight).toHaveCSS("opacity", "0");
|
||||
const idleBackground = await handle.evaluate((node) => getComputedStyle(node).backgroundColor);
|
||||
await handle.hover({ position: rightBlank });
|
||||
await expect(highlight).toHaveCSS("opacity", "1");
|
||||
await expect(handle).not.toHaveCSS("background-color", idleBackground);
|
||||
expect(await handle.evaluate((node, point) => {
|
||||
const bounds = node.getBoundingClientRect();
|
||||
return document.elementFromPoint(bounds.x + point.x, bounds.y + point.y)?.closest("button") === node;
|
||||
}, rightBlank)).toBe(true);
|
||||
await testInfo.attach("position-bar-right-hover", { body: await positionBar.screenshot({ path: testInfo.outputPath("position-bar-right-hover.png") }), contentType: "image/png" });
|
||||
const destination = (await manager.locator('[data-chart-slot="3"]').boundingBox())!;
|
||||
await card(manager, "P1").evaluate(async (node) => {
|
||||
await new Promise<void>((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
|
||||
Object.assign(window, { chartSwapSvgMutations: 0 });
|
||||
const observer = new MutationObserver((records) => {
|
||||
(window as unknown as { chartSwapSvgMutations: number }).chartSwapSvgMutations += records.length;
|
||||
});
|
||||
node.querySelectorAll(".echarts-for-react svg").forEach((svg) => observer.observe(svg, { subtree: true, attributes: true, childList: true }));
|
||||
Object.assign(window, { stopChartSwapWatch: () => observer.disconnect() });
|
||||
});
|
||||
await page.mouse.move(start.x + rightBlank.x, start.y + rightBlank.y);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(start.x + rightBlank.x + 12, start.y + rightBlank.y + 8, { steps: 3 });
|
||||
await page.mouse.move(destination.x + destination.width / 2, destination.y + destination.height / 2, { steps: 16 });
|
||||
await page.evaluate(() => new Promise<void>((resolve) => requestAnimationFrame(() => resolve())));
|
||||
const during = await page.evaluate(() => {
|
||||
const state = window as unknown as { chartSwapSvgMutations: number; stopChartSwapWatch: () => void };
|
||||
state.stopChartSwapWatch();
|
||||
return state.chartSwapSvgMutations;
|
||||
});
|
||||
expect(during).toBe(0);
|
||||
await expect(source).toHaveAttribute("data-chart-dragging", "true");
|
||||
await expect(highlight).toHaveCSS("opacity", "1");
|
||||
await expect(manager.locator('[data-chart-slot="3"]')).not.toHaveCSS("background-color", "rgba(0, 0, 0, 0)");
|
||||
await expect(source).toHaveCSS("opacity", "1");
|
||||
await expect(handle).not.toHaveCSS("color", "rgb(255, 255, 255)");
|
||||
await expect(handle).not.toHaveCSS("background-color", await resolvedColor(handle, "--action-blue", "background-color"));
|
||||
const drawingOpacity = await source.locator(".echarts-for-react svg").evaluate((svg) => {
|
||||
const ancestors = [];
|
||||
for (let node: Element | null = svg; node; node = node.parentElement) {
|
||||
ancestors.push({ element: node.tagName, className: node.getAttribute("class"), opacity: getComputedStyle(node).opacity });
|
||||
if (node.matches("[data-managed-chart]")) break;
|
||||
}
|
||||
return ancestors;
|
||||
});
|
||||
for (const ancestor of drawingOpacity) expect(ancestor.opacity, `${ancestor.element} must keep the plot fully opaque during dragging`).toBe("1");
|
||||
const overlay = page.locator("[data-chart-drag-preview]");
|
||||
await expect(overlay).toBeVisible();
|
||||
await expect(overlay).toHaveCSS("border-top-color", await resolvedColor(overlay, "--surface-border", "border-top-color"));
|
||||
await expectNoHorizontalOverflow(manager);
|
||||
await testInfo.attach("dragging-source", { body: await source.screenshot({ path: testInfo.outputPath("dragging-source.png") }), contentType: "image/png" });
|
||||
await testInfo.attach("dragging-manager", { body: await manager.screenshot({ path: testInfo.outputPath("dragging-manager.png") }), contentType: "image/png" });
|
||||
await testInfo.attach("dragging-plot-opacity", { body: JSON.stringify(drawingOpacity), contentType: "application/json" });
|
||||
// A lightweight preview must not clone the chart engine while the original stays in its slot.
|
||||
await expect(manager.locator(".echarts-for-react")).toHaveCount(4);
|
||||
await expect(page.locator(".echarts-for-react")).toHaveCount(chartsBefore);
|
||||
await page.mouse.up();
|
||||
await expect(source).toHaveAttribute("data-chart-dragging", "false");
|
||||
await expect.poll(() => positions(manager)).toEqual({ "合成监测 P1": 3, "合成监测 P2": 1, "合成监测 P3": 2, "合成监测 P4": 0 });
|
||||
await expect(card(manager, "P1").locator(".echarts-for-react")).toHaveAttribute("_echarts_instance_", instance!);
|
||||
await expect(card(manager, "P1").getByLabel("显示的数据序列").getByRole("button", { name: "参考(m)", exact: true })).toHaveAttribute("aria-pressed", "false");
|
||||
await mock.emit(task, chart("P2", 17));
|
||||
await card(manager, "P2").getByRole("tab", { name: "数据", exact: true }).click();
|
||||
await expect(card(manager, "P2").getByRole("table")).toContainText("17");
|
||||
await expect(card(manager, "P2")).toHaveAttribute("data-slot-index", "1");
|
||||
await mock.emit(task, { action: "close_chart", chart_id: "P2" });
|
||||
await expect(card(manager, "P2")).toHaveCount(0);
|
||||
await expect(slots(manager)).toHaveCount(4);
|
||||
await expect(card(manager, "P1")).toHaveAttribute("data-slot-index", "3");
|
||||
await mock.emit(task, chart("P5"));
|
||||
await expect(card(manager, "P5")).toHaveAttribute("data-slot-index", "1");
|
||||
await mock.emit(task, chart("P6"));
|
||||
await expect(card(manager, "P6")).toHaveAttribute("data-slot-index", "4");
|
||||
await expect(card(manager, "P6")).toBeVisible();
|
||||
await expect(card(manager, "P6")).not.toBeInViewport();
|
||||
await expect(slots(manager)).toHaveCount(6);
|
||||
await expect(manager.getByRole("button", { name: /[上下]一页图表/ })).toHaveCount(0);
|
||||
const scroll = manager.locator("[data-chart-manager-scroll]");
|
||||
await expect.poll(() => scroll.evaluate((node) => node.scrollHeight - node.clientHeight)).toBeGreaterThan(100);
|
||||
await scroll.hover();
|
||||
await page.mouse.wheel(0, 700);
|
||||
await expect(card(manager, "P6")).toBeInViewport();
|
||||
await manager.getByRole("combobox", { name: "快速定位图表", exact: true }).click();
|
||||
await page.getByRole("option").filter({ hasText: "合成监测 P6" }).click();
|
||||
await expect(card(manager, "P6")).toBeVisible();
|
||||
await expect(card(manager, "P6")).toBeFocused();
|
||||
await expect(slots(manager)).toHaveCount(6);
|
||||
await expect.poll(() => scroll.evaluate((node) => node.scrollTop)).toBeGreaterThan(0);
|
||||
await manager.getByRole("button", { name: "重置图表排列", exact: true }).click();
|
||||
await expect.poll(() => scroll.evaluate((node) => node.scrollTop)).toBe(0);
|
||||
await expect(card(manager, "P1")).toBeVisible();
|
||||
await expect(card(manager, "P1").locator(".echarts-for-react")).toHaveAttribute("_echarts_instance_", instance!);
|
||||
await page.evaluate(() => document.fonts.ready);
|
||||
await testInfo.attach("fixed-slots-desktop", { body: await manager.screenshot({ path: testInfo.outputPath("fixed-slots-desktop.png") }), contentType: "image/png" });
|
||||
expect(mock.mutations).toEqual([]);
|
||||
});
|
||||
|
||||
test("scrolling layouts adapt charts and retain empty places without turning into an insertion list", async ({ page }, testInfo) => {
|
||||
const { manager, mock } = await setup(page, 3);
|
||||
const dimensions: Record<string, { width: number; height: number }> = {};
|
||||
for (const [label, layout, count] of [["单图布局", "one", 1], ["左右布局", "two", 2], ["四宫格布局", "four", 4]] as const) {
|
||||
await manager.getByRole("button", { name: label, exact: true }).click();
|
||||
await expect(manager.locator("[data-chart-layout]")).toHaveAttribute("data-chart-layout", layout);
|
||||
await expect(slots(manager)).toHaveCount(layout === "one" ? 3 : 4);
|
||||
const first = (await slots(manager).first().boundingBox())!;
|
||||
dimensions[layout] = first;
|
||||
if (count > 1) {
|
||||
const second = (await slots(manager).nth(1).boundingBox())!;
|
||||
expect(second.x).toBeGreaterThan(first.x + first.width - 1);
|
||||
expect(Math.abs(second.y - first.y)).toBeLessThanOrEqual(1);
|
||||
}
|
||||
if (count === 4) {
|
||||
const third = (await slots(manager).nth(2).boundingBox())!;
|
||||
expect(third.y).toBeGreaterThan(first.y + first.height - 1);
|
||||
expect(Math.abs(third.x - first.x)).toBeLessThanOrEqual(1);
|
||||
expect(await manager.locator('[data-managed-chart][data-slot-index="3"]').count()).toBe(0);
|
||||
}
|
||||
expect(await manager.evaluate((node) => node.scrollWidth - node.clientWidth)).toBeLessThanOrEqual(1);
|
||||
for (const article of await manager.locator("[data-managed-chart]:visible").all()) await expectCompleteLegendRow(article);
|
||||
await testInfo.attach(`fixed-layout-${layout}`, { body: await manager.screenshot({ path: testInfo.outputPath(`fixed-layout-${layout}.png`) }), contentType: "image/png" });
|
||||
}
|
||||
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 manager.getByRole("combobox", { name: "快速定位图表", exact: true }).click();
|
||||
await page.getByRole("option").filter({ hasText: "合成监测 P3" }).click();
|
||||
await expect(card(manager, "P3")).toBeFocused();
|
||||
const viewport = (await manager.locator("[data-chart-manager-scroll]").boundingBox())!;
|
||||
for (const target of [card(manager, "P3").getByRole("button", { name: "拖动图表 合成监测 P3", exact: true }), card(manager, "P3").getByRole("heading", { level: 3 })]) {
|
||||
const bounds = (await target.boundingBox())!;
|
||||
expect(bounds.y).toBeGreaterThanOrEqual(viewport.y - 1);
|
||||
expect(bounds.y + bounds.height).toBeLessThanOrEqual(viewport.y + viewport.height + 1);
|
||||
}
|
||||
expect(await manager.locator("[data-chart-manager-scroll]").evaluate((node) => node.scrollTop)).toBeGreaterThan(0);
|
||||
await testInfo.attach("fixed-layout-short-locate", { body: await manager.screenshot({ path: testInfo.outputPath("fixed-layout-short-locate.png") }), contentType: "image/png" });
|
||||
expect(mock.mutations).toEqual([]);
|
||||
});
|
||||
|
||||
test("default keyboard dragging swaps fixed slots and narrow scrolling grids retain location and enlargement", async ({ page }, testInfo) => {
|
||||
const { manager, mock, task } = await setup(page, 4);
|
||||
await manager.getByRole("button", { name: "四宫格布局", exact: true }).click();
|
||||
const source = card(manager, "P1");
|
||||
const sourceBounds = (await source.boundingBox())!;
|
||||
const targetBounds = (await manager.locator('[data-chart-slot="3"]').boundingBox())!;
|
||||
const moveX = targetBounds.x + targetBounds.width / 2 - sourceBounds.x - sourceBounds.width / 2;
|
||||
const moveY = targetBounds.y + targetBounds.height / 2 - sourceBounds.y - sourceBounds.height / 2;
|
||||
await source.getByRole("button", { name: "拖动图表 合成监测 P1", exact: true }).focus();
|
||||
await page.keyboard.press("Space");
|
||||
await expect(source).toHaveAttribute("data-chart-dragging", "true");
|
||||
await expect(source).toHaveCSS("opacity", "1");
|
||||
// The SDK's default KeyboardSensor advances 50px with Shift + an arrow.
|
||||
for (let step = 0; step < Math.round(Math.abs(moveX) / 50); step++) await page.keyboard.press(moveX > 0 ? "Shift+ArrowRight" : "Shift+ArrowLeft");
|
||||
for (let step = 0; step < Math.round(Math.abs(moveY) / 50); step++) await page.keyboard.press(moveY > 0 ? "Shift+ArrowDown" : "Shift+ArrowUp");
|
||||
await expect(manager.locator('[data-chart-slot="3"]')).toHaveClass(/border-primary/);
|
||||
await page.keyboard.press("Space");
|
||||
await expect.poll(() => positions(manager)).toEqual({ "合成监测 P1": 3, "合成监测 P2": 1, "合成监测 P3": 2, "合成监测 P4": 0 });
|
||||
await expect(source).toHaveAttribute("data-chart-dragging", "false");
|
||||
await expect(source).toHaveCSS("opacity", "1");
|
||||
await mock.emit(task, chart("P5"));
|
||||
await expect(card(manager, "P5")).toHaveAttribute("data-slot-index", "4");
|
||||
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 expect(slots(manager)).toHaveCount(5);
|
||||
await manager.getByRole("combobox", { name: "快速定位图表", exact: true }).click();
|
||||
await page.getByRole("option").filter({ hasText: "合成监测 P1" }).click();
|
||||
await expect(card(manager, "P1")).toBeVisible();
|
||||
await expect(card(manager, "P1")).toHaveAttribute("data-slot-index", "3");
|
||||
await expect(card(manager, "P1")).toBeFocused();
|
||||
const scroll = manager.locator("[data-chart-manager-scroll]");
|
||||
await scroll.focus();
|
||||
await scroll.press("End");
|
||||
await expect(card(manager, "P5")).toBeInViewport();
|
||||
await scroll.press("Home");
|
||||
await expect(card(manager, "P4")).toBeInViewport();
|
||||
await manager.getByRole("combobox", { name: "快速定位图表", exact: true }).click();
|
||||
await page.getByRole("option").filter({ hasText: "合成监测 P1" }).click();
|
||||
await expect(card(manager, "P1")).toBeInViewport();
|
||||
expect(await manager.evaluate((node) => node.scrollWidth - node.clientWidth)).toBeLessThanOrEqual(1);
|
||||
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(width);
|
||||
await page.evaluate(() => document.fonts.ready);
|
||||
await testInfo.attach(`fixed-slots-${width}`, { body: await manager.screenshot({ path: testInfo.outputPath(`fixed-slots-${width}.png`) }), contentType: "image/png" });
|
||||
await manager.getByRole("combobox", { name: "快速定位图表", exact: true }).click();
|
||||
await page.getByRole("option").filter({ hasText: "合成监测 P4" }).click();
|
||||
await expect(card(manager, "P4")).toBeVisible();
|
||||
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 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();
|
||||
await page.getByRole("option").filter({ hasText: "合成监测 P1" }).click();
|
||||
await card(manager, "P1").getByRole("button", { name: "放大查看 合成监测 P1", exact: true }).click();
|
||||
await expect(page.getByRole("dialog", { name: "合成监测 P1", exact: true })).toBeVisible();
|
||||
expect(mock.mutations).toEqual([]);
|
||||
});
|
||||
|
||||
test("compound comparisons fit each slot with bottom scrolling legends and accessible object selection", async ({ page }, testInfo) => {
|
||||
const { manager, mock, task } = await setup(page, 1);
|
||||
for (const id of ["170486", "175618", "159673"]) await mock.emit(task, comparison(id));
|
||||
await mock.emit(task, chart("P2"));
|
||||
for (const id of ["542017", "486033", "524529"]) await mock.emit(task, comparison(id, true));
|
||||
await expect(manager.locator("[data-managed-chart]")).toHaveCount(4);
|
||||
const compound = manager.locator('[data-managed-chart][data-chart-title="压力七日同时刻对照(m)"]');
|
||||
await expect(compound).toHaveAttribute("data-slot-index", "1");
|
||||
await manager.getByRole("button", { name: "四宫格布局", exact: true }).click();
|
||||
const objectButtons = compound.locator('aside[aria-label="选择图表对象"]');
|
||||
await expect(objectButtons).toBeVisible();
|
||||
await expect(objectButtons.getByRole("button")).toHaveCount(3);
|
||||
await objectButtons.getByRole("button", { name: "切换对象 175618", exact: true }).click();
|
||||
const plot = compound.getByRole("img", { name: "175618 压力七日同时刻对照(m)", exact: true });
|
||||
await expect(plot).toBeVisible();
|
||||
const legend = compound.getByLabel("图表图例", { exact: true });
|
||||
await expect(legend.getByRole("button")).toHaveCount(8);
|
||||
await page.evaluate(() => document.fonts.ready);
|
||||
await expect.poll(() => plot.locator("svg").boundingBox().then((box) => box?.height ?? 0)).toBeGreaterThanOrEqual(100);
|
||||
const geometry = async () => {
|
||||
const slot = (await compound.boundingBox())!;
|
||||
const drawing = (await plot.boundingBox())!;
|
||||
const svg = (await plot.locator("svg").boundingBox())!;
|
||||
const footer = (await legend.boundingBox())!;
|
||||
const coordinates = await coordinateBounds(plot);
|
||||
for (const box of [drawing, svg, footer]) {
|
||||
expect(box.x).toBeGreaterThanOrEqual(slot.x - 1);
|
||||
expect(box.y).toBeGreaterThanOrEqual(slot.y - 1);
|
||||
expect(box.x + box.width).toBeLessThanOrEqual(slot.x + slot.width + 1);
|
||||
expect(box.y + box.height).toBeLessThanOrEqual(slot.y + slot.height + 1);
|
||||
}
|
||||
expect(drawing.height).toBeGreaterThanOrEqual(100);
|
||||
expect(coordinates.height, "compact axes must leave at least 40px for the actual plotted data").toBeGreaterThanOrEqual(40);
|
||||
expect(footer.y).toBeGreaterThanOrEqual(drawing.y + drawing.height - 1);
|
||||
expect(footer.height).toBeLessThanOrEqual(56);
|
||||
await expectCompleteLegendRow(compound);
|
||||
expect(slot.height).toBeGreaterThanOrEqual(234);
|
||||
const overflow = await expectNoHorizontalOverflow(manager);
|
||||
return { slot, drawing, svg, coordinates, footer, overflow };
|
||||
};
|
||||
const fourGeometry = await geometry();
|
||||
const flow = manager.locator('[data-managed-chart][data-chart-title="管流七日同时刻对照(m³/h)"]');
|
||||
const flowDrawing = flow.getByRole("img", { name: "542017 管流七日同时刻对照(m³/h)", exact: true });
|
||||
await expect(flowDrawing).toBeVisible();
|
||||
await expectCompleteLegendRow(flow);
|
||||
await expect(flow.getByLabel("图表图例", { exact: true }).getByRole("button")).toHaveCount(8);
|
||||
expect((await coordinateBounds(flowDrawing)).height, "large flow values and eight legend entries must preserve the coordinate area").toBeGreaterThanOrEqual(40);
|
||||
expect((await objectButtons.boundingBox())!.x).toBeGreaterThanOrEqual(fourGeometry.drawing.x + fourGeometry.drawing.width);
|
||||
const scroll = manager.locator("[data-chart-manager-scroll]");
|
||||
const scrollBefore = await scroll.evaluate((node) => node.scrollTop);
|
||||
const legendScroll = await legend.evaluate((node) => {
|
||||
node.scrollTop = node.scrollHeight;
|
||||
return { height: node.clientHeight, content: node.scrollHeight, top: node.scrollTop, overflow: getComputedStyle(node).overflowY };
|
||||
});
|
||||
expect(legendScroll.overflow).toBe("auto");
|
||||
expect(legendScroll.content).toBeGreaterThan(legendScroll.height);
|
||||
expect(legendScroll.top).toBeGreaterThan(0);
|
||||
expect(await scroll.evaluate((node) => node.scrollTop)).toBe(scrollBefore);
|
||||
await legend.getByRole("button", { name: "175618 2026-07-15", exact: true }).click();
|
||||
await expect(legend.getByRole("button", { name: "175618 2026-07-15", exact: true })).toHaveAttribute("aria-pressed", "false");
|
||||
await testInfo.attach("compound-fixed-four", { body: await manager.screenshot({ path: testInfo.outputPath("compound-fixed-four.png") }), contentType: "image/png" });
|
||||
|
||||
await manager.getByRole("button", { name: "单图布局", exact: true }).click();
|
||||
await manager.getByRole("combobox", { name: "快速定位图表", exact: true }).click();
|
||||
await page.getByRole("option").filter({ hasText: "压力七日同时刻对照(m)" }).click();
|
||||
await expect(compound).toBeVisible();
|
||||
await expect(objectButtons).toBeVisible();
|
||||
await expect(compound.getByRole("combobox", { name: "选择图表对象", exact: true })).toBeHidden();
|
||||
await expect(compound).toBeFocused();
|
||||
const oneGeometry = await geometry();
|
||||
await testInfo.attach("compound-fixed-one", { body: await manager.screenshot({ path: testInfo.outputPath("compound-fixed-one.png") }), contentType: "image/png" });
|
||||
|
||||
await page.setViewportSize({ width: 375, height: 812 });
|
||||
await expect(manager.locator("[data-chart-layout]")).toHaveAttribute("data-chart-layout", "one");
|
||||
await expect(compound).toBeVisible();
|
||||
await expect(objectButtons).toBeHidden();
|
||||
await compound.getByRole("combobox", { name: "选择图表对象", exact: true }).click();
|
||||
await page.getByRole("option", { name: "159673", exact: true }).click();
|
||||
await expect(compound.getByRole("img", { name: "159673 压力七日同时刻对照(m)", exact: true })).toBeVisible();
|
||||
await expect.poll(() => compound.getByRole("img", { name: "159673 压力七日同时刻对照(m)", exact: true }).locator("svg").boundingBox().then((box) => box?.height ?? 0)).toBeGreaterThanOrEqual(100);
|
||||
const narrowPlot = (await compound.getByRole("img", { name: "159673 压力七日同时刻对照(m)", exact: true }).boundingBox())!;
|
||||
const narrowSvg = (await compound.getByRole("img", { name: "159673 压力七日同时刻对照(m)", exact: true }).locator("svg").boundingBox())!;
|
||||
const narrowSlot = (await compound.boundingBox())!;
|
||||
const narrowLegend = (await legend.boundingBox())!;
|
||||
const narrowCoordinates = await coordinateBounds(compound.getByRole("img", { name: "159673 压力七日同时刻对照(m)", exact: true }));
|
||||
expect(narrowPlot.height).toBeGreaterThanOrEqual(100);
|
||||
expect(narrowPlot.x + narrowPlot.width).toBeLessThanOrEqual(narrowSlot.x + narrowSlot.width + 1);
|
||||
expect(narrowSvg.x + narrowSvg.width).toBeLessThanOrEqual(narrowSlot.x + narrowSlot.width + 1);
|
||||
expect(narrowSvg.y + narrowSvg.height).toBeLessThanOrEqual(narrowSlot.y + narrowSlot.height + 1);
|
||||
expect(narrowLegend.y).toBeGreaterThanOrEqual(narrowPlot.y + narrowPlot.height - 1);
|
||||
expect(narrowLegend.y + narrowLegend.height).toBeLessThanOrEqual(narrowSlot.y + narrowSlot.height + 1);
|
||||
expect(await manager.evaluate((node) => node.scrollWidth - node.clientWidth)).toBeLessThanOrEqual(1);
|
||||
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(375);
|
||||
await testInfo.attach("compound-fixed-narrow", { body: await manager.screenshot({ path: testInfo.outputPath("compound-fixed-narrow.png") }), contentType: "image/png" });
|
||||
const geometryPath = testInfo.outputPath("compound-slot-geometry.json");
|
||||
await writeFile(geometryPath, JSON.stringify({ four: fourGeometry, one: oneGeometry, legendScroll, narrow: { slot: narrowSlot, drawing: narrowPlot, svg: narrowSvg, coordinates: narrowCoordinates, footer: narrowLegend } }, null, 2));
|
||||
await testInfo.attach("compound-slot-geometry", { path: geometryPath, contentType: "application/json" });
|
||||
expect(mock.mutations).toEqual([]);
|
||||
});
|
||||
@@ -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([]);
|
||||
});
|
||||
@@ -0,0 +1,131 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { mockInvestigationPresentation } from "./support/mock-investigation-presentation";
|
||||
|
||||
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) => {
|
||||
test.setTimeout(90_000);
|
||||
await mockInvestigationPresentation(page, []);
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
await page.getByRole("button", { name: "切换地图 Dev Panel" }).click();
|
||||
const tools = page.getByTestId("investigation-dev-tools");
|
||||
await tools.getByRole("combobox", { name: "JSONL 测试案例", exact: true }).click();
|
||||
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 });
|
||||
const panel = page.getByLabel("本地预览展示", { exact: true });
|
||||
await panel.getByRole("button", { name: "打开图表管理", exact: true }).click();
|
||||
const manager = page.getByRole("dialog", { name: "图表管理", exact: true });
|
||||
if (target === "child") await manager.getByRole("button", { name: "放大查看 合成全天压力对比", exact: true }).click();
|
||||
const popup = target === "manager" ? manager : page.getByRole("dialog", { name: "合成全天压力对比", exact: true });
|
||||
await expect(popup.locator(".echarts-for-react svg").first()).toBeVisible();
|
||||
await popup.locator(".floating-window-heading").hover();
|
||||
await page.evaluate(() => document.fonts.ready);
|
||||
const instances = await popup.locator(".echarts-for-react").evaluateAll((nodes) => nodes.map((node) => node.getAttribute("_echarts_instance_")));
|
||||
const drawingSize = await popup.locator(".echarts-for-react").first().evaluate((node) => ({ width: node.clientWidth, height: node.clientHeight }));
|
||||
const font = await popup.locator(".echarts-for-react svg text").first().evaluate((node) => ({
|
||||
size: getComputedStyle(node).fontSize, height: node.getBoundingClientRect().height
|
||||
}));
|
||||
const before = (await popup.boundingBox())!;
|
||||
const handle = (await popup.locator(".floating-window-resize").boundingBox())!;
|
||||
const session = await page.context().newCDPSession(page);
|
||||
await session.send("Emulation.setCPUThrottlingRate", { rate: 4 });
|
||||
await session.send("Performance.enable");
|
||||
await popup.evaluate(async (node) => {
|
||||
await new Promise<void>((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
|
||||
Object.assign(window, { chartResizeMutations: 0 });
|
||||
const observer = new MutationObserver((records) => {
|
||||
(window as unknown as { chartResizeMutations: number }).chartResizeMutations += records.length;
|
||||
});
|
||||
node.querySelectorAll(".echarts-for-react svg").forEach((svg) => observer.observe(svg, { subtree: true, attributes: true, childList: true }));
|
||||
});
|
||||
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);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(handle.x + handle.width - 3 + 128, handle.y + handle.height - 3 + 64, { 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);
|
||||
const during = await page.evaluate(() => (window as unknown as { chartResizeMutations: number }).chartResizeMutations);
|
||||
const preview = (await popup.boundingBox())!;
|
||||
await expect(popup.locator("[data-floating-window-document]")).toHaveCSS("transform", "none");
|
||||
expect(await popup.locator("[data-floating-window-document]").evaluate((node) => ({ width: (node as HTMLElement).style.width, height: (node as HTMLElement).style.height }))).toEqual({ width: "", height: "" });
|
||||
const resizedFont = await popup.locator(".echarts-for-react svg text").first().evaluate((node) => ({ size: getComputedStyle(node).fontSize, height: node.getBoundingClientRect().height }));
|
||||
expect(resizedFont.size).toBe(font.size);
|
||||
expect(Math.abs(resizedFont.height - font.height)).toBeLessThan(1);
|
||||
const actualDrawing = await popup.locator(".echarts-for-react").first().evaluate((node) => ({ width: node.clientWidth, height: node.clientHeight }));
|
||||
expect(actualDrawing.width).toBeGreaterThan(drawingSize.width);
|
||||
expect(actualDrawing.height).toBeGreaterThanOrEqual(drawingSize.height);
|
||||
await page.mouse.up();
|
||||
const last = await session.send("Performance.getMetrics");
|
||||
const metric = (name: string) => (last.metrics.find((item) => item.name === name)?.value ?? 0) - (first.metrics.find((item) => item.name === name)?.value ?? 0);
|
||||
const summary = { target, svgMutationsDuringResize: during, elapsedMs: Date.now() - started,
|
||||
scriptMs: metric("ScriptDuration") * 1000, layoutMs: metric("LayoutDuration") * 1000, taskMs: metric("TaskDuration") * 1000 };
|
||||
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(during).toBeGreaterThan(0);
|
||||
// Bound the expensive work on the same 4x CPU fixture used to reproduce the lag.
|
||||
expect(summary.elapsedMs).toBeLessThan(12_000);
|
||||
expect(summary.scriptMs).toBeLessThan(6_000);
|
||||
expect(await popup.locator(".echarts-for-react").evaluateAll((nodes) => nodes.map((node) => node.getAttribute("_echarts_instance_")))).toEqual(instances);
|
||||
await expect.poll(() => popup.locator(".echarts-for-react").first().evaluate((node) => Math.abs(node.clientWidth - Number(node.querySelector("svg")?.getAttribute("width"))))).toBeLessThan(1);
|
||||
const layout = await popup.evaluate((node) => ({
|
||||
plots: Array.from(node.querySelectorAll<HTMLElement>(".echarts-for-react")).filter((plot) => plot.getBoundingClientRect().height > 0).map((plot) => plot.clientHeight),
|
||||
legends: Array.from(node.querySelectorAll<HTMLElement>('[data-chart-group-tile="true"]')).map((card) => {
|
||||
const legend = card.querySelector<HTMLElement>('[aria-label="图表图例"]')!;
|
||||
const first = legend.querySelector("button")!;
|
||||
return { height: legend.clientHeight, firstRowHeight: first.getBoundingClientRect().height,
|
||||
bottomOverflow: legend.getBoundingClientRect().bottom - card.getBoundingClientRect().bottom };
|
||||
})
|
||||
}));
|
||||
for (const height of layout.plots) expect(height).toBeGreaterThanOrEqual(100);
|
||||
for (const legend of layout.legends) {
|
||||
expect(legend.height).toBeGreaterThanOrEqual(legend.firstRowHeight - 1);
|
||||
expect(legend.bottomOverflow).toBeLessThanOrEqual(1);
|
||||
}
|
||||
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 hiddenSeries = chartSurface.getByLabel("显示的数据序列").getByRole("button").last();
|
||||
await hiddenSeries.click();
|
||||
await expect(selectedObject).toHaveAttribute("aria-pressed", "true");
|
||||
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");
|
||||
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);
|
||||
await testInfo.attach(`resize-${target}`, { body: await popup.screenshot({ path: testInfo.outputPath(`resize-${target}.png`) }), contentType: "image/png" });
|
||||
});
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { expect, test, type Page } from "@playwright/test";
|
||||
import { investigationFixture, mockInvestigationPresentation, selectInvestigation } from "./support/mock-investigation-presentation";
|
||||
|
||||
const chart = (index: number) => ({
|
||||
@@ -7,15 +7,51 @@ const chart = (index: number) => ({
|
||||
series: [{ name: "压力(m)", data: [30, 30 - index] }] }
|
||||
});
|
||||
|
||||
const managedPages = new WeakSet<Page>();
|
||||
async function openChild(page: Page, title: string, live = true) {
|
||||
const manager = page.getByRole("dialog", { name: "图表管理", exact: true });
|
||||
if (live && !managedPages.has(page)) {
|
||||
await expect(manager).toBeVisible();
|
||||
managedPages.add(page);
|
||||
}
|
||||
if (await manager.isVisible()) await manager.getByRole("button", { name: "关闭窗口", exact: true }).click();
|
||||
await page.getByLabel("调查实时展示", { exact: true }).getByRole("button", { name: `放大查看 ${title}`, exact: true }).click();
|
||||
}
|
||||
|
||||
test.setTimeout(60_000);
|
||||
// Keep unrelated avatar animation out of chart geometry and drag measurements.
|
||||
test.use({ reducedMotion: "reduce" });
|
||||
|
||||
test("losing window focus cancels a drag and allows a fresh gesture", async ({ page }) => {
|
||||
const task = investigationFixture(1, "图窗失焦恢复核查");
|
||||
const mock = await mockInvestigationPresentation(page, [task]);
|
||||
await selectInvestigation(page, task.title);
|
||||
await mock.emit(task, chart(1));
|
||||
await openChild(page, "合成压力分析 1");
|
||||
const popup = page.getByRole("dialog", { name: "合成压力分析 1", exact: true });
|
||||
const before = (await popup.boundingBox())!;
|
||||
await page.mouse.move(before.x + 180, before.y + 24);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(before.x + 100, before.y + 64, { steps: 4 });
|
||||
await expect.poll(async () => Math.round((await popup.boundingBox())!.x - before.x)).toBe(-80);
|
||||
await page.evaluate(() => window.dispatchEvent(new Event("blur")));
|
||||
expect(await popup.boundingBox()).toEqual(before);
|
||||
expect(await popup.evaluate((node) => (node as HTMLElement).style.willChange)).toBe("");
|
||||
await page.mouse.up();
|
||||
await page.mouse.move(before.x + 180, before.y + 24);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(before.x + 140, before.y + 44, { steps: 4 });
|
||||
await page.mouse.up();
|
||||
expect(Math.round((await popup.boundingBox())!.x - before.x)).toBe(-40);
|
||||
expect(Math.round((await popup.boundingBox())!.y - before.y)).toBe(20);
|
||||
});
|
||||
|
||||
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));
|
||||
await openChild(page, "合成压力分析 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()));
|
||||
@@ -37,6 +73,7 @@ test("cancelled drags release capture and preserve restore, resizing and keyboar
|
||||
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);
|
||||
expect((await popup.boundingBox())!.y).toBe(64);
|
||||
await popup.getByRole("button", { name: "还原窗口", exact: true }).click();
|
||||
expect(await popup.boundingBox()).toEqual(original);
|
||||
await heading.focus();
|
||||
@@ -69,6 +106,8 @@ test("profiles a single drag with eight comparison chart windows", async ({ page
|
||||
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.getByLabel("调查实时展示", { exact: true }).getByRole("button", { name: `放大查看 合成七日同期曲线 ${index}`, exact: true })).toBeVisible();
|
||||
await openChild(page, `合成七日同期曲线 ${index}`);
|
||||
await expect(page.getByRole("dialog", { name: `合成七日同期曲线 ${index}`, exact: true })).toBeVisible();
|
||||
}
|
||||
await expect(page.locator("[data-floating-window]")).toHaveCount(8);
|
||||
@@ -144,6 +183,8 @@ for (const height of [900, 720]) {
|
||||
await selectInvestigation(page, task.title);
|
||||
for (let index = 1; index <= 4; index++) {
|
||||
await mock.emit(task, { ...chart(index), chart_id: `pressure-${index}` });
|
||||
await expect(page.getByLabel("调查实时展示", { exact: true }).getByRole("button", { name: `放大查看 合成压力分析 ${index}`, exact: true })).toBeVisible();
|
||||
await openChild(page, `合成压力分析 ${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()));
|
||||
@@ -172,7 +213,7 @@ for (const height of [900, 720]) {
|
||||
await page.mouse.move(start.x - 120, start.y - 32, { steps: 12 });
|
||||
await page.mouse.up();
|
||||
await expect.poll(async () => Math.round((await popup.boundingBox())!.x - before.x)).toBe(-120);
|
||||
await expect.poll(async () => Math.round((await popup.boundingBox())!.y - before.y)).toBe(-32);
|
||||
await expect.poll(async () => Math.round((await popup.boundingBox())!.y - before.y)).toBe(Math.max(64, before.y - 32) - before.y);
|
||||
expect((await popup.boundingBox())!.width).toBe(before.width);
|
||||
expect((await popup.boundingBox())!.height).toBe(before.height);
|
||||
}
|
||||
@@ -183,49 +224,36 @@ for (const height of [900, 720]) {
|
||||
});
|
||||
}
|
||||
|
||||
test("live analysis opens a window, preserves its position and respects closing and task switches", async ({ page }, testInfo) => {
|
||||
const task = investigationFixture(1, "图表自动展示核查");
|
||||
const other = investigationFixture(2, "另一项调查");
|
||||
const mock = await mockInvestigationPresentation(page, [task, other]);
|
||||
test("live analysis updates one manager and respects dismissal", async ({ page }, testInfo) => {
|
||||
const task = investigationFixture(1, "图表统一展示核查");
|
||||
const mock = await mockInvestigationPresentation(page, [task]);
|
||||
await selectInvestigation(page, task.title);
|
||||
const panel = page.getByLabel("调查实时展示", { exact: true });
|
||||
await expect(panel.getByText("实时跟随", { exact: true })).toBeVisible();
|
||||
const collapse = panel.getByRole("button", { name: "收起展示", exact: true });
|
||||
await collapse.focus();
|
||||
await mock.emit(task, chart(1));
|
||||
let popup = page.getByRole("dialog", { name: "合成压力分析 1", exact: true });
|
||||
await expect(popup).toBeVisible();
|
||||
await expect(popup.getByRole("img", { name: "合成压力分析 1", exact: true })).toBeVisible();
|
||||
const manager = page.getByRole("dialog", { name: "图表管理", exact: true });
|
||||
await expect(manager).toBeVisible();
|
||||
await expect(collapse).toBeFocused();
|
||||
const heading = popup.getByLabel("窗口标题栏,可用方向键移动,Shift 加方向键调整大小");
|
||||
await heading.focus();
|
||||
await expect(page.locator("[data-floating-window]")).toHaveCount(1);
|
||||
await manager.locator(".floating-window-heading").focus();
|
||||
await page.keyboard.press("ArrowRight");
|
||||
const moved = (await popup.boundingBox())!;
|
||||
const moved = await manager.boundingBox();
|
||||
await mock.emit(task, chart(2));
|
||||
popup = page.getByRole("dialog", { name: "合成压力分析 2", exact: true });
|
||||
await expect(popup).toBeVisible();
|
||||
expect((await popup.boundingBox())!.x).toBe(moved.x);
|
||||
await expect(manager.getByRole("img", { name: "合成压力分析 2", exact: true })).toBeVisible();
|
||||
expect(await manager.boundingBox()).toEqual(moved);
|
||||
await page.keyboard.press("Escape");
|
||||
await expect(page.locator("[data-floating-window]")).toHaveCount(0);
|
||||
await mock.emit(task, { action: "zoom_to_map", center: [121.48, 31.1], zoom: 13 });
|
||||
await expect(panel.getByRole("combobox", { name: "切换展示动作" })).toContainText("3 个动作");
|
||||
await expect(page.locator("[data-floating-window]")).toHaveCount(0);
|
||||
await panel.getByRole("button", { name: "放大查看 合成压力分析 2", exact: true }).click();
|
||||
await expect(popup).toBeVisible();
|
||||
await testInfo.attach("live-chart-window", { body: await page.screenshot({ path: testInfo.outputPath("live-chart-window.png") }), contentType: "image/png" });
|
||||
await popup.getByRole("button", { name: "关闭窗口", exact: true }).click();
|
||||
await mock.emit(task, chart(3));
|
||||
await expect(page.getByRole("dialog", { name: "合成压力分析 3", exact: true })).toBeVisible();
|
||||
await mock.emit(task, { action: "close_chart", chart_id: "pressure" });
|
||||
await expect(page.locator("[data-floating-window]")).toHaveCount(0);
|
||||
await mock.emit(task, chart(4));
|
||||
await expect(page.getByRole("dialog", { name: "合成压力分析 4", exact: true })).toBeVisible();
|
||||
await page.getByRole("region", { name: "工况任务", exact: true }).getByText(other.title, { exact: true }).first().click();
|
||||
await expect(panel.getByRole("img", { name: "合成压力分析 3", exact: true })).toBeVisible();
|
||||
await expect(page.locator("[data-floating-window]")).toHaveCount(0);
|
||||
await panel.getByRole("button", { name: "打开图表管理", exact: true }).click();
|
||||
await expect(manager.getByRole("img", { name: "合成压力分析 3", exact: true })).toBeVisible();
|
||||
await testInfo.attach("live-chart-manager", { body: await manager.screenshot(), contentType: "image/png" });
|
||||
expect(mock.mutations).toEqual([]);
|
||||
});
|
||||
|
||||
test("playback windows follow saved snapshots while live results remain separate", async ({ page }, testInfo) => {
|
||||
test("playback restores charts without opening windows while live results remain separate", async ({ page }, testInfo) => {
|
||||
const task = investigationFixture(1, "图表回放核查");
|
||||
const commands = [chart(1), { action: "zoom_to_map", center: [121.48, 31.1], zoom: 13 }, chart(2), { action: "close_chart", chart_id: "pressure" }];
|
||||
const saved = commands.map((command, index) => ({ task_id: task.task_id, event_seq: index + 1,
|
||||
@@ -237,6 +265,8 @@ test("playback windows follow saved snapshots while live results remain separate
|
||||
await expect(page.locator("[data-floating-window]")).toHaveCount(0);
|
||||
await panel.getByRole("button", { name: "播放历史", exact: true }).click();
|
||||
await panel.getByRole("button", { name: "暂停历史播放", exact: true }).press("Enter");
|
||||
await expect(page.locator("[data-floating-window]")).toHaveCount(0);
|
||||
await openChild(page, "合成压力分析 1", false);
|
||||
const popup = page.getByRole("dialog", { name: "合成压力分析 1", exact: true });
|
||||
await expect(popup).toBeVisible();
|
||||
await mock.emit(task, chart(3));
|
||||
@@ -249,17 +279,19 @@ test("playback windows follow saved snapshots while live results remain separate
|
||||
await expect(panel.getByRole("combobox", { name: "切换展示动作" })).toContainText("2.");
|
||||
await expect(page.locator("[data-floating-window]")).toHaveCount(0);
|
||||
await page.keyboard.press("ArrowRight");
|
||||
await expect(page.getByRole("dialog", { name: "合成压力分析 2", exact: true })).toBeVisible();
|
||||
await expect(page.locator("[data-floating-window]")).toHaveCount(0);
|
||||
await expect(panel.getByRole("img", { name: "合成压力分析 2", exact: true })).toBeVisible();
|
||||
await expect(progress).toBeFocused();
|
||||
await testInfo.attach("playback-chart-window", { body: await page.screenshot({ path: testInfo.outputPath("playback-chart-window.png") }), contentType: "image/png" });
|
||||
await page.keyboard.press("ArrowRight");
|
||||
await expect(page.locator("[data-floating-window]")).toHaveCount(0);
|
||||
await panel.getByRole("button", { name: "回到当前", exact: true }).click();
|
||||
await expect(page.getByRole("dialog", { name: "合成压力分析 3", exact: true })).toBeVisible();
|
||||
await expect(panel.getByRole("img", { name: "合成压力分析 3", exact: true })).toBeVisible();
|
||||
await expect(page.locator("[data-floating-window]")).toHaveCount(0);
|
||||
expect(mock.mutations).toEqual([]);
|
||||
});
|
||||
|
||||
test("structured analysis charts open responsive windows in local preview and history", async ({ page }, testInfo) => {
|
||||
test("structured analysis charts retain manual enlargement without opening during history", async ({ page }, testInfo) => {
|
||||
const task = investigationFixture(1, "结构化分析展示核查");
|
||||
const mock = await mockInvestigationPresentation(page, [task]);
|
||||
await selectInvestigation(page, task.title);
|
||||
@@ -272,6 +304,9 @@ test("structured analysis charts open responsive windows in local preview and hi
|
||||
}
|
||||
} }));
|
||||
await tools.getByRole("button", { name: "执行本地预览", exact: true }).click();
|
||||
const preview = page.getByLabel("本地预览展示", { exact: true });
|
||||
await expect(page.locator("[data-floating-window]")).toHaveCount(0);
|
||||
await preview.getByRole("button", { name: "放大查看 合成压力分析 1", exact: true }).click();
|
||||
const popup = page.getByRole("dialog", { name: "合成压力分析 1", exact: true });
|
||||
await expect(popup).toBeVisible();
|
||||
await expect(popup.getByRole("img", { name: "合成压力分析 1", exact: true })).toBeVisible();
|
||||
@@ -285,9 +320,8 @@ test("structured analysis charts open responsive windows in local preview and hi
|
||||
}
|
||||
await popup.getByRole("button", { name: "关闭窗口", exact: true }).click();
|
||||
await page.setViewportSize({ width: 1440, height: 900 });
|
||||
const preview = page.getByLabel("本地预览展示", { exact: true });
|
||||
await preview.getByRole("button", { name: "播放历史", exact: true }).click();
|
||||
await expect(popup).toBeVisible();
|
||||
await expect(page.locator("[data-floating-window]")).toHaveCount(0);
|
||||
expect(mock.mutations).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -296,6 +330,7 @@ test("live charts fit desktop and narrow windows without outer scrollbars", asyn
|
||||
const mock = await mockInvestigationPresentation(page, [task]);
|
||||
await selectInvestigation(page, task.title);
|
||||
await mock.emit(task, chart(1));
|
||||
await openChild(page, "合成压力分析 1");
|
||||
const popup = page.getByRole("dialog", { name: "合成压力分析 1", exact: true });
|
||||
const content = popup.locator("[data-floating-window-content]");
|
||||
await expect(popup.getByRole("img", { name: "合成压力分析 1", exact: true })).toBeVisible();
|
||||
@@ -330,6 +365,7 @@ test("SCADA chart axes and legends fit every nested container", async ({ page },
|
||||
series: [{ name: "测点 123748(实测回放)", data: [26.2, 25.7, 23.5] },
|
||||
{ name: "测点 159673(实测回放)", data: [24.9, 24.87, 21.6] }]
|
||||
} });
|
||||
await openChild(page, "SCADA 回放压力:异常前后(2026-07-16)");
|
||||
const popup = page.getByRole("dialog", { name: "SCADA 回放压力:异常前后(2026-07-16)", exact: true });
|
||||
await expect(popup).toBeVisible();
|
||||
await expect.poll(() => popup.evaluate((node) => Array.from(node.querySelectorAll<HTMLElement>("*"))
|
||||
@@ -362,6 +398,8 @@ test("SCADA pressure and flow keep full comparison data without timezone wording
|
||||
...Array.from({ length: 7 }, (_, day) => ({ name: `参考 07-${String(9 + day).padStart(2, "0")}`, type: "scatter",
|
||||
data: labels.map((_, i) => i < 10 ? null : 28 + day) }))]
|
||||
} });
|
||||
await expect(page.getByLabel("调查实时展示", { exact: true }).getByRole("button", { name: `放大查看 回放${metric}:${device}(${unit},07-16 04:45–07:45)`, exact: true })).toBeVisible();
|
||||
await openChild(page, `回放${metric}:${device}(${unit},07-16 04:45–07:45)`);
|
||||
const popup = page.getByRole("dialog", { name: `回放${metric}:${device}(${unit},07-16 04:45–07:45)`, exact: true });
|
||||
await expect(popup).toBeVisible();
|
||||
await expect(popup.getByText(/上海时间|UTC\+8|Asia\/Shanghai/)).toHaveCount(0);
|
||||
|
||||
@@ -12,6 +12,7 @@ test("live investigation charts remain visible while Dev is open", async ({ page
|
||||
chart: { title: "实时合成压力核查", chart_type: "line", x_data: ["08:00", "08:15"], series: [{ name: "压力水头(m)", data: [32, 28] }] } });
|
||||
await expect(page.locator('[data-investigation-chart="live-pressure"]')).toBeVisible();
|
||||
await expect(page.getByLabel("调查实时展示", { exact: true })).not.toContainText("图表当前不可见");
|
||||
await page.getByRole("dialog", { name: "图表管理", exact: true }).getByRole("button", { name: "关闭窗口", exact: true }).click();
|
||||
await page.getByRole("button", { name: "收起展示", exact: true }).click();
|
||||
await mock.emit(task, { action: "open_chart", chart_id: "while-collapsed", evidence_refs: ["dev:synthetic"],
|
||||
chart: { title: "收起期间的合成图表", chart_type: "line", x_data: ["08:00"], series: [{ name: "压力水头(m)", data: [32] }] } });
|
||||
@@ -198,7 +199,8 @@ test("JSONL cases switch directly, reset validation and remove the previous loca
|
||||
for (const [label, error] of [
|
||||
["错误:JSON 语法", "第 2 行不是合法 JSON"],
|
||||
["错误:不支持的动作", "第 1 行展示指令不合法"],
|
||||
["错误:图表数据不齐", "第 1 行展示指令不合法"]
|
||||
["错误:图表数据不齐", "第 1 行展示指令不合法"],
|
||||
["错误:同期时刻错位", "换算为 UTC+8 后须与 x_data"]
|
||||
]) {
|
||||
await choose(label);
|
||||
await tools.getByRole("button", { name: "校验 JSONL", exact: true }).click();
|
||||
@@ -223,7 +225,7 @@ test("JSONL cases preview tables, charts and map overlays without an investigati
|
||||
await expect(cases).toBeEnabled();
|
||||
await cases.click();
|
||||
await expect(page.getByRole("listbox")).toBeVisible();
|
||||
await expect(page.getByRole("option")).toHaveCount(14);
|
||||
await expect(page.getByRole("option")).toHaveCount(15);
|
||||
await page.getByRole("button", { name: "关闭通知", exact: true }).evaluateAll((buttons) => buttons.forEach((button) => (button as HTMLButtonElement).click()));
|
||||
await testInfo.attach("case-dropdown-without-task", {
|
||||
body: await page.screenshot({ path: testInfo.outputPath("dropdown-without-task.png") }), contentType: "image/png"
|
||||
@@ -391,9 +393,10 @@ test("individual charts float, move and resize while the preview stays in place"
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(handle.x + 88, handle.y + 58, { steps: 8 });
|
||||
const resizedPlot = firstPlot.locator("svg");
|
||||
await expect(first).toHaveAttribute("data-floating-window-resizing", "true");
|
||||
await page.mouse.up();
|
||||
await expect.poll(async () => Math.abs(Number(await resizedPlot.getAttribute("width")) - (await firstPlot.boundingBox())!.width)).toBeLessThan(1);
|
||||
await expect.poll(async () => Math.abs(Number(await resizedPlot.getAttribute("height")) - (await firstPlot.boundingBox())!.height)).toBeLessThan(1);
|
||||
await page.mouse.up();
|
||||
await expect.poll(async () => (await first.boundingBox())!.width - beforeResize.width).toBeGreaterThan(70);
|
||||
await expect.poll(async () => (await first.boundingBox())!.height - beforeResize.height).toBeGreaterThan(40);
|
||||
expect(await panel.boundingBox()).toEqual(original);
|
||||
@@ -544,7 +547,7 @@ test("complex investigation cases show multi-chart and multi-table results witho
|
||||
await expect(page.getByRole("option", { name: "地图定位与高亮", exact: true })).toHaveCount(0);
|
||||
await page.getByRole("option", { name: "复杂调查:多图多表联动", exact: true }).click();
|
||||
await tools.getByRole("button", { name: "执行本地预览", exact: true }).click();
|
||||
await expect(tools.getByRole("status")).toContainText("本地预览完成:2 条展示指令、4 个界面块");
|
||||
await expect(tools.getByRole("status")).toContainText("本地预览完成:14 条展示指令、2 个界面块", { timeout: 20_000 });
|
||||
const panel = page.getByLabel("本地预览展示", { exact: true });
|
||||
await expect(panel.getByRole("button", { name: "放大查看 合成全天压力对比", exact: true })).toBeVisible();
|
||||
await expect(panel.getByRole("button", { name: "放大查看 合成分区流量趋势", exact: true })).toHaveCount(1);
|
||||
@@ -566,7 +569,7 @@ test("expanded bars fill the window while preserving legends, type size and the
|
||||
await tools.getByRole("combobox", { name: "JSONL 测试案例", exact: true }).click();
|
||||
await page.getByRole("option", { name: "复杂调查:多图多表联动", exact: true }).click();
|
||||
await tools.getByRole("button", { name: "执行本地预览", exact: true }).click();
|
||||
await expect(tools.getByRole("status")).toContainText("本地预览完成");
|
||||
await expect(tools.getByRole("status")).toContainText("本地预览完成", { timeout: 20_000 });
|
||||
await page.getByLabel("本地预览展示", { exact: true }).getByRole("button", { name: "放大查看 合成候选方案影响对比", exact: true }).click();
|
||||
const floating = page.getByRole("dialog", { name: "合成候选方案影响对比", exact: true });
|
||||
const legend = floating.getByLabel("类别图例", { exact: true });
|
||||
@@ -579,20 +582,25 @@ test("expanded bars fill the window while preserving legends, type size and the
|
||||
const svg = plot.locator("svg");
|
||||
await expect.poll(async () => Number(await svg.getAttribute("width"))).toBe((await plot.boundingBox())!.width);
|
||||
const drawing = (await plot.boundingBox())!;
|
||||
const sidebar = (await floating.locator("aside").boundingBox())!;
|
||||
const footer = floating.getByLabel("图表图例");
|
||||
const plotViewport = floating.getByRole("group", { name: /按回车查看数据点/ });
|
||||
const initialViewport = (await plotViewport.boundingBox())!;
|
||||
const initialWindowHeight = (await floating.boundingBox())!.height;
|
||||
expect((await footer.boundingBox())!.y).toBeGreaterThanOrEqual(initialViewport.y + initialViewport.height);
|
||||
const instance = await plot.locator(".echarts-for-react").getAttribute("_echarts_instance_");
|
||||
const fontSize = await svg.locator("text").first().evaluate((element) => getComputedStyle(element).fontSize);
|
||||
await floating.locator(".floating-window-heading").focus();
|
||||
await page.keyboard.press("Shift+ArrowRight");
|
||||
await page.keyboard.press("Shift+ArrowDown");
|
||||
await expect.poll(async () => (await plot.boundingBox())!.width).toBe(drawing.width + 16);
|
||||
await expect.poll(async () => (await plot.boundingBox())!.height).toBe(drawing.height + 16);
|
||||
await expect.poll(async () => (await floating.boundingBox())!.height).toBe(initialWindowHeight + 16);
|
||||
expect((await plotViewport.boundingBox())!.height).toBeGreaterThanOrEqual(initialViewport.height);
|
||||
await floating.getByRole("button", { name: "最大化窗口", exact: true }).click();
|
||||
await expect.poll(async () => (await plot.boundingBox())!.width).toBeGreaterThan(drawing.width + 400);
|
||||
await expect.poll(async () => (await plot.boundingBox())!.height).toBeGreaterThan(drawing.height + 100);
|
||||
await expect.poll(async () => Number(await svg.getAttribute("width"))).toBe((await plot.boundingBox())!.width);
|
||||
await expect.poll(async () => Number(await svg.getAttribute("height"))).toBe((await plot.boundingBox())!.height);
|
||||
expect((await floating.locator("aside").boundingBox())!.width).toBe(sidebar.width);
|
||||
expect((await footer.boundingBox())!.y).toBeGreaterThanOrEqual((await plotViewport.boundingBox())!.y + (await plotViewport.boundingBox())!.height);
|
||||
expect(await svg.locator("text").first().evaluate((element) => getComputedStyle(element).fontSize)).toBe(fontSize);
|
||||
await expect(plot.locator(".echarts-for-react")).toHaveAttribute("_echarts_instance_", instance!);
|
||||
await page.getByRole("button", { name: "关闭通知", exact: true }).evaluateAll((buttons) => buttons.forEach((button) => (button as HTMLButtonElement).click()));
|
||||
@@ -609,7 +617,12 @@ test("expanded bars fill the window while preserving legends, type size and the
|
||||
await expect(floating.getByRole("table")).toContainText("合成方案 E:待补充资料");
|
||||
});
|
||||
|
||||
test("full-day charts expose all eight series and retain legend selection while resizing", async ({ page }) => {
|
||||
test("full-day chart examples group objects and retain their eight series while resizing", async ({ page }, testInfo) => {
|
||||
test.setTimeout(60_000);
|
||||
await page.route("**/@fs/**/noto-sans-sc-*.woff2", (route) => route.fulfill({
|
||||
path: `node_modules/@fontsource-variable/noto-sans-sc/files/${new URL(route.request().url()).pathname.split("/").at(-1)}`,
|
||||
contentType: "font/woff2"
|
||||
}));
|
||||
await mockInvestigationPresentation(page, []);
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
await page.getByRole("button", { name: "切换地图 Dev Panel" }).click();
|
||||
@@ -617,23 +630,37 @@ test("full-day charts expose all eight series and retain legend selection while
|
||||
await tools.getByRole("combobox", { name: "JSONL 测试案例", exact: true }).click();
|
||||
await page.getByRole("option", { name: "复杂图表:多测点与长时序", exact: true }).click();
|
||||
await tools.getByRole("button", { name: "执行本地预览", exact: true }).click();
|
||||
await expect(tools.getByRole("status")).toContainText("本地预览完成");
|
||||
await expect(tools.getByRole("status")).toContainText("本地预览完成", { timeout: 20_000 });
|
||||
await page.getByLabel("本地预览展示", { exact: true }).getByRole("button", { name: "放大查看 合成全天压力对比", exact: true }).click();
|
||||
const floating = page.getByRole("dialog", { name: "合成全天压力对比", exact: true });
|
||||
await floating.locator(".floating-window-heading").click({ position: { x: 100, y: 20 } });
|
||||
const objects = floating.getByLabel("选择图表对象");
|
||||
await expect(objects.getByRole("button")).toHaveCount(8);
|
||||
const object = INVESTIGATION_DEV_NETWORK.nodeAnchors[1].id;
|
||||
await objects.getByRole("button", { name: `切换对象 ${object}`, exact: true }).click();
|
||||
await expect(objects.getByRole("button", { pressed: true })).toHaveCount(1);
|
||||
const series = floating.getByLabel("显示的数据序列").getByRole("button");
|
||||
await expect(series).toHaveCount(8);
|
||||
await series.last().click();
|
||||
await expect(series.last()).toHaveAttribute("aria-pressed", "false");
|
||||
const plot = floating.getByRole("img", { name: "合成全天压力对比", exact: true });
|
||||
const plot = floating.getByRole("img", { name: `${object} 合成全天压力对比`, exact: true });
|
||||
const drawing = (await plot.boundingBox())!;
|
||||
const windowHeight = (await floating.boundingBox())!.height;
|
||||
await floating.locator(".floating-window-heading").focus();
|
||||
await page.keyboard.press("Shift+ArrowDown");
|
||||
await expect.poll(async () => (await plot.boundingBox())!.height).toBe(drawing.height + 16);
|
||||
await expect.poll(async () => (await floating.boundingBox())!.height).toBe(windowHeight + 16);
|
||||
expect((await plot.boundingBox())!.height).toBeGreaterThanOrEqual(drawing.height);
|
||||
await expect(series.last()).toHaveAttribute("aria-pressed", "false");
|
||||
await floating.getByRole("button", { name: "显示全部序列", exact: true }).click();
|
||||
await expect(series.last()).toHaveAttribute("aria-pressed", "true");
|
||||
await floating.getByRole("tab", { name: "数据", exact: true }).click();
|
||||
await expect(floating.locator("tbody tr")).toHaveCount(96);
|
||||
await expect(floating.getByRole("table")).toContainText("2026-07-16 23:45:00+08:00");
|
||||
await floating.getByRole("tab", { name: "复合", exact: true }).click();
|
||||
await floating.getByRole("button", { name: "最大化窗口", exact: true }).click();
|
||||
await expect.poll(async () => (await plot.boundingBox())!.height).toBeGreaterThan(drawing.height + 100);
|
||||
await page.evaluate(() => document.fonts.ready);
|
||||
await testInfo.attach("migrated-chart-example", { body: await floating.screenshot({ path: testInfo.outputPath("migrated-chart-example.png") }), contentType: "image/png" });
|
||||
});
|
||||
|
||||
for (const reducedMotion of ["no-preference", "reduce"] as const) {
|
||||
@@ -746,6 +773,9 @@ test("live actions continue recording without interrupting a historical view and
|
||||
await expect(panel.getByRole("group", { name: "压力快照 1", exact: true })).toBeVisible();
|
||||
const switcher = panel.getByRole("combobox", { name: "切换展示动作" });
|
||||
await expect(switcher).toContainText("压力快照 1");
|
||||
const manager = page.getByRole("dialog", { name: "图表管理", exact: true });
|
||||
await expect(manager).toBeVisible();
|
||||
await manager.getByRole("button", { name: "关闭窗口", exact: true }).click();
|
||||
await switcher.click();
|
||||
await expect(page.getByRole("option")).toHaveCount(1);
|
||||
await expect(page.getByRole("option", { name: /当前(最新).*压力快照 1/ })).toBeVisible();
|
||||
|
||||
@@ -72,6 +72,7 @@ const previousReport: NonNullable<OperationalTask["latest_report"]> = {
|
||||
};
|
||||
|
||||
const task: OperationalTask = {
|
||||
execution_scope: "production",
|
||||
project_id: projectId,
|
||||
task_id: taskId,
|
||||
task_type: "event_investigation",
|
||||
|
||||
@@ -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([]);
|
||||
});
|
||||
@@ -62,6 +62,7 @@ test("调查进度替换整图,跟随只展示新指令并在关闭时清理
|
||||
};
|
||||
});
|
||||
let task: OperationalTask = {
|
||||
execution_scope: "production",
|
||||
project_id: projectId, task_id: taskId, task_type: "event_investigation", workflow: "water_quality_complaint",
|
||||
lifecycle_status: "open", status: "running", priority: "normal", revision: 1, execution_generation: 1,
|
||||
execution_request_id: requestId, title: "合成投诉联动调查", summary: "浏览器验证用合成投诉,不是真实工单",
|
||||
@@ -106,6 +107,7 @@ test("调查进度替换整图,跟随只展示新指令并在关闭时清理
|
||||
return route.fulfill({ json: { task_id: taskId, execution_generation: 1,
|
||||
active_run: { run_id: runId, generation: 1, status: "running" }, event_cursor: eventCursor } });
|
||||
}
|
||||
if (route.request().url().includes("/presentation-events")) return route.fulfill({ json: { task_id: taskId, event_cursor: eventCursor, items: [], next_after: null } });
|
||||
if (route.request().url().includes("/reports")) return route.fulfill({ json: { items: [], next_offset: null } });
|
||||
return route.fulfill({ json: task });
|
||||
});
|
||||
@@ -127,6 +129,7 @@ test("调查进度替换整图,跟随只展示新指令并在关闭时清理
|
||||
await panel.getByText("合成投诉联动调查", { exact: true }).first().click();
|
||||
await expect(panel.getByText("正在核对投诉点位", { exact: true }).first()).toBeVisible();
|
||||
const diagram = panel.getByLabel("调查脉络", { exact: true });
|
||||
await diagram.scrollIntoViewIfNeeded();
|
||||
await expect(diagram.locator("svg g.node.tjRunning")).toHaveCount(1);
|
||||
const animations = () => page.evaluate(() => (window as unknown as { reportAnimations: string[] }).reportAnimations);
|
||||
expect(await animations()).toEqual([]);
|
||||
@@ -142,7 +145,7 @@ test("调查进度替换整图,跟随只展示新指令并在关闭时清理
|
||||
await expect(diagram).not.toContainText("投诉点位核对");
|
||||
await expect(diagram.locator("svg g.node.tjRunning")).toHaveCount(0);
|
||||
await expect(diagram).toHaveAttribute("data-retained-reader", "true");
|
||||
await expect.poll(animations).toEqual(["当前调查进展"]);
|
||||
await expect.poll(animations).toContain("当前调查进展");
|
||||
|
||||
task = { ...task, revision: 3, latest_report: { revision: 1, execution_generation: 1,
|
||||
source_session_id: "investigation-test", published_at: now, published_by: "agent", report: {
|
||||
@@ -153,24 +156,24 @@ test("调查进度替换整图,跟随只展示新指令并在关闭时清理
|
||||
await panel.getByRole("button", { name: "刷新工况任务", exact: true }).click();
|
||||
const report = panel.getByRole("region", { name: "任务报告", exact: true });
|
||||
await expect(report).toContainText("合成结论:等待现场核查");
|
||||
await expect.poll(animations).toEqual(["当前调查进展", "任务报告"]);
|
||||
await report.getByText("判断说明与建议", { exact: true }).click();
|
||||
await report.locator("details").evaluate((element) => element.setAttribute("data-retained-reader", "true"));
|
||||
await expect.poll(animations).toContain("任务报告");
|
||||
await report.evaluate((element) => element.setAttribute("data-retained-reader", "true"));
|
||||
const beforeReportUpdate = (await animations()).filter((label) => label === "任务报告").length;
|
||||
task = { ...task, revision: 4, latest_report: { ...task.latest_report!, revision: 2,
|
||||
report: { ...task.latest_report!.report, current_conclusion: "合成结论:现场核查已更新" } } };
|
||||
await panel.getByRole("button", { name: "刷新工况任务", exact: true }).click();
|
||||
await expect(report).toContainText("合成结论:现场核查已更新");
|
||||
await expect(report.locator("details")).toHaveAttribute("open", "");
|
||||
await expect(report.locator("details")).toHaveAttribute("data-retained-reader", "true");
|
||||
await expect.poll(animations).toEqual(["当前调查进展", "任务报告", "任务报告"]);
|
||||
await expect(report).toHaveAttribute("data-retained-reader", "true");
|
||||
await expect.poll(() => animations().then((entries) => entries.filter((label) => label === "任务报告").length)).toBeGreaterThan(beforeReportUpdate);
|
||||
await testInfo.attach("report-update", { body: await report.screenshot(), contentType: "image/png" });
|
||||
|
||||
await page.emulateMedia({ reducedMotion: "reduce" });
|
||||
const beforeReducedMotion = await animations();
|
||||
task = { ...task, revision: 5, latest_report: { ...task.latest_report!, revision: 3,
|
||||
report: { ...task.latest_report!.report, current_conclusion: "合成结论:减少动态效果下更新" } } };
|
||||
await panel.getByRole("button", { name: "刷新工况任务", exact: true }).click();
|
||||
await expect(report).toContainText("合成结论:减少动态效果下更新");
|
||||
expect(await animations()).toEqual(["当前调查进展", "任务报告", "任务报告"]);
|
||||
expect(await animations()).toEqual(beforeReducedMotion);
|
||||
await page.emulateMedia({ reducedMotion: "no-preference" });
|
||||
await page.getByRole("button", { name: "切换地图 Dev Panel" }).click();
|
||||
const follow = page.getByRole("checkbox", { name: "跟随所选调查" });
|
||||
@@ -185,9 +188,11 @@ test("调查进度替换整图,跟随只展示新指令并在关闭时清理
|
||||
expect(checkboxAlignment).toEqual({ width: 20, height: 20, offsetX: 0, offsetY: 0 });
|
||||
await expect.poll(() => cursorReads).toBeGreaterThan(0);
|
||||
const dev = page.getByTestId("map-dev-panel");
|
||||
const sections = dev.locator("details");
|
||||
await expect(sections).toHaveCount(5);
|
||||
await expect(sections.filter({ hasText: "调查联动" })).toHaveAttribute("open", "");
|
||||
await expect(dev.getByRole("tab", { name: "调查测试", exact: true })).toHaveAttribute("aria-selected", "true");
|
||||
await expect(dev.getByRole("region", { name: "调查联动", exact: true })).toBeVisible();
|
||||
await dev.getByRole("tab", { name: "地图调试", exact: true }).click();
|
||||
const sections = dev.getByRole("tabpanel", { name: "地图调试", exact: true }).locator("details");
|
||||
await expect(sections).toHaveCount(4);
|
||||
await expect(sections.filter({ hasText: "要素定位" })).toHaveAttribute("open", "");
|
||||
await expect(sections.filter({ hasText: "水流调试" })).not.toHaveAttribute("open", "");
|
||||
await testInfo.attach("dev-panel-default", { body: await page.screenshot({ path: testInfo.outputPath("dev-panel-default.png") }), contentType: "image/png" });
|
||||
@@ -238,6 +243,9 @@ test("调查进度替换整图,跟随只展示新指令并在关闭时清理
|
||||
await expect(page.locator('[data-investigation-chart="pressure"]')).toHaveCount(0);
|
||||
await emit({ action: "open_chart", ...chart });
|
||||
await expect(page.locator('[data-investigation-chart="pressure"]')).toBeVisible();
|
||||
const chartManager = page.getByRole("dialog", { name: "图表管理", exact: true });
|
||||
await expect(chartManager).toBeVisible();
|
||||
await chartManager.getByRole("button", { name: "关闭窗口", exact: true }).click();
|
||||
await emit({ action: "close_chart", chart_id: "pressure" });
|
||||
await expect(page.locator('[data-investigation-chart="pressure"]')).toHaveCount(0);
|
||||
const data = { type: "FeatureCollection", features: [
|
||||
@@ -263,6 +271,7 @@ test("调查进度替换整图,跟随只展示新指令并在关闭时清理
|
||||
await testInfo.attach("live-investigation-mobile", { body: await page.screenshot({ path: testInfo.outputPath("live-investigation-mobile.png") }), contentType: "image/png" });
|
||||
await page.setViewportSize({ width: 1440, height: 900 });
|
||||
await page.getByRole("button", { name: "切换地图 Dev Panel" }).click();
|
||||
await dev.getByRole("tab", { name: "调查测试", exact: true }).click();
|
||||
await follow.uncheck();
|
||||
await expect(follow).not.toBeChecked();
|
||||
await follow.focus();
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
import { expect, test, type Locator, type Page } from "@playwright/test";
|
||||
import { mockInvestigationPresentation } from "./support/mock-investigation-presentation";
|
||||
|
||||
test.use({ reducedMotion: "reduce" });
|
||||
|
||||
async function themeColor(element: Locator, token: string, property: string) {
|
||||
return cssColor(element, `var(${token})`, property);
|
||||
}
|
||||
|
||||
async function cssColor(element: Locator, color: string, property: string) {
|
||||
return element.evaluate((node, { color, property }) => {
|
||||
const probe = document.createElement("span");
|
||||
probe.style.setProperty(property, color);
|
||||
node.append(probe);
|
||||
const value = getComputedStyle(probe).getPropertyValue(property);
|
||||
probe.remove();
|
||||
return value;
|
||||
}, { color, property });
|
||||
}
|
||||
|
||||
async function expectOrdinaryInteraction(row: Locator, label?: string, description?: string) {
|
||||
await expect(row).toHaveCSS("background-color", await themeColor(row, "--surface-reading", "background-color"));
|
||||
await expect(row).toHaveCSS("border-top-color", await themeColor(row, "--surface-border", "border-top-color"));
|
||||
await expect(row).toHaveCSS("color", await themeColor(row, "--action-blue-hover", "color"));
|
||||
if (label) await expect(row.getByText(label, { exact: true })).toHaveCSS("color", await themeColor(row, "--action-blue-hover", "color"));
|
||||
if (await row.locator("svg").count()) await expect(row.locator("svg").first()).toHaveCSS("color", await themeColor(row, "--action-blue-hover", "color"));
|
||||
if (description) await expect(row.getByText(description, { exact: true })).toHaveCSS("color", await themeColor(row, "--ink-muted", "color"));
|
||||
}
|
||||
|
||||
async function appearance(row: Locator) {
|
||||
return row.evaluate((node) => {
|
||||
const style = getComputedStyle(node);
|
||||
return { background: style.backgroundColor, border: style.borderTopColor, color: style.color };
|
||||
});
|
||||
}
|
||||
|
||||
async function expectPreservedOnHover(page: Page, row: Locator, selected = false) {
|
||||
await page.mouse.move(1, 1);
|
||||
const before = await appearance(row);
|
||||
await row.scrollIntoViewIfNeeded();
|
||||
const bounds = (await row.boundingBox())!;
|
||||
await page.mouse.move(bounds.x + bounds.width / 2, bounds.y + bounds.height / 2);
|
||||
if (selected) {
|
||||
await expect(row).toHaveCSS("background-color", before.background);
|
||||
await expect(row).toHaveCSS("color", before.color);
|
||||
await expect(row).toHaveCSS("border-top-color", await themeColor(row, "--action-border-hover", "border-top-color"));
|
||||
} else {
|
||||
await expect.poll(() => appearance(row)).toEqual(before);
|
||||
}
|
||||
}
|
||||
|
||||
async function expectContained(row: Locator, width: number) {
|
||||
const bounds = (await row.boundingBox())!;
|
||||
expect(bounds.x).toBeGreaterThanOrEqual(0);
|
||||
expect(bounds.x + bounds.width).toBeLessThanOrEqual(width);
|
||||
expect(await row.evaluate((node) => node.scrollWidth - node.clientWidth)).toBeLessThanOrEqual(1);
|
||||
}
|
||||
|
||||
for (const width of [1440, 375]) {
|
||||
test(`map options share hover and keyboard colors while keeping selected and disabled states at ${width}px`, async ({ page }, testInfo) => {
|
||||
test.setTimeout(90_000);
|
||||
await page.setViewportSize({ width, height: 900 });
|
||||
const mock = await mockInvestigationPresentation(page, []);
|
||||
// Enable an alternate basemap with a synthetic token; every tile stays local.
|
||||
await page.route("**/runtime-config.js", (route) => route.fulfill({ contentType: "application/javascript", body: `globalThis.__TJWATER_CONFIG__=${JSON.stringify({
|
||||
TJWATER_AUTH_MODE: "disabled", TJWATER_SERVER_API_BASE_URL: "https://tjwater-api.invalid",
|
||||
TJWATER_AGENT_URL: "http://127.0.0.1:8787", TJWATER_ENABLE_DEV_PANEL: "true",
|
||||
TJWATER_MAP_URL: "https://workbench-map.invalid/geoserver", TJWATER_GEOSERVER_WORKSPACE: "tjwater_next",
|
||||
TJWATER_MAPBOX_ACCESS_TOKEN: "", TJWATER_TIANDITU_TOKEN: "synthetic-browser-test", TJWATER_ENABLE_MSW: "false"
|
||||
})};` }));
|
||||
await page.route("https://*.tianditu.gov.cn/**", (route) => route.fulfill({ contentType: "image/png", body: Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGNgYGBgAAAABQABpfZFQAAAAABJRU5ErkJggg==", "base64") }));
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => Boolean(window.__waterNetworkMap?.getLayer("scada-pressure-hit")));
|
||||
|
||||
await page.getByRole("button", { name: "更多:更多地图操作", exact: true }).click();
|
||||
const more = page.getByRole("region", { name: "更多", exact: true });
|
||||
const refresh = more.getByRole("button", { name: /刷新瓦片/ });
|
||||
const exportView = more.getByRole("button", { name: /当前分辨率/ });
|
||||
const restingRefresh = await appearance(refresh);
|
||||
const restingSibling = await appearance(exportView);
|
||||
await refresh.hover();
|
||||
await expectOrdinaryInteraction(refresh, "刷新瓦片", "请求地图重新渲染");
|
||||
expect(await appearance(exportView)).toEqual(restingSibling);
|
||||
await exportView.hover();
|
||||
await expect.poll(() => appearance(refresh)).toEqual(restingRefresh);
|
||||
await expectOrdinaryInteraction(exportView, "当前分辨率", "按当前地图画布尺寸导出");
|
||||
await refresh.hover();
|
||||
await expectContained(more, width);
|
||||
await testInfo.attach(`more-hover-${width}`, { body: await more.screenshot({ path: testInfo.outputPath(`more-hover-${width}.png`) }), contentType: "image/png" });
|
||||
|
||||
await page.getByRole("button", { name: "测量:距离与范围测量", exact: true }).click();
|
||||
const measure = page.getByRole("region", { name: "测量", exact: true });
|
||||
const distance = measure.getByRole("button", { name: "距离", exact: true });
|
||||
const area = measure.getByRole("button", { name: "面积", exact: true });
|
||||
await expect(distance).toHaveAttribute("aria-pressed", "true");
|
||||
await expectPreservedOnHover(page, distance, true);
|
||||
await area.hover();
|
||||
await expectOrdinaryInteraction(area, "面积");
|
||||
const meters = measure.getByRole("button", { name: "m", exact: true });
|
||||
const kilometers = measure.getByRole("button", { name: "km", exact: true });
|
||||
await expectPreservedOnHover(page, kilometers, true);
|
||||
await meters.hover();
|
||||
await expectOrdinaryInteraction(meters);
|
||||
const clear = measure.getByRole("button", { name: "清除测量", exact: true });
|
||||
await expect(clear).toBeDisabled();
|
||||
await expectPreservedOnHover(page, clear);
|
||||
const start = measure.getByRole("button", { name: /开始测量/ });
|
||||
await start.hover();
|
||||
await expect(start).toHaveCSS("background-color", await themeColor(start, "--action-blue-hover", "background-color"));
|
||||
await expect(start).toHaveCSS("color", "rgb(255, 255, 255)");
|
||||
await expect(start.getByText("开始测量", { exact: true })).toHaveCSS("color", "rgb(255, 255, 255)");
|
||||
await expect(start.locator("svg").first()).toHaveCSS("color", "rgb(255, 255, 255)");
|
||||
await expect(start.getByText("在地图中连续取点", { exact: true })).toHaveCSS("color", await cssColor(start, "color-mix(in oklab, white 85%, transparent)", "color"));
|
||||
await start.click();
|
||||
for (const close of await page.getByRole("button", { name: "关闭通知", exact: true }).all()) {
|
||||
if (await close.isVisible()) await close.click();
|
||||
}
|
||||
const canvas = page.locator("canvas.maplibregl-canvas");
|
||||
await canvas.click({ position: { x: 100, y: 175 }, timeout: 10_000 });
|
||||
await canvas.click({ position: { x: 150, y: 205 }, timeout: 10_000 });
|
||||
const copy = measure.getByRole("button", { name: "复制结果", exact: true });
|
||||
await expect(copy).toBeEnabled();
|
||||
await copy.hover();
|
||||
await expectOrdinaryInteraction(copy, "复制结果");
|
||||
await expect(clear).toBeEnabled();
|
||||
await clear.hover();
|
||||
await expect(clear).toHaveCSS("background-color", await themeColor(clear, "--status-danger-soft", "background-color"));
|
||||
await expect(clear).toHaveCSS("border-top-color", await themeColor(clear, "--status-danger-border", "border-top-color"));
|
||||
await expect(clear).toHaveCSS("color", await themeColor(clear, "--status-danger-foreground", "color"));
|
||||
await expect(clear.locator("svg").first()).toHaveCSS("color", await themeColor(clear, "--status-danger-foreground", "color"));
|
||||
await expectContained(measure, width);
|
||||
await testInfo.attach(`measure-hover-${width}`, { body: await measure.screenshot({ path: testInfo.outputPath(`measure-hover-${width}.png`) }), contentType: "image/png" });
|
||||
|
||||
await page.getByRole("button", { name: "图层:管理地图图层", exact: true }).click();
|
||||
const layers = page.getByRole("region", { name: "图层", exact: true });
|
||||
const noBasemap = layers.getByRole("button", { name: "无底图", exact: true });
|
||||
await noBasemap.scrollIntoViewIfNeeded();
|
||||
await noBasemap.hover();
|
||||
await expectOrdinaryInteraction(noBasemap, "无底图");
|
||||
const vector = layers.getByRole("button", { name: "天地图矢量", exact: true });
|
||||
await expect(vector).toHaveAttribute("aria-pressed", "true");
|
||||
await expectPreservedOnHover(page, vector, true);
|
||||
const light = layers.getByRole("button", { name: "浅色", exact: true });
|
||||
await expect(light).toBeDisabled();
|
||||
await expectPreservedOnHover(page, light);
|
||||
await expectContained(layers, width);
|
||||
await testInfo.attach(`basemap-hover-${width}`, { body: await layers.screenshot({ path: testInfo.outputPath(`basemap-hover-${width}.png`) }), contentType: "image/png" });
|
||||
await page.getByRole("button", { name: "图层:管理地图图层", exact: true }).click();
|
||||
|
||||
const header = page.locator(".workbench-top-bar");
|
||||
const user = header.getByRole("button", { name: "打开用户菜单", exact: true });
|
||||
await user.click();
|
||||
const status = page.getByRole("menuitem", { name: /查看运行状态/ });
|
||||
await status.hover();
|
||||
await expectOrdinaryInteraction(status, "查看运行状态", "检查数据源与运行健康度");
|
||||
const logout = page.getByRole("menuitem", { name: "退出登录", exact: true });
|
||||
await expect(logout).toHaveAttribute("aria-disabled", "true");
|
||||
await expectPreservedOnHover(page, logout);
|
||||
const userMenu = page.getByRole("menu");
|
||||
await expectContained(userMenu, width);
|
||||
await page.keyboard.press("Escape");
|
||||
await expect(userMenu).toHaveCount(0);
|
||||
await page.mouse.move(1, 1);
|
||||
await user.focus();
|
||||
await expect(user).toBeFocused();
|
||||
await user.press("ArrowDown");
|
||||
await expect(userMenu).toBeVisible();
|
||||
const menuItems = userMenu.getByRole("menuitem");
|
||||
for (let index = 0; index < await menuItems.count(); index++) {
|
||||
await expect(menuItems.nth(index)).toBeFocused();
|
||||
if (await status.evaluate((node) => document.activeElement === node)) break;
|
||||
await page.keyboard.press("ArrowDown");
|
||||
}
|
||||
await expect(status).toBeFocused();
|
||||
await expect(status).toHaveAttribute("data-highlighted", "");
|
||||
await expectOrdinaryInteraction(status, "查看运行状态", "检查数据源与运行健康度");
|
||||
await expect(status).toHaveCSS("outline-width", "1px");
|
||||
await expect(status).toHaveCSS("outline-offset", "-1px");
|
||||
await testInfo.attach(`header-keyboard-${width}`, { body: await userMenu.screenshot({ path: testInfo.outputPath(`header-keyboard-${width}.png`) }), contentType: "image/png" });
|
||||
await page.keyboard.press("Escape");
|
||||
await expect(userMenu).toHaveCount(0);
|
||||
|
||||
// Dev tools are a desktop entry; verify the resulting window menu at the target width.
|
||||
if (width < 800) await page.setViewportSize({ width: 1440, height: 900 });
|
||||
await page.getByRole("button", { name: "切换地图 Dev Panel", exact: true }).click();
|
||||
const tools = page.getByTestId("investigation-dev-tools");
|
||||
await tools.getByLabel("调查展示 JSONL", { exact: true }).fill(JSON.stringify({ record_type: "presentation", command: {
|
||||
action: "open_chart", chart_id: "hover-chart", evidence_refs: ["dev:synthetic"], chart: {
|
||||
title: "交互验证压力趋势", chart_type: "line", x_data: ["08:00", "08:15"], series: [{ name: "压力", data: [30, 28] }]
|
||||
}
|
||||
} }));
|
||||
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.getByLabel("本地预览展示", { exact: true }).getByRole("button", { name: "打开图表管理", exact: true }).click();
|
||||
await expect(page.getByRole("dialog", { name: "图表管理", exact: true })).toBeVisible();
|
||||
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: /定位窗口 .*图表管理/ });
|
||||
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"));
|
||||
await expect(currentWindow).toHaveCSS("color", await themeColor(currentWindow, "--action-blue-hover", "color"));
|
||||
const closeWindows = windowMenu.getByRole("menuitem", { name: "关闭全部窗口", exact: true });
|
||||
await closeWindows.hover();
|
||||
await expectOrdinaryInteraction(closeWindows);
|
||||
await expectContained(windowMenu, width);
|
||||
await expectContained(header, width);
|
||||
expect(await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth)).toBeLessThanOrEqual(1);
|
||||
await testInfo.attach(`window-hover-${width}`, { body: await windowMenu.screenshot({ path: testInfo.outputPath(`window-hover-${width}.png`) }), contentType: "image/png" });
|
||||
expect(mock.mutations).toEqual([]);
|
||||
});
|
||||
}
|
||||
|
||||
test.describe("touch map options", () => {
|
||||
test.use({ hasTouch: true, isMobile: true, viewport: { width: 375, height: 812 } });
|
||||
test("touch taps keep ordinary rows free of sticky hover colors", async ({ page }, testInfo) => {
|
||||
await mockInvestigationPresentation(page, []);
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
expect(await page.evaluate(() => matchMedia("(hover: hover)").matches)).toBe(false);
|
||||
await page.getByRole("button", { name: "更多:更多地图操作", exact: true }).tap();
|
||||
const more = page.getByRole("region", { name: "更多", exact: true });
|
||||
const refresh = more.getByRole("button", { name: /刷新瓦片/ });
|
||||
await refresh.tap({ trial: true });
|
||||
const before = await appearance(refresh);
|
||||
const bounds = await refresh.boundingBox();
|
||||
await refresh.tap();
|
||||
await expect.poll(() => appearance(refresh)).toEqual(before);
|
||||
expect(await refresh.boundingBox()).toEqual(bounds);
|
||||
await expect(refresh).not.toHaveCSS("color", await themeColor(refresh, "--action-blue-hover", "color"));
|
||||
await expectContained(more, 375);
|
||||
await testInfo.attach("touch-options-375", { body: await more.screenshot({ path: testInfo.outputPath("touch-options-375.png") }), contentType: "image/png" });
|
||||
});
|
||||
});
|
||||
@@ -53,11 +53,14 @@ for (const width of [1440, 375, 320]) {
|
||||
await expect(ui.locator(`svg path[fill="${color}"]`)).toHaveCount(3);
|
||||
}
|
||||
const axisValues = () => ui.locator("svg text").allTextContents().then((labels) => labels.filter((label) => /^-?\d+(\.\d+)?$/.test(label)).map(Number));
|
||||
expect((await axisValues()).every((value) => value > 0)).toBe(true);
|
||||
const initialTicks = await axisValues();
|
||||
expect(initialTicks.length).toBeGreaterThanOrEqual(2);
|
||||
expect(initialTicks.every((value) => value > 0 && Number.isInteger(value))).toBe(true);
|
||||
await page.evaluate(() => document.fonts.ready);
|
||||
await testInfo.attach(`scada-week-${width}`, { body: await ui.screenshot({ path: testInfo.outputPath(`scada-week-${width}.png`) }), contentType: "image/png" });
|
||||
await ui.locator('svg path[fill="#059669"]').last().hover({ position: { x: 4, y: 6 } });
|
||||
await expect(ui).toContainText("采样时间 2026-06-13 12:00:00+08:00");
|
||||
// Hover the topmost historical day so neighboring scatter symbols cannot intercept.
|
||||
await ui.locator('svg path[fill="#a16207"]').last().hover({ position: { x: 4, y: 4 } });
|
||||
await expect(ui).toContainText("采样时间 2026-06-07 12:00:00+08:00");
|
||||
await page.evaluate(() => document.fonts.ready);
|
||||
await testInfo.attach(`scada-week-tooltip-${width}`, { body: await ui.screenshot({ path: testInfo.outputPath(`scada-week-tooltip-${width}.png`) }), contentType: "image/png" });
|
||||
const plot = ui.getByRole("group", { name: /按回车查看数据点/ });
|
||||
@@ -91,22 +94,21 @@ for (const width of [1440, 375, 320]) {
|
||||
const restored = (await floating.boundingBox())!;
|
||||
await floating.getByRole("button", { name: "最大化窗口", exact: true }).click();
|
||||
await expect.poll(async () => (await floating.boundingBox())!.width).toBe(width - 16);
|
||||
await expect.poll(async () => (await floating.boundingBox())!.height).toBe((width < 640 ? 640 : 1000) - 16);
|
||||
await expect.poll(async () => (await floating.boundingBox())!.height).toBe((width < 640 ? 640 : 1000) - 72);
|
||||
await expect.poll(async () => Math.abs(Number(await drawing.locator("svg").getAttribute("width")) - (await drawing.boundingBox())!.width)).toBeLessThan(1);
|
||||
await expect.poll(async () => Math.abs(Number(await drawing.locator("svg").getAttribute("height")) - (await drawing.boundingBox())!.height)).toBeLessThan(1);
|
||||
await expect(drawing.locator(".echarts-for-react")).toHaveAttribute("_echarts_instance_", instance!);
|
||||
const content = floating.locator("[data-floating-window-content]");
|
||||
expect(await content.evaluate((element) => element.scrollWidth - element.clientWidth)).toBeLessThanOrEqual(1);
|
||||
const plotBox = (await drawing.boundingBox())!;
|
||||
const sidebar = (await floating.locator("aside").boundingBox())!;
|
||||
if (width < 640) expect(sidebar.y).toBeGreaterThanOrEqual(plotBox.y + plotBox.height);
|
||||
else expect(sidebar.x).toBeGreaterThanOrEqual(plotBox.x + plotBox.width);
|
||||
expect(plotBox.height).toBeGreaterThanOrEqual(220);
|
||||
const legend = (await floating.getByLabel("图表图例").boundingBox())!;
|
||||
expect(legend.y).toBeGreaterThanOrEqual(plotBox.y + plotBox.height);
|
||||
expect(plotBox.height).toBeGreaterThanOrEqual(180);
|
||||
await floating.getByLabel("显示的数据序列").getByRole("button", { name: "6月12日", exact: true }).click();
|
||||
await expect(ui.getByLabel("显示的数据序列").getByRole("button", { name: "6月12日", exact: true })).toHaveAttribute("aria-pressed", "false");
|
||||
await floating.getByLabel("显示的数据序列").getByRole("button", { name: "6月12日", exact: true }).click();
|
||||
await floating.locator('svg path[fill="#059669"]').last().hover({ position: { x: 4, y: 6 } });
|
||||
await expect(floating).toContainText("采样时间 2026-06-13 12:00:00+08:00");
|
||||
await floating.locator('svg path[fill="#a16207"]').last().hover({ position: { x: 4, y: 4 } });
|
||||
await expect(floating).toContainText("采样时间 2026-06-07 12:00:00+08:00");
|
||||
await floating.locator(".floating-window-heading").hover();
|
||||
await page.evaluate(() => document.fonts.ready);
|
||||
await testInfo.attach(`scada-week-fullscreen-${width}`, { body: await floating.screenshot({ path: testInfo.outputPath(`scada-week-fullscreen-${width}.png`) }), contentType: "image/png" });
|
||||
|
||||
@@ -132,10 +132,15 @@ export async function mockInvestigationPresentation(page: Page, tasks: Operation
|
||||
|
||||
export async function selectInvestigation(page: Page, title: string) {
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
await page.getByRole("button", { name: "工况任务", exact: true }).click();
|
||||
await page.getByRole("button", { name: /^(?:打开)?工况任务$/ }).click();
|
||||
const panel = page.getByRole("region", { name: "工况任务", exact: true });
|
||||
const expand = panel.getByRole("button", { name: "展开工况任务" });
|
||||
if (await expand.isVisible()) await expand.click();
|
||||
await panel.getByRole("button", { name: /调查任务/ }).click();
|
||||
await panel.getByText(title, { exact: true }).first().click();
|
||||
const task = panel.getByRole("button").filter({ hasText: title }).first();
|
||||
if (!await task.isVisible()) {
|
||||
const collapse = panel.getByRole("button", { name: "收起工况任务", exact: true });
|
||||
if (await collapse.isVisible()) await collapse.click();
|
||||
}
|
||||
await task.click();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { basename, resolve } from "node:path";
|
||||
import { mockInvestigationPresentation } from "./support/mock-investigation-presentation";
|
||||
|
||||
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.setTimeout(60_000);
|
||||
await page.setViewportSize({ width: 1440, height: 900 });
|
||||
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"
|
||||
}));
|
||||
const mock = await mockInvestigationPresentation(page, []);
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
const header = page.locator(".workbench-top-bar");
|
||||
const navigator = header.getByRole("button", { name: /窗口管理,当前/ });
|
||||
await expect(navigator).toHaveCount(0);
|
||||
await page.getByRole("button", { name: "切换地图 Dev Panel" }).click();
|
||||
const tools = page.getByTestId("investigation-dev-tools");
|
||||
await tools.getByLabel("调查展示 JSONL", { exact: true }).fill([
|
||||
{ record_type: "presentation", command: { action: "open_chart", chart_id: "window-pressure", evidence_refs: ["dev:synthetic"], chart: {
|
||||
title: "窗口定位压力趋势", chart_type: "line", x_data: ["07:45", "08:00"], series: [{ name: "压力(m)", data: [32, 28] }]
|
||||
} } },
|
||||
{ record_type: "ui_block", block: { version: "answer-ui@1", block_id: "window-table", fallback_text: "窗口定位表格", spec: { root: "table", elements: {
|
||||
table: { type: "Table", props: { title: "窗口定位核查表", columns: ["测点", "压力"], rows: [["P1", "28"], ["P2", "32"]] }, children: [] }
|
||||
} } } }
|
||||
].map((record) => JSON.stringify(record)).join("\n"));
|
||||
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("本地预览完成");
|
||||
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();
|
||||
const manager = page.getByRole("dialog", { name: "图表管理", exact: true });
|
||||
await manager.getByRole("button", { name: "放大查看 窗口定位压力趋势", exact: true }).click();
|
||||
const child = page.getByRole("dialog", { name: "窗口定位压力趋势", exact: true });
|
||||
await expect(child).toBeVisible();
|
||||
await child.getByRole("tab", { name: "数据", exact: true }).click();
|
||||
const search = table.getByRole("textbox", { name: /搜索窗口定位核查表/ });
|
||||
await table.locator(".floating-window-heading").focus();
|
||||
await search.fill("P2");
|
||||
await page.setViewportSize({ width, height: 900 });
|
||||
await expect(navigator).toHaveAttribute("aria-label", "窗口管理,当前 3 个窗口");
|
||||
if (width === 1440) {
|
||||
const alignment = await navigator.evaluate((node) => {
|
||||
const label = node.children[1].getBoundingClientRect();
|
||||
const count = node.children[2].getBoundingClientRect();
|
||||
return { labelCenter: label.y + label.height / 2, countCenter: count.y + count.height / 2 };
|
||||
});
|
||||
expect(Math.abs(alignment.labelCenter - alignment.countCenter)).toBeLessThanOrEqual(1);
|
||||
}
|
||||
await expect.poll(() => header.evaluate((node) => node.scrollWidth - node.clientWidth)).toBeLessThanOrEqual(1);
|
||||
const tableBounds = await table.boundingBox();
|
||||
await navigator.click();
|
||||
const menu = page.getByRole("menu", { name: "已打开的窗口", exact: true });
|
||||
await expect(menu.getByRole("menuitem")).toHaveCount(4);
|
||||
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();
|
||||
await navigator.focus();
|
||||
await page.keyboard.press("Enter");
|
||||
await menu.getByRole("menuitem", { name: /定位窗口 .*图表管理/ }).focus();
|
||||
await page.keyboard.press("Enter");
|
||||
await expect(manager.locator(".floating-window-heading")).toBeFocused();
|
||||
expect(await manager.boundingBox()).toEqual(managerBounds);
|
||||
await navigator.click();
|
||||
await header.getByRole("button", { name: "打开用户菜单", exact: true }).click();
|
||||
await expect(menu).toHaveCount(0);
|
||||
await expect(page.getByRole("menuitem", { name: /查看运行状态/ })).toBeVisible();
|
||||
await page.keyboard.press("Escape");
|
||||
await child.locator(".floating-window-heading").focus();
|
||||
await child.getByRole("button", { name: "最大化窗口", exact: true }).click();
|
||||
const maximized = (await child.boundingBox())!;
|
||||
expect(maximized.y).toBe(64);
|
||||
expect(maximized.height).toBe(828);
|
||||
await expect(child.getByRole("tab", { name: "数据", exact: true })).toHaveAttribute("aria-selected", "true");
|
||||
await navigator.click();
|
||||
await expect(menu).toBeVisible();
|
||||
expect(await menu.evaluate((node) => node.scrollWidth - node.clientWidth)).toBeLessThanOrEqual(1);
|
||||
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(navigator).toHaveCount(0);
|
||||
await expect(header.getByRole("button", { name: "打开用户菜单", exact: true })).toBeFocused();
|
||||
expect(mock.mutations).toEqual([]);
|
||||
});
|
||||
}
|
||||
+992
@@ -0,0 +1,992 @@
|
||||
[
|
||||
{
|
||||
"name": "zero and sparse historical data",
|
||||
"accepted": true,
|
||||
"chart": {
|
||||
"title": "P1 合成压力七日对照(m)",
|
||||
"chart_type": "time_comparison",
|
||||
"x_axis_name": "时间",
|
||||
"y_axis_name": "压力(m)",
|
||||
"x_data": [
|
||||
"08:00",
|
||||
"08:15",
|
||||
"08:30"
|
||||
],
|
||||
"series": [
|
||||
{
|
||||
"name": "P1 当前 2026-07-16",
|
||||
"type": "line",
|
||||
"data": [
|
||||
0,
|
||||
28,
|
||||
30
|
||||
],
|
||||
"sample_times": [
|
||||
"2026-07-16T08:00:00+08:00",
|
||||
"2026-07-16T08:15:00+08:00",
|
||||
"2026-07-16T08:30:00+08:00"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "P1 2026-07-09",
|
||||
"type": "scatter",
|
||||
"data": [
|
||||
null,
|
||||
32,
|
||||
33
|
||||
],
|
||||
"sample_times": [
|
||||
null,
|
||||
"2026-07-09T08:15:00+08:00",
|
||||
"2026-07-09T08:30:00+08:00"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "old mixed charts are read only",
|
||||
"accepted": false,
|
||||
"chart": {
|
||||
"title": "P1 合成压力七日对照(m)",
|
||||
"chart_type": "line",
|
||||
"x_axis_name": "时间",
|
||||
"y_axis_name": "压力(m)",
|
||||
"x_data": [
|
||||
"08:00",
|
||||
"08:15",
|
||||
"08:30"
|
||||
],
|
||||
"series": [
|
||||
{
|
||||
"name": "P1 当前 2026-07-16",
|
||||
"type": "line",
|
||||
"data": [
|
||||
0,
|
||||
28,
|
||||
30
|
||||
],
|
||||
"sample_times": [
|
||||
"2026-07-16T08:00:00+08:00",
|
||||
"2026-07-16T08:15:00+08:00",
|
||||
"2026-07-16T08:30:00+08:00"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "P1 2026-07-09",
|
||||
"type": "scatter",
|
||||
"data": [
|
||||
null,
|
||||
32,
|
||||
33
|
||||
],
|
||||
"sample_times": [
|
||||
null,
|
||||
"2026-07-09T08:15:00+08:00",
|
||||
"2026-07-09T08:30:00+08:00"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "missing history timestamps",
|
||||
"accepted": false,
|
||||
"chart": {
|
||||
"title": "P1 合成压力七日对照(m)",
|
||||
"chart_type": "time_comparison",
|
||||
"x_axis_name": "时间",
|
||||
"y_axis_name": "压力(m)",
|
||||
"x_data": [
|
||||
"08:00",
|
||||
"08:15",
|
||||
"08:30"
|
||||
],
|
||||
"series": [
|
||||
{
|
||||
"name": "P1 当前 2026-07-16",
|
||||
"type": "line",
|
||||
"data": [
|
||||
0,
|
||||
28,
|
||||
30
|
||||
],
|
||||
"sample_times": [
|
||||
"2026-07-16T08:00:00+08:00",
|
||||
"2026-07-16T08:15:00+08:00",
|
||||
"2026-07-16T08:30:00+08:00"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "P1 2026-07-09",
|
||||
"type": "scatter",
|
||||
"data": [
|
||||
null,
|
||||
32,
|
||||
33
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "missing observed timestamp",
|
||||
"accepted": false,
|
||||
"chart": {
|
||||
"title": "P1 合成压力七日对照(m)",
|
||||
"chart_type": "time_comparison",
|
||||
"x_axis_name": "时间",
|
||||
"y_axis_name": "压力(m)",
|
||||
"x_data": [
|
||||
"08:00",
|
||||
"08:15",
|
||||
"08:30"
|
||||
],
|
||||
"series": [
|
||||
{
|
||||
"name": "P1 当前 2026-07-16",
|
||||
"type": "line",
|
||||
"data": [
|
||||
0,
|
||||
28,
|
||||
30
|
||||
],
|
||||
"sample_times": [
|
||||
"2026-07-16T08:00:00+08:00",
|
||||
"2026-07-16T08:15:00+08:00",
|
||||
"2026-07-16T08:30:00+08:00"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "P1 2026-07-09",
|
||||
"type": "scatter",
|
||||
"data": [
|
||||
null,
|
||||
32,
|
||||
33
|
||||
],
|
||||
"sample_times": [
|
||||
null,
|
||||
null,
|
||||
"2026-07-09T08:30:00+08:00"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "timestamp on a missing value",
|
||||
"accepted": false,
|
||||
"chart": {
|
||||
"title": "P1 合成压力七日对照(m)",
|
||||
"chart_type": "time_comparison",
|
||||
"x_axis_name": "时间",
|
||||
"y_axis_name": "压力(m)",
|
||||
"x_data": [
|
||||
"08:00",
|
||||
"08:15",
|
||||
"08:30"
|
||||
],
|
||||
"series": [
|
||||
{
|
||||
"name": "P1 当前 2026-07-16",
|
||||
"type": "line",
|
||||
"data": [
|
||||
0,
|
||||
28,
|
||||
30
|
||||
],
|
||||
"sample_times": [
|
||||
"2026-07-16T08:00:00+08:00",
|
||||
"2026-07-16T08:15:00+08:00",
|
||||
"2026-07-16T08:30:00+08:00"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "P1 2026-07-09",
|
||||
"type": "scatter",
|
||||
"data": [
|
||||
null,
|
||||
32,
|
||||
33
|
||||
],
|
||||
"sample_times": [
|
||||
"2026-07-09T08:00:00+08:00",
|
||||
"2026-07-09T08:15:00+08:00",
|
||||
"2026-07-09T08:30:00+08:00"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "wrong projected slot",
|
||||
"accepted": false,
|
||||
"chart": {
|
||||
"title": "P1 合成压力七日对照(m)",
|
||||
"chart_type": "time_comparison",
|
||||
"x_axis_name": "时间",
|
||||
"y_axis_name": "压力(m)",
|
||||
"x_data": [
|
||||
"08:00",
|
||||
"08:15",
|
||||
"08:30"
|
||||
],
|
||||
"series": [
|
||||
{
|
||||
"name": "P1 当前 2026-07-16",
|
||||
"type": "line",
|
||||
"data": [
|
||||
0,
|
||||
28,
|
||||
30
|
||||
],
|
||||
"sample_times": [
|
||||
"2026-07-16T08:00:00+08:00",
|
||||
"2026-07-16T08:15:00+08:00",
|
||||
"2026-07-16T08:30:00+08:00"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "P1 2026-07-09",
|
||||
"type": "scatter",
|
||||
"data": [
|
||||
null,
|
||||
32,
|
||||
33
|
||||
],
|
||||
"sample_times": [
|
||||
null,
|
||||
"2026-07-09T08:30:00+08:00",
|
||||
"2026-07-09T08:30:00+08:00"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "mixed historical dates",
|
||||
"accepted": false,
|
||||
"chart": {
|
||||
"title": "P1 合成压力七日对照(m)",
|
||||
"chart_type": "time_comparison",
|
||||
"x_axis_name": "时间",
|
||||
"y_axis_name": "压力(m)",
|
||||
"x_data": [
|
||||
"08:00",
|
||||
"08:15",
|
||||
"08:30"
|
||||
],
|
||||
"series": [
|
||||
{
|
||||
"name": "P1 当前 2026-07-16",
|
||||
"type": "line",
|
||||
"data": [
|
||||
0,
|
||||
28,
|
||||
30
|
||||
],
|
||||
"sample_times": [
|
||||
"2026-07-16T08:00:00+08:00",
|
||||
"2026-07-16T08:15:00+08:00",
|
||||
"2026-07-16T08:30:00+08:00"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "P1 2026-07-09",
|
||||
"type": "scatter",
|
||||
"data": [
|
||||
null,
|
||||
32,
|
||||
33
|
||||
],
|
||||
"sample_times": [
|
||||
null,
|
||||
"2026-07-10T08:15:00+08:00",
|
||||
"2026-07-09T08:30:00+08:00"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "same day history",
|
||||
"accepted": false,
|
||||
"chart": {
|
||||
"title": "P1 合成压力七日对照(m)",
|
||||
"chart_type": "time_comparison",
|
||||
"x_axis_name": "时间",
|
||||
"y_axis_name": "压力(m)",
|
||||
"x_data": [
|
||||
"08:00",
|
||||
"08:15",
|
||||
"08:30"
|
||||
],
|
||||
"series": [
|
||||
{
|
||||
"name": "P1 当前 2026-07-16",
|
||||
"type": "line",
|
||||
"data": [
|
||||
0,
|
||||
28,
|
||||
30
|
||||
],
|
||||
"sample_times": [
|
||||
"2026-07-16T08:00:00+08:00",
|
||||
"2026-07-16T08:15:00+08:00",
|
||||
"2026-07-16T08:30:00+08:00"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "P1 2026-07-09",
|
||||
"type": "scatter",
|
||||
"data": [
|
||||
null,
|
||||
32,
|
||||
33
|
||||
],
|
||||
"sample_times": [
|
||||
null,
|
||||
"2026-07-16T08:15:00+08:00",
|
||||
"2026-07-16T08:30:00+08:00"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "future history",
|
||||
"accepted": false,
|
||||
"chart": {
|
||||
"title": "P1 合成压力七日对照(m)",
|
||||
"chart_type": "time_comparison",
|
||||
"x_axis_name": "时间",
|
||||
"y_axis_name": "压力(m)",
|
||||
"x_data": [
|
||||
"08:00",
|
||||
"08:15",
|
||||
"08:30"
|
||||
],
|
||||
"series": [
|
||||
{
|
||||
"name": "P1 当前 2026-07-16",
|
||||
"type": "line",
|
||||
"data": [
|
||||
0,
|
||||
28,
|
||||
30
|
||||
],
|
||||
"sample_times": [
|
||||
"2026-07-16T08:00:00+08:00",
|
||||
"2026-07-16T08:15:00+08:00",
|
||||
"2026-07-16T08:30:00+08:00"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "P1 2026-07-09",
|
||||
"type": "scatter",
|
||||
"data": [
|
||||
null,
|
||||
32,
|
||||
33
|
||||
],
|
||||
"sample_times": [
|
||||
null,
|
||||
"2026-07-17T08:15:00+08:00",
|
||||
"2026-07-17T08:30:00+08:00"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "duplicate slots",
|
||||
"accepted": false,
|
||||
"chart": {
|
||||
"title": "P1 合成压力七日对照(m)",
|
||||
"chart_type": "time_comparison",
|
||||
"x_axis_name": "时间",
|
||||
"y_axis_name": "压力(m)",
|
||||
"x_data": [
|
||||
"08:00",
|
||||
"08:00",
|
||||
"08:30"
|
||||
],
|
||||
"series": [
|
||||
{
|
||||
"name": "P1 当前 2026-07-16",
|
||||
"type": "line",
|
||||
"data": [
|
||||
0,
|
||||
28,
|
||||
30
|
||||
],
|
||||
"sample_times": [
|
||||
"2026-07-16T08:00:00+08:00",
|
||||
"2026-07-16T08:15:00+08:00",
|
||||
"2026-07-16T08:30:00+08:00"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "P1 2026-07-09",
|
||||
"type": "scatter",
|
||||
"data": [
|
||||
null,
|
||||
32,
|
||||
33
|
||||
],
|
||||
"sample_times": [
|
||||
null,
|
||||
"2026-07-09T08:15:00+08:00",
|
||||
"2026-07-09T08:30:00+08:00"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "date appended axis",
|
||||
"accepted": false,
|
||||
"chart": {
|
||||
"title": "P1 合成压力七日对照(m)",
|
||||
"chart_type": "time_comparison",
|
||||
"x_axis_name": "时间",
|
||||
"y_axis_name": "压力(m)",
|
||||
"x_data": [
|
||||
"07-16 08:00",
|
||||
"07-16 08:15",
|
||||
"07-16 08:30"
|
||||
],
|
||||
"series": [
|
||||
{
|
||||
"name": "P1 当前 2026-07-16",
|
||||
"type": "line",
|
||||
"data": [
|
||||
0,
|
||||
28,
|
||||
30
|
||||
],
|
||||
"sample_times": [
|
||||
"2026-07-16T08:00:00+08:00",
|
||||
"2026-07-16T08:15:00+08:00",
|
||||
"2026-07-16T08:30:00+08:00"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "P1 2026-07-09",
|
||||
"type": "scatter",
|
||||
"data": [
|
||||
null,
|
||||
32,
|
||||
33
|
||||
],
|
||||
"sample_times": [
|
||||
null,
|
||||
"2026-07-09T08:15:00+08:00",
|
||||
"2026-07-09T08:30:00+08:00"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "inconsistent slot precision",
|
||||
"accepted": false,
|
||||
"chart": {
|
||||
"title": "P1 合成压力七日对照(m)",
|
||||
"chart_type": "time_comparison",
|
||||
"x_axis_name": "时间",
|
||||
"y_axis_name": "压力(m)",
|
||||
"x_data": [
|
||||
"08:00:00",
|
||||
"08:15",
|
||||
"08:30"
|
||||
],
|
||||
"series": [
|
||||
{
|
||||
"name": "P1 当前 2026-07-16",
|
||||
"type": "line",
|
||||
"data": [
|
||||
0,
|
||||
28,
|
||||
30
|
||||
],
|
||||
"sample_times": [
|
||||
"2026-07-16T08:00:00+08:00",
|
||||
"2026-07-16T08:15:00+08:00",
|
||||
"2026-07-16T08:30:00+08:00"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "P1 2026-07-09",
|
||||
"type": "scatter",
|
||||
"data": [
|
||||
null,
|
||||
32,
|
||||
33
|
||||
],
|
||||
"sample_times": [
|
||||
null,
|
||||
"2026-07-09T08:15:00+08:00",
|
||||
"2026-07-09T08:30:00+08:00"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "bars mixed with comparison",
|
||||
"accepted": false,
|
||||
"chart": {
|
||||
"title": "P1 合成压力七日对照(m)",
|
||||
"chart_type": "time_comparison",
|
||||
"x_axis_name": "时间",
|
||||
"y_axis_name": "压力(m)",
|
||||
"x_data": [
|
||||
"08:00",
|
||||
"08:15",
|
||||
"08:30"
|
||||
],
|
||||
"series": [
|
||||
{
|
||||
"name": "P1 当前 2026-07-16",
|
||||
"type": "line",
|
||||
"data": [
|
||||
0,
|
||||
28,
|
||||
30
|
||||
],
|
||||
"sample_times": [
|
||||
"2026-07-16T08:00:00+08:00",
|
||||
"2026-07-16T08:15:00+08:00",
|
||||
"2026-07-16T08:30:00+08:00"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "P1 2026-07-09",
|
||||
"type": "bar",
|
||||
"data": [
|
||||
null,
|
||||
32,
|
||||
33
|
||||
],
|
||||
"sample_times": [
|
||||
null,
|
||||
"2026-07-09T08:15:00+08:00",
|
||||
"2026-07-09T08:30:00+08:00"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "no historical series",
|
||||
"accepted": false,
|
||||
"chart": {
|
||||
"title": "P1 合成压力七日对照(m)",
|
||||
"chart_type": "time_comparison",
|
||||
"x_axis_name": "时间",
|
||||
"y_axis_name": "压力(m)",
|
||||
"x_data": [
|
||||
"08:00",
|
||||
"08:15",
|
||||
"08:30"
|
||||
],
|
||||
"series": [
|
||||
{
|
||||
"name": "P1 当前 2026-07-16",
|
||||
"type": "line",
|
||||
"data": [
|
||||
0,
|
||||
28,
|
||||
30
|
||||
],
|
||||
"sample_times": [
|
||||
"2026-07-16T08:00:00+08:00",
|
||||
"2026-07-16T08:15:00+08:00",
|
||||
"2026-07-16T08:30:00+08:00"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "no current observations",
|
||||
"accepted": false,
|
||||
"chart": {
|
||||
"title": "P1 合成压力七日对照(m)",
|
||||
"chart_type": "time_comparison",
|
||||
"x_axis_name": "时间",
|
||||
"y_axis_name": "压力(m)",
|
||||
"x_data": [
|
||||
"08:00",
|
||||
"08:15",
|
||||
"08:30"
|
||||
],
|
||||
"series": [
|
||||
{
|
||||
"name": "P1 当前 2026-07-16",
|
||||
"type": "line",
|
||||
"data": [
|
||||
null,
|
||||
null,
|
||||
null
|
||||
],
|
||||
"sample_times": [
|
||||
null,
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "P1 2026-07-09",
|
||||
"type": "scatter",
|
||||
"data": [
|
||||
null,
|
||||
32,
|
||||
33
|
||||
],
|
||||
"sample_times": [
|
||||
null,
|
||||
"2026-07-09T08:15:00+08:00",
|
||||
"2026-07-09T08:30:00+08:00"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "missing samples with missing timestamps",
|
||||
"accepted": true,
|
||||
"chart": {
|
||||
"title": "P1 合成压力七日对照(m)",
|
||||
"chart_type": "time_comparison",
|
||||
"x_axis_name": "时间",
|
||||
"y_axis_name": "压力(m)",
|
||||
"x_data": [
|
||||
"08:00",
|
||||
"08:15",
|
||||
"08:30"
|
||||
],
|
||||
"series": [
|
||||
{
|
||||
"name": "P1 当前 2026-07-16",
|
||||
"type": "line",
|
||||
"data": [
|
||||
null,
|
||||
28,
|
||||
30
|
||||
],
|
||||
"sample_times": [
|
||||
null,
|
||||
"2026-07-16T08:15:00+08:00",
|
||||
"2026-07-16T08:30:00+08:00"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "P1 2026-07-09",
|
||||
"type": "scatter",
|
||||
"data": [
|
||||
null,
|
||||
32,
|
||||
33
|
||||
],
|
||||
"sample_times": [
|
||||
null,
|
||||
"2026-07-09T08:15:00+08:00",
|
||||
"2026-07-09T08:30:00+08:00"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "UTC spelling preserves projected instant",
|
||||
"accepted": true,
|
||||
"chart": {
|
||||
"title": "P1 合成压力七日对照(m)",
|
||||
"chart_type": "time_comparison",
|
||||
"x_axis_name": "时间",
|
||||
"y_axis_name": "压力(m)",
|
||||
"x_data": [
|
||||
"08:00",
|
||||
"08:15",
|
||||
"08:30"
|
||||
],
|
||||
"series": [
|
||||
{
|
||||
"name": "P1 当前 2026-07-16",
|
||||
"type": "line",
|
||||
"data": [
|
||||
0,
|
||||
28,
|
||||
30
|
||||
],
|
||||
"sample_times": [
|
||||
"2026-07-16T08:00:00+08:00",
|
||||
"2026-07-16T08:15:00+08:00",
|
||||
"2026-07-16T08:30:00+08:00"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "P1 2026-07-09",
|
||||
"type": "scatter",
|
||||
"data": [
|
||||
null,
|
||||
32,
|
||||
33
|
||||
],
|
||||
"sample_times": [
|
||||
null,
|
||||
"2026-07-09T00:15:00Z",
|
||||
"2026-07-09T00:30:00Z"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ordinary line",
|
||||
"accepted": true,
|
||||
"chart": {
|
||||
"title": "P1 合成压力七日对照(m)",
|
||||
"chart_type": "line",
|
||||
"x_axis_name": "时间",
|
||||
"y_axis_name": "压力(m)",
|
||||
"x_data": [
|
||||
"08:00",
|
||||
"08:15",
|
||||
"08:30"
|
||||
],
|
||||
"series": [
|
||||
{
|
||||
"name": "合成读数",
|
||||
"data": [
|
||||
0,
|
||||
null,
|
||||
30
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ordinary bar",
|
||||
"accepted": true,
|
||||
"chart": {
|
||||
"title": "P1 合成压力七日对照(m)",
|
||||
"chart_type": "bar",
|
||||
"x_axis_name": "时间",
|
||||
"y_axis_name": "压力(m)",
|
||||
"x_data": [
|
||||
"08:00",
|
||||
"08:15",
|
||||
"08:30"
|
||||
],
|
||||
"series": [
|
||||
{
|
||||
"name": "合成读数",
|
||||
"data": [
|
||||
0,
|
||||
null,
|
||||
30
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "midnight rollover keeps one window",
|
||||
"accepted": true,
|
||||
"chart": {
|
||||
"title": "跨午夜同期对比",
|
||||
"chart_type": "time_comparison",
|
||||
"x_data": [
|
||||
"23:45",
|
||||
"00:00"
|
||||
],
|
||||
"series": [
|
||||
{
|
||||
"name": "当前",
|
||||
"type": "line",
|
||||
"data": [
|
||||
0,
|
||||
25
|
||||
],
|
||||
"sample_times": [
|
||||
"2026-07-16T23:45:00+08:00",
|
||||
"2026-07-17T00:00:00+08:00"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "历史",
|
||||
"type": "scatter",
|
||||
"data": [
|
||||
26,
|
||||
27
|
||||
],
|
||||
"sample_times": [
|
||||
"2026-07-15T23:45:00+08:00",
|
||||
"2026-07-16T00:00:00+08:00"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "microseconds are not aligned",
|
||||
"accepted": false,
|
||||
"chart": {
|
||||
"title": "P1 合成压力七日对照(m)",
|
||||
"chart_type": "time_comparison",
|
||||
"x_axis_name": "时间",
|
||||
"y_axis_name": "压力(m)",
|
||||
"x_data": [
|
||||
"08:00",
|
||||
"08:15",
|
||||
"08:30"
|
||||
],
|
||||
"series": [
|
||||
{
|
||||
"name": "P1 当前 2026-07-16",
|
||||
"type": "line",
|
||||
"data": [
|
||||
0,
|
||||
28,
|
||||
30
|
||||
],
|
||||
"sample_times": [
|
||||
"2026-07-16T08:00:00.000001+08:00",
|
||||
"2026-07-16T08:15:00+08:00",
|
||||
"2026-07-16T08:30:00+08:00"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "P1 2026-07-09",
|
||||
"type": "scatter",
|
||||
"data": [
|
||||
null,
|
||||
32,
|
||||
33
|
||||
],
|
||||
"sample_times": [
|
||||
null,
|
||||
"2026-07-09T08:15:00+08:00",
|
||||
"2026-07-09T08:30:00+08:00"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "fraction beyond Python microsecond precision",
|
||||
"accepted": true,
|
||||
"chart": {
|
||||
"title": "P1 合成压力七日对照(m)",
|
||||
"chart_type": "time_comparison",
|
||||
"x_axis_name": "时间",
|
||||
"y_axis_name": "压力(m)",
|
||||
"x_data": [
|
||||
"08:00",
|
||||
"08:15",
|
||||
"08:30"
|
||||
],
|
||||
"series": [
|
||||
{
|
||||
"name": "P1 当前 2026-07-16",
|
||||
"type": "line",
|
||||
"data": [
|
||||
0,
|
||||
28,
|
||||
30
|
||||
],
|
||||
"sample_times": [
|
||||
"2026-07-16T08:00:00.0000001+08:00",
|
||||
"2026-07-16T08:15:00+08:00",
|
||||
"2026-07-16T08:30:00+08:00"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "P1 2026-07-09",
|
||||
"type": "scatter",
|
||||
"data": [
|
||||
null,
|
||||
32,
|
||||
33
|
||||
],
|
||||
"sample_times": [
|
||||
null,
|
||||
"2026-07-09T08:15:00+08:00",
|
||||
"2026-07-09T08:30:00+08:00"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "calendar overflow",
|
||||
"accepted": false,
|
||||
"chart": {
|
||||
"title": "calendar overflow",
|
||||
"chart_type": "time_comparison",
|
||||
"x_data": [
|
||||
"07:45"
|
||||
],
|
||||
"series": [
|
||||
{
|
||||
"name": "当前",
|
||||
"type": "line",
|
||||
"data": [
|
||||
1
|
||||
],
|
||||
"sample_times": [
|
||||
"9999-12-31T23:45:00Z"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "历史缺测",
|
||||
"type": "scatter",
|
||||
"data": [
|
||||
null
|
||||
],
|
||||
"sample_times": [
|
||||
null
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "calendar underflow",
|
||||
"accepted": false,
|
||||
"chart": {
|
||||
"title": "calendar underflow",
|
||||
"chart_type": "time_comparison",
|
||||
"x_data": [
|
||||
"08:00"
|
||||
],
|
||||
"series": [
|
||||
{
|
||||
"name": "当前",
|
||||
"type": "line",
|
||||
"data": [
|
||||
1
|
||||
],
|
||||
"sample_times": [
|
||||
"0001-01-01T00:00:00+23:59"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "历史缺测",
|
||||
"type": "scatter",
|
||||
"data": [
|
||||
null
|
||||
],
|
||||
"sample_times": [
|
||||
null
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
+4
-2
@@ -1,12 +1,14 @@
|
||||
{"record_type":"presentation","command":{"action":"locate_features","layer":"scada","feature_ids":["P1"]}}
|
||||
{"record_type":"presentation","command":{"action":"zoom_to_map","center":[121.48,31.1],"zoom":14}}
|
||||
{"record_type":"presentation","command":{"action":"highlight_features","layer":"scada","feature_ids":["P1"],"effect":"pulse"}}
|
||||
{"record_type":"presentation","command":{"action":"open_chart","chart_id":"fixture-pressure","chart":{"title":"合成压力对比","chart_type":"line","x_data":["08:00","08:15","08:30"],"series":[{"name":"异常日压力水头(m)","data":[32,28,30]},{"name":"对照压力水头(m)","data":[33,32,32]}]},"evidence_refs":["dev:synthetic"]}}
|
||||
{"record_type":"presentation","command":{"action":"open_chart","chart_id":"fixture-pressure-P1","evidence_refs":["dev:synthetic"],"chart":{"title":"P1 合成压力七日对照(m)","chart_type":"time_comparison","x_axis_name":"时间","y_axis_name":"压力(m)","x_data":["08:00","08:15","08:30"],"series":[{"name":"P1 当前 2026-07-16","type":"line","data":[32,28,30],"sample_times":["2026-07-16T08:00:00+08:00","2026-07-16T08:15:00+08:00","2026-07-16T08:30:00+08:00"]},{"name":"P1 2026-07-09","type":"scatter","data":[null,32,33],"sample_times":[null,"2026-07-09T08:15:00+08:00","2026-07-09T08:30:00+08:00"]},{"name":"P1 2026-07-10","type":"scatter","data":[null,32,33],"sample_times":[null,"2026-07-10T08:15:00+08:00","2026-07-10T08:30:00+08:00"]},{"name":"P1 2026-07-11","type":"scatter","data":[null,32,33],"sample_times":[null,"2026-07-11T08:15:00+08:00","2026-07-11T08:30:00+08:00"]},{"name":"P1 2026-07-12","type":"scatter","data":[null,32,33],"sample_times":[null,"2026-07-12T08:15:00+08:00","2026-07-12T08:30:00+08:00"]},{"name":"P1 2026-07-13","type":"scatter","data":[null,32,33],"sample_times":[null,"2026-07-13T08:15:00+08:00","2026-07-13T08:30:00+08:00"]},{"name":"P1 2026-07-14","type":"scatter","data":[null,32,33],"sample_times":[null,"2026-07-14T08:15:00+08:00","2026-07-14T08:30:00+08:00"]},{"name":"P1 2026-07-15","type":"scatter","data":[null,32,33],"sample_times":[null,"2026-07-15T08:15:00+08:00","2026-07-15T08:30:00+08:00"]}]}}}
|
||||
{"record_type":"presentation","command":{"action":"open_chart","chart_id":"fixture-pressure-P2","evidence_refs":["dev:synthetic"],"chart":{"title":"P2 合成压力七日对照(m)","chart_type":"time_comparison","x_axis_name":"时间","y_axis_name":"压力(m)","x_data":["08:00","08:15","08:30"],"series":[{"name":"P2 当前 2026-07-16","type":"line","data":[31,27,29],"sample_times":["2026-07-16T08:00:00+08:00","2026-07-16T08:15:00+08:00","2026-07-16T08:30:00+08:00"]},{"name":"P2 2026-07-09","type":"scatter","data":[null,32,33],"sample_times":[null,"2026-07-09T08:15:00+08:00","2026-07-09T08:30:00+08:00"]},{"name":"P2 2026-07-10","type":"scatter","data":[null,32,33],"sample_times":[null,"2026-07-10T08:15:00+08:00","2026-07-10T08:30:00+08:00"]},{"name":"P2 2026-07-11","type":"scatter","data":[null,32,33],"sample_times":[null,"2026-07-11T08:15:00+08:00","2026-07-11T08:30:00+08:00"]},{"name":"P2 2026-07-12","type":"scatter","data":[null,32,33],"sample_times":[null,"2026-07-12T08:15:00+08:00","2026-07-12T08:30:00+08:00"]},{"name":"P2 2026-07-13","type":"scatter","data":[null,32,33],"sample_times":[null,"2026-07-13T08:15:00+08:00","2026-07-13T08:30:00+08:00"]},{"name":"P2 2026-07-14","type":"scatter","data":[null,32,33],"sample_times":[null,"2026-07-14T08:15:00+08:00","2026-07-14T08:30:00+08:00"]},{"name":"P2 2026-07-15","type":"scatter","data":[null,32,33],"sample_times":[null,"2026-07-15T08:15:00+08:00","2026-07-15T08:30:00+08:00"]}]}}}
|
||||
{"record_type":"presentation","command":{"action":"set_overlay","overlay_id":"fixture-area","title":"合成空间样例","mode":"geometry","evidence_id":"dev:synthetic","data":{"type":"FeatureCollection","features":[{"type":"Feature","properties":{"origin":"synthetic"},"geometry":{"type":"Point","coordinates":[121.48,31.1]}}]}}}
|
||||
{"record_type":"presentation","command":{"action":"render_scada_analysis","items":[{"device_id":"P1","level":"high"}]}}
|
||||
{"record_type":"ui_block","evidence_refs":[],"block":{"version":"answer-ui@1","block_id":"fixture-summary","phase":"final","fallback_text":"合成候选对比,仅供展示测试。","spec":{"root":"table","elements":{"table":{"type":"Table","props":{"title":"合成候选对比","columns":["候选","说明"],"rows":[["DEMO-PIPE-01","仅供展示测试"],["DEMO-PIPE-02","未执行定位分析"]]},"children":[]}}}}}
|
||||
{"record_type":"candidate","origin":"synthetic","source_evidence_refs":[],"element_id":"DEMO-PIPE-01","rank":1,"status":"example_only"}
|
||||
{"record_type":"presentation","command":{"action":"close_chart","chart_id":"fixture-pressure"}}
|
||||
{"record_type":"presentation","command":{"action":"close_chart","chart_id":"fixture-pressure-P1"}}
|
||||
{"record_type":"presentation","command":{"action":"close_chart","chart_id":"fixture-pressure-P2"}}
|
||||
{"record_type":"presentation","command":{"action":"clear_overlay","overlay_id":"fixture-area"}}
|
||||
{"record_type":"presentation","command":{"action":"clear_highlights"}}
|
||||
{"record_type":"presentation","command":{"action":"clear_scada_analysis"}}
|
||||
|
||||
Reference in New Issue
Block a user