Files
next-tjwater-frontend/tests/browser/support/mock-investigation-presentation.ts
T
jiang 03827a2fc0
Generic Container CI/CD / test-build-publish (push) Successful in 1m12s
Frontend CI/CD / build-test-publish-and-deploy (push) Successful in 1m13s
feat(workbench): group live presentation steps into expandable categories
Group adjacent live display steps into category menus with merged snapshots, original-step submenus and scoped playback.

The submenu exposes the latest raw step, so explicit selections must remain pinned. Locate failed and cancelled steps using the full group sequence, and prevent an exiting menu from dismissing a trigger-owned reopen. Regression tests reproduce these paths before the fixes.

Validation: lint, typecheck, 1178 unit tests and 10 focused browser cases passed; update the presentation history guide.
2026-11-12 12:54:09 +08:00

160 lines
10 KiB
TypeScript

import type { Page } from "@playwright/test";
import type { OperationalTask, OperationalTaskNotification } from "../../../src/features/workbench/api/operational-task-client";
import { mockAgentApi } from "./mock-agent-api";
import { mockWorkbenchContext } from "./mock-workbench-context";
const projectId = "324ff7ee-c2d4-5184-a84b-48fb14a2933e";
const now = "2026-10-27T08:00:00Z";
export function investigationFixture(index: number, title: string): OperationalTask {
const taskId = `92000000-0000-4000-8000-${String(index).padStart(12, "0")}`;
const runId = `93000000-0000-4000-8000-${String(index).padStart(12, "0")}`;
const requestId = `94000000-0000-4000-8000-${String(index).padStart(12, "0")}`;
return {
execution_scope: "production",
project_id: projectId, task_id: taskId, task_type: "event_investigation", workflow: "burst_report",
lifecycle_status: "open", status: "running", priority: "normal", revision: 1, execution_generation: 1,
execution_request_id: requestId, title, summary: "仅供浏览器测试的合成调查",
source: { kind: "operator", external_id: null, external_revision: null, event_kind: "burst_report" },
trigger: { kind: "manual", occurred_at: now, detail: "synthetic" }, scheduled_for: null,
agent_session_id: "investigation-dev-test", agent_lease_expires_at: now, current_plan_revision: null,
current_report_revision: null, created_at: now, updated_at: now,
active_run: { run_id: runId, task_id: taskId, generation: 1, execution_request_id: requestId,
status: "running", revision: 1, agent_session_id: "investigation-dev-test", lease_expires_at: now,
heartbeat_at: now, started_at: now, finished_at: null, failure_code: null, failure_detail: null,
dispatch_attempts: 1, next_dispatch_at: now, created_at: now, updated_at: now },
latest_plan: null, latest_report: null,
latest_progress: { revision: 1, run_id: runId, execution_generation: 1, published_at: now,
source_session_id: "investigation-dev-test", progress: { summary: "正在核对候选管段", diagram: {
title: "合成调查", mermaid: 'flowchart TD\nA["核查监测"]', node_refs: [{ node_id: "A", label: "核查监测",
status: "running", evidence_refs: [], action_refs: [], map_targets: [{ source_id: "scada", feature_id: "P1" }] }]
} } },
evidence: [], actions: [], access_requests: [], work_order_snapshot: null,
permissions: { can_publish_artifacts: false, can_propose_actions: false, can_sync_external_feedback: false,
can_decide_work_orders: false, can_execute_work_orders: false, can_decide_commands: false,
can_execute_commands: false, can_decide_access: false }
};
}
export type SavedPresentationEvent = { task_id: string; event_seq: number; run_id: string; execution_generation: number; occurred_at: string; command: unknown };
export async function mockInvestigationPresentation(page: Page, tasks: OperationalTask[], savedEvents: SavedPresentationEvent[] = [], notifications: OperationalTaskNotification[] = []) {
await mockWorkbenchContext(page);
await mockAgentApi(page);
const mutations: string[] = [];
page.on("request", (request) => {
if (request.url().startsWith("https://tjwater-api.invalid/") && ["POST", "PATCH", "DELETE", "PUT"].includes(request.method()))
mutations.push(`${request.method()} ${new URL(request.url()).pathname}`);
});
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: "", TJWATER_ENABLE_MSW: "false"
})};`
}));
await page.route("https://tjwater-api.invalid/api/v1/scada-devices**", (route) => route.fulfill({ json: {
items: [{ device_id: "P1", device_type: "pressure", node_id: null, link_id: null, api_query_id: null,
measurement_unit: "m", transmission_mode: "test", transmission_frequency: "15min", reliability: 1,
x: null, y: null, longitude: 121.48, latitude: 31.10 }], total: 1, limit: 1000, offset: 0
} }));
await page.route("https://tjwater-api.invalid/api/v1/workbench/sync-snapshot", (route) => {
return route.fulfill({ json: { schema: "workbench-sync-snapshot@1", event_cursor: 10, generated_at: now,
runtime: { availability: "not_configured", source: "none", scenario_availability: "not_configured",
artifact_availability: "not_configured", schedule_availability: "not_configured", work_order_availability: "not_configured",
alert_availability: "not_configured", generated_at: now, conditions: [], alerts: [], scenarios: [], latest_analysis: null, work_orders: [] },
tasks: tasks.map((task) => Object.fromEntries(Object.entries(task).filter(([key]) =>
!["latest_plan", "latest_report", "latest_progress", "evidence", "actions", "access_requests", "work_order_snapshot", "permissions", "questions"].includes(key)))),
notifications } });
});
await page.route("https://tjwater-api.invalid/api/v1/operational-tasks/**", (route) => {
const task = tasks.find((item) => route.request().url().includes(item.task_id));
if (!task) return route.fulfill({ status: 404 });
if (route.request().url().endsWith("/presentation-cursor")) return route.fulfill({ json: {
task_id: task.task_id, execution_generation: task.execution_generation, active_run: task.active_run ? {
run_id: task.active_run.run_id, generation: task.execution_generation, status: "running"
} : null, event_cursor: sequence
} });
if (route.request().url().includes("/presentation-events")) {
const url = new URL(route.request().url());
const through = Number(url.searchParams.get("through") ?? sequence);
const after = Number(url.searchParams.get("after") ?? 0);
const limit = Number(url.searchParams.get("limit") ?? 100);
const events = savedEvents.filter((item) => item.task_id === task.task_id && item.event_seq > after && item.event_seq <= through).sort((a, b) => a.event_seq - b.event_seq);
return route.fulfill({ json: { task_id: task.task_id, event_cursor: through,
items: events.slice(0, limit).map(({ task_id: _, ...item }) => item),
next_after: events.length > limit ? events[limit - 1].event_seq : null
} });
}
if (route.request().url().includes("/reports")) return route.fulfill({ json: { items: [], next_offset: null } });
return route.fulfill({ json: task });
});
await page.addInitScript(() => {
const fetch = window.fetch.bind(window);
const streams = new Set<ReadableStreamDefaultController<Uint8Array>>();
let connectionAvailable = true;
Object.assign(window, { setInvestigationTestConnection: (available: boolean) => {
connectionAvailable = available;
if (!available) {
for (const stream of streams) stream.error(new TypeError("synthetic connection lost"));
streams.clear();
}
}, emitInvestigationTestEvent: (event: unknown) => {
const data = new TextEncoder().encode(`event: workbench_event\ndata: ${JSON.stringify(event)}\n\n`);
for (const stream of streams) stream.enqueue(data);
} });
window.fetch = async (input, init) => {
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
if (!url.includes("/api/v1/workbench/events")) return fetch(input, init);
if (!connectionAvailable) throw new TypeError("synthetic connection unavailable");
let current: ReadableStreamDefaultController<Uint8Array>;
const body = new ReadableStream<Uint8Array>({ start(controller) { current = controller; streams.add(controller); }, cancel() { streams.delete(current); } });
init?.signal?.addEventListener("abort", () => { if (streams.delete(current)) current.close(); }, { once: true });
return new Response(body, { headers: { "Content-Type": "text/event-stream" } });
};
});
let sequence = Math.max(10, ...savedEvents.map((item) => item.event_seq));
return {
mutations,
emit: async (task: OperationalTask, command: unknown) => {
savedEvents.push({ task_id: task.task_id, run_id: task.active_run!.run_id, event_seq: sequence + 1, execution_generation: task.execution_generation, occurred_at: now, command });
return page.evaluate((event) => {
(window as unknown as { emitInvestigationTestEvent: (event: unknown) => void }).emitInvestigationTestEvent(event);
}, { schema: "workbench-event@1", event_seq: ++sequence,
event_id: `95000000-0000-4000-8000-${String(sequence).padStart(12, "0")}`, project_id: projectId,
topic: "operational.presentation.requested", aggregate_type: "operational_task", aggregate_id: task.task_id,
aggregate_revision: 1, occurred_at: now, metadata: {
task_id: task.task_id, run_id: task.active_run!.run_id, execution_generation: task.execution_generation, command
} });
}
};
}
export async function selectInvestigation(page: Page, title: string, input: "pointer" | "keyboard" = "pointer") {
await page.goto("/", { waitUntil: "domcontentloaded" });
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();
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();
}
if (input === "keyboard") await task.press("Enter");
else await task.click();
}
export async function showInvestigationMap(page: Page) {
const showMap = page.getByRole("button", { name: "显示地图", exact: true });
if (await showMap.isVisible()) await showMap.click();
}
export async function showInvestigationTasks(page: Page) {
if (await page.getByRole("region", { name: "工况任务", exact: true }).isVisible()) return;
const returnToTasks = page.getByRole("button", { name: "返回工况任务", exact: true });
if (await returnToTasks.isVisible()) await returnToTasks.click();
else await page.getByRole("button", { name: /^(?:打开)?工况任务$/ }).click();
}