Support batch feature targeting and clear map highlights and overlays from the More panel. Remove misleading action chevrons and unused configuration export entries. Validated the isolated patch with lint, typecheck, 36 focused tests and 5 browser cases using mocked data.
118 lines
7.9 KiB
TypeScript
118 lines
7.9 KiB
TypeScript
import type { Page } from "@playwright/test";
|
|
import type { OperationalTask } 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 {
|
|
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 async function mockInvestigationPresentation(page: Page, tasks: OperationalTask[]) {
|
|
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"].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: {
|
|
run_id: task.active_run!.run_id, generation: task.execution_generation, status: "running"
|
|
}, event_cursor: 10
|
|
} });
|
|
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>>();
|
|
Object.assign(window, { 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);
|
|
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 = 10;
|
|
return {
|
|
mutations,
|
|
emit: async (task: OperationalTask, command: unknown) => 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) {
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
await page.getByRole("button", { name: "工况任务", exact: true }).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();
|
|
}
|