feat(workbench): refine dynamic task collaboration

This commit is contained in:
2026-09-24 16:36:12 +08:00
parent b64d2b78bb
commit 87cc7785da
25 changed files with 1216 additions and 470 deletions
@@ -0,0 +1,181 @@
import { expect, test } from "@playwright/test";
import type { OperationalTask, OperationalTaskSummary } from "../../src/features/workbench/api/operational-task-client";
import { mockAgentApi } from "./support/mock-agent-api";
const projectId = "324ff7ee-c2d4-5184-a84b-48fb14a2933e";
const taskId = "82000000-0000-4000-8000-000000000001";
const actionId = "83000000-0000-4000-8000-000000000001";
const accessId = "84000000-0000-4000-8000-000000000001";
const now = "2026-09-24T08:00:00Z";
test("动态调查显示选定路线与嵌套并行,并完成授权和工单草稿确认", async ({ page }, testInfo) => {
test.setTimeout(90_000);
await mockAgentApi(page);
await page.route("**/runtime-config.js", (route) => route.fulfill({ contentType: "text/javascript", body: `globalThis.__TJWATER_CONFIG__=${JSON.stringify({
TJWATER_AUTH_MODE: "disabled", TJWATER_SERVER_API_BASE_URL: "http://server.test",
TJWATER_MAP_URL: "http://map.test/geoserver", TJWATER_GEOSERVER_WORKSPACE: "tjwater_next"
})}` }));
await page.route("http://server.test/api/v1/projects**", (route) => route.fulfill({ json: {
items: [{ project_id: projectId, gs_workspace: "tjwater_next", status: "active" }], total: 1, limit: 1000, offset: 0
} }));
await page.route("http://server.test/api/v1/scada-devices**", (route) => route.fulfill({ json: { items: [], total: 0, limit: 1000, offset: 0 } }));
await page.route("http://map.test/**", (route) => route.fulfill({ status: 204 }));
const planNode = (id: string, title: string, kind: "activity" | "decision" | "parallel_split" | "parallel_join" | "gate", status: "completed" | "waiting" | "skipped") => ({
id, title, owner: "agent" as const, status, detail: `${title}的执行记录`, kind,
evidence_refs: [], action_refs: [], selected_edge_id: null, gate_ref: null, revisits_node_id: null
});
const plan = {
schema: "execution-plan@2" as const, title: "水质投诉动态调查", reason: "按投诉位置和风险规划",
nodes: [
{ ...planNode("risk", "判断紧急风险", "decision", "completed"), selected_edge_id: "urgent-route" },
planNode("urgent", "立即现场核查", "activity", "completed"),
planNode("routine", "常规现场核查", "activity", "skipped"),
planNode("split", "同步核对现场与数据", "parallel_split", "completed"),
planNode("field", "现场采样", "activity", "waiting"),
planNode("data-split", "多源数据核对", "parallel_split", "completed"),
planNode("upstream", "上游监测", "activity", "completed"),
planNode("network", "管网运行", "activity", "completed"),
planNode("data-join", "数据证据汇合", "parallel_join", "completed"),
planNode("join", "现场与数据汇合", "parallel_join", "waiting"),
{ ...planNode("gate", "确认采样工单", "gate", "waiting"), gate_ref: actionId }
],
edges: [
{ id: "urgent-route", from: "risk", to: "urgent", condition: "有紧急风险" },
{ id: "routine-route", from: "risk", to: "routine", condition: "无紧急风险" },
{ id: "e1", from: "urgent", to: "split", condition: null },
{ id: "e2", from: "split", to: "field", condition: "现场" },
{ id: "e3", from: "split", to: "data-split", condition: "Agent 数据" },
{ id: "e4", from: "field", to: "join", condition: null },
{ id: "e5", from: "data-split", to: "upstream", condition: "上游" },
{ id: "e6", from: "data-split", to: "network", condition: "管网" },
{ id: "e7", from: "upstream", to: "data-join", condition: null },
{ id: "e8", from: "network", to: "data-join", condition: null },
{ id: "e9", from: "data-join", to: "join", condition: null },
{ id: "e10", from: "join", to: "gate", condition: null }
]
};
let task: OperationalTask = {
project_id: projectId, task_id: taskId, task_type: "event_investigation", workflow: "water_quality_complaint",
lifecycle_status: "waiting_human", active_run: null, status: "waiting_human", priority: "urgent",
revision: 2, execution_generation: 1, execution_request_id: "85000000-0000-4000-8000-000000000001",
title: "水质投诉动态调查", summary: "小区供水有异味", source: { kind: "operator", external_id: null, external_revision: null, event_kind: "water_quality_complaint" },
trigger: { kind: "manual", occurred_at: now, detail: "演示小区供水投诉" }, scheduled_for: null,
agent_session_id: "dynamic-flow-session", agent_lease_expires_at: null, current_plan_revision: 1, current_report_revision: null,
created_at: now, updated_at: now,
latest_plan: { revision: 1, execution_generation: 1, source_session_id: "dynamic-flow-session", published_at: now, published_by: "agent", plan },
latest_report: null, evidence: [],
access_requests: [{ access_request_id: accessId, task_id: taskId, node_id: "data-split", scopes: ["scada_devices", "scada_readings"],
reason: "需要核对上游设备和读数", status: "pending", approved_scopes: [], version: 1,
requested_by: "agent", requested_at: now, decided_by: null, decided_at: null, comment: null }],
actions: [{ action_id: actionId, task_id: taskId, version: 1, kind: "create_work_order", node_id: "gate", reason: "现场采样需要派单",
payload: { title: "旧采样草稿", description: "现场采样", work_order_type: "water_quality_complaint", priority: "urgent", location: "演示小区" },
evidence_refs: [], required_approval_permission: "operational_action.work_order.approve", required_execution_permission: "operational_action.work_order.execute",
approval_status: "pending", execution_status: "not_submitted", execution_request_id: null, proposed_by: "agent", proposed_at: now,
decided_by: null, decided_at: null, decision_comment: null, executed_by: null, executed_at: null, receipt: null, execution_detail: null }],
permissions: { can_publish_artifacts: false, can_propose_actions: false, can_sync_external_feedback: false,
can_decide_work_orders: true, can_execute_work_orders: true, can_decide_commands: false, can_execute_commands: false, can_decide_access: true },
work_order_snapshot: null
};
const summary = (item: OperationalTask): OperationalTaskSummary => {
const { latest_plan, latest_report, evidence, actions, access_requests, permissions, work_order_snapshot, ...value } = item;
void latest_plan; void latest_report; void evidence; void actions; void access_requests; void permissions; void work_order_snapshot;
return value;
};
await page.route("http://server.test/api/v1/workbench/sync-snapshot", (route) => route.fulfill({ json: {
schema: "workbench-sync-snapshot@1", event_cursor: 0,
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: [summary(task)], notifications: [], generated_at: now
} }));
await page.route("http://server.test/api/v1/workbench/events**", (route) => route.fulfill({ contentType: "text/event-stream", body: ": keepalive\n\n" }));
await page.route("http://server.test/api/v1/operational-task-notifications**", (route) => route.fulfill({ json: { items: [], limit: 100, offset: 0, next_offset: null } }));
await page.route("http://server.test/api/v1/operational-tasks**", async (route) => {
const path = new URL(route.request().url()).pathname;
const method = route.request().method();
if (path.endsWith("/operational-tasks/schedules")) return route.fulfill({ json: { items: [] } });
if (method === "GET" && path.endsWith("/operational-tasks")) return route.fulfill({ json: { items: [summary(task)], limit: 50, offset: 0, next_offset: null } });
if (method === "GET" && path.endsWith(`/operational-tasks/${taskId}`)) return route.fulfill({ json: task });
if (method === "POST" && path.endsWith(`/access-requests/${accessId}/decision`)) {
const body = route.request().postDataJSON();
expect(body.approved_scopes).toEqual(["scada_devices"]);
const access = { ...task.access_requests[0], version: 2, status: "approved" as const, approved_scopes: body.approved_scopes, decided_by: "reviewer", decided_at: now };
task = { ...task, access_requests: [access] };
return route.fulfill({ json: access });
}
if (method === "PATCH" && path.endsWith(`/actions/${actionId}/draft`)) {
const body = route.request().postDataJSON();
expect(body.payload.title).toBe("现场采样修订稿");
const action = { ...task.actions[0], version: 2, payload: body.payload, edited_by: "reviewer", edited_at: now };
task = { ...task, actions: [action] };
return route.fulfill({ json: action });
}
if (method === "POST" && path.endsWith(`/actions/${actionId}/confirm-dispatch`)) {
const body = route.request().postDataJSON();
expect(body.expected_version).toBe(2);
const action = { ...task.actions[0], version: 4, approval_status: "approved" as const, execution_status: "succeeded" as const,
decided_by: "reviewer", decided_at: now, executed_by: "reviewer", executed_at: now,
execution_request_id: "86000000-0000-4000-8000-000000000001",
receipt: { adapter: "work-order", receipt_id: "WO-20260924-1", submitted_at: now, completed_at: now, external_status: "completed", detail: "采样工单已创建" } };
task = { ...task, actions: [action] };
return route.fulfill({ json: action });
}
return route.fulfill({ status: 404, json: {} });
});
await page.goto("/");
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("水质投诉动态调查", { exact: true }).first().click();
await expect(panel.getByText(/路线选择:/).first().locator("..")).toContainText("有紧急风险");
await expect(panel.getByLabel("并行处理 2 路")).toHaveCount(2);
await expect(panel.locator('[data-execution-node="data-join"]')).toHaveCount(1);
await expect(panel.getByText("其他规划路线 1 项")).toBeVisible();
await testInfo.attach("dynamic-investigation-desktop", {
body: await panel.screenshot({ path: testInfo.outputPath("dynamic-investigation-desktop.png") }), contentType: "image/png"
});
await panel.getByRole("button", { name: "Agent 协作" }).click();
const agentPanel = page.getByRole("complementary", { name: "Agent 命令面板" });
const review = agentPanel.getByRole("region", { name: "当前任务审核" });
await expect(review).toContainText("需要核对上游设备和读数");
await page.setViewportSize({ width: 375, height: 720 });
const mobileAgentSheet = page.getByRole("region", { name: "Agent 工作台抽屉" });
if (!await mobileAgentSheet.isVisible()) await page.getByRole("button", { name: "打开 Agent 面板" }).click();
await expect(mobileAgentSheet.getByRole("region", { name: "当前任务审核" })).toBeVisible();
await page.setViewportSize({ width: 1280, height: 720 });
await expect(review).toBeVisible();
await review.getByLabel("SCADA 监测读数").uncheck();
await review.getByRole("button", { name: "批准所选范围" }).click();
await expect(panel.getByText("已授权:SCADA 设备")).toBeVisible();
await review.getByRole("button", { name: "编辑草稿" }).click();
await review.getByRole("textbox", { name: "工单标题" }).fill("现场采样修订稿");
await review.getByRole("button", { name: "保存草稿" }).click();
await expect(panel.getByText("现场采样修订稿").first()).toBeVisible();
await review.getByRole("button", { name: "确认并下发工单" }).click();
await expect(panel.getByRole("region", { name: "执行回执" })).toContainText("WO-20260924-1");
for (const width of [375, 320]) {
await page.setViewportSize({ width, height: 720 });
const mobilePanel = page.getByRole("region", { name: "工况任务", exact: true });
if (!await mobilePanel.isVisible()) await page.getByRole("button", { name: "打开工况任务", exact: true }).click();
const sheet = page.getByRole("region", { name: "工况任务抽屉" });
if (await sheet.getAttribute("data-snap") === "half") {
await page.getByRole("button", { name: /工况任务抽屉高度/ }).click();
}
await expect(sheet).toHaveAttribute("data-snap", "full");
const mobileExpand = mobilePanel.getByRole("button", { name: "展开工况任务" });
if (await mobileExpand.isVisible()) await mobileExpand.click();
await mobilePanel.getByRole("button", { name: /调查任务/ }).click();
await expect(mobilePanel.getByLabel("并行处理 2 路")).toHaveCount(2);
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(width);
await mobilePanel.locator('[data-execution-node="split"]').scrollIntoViewIfNeeded();
await testInfo.attach(`dynamic-investigation-${width}`, {
body: await sheet.screenshot({ path: testInfo.outputPath(`dynamic-investigation-${width}.png`) }), contentType: "image/png"
});
}
});
+31 -24
View File
@@ -125,30 +125,29 @@ test.describe("mobile workbench sheet", () => {
await expect(page.getByRole("button", { name: "打开工况任务" })).toBeVisible();
});
test("keeps refresh to the right of schedule on a 320px viewport", async ({ page }) => {
await page.setViewportSize({ width: 320, height: 700 });
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.getByRole("button", { name: "打开工况任务" }).click();
for (const width of [320, 375]) {
test(`keeps refresh accessible without a schedule-create action at ${width}px`, async ({ page }) => {
await page.setViewportSize({ width, height: 700 });
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.getByRole("button", { name: "打开工况任务" }).click();
const sheet = page.getByRole("region", { name: "工况任务抽屉" });
const scheduleButton = sheet.getByRole("button", { name: "配置定时流程" });
const refreshButton = sheet.getByRole("button", { name: "刷新工况任务" });
await expect(scheduleButton).toBeVisible();
await expect(refreshButton).toBeVisible();
const sheet = page.getByRole("region", { name: "工况任务抽屉" });
const refreshButton = sheet.getByRole("button", { name: "刷新工况任务" });
await expect(sheet.getByRole("button", { name: "配置定时流程" })).toHaveCount(0);
await expect(refreshButton).toBeVisible();
const [sheetBox, scheduleBox, refreshBox] = await Promise.all([
sheet.boundingBox(),
scheduleButton.boundingBox(),
refreshButton.boundingBox()
]);
expect(sheetBox).not.toBeNull();
expect(scheduleBox).not.toBeNull();
expect(refreshBox).not.toBeNull();
expect(refreshBox!.x).toBeGreaterThan(scheduleBox!.x);
expect(refreshBox!.x + refreshBox!.width).toBeLessThanOrEqual(
sheetBox!.x + sheetBox!.width
);
});
const [sheetBox, refreshBox] = await Promise.all([
sheet.boundingBox(),
refreshButton.boundingBox()
]);
expect(sheetBox).not.toBeNull();
expect(refreshBox).not.toBeNull();
expect(refreshBox!.x).toBeGreaterThanOrEqual(sheetBox!.x);
expect(refreshBox!.x + refreshBox!.width).toBeLessThanOrEqual(
sheetBox!.x + sheetBox!.width
);
});
}
test("keeps condition detail expanded inside the mobile sheet", async ({ page }) => {
await page.goto("/", { waitUntil: "domcontentloaded" });
@@ -162,7 +161,7 @@ test.describe("mobile workbench sheet", () => {
await expect(workflowTitle.locator("..").locator("svg")).toHaveCount(0);
});
test("keeps the waiting-for-reply state readable in the narrow sheet", async ({ page }) => {
test("keeps the waiting-for-reply state readable in the narrow sheet", async ({ page }, testInfo) => {
await mockFlatWorkOrder(page, "reply");
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.getByRole("button", { name: "打开工况任务" }).click();
@@ -179,7 +178,15 @@ test.describe("mobile workbench sheet", () => {
await expect(awaitingReplyWorkOrder.getByText("待复令", { exact: true })).toBeVisible();
await awaitingReplyWorkOrder.click();
await expect(sheet.getByText("确认模型边界已同步更新", { exact: true })).toBeVisible();
await expect(sheet.getByText("活动记录", { exact: true })).toBeVisible();
await expect(sheet.getByText("工单报告", { exact: true })).toBeVisible();
await expect(sheet.getByText("活动记录", { exact: true })).toHaveCount(0);
await page.evaluate(() => document.fonts.ready);
await sheet.getByText("工单报告", { exact: true }).scrollIntoViewIfNeeded();
await sheet.screenshot({ path: testInfo.outputPath("work-order-mobile-report.png") });
await sheet.getByRole("list", { name: "工单进度" }).locator("li").nth(1).scrollIntoViewIfNeeded();
await sheet.screenshot({ path: testInfo.outputPath("work-order-mobile-stages.png") });
await sheet.getByLabel("回执信息").scrollIntoViewIfNeeded();
await sheet.screenshot({ path: testInfo.outputPath("work-order-mobile-receipt.png") });
});
test("clears mobile sheet state when crossing into the desktop layout", async ({ page }) => {
+11 -9
View File
@@ -19,6 +19,7 @@ test("审批动作展示中文参数和证据,viewer 不可审批,冲突后
body: `globalThis.__TJWATER_CONFIG__=${JSON.stringify({
TJWATER_AUTH_MODE: "disabled",
TJWATER_SERVER_API_BASE_URL: "http://server.test",
TJWATER_PERIPHERAL_SAMPLE_API_BASE_URL: "http://sample.test",
TJWATER_MAP_URL: "http://map.test/geoserver",
TJWATER_GEOSERVER_WORKSPACE: "tjwater_next"
})}`
@@ -280,7 +281,7 @@ test("审批动作展示中文参数和证据,viewer 不可审批,冲突后
await expect(page.getByText("正在重连,业务数据可能陈旧", { exact: true })).toHaveCount(0);
await page.evaluate(() => document.fonts.ready);
await testInfo.attach("workbench-reconnect-notice", {
body: await reconnectNotice.screenshot({ path: testInfo.outputPath("workbench-reconnect-notice.png") }),
body: await page.screenshot({ path: testInfo.outputPath("workbench-reconnect-notice.png") }),
contentType: "image/png"
});
let conditionPanel = await openTask();
@@ -296,23 +297,24 @@ test("审批动作展示中文参数和证据,viewer 不可审批,冲突后
const refreshingButton = conditionPanel.getByRole("button", { name: "正在刷新工况任务" });
await expect(refreshingButton).toBeDisabled();
await refreshingButton.evaluate((button: HTMLButtonElement) => button.click());
await page.waitForTimeout(500);
expect(snapshotRequests).toBe(2);
await expect(conditionRefreshButton).toBeEnabled({ timeout: 4_000 });
expect(Date.now() - refreshStartedAt).toBeGreaterThanOrEqual(2_900);
await conditionPanel.getByRole("button", { name: "配置定时流程" }).click();
await conditionPanel.getByLabel("定时流程名称").fill("夜间阀门巡检");
await conditionPanel.getByLabel("任务说明").fill("每十五分钟检查预约维修涉及的边界阀门。");
await conditionPanel.getByRole("button", { name: "创建定时流程" }).click();
await expect(conditionPanel.getByText("夜间阀门巡检", { exact: true }).first()).toBeVisible();
await expect(conditionPanel.getByRole("button", { name: "配置定时流程" })).toHaveCount(0);
await expect(conditionPanel.getByRole("button", { name: "创建定时流程" })).toHaveCount(0);
await conditionPanel.getByRole("button", { name: /预约任务/ }).click();
await expect(conditionPanel.getByText("最近工况", { exact: true })).toHaveCount(1);
await expect(conditionPanel.getByText("城北阀井预约维修", { exact: true }).first()).toBeVisible();
await conditionPanel.getByText("城北阀井预约维修", { exact: true }).first().click();
await expect(conditionPanel.getByText("回执要求", { exact: true })).toBeVisible();
await expect(conditionPanel.getByText("复令要求", { exact: true })).toBeVisible();
await expect(conditionPanel.getByText("回填现场处理结果", { exact: true })).toBeVisible();
const appointmentDetail = conditionPanel.getByTestId("scheduled-condition-detail-scroll");
await expect(appointmentDetail.getByText("工单报告", { exact: true })).toBeVisible();
await expect(appointmentDetail.getByText("派单信息", { exact: true })).toBeVisible();
await expect(appointmentDetail.getByText("活动记录", { exact: true })).toHaveCount(0);
await expect(appointmentDetail.getByText("关键证据", { exact: true })).toHaveCount(0);
await expect(conditionPanel.getByText("测试班组乙", { exact: true }).first()).toBeVisible();
await expect(conditionPanel.getByText(/位置:联调测试点东侧/)).toBeVisible();
await expect(appointmentDetail.getByText("联调测试点东侧", { exact: true })).toBeVisible();
await expect(conditionPanel.getByText("编辑后的预约维修说明", { exact: true })).toBeVisible();
await expect(conditionPanel.getByText("预约 09/23 18:00 由北片区维修组处理", { exact: true })).toHaveCount(0);
await page.waitForTimeout(3_500);
+46 -11
View File
@@ -109,7 +109,9 @@ test.describe("外围工单测试", () => {
await expect(workOrderList).toContainText("待处理");
await expect(workOrderList).toContainText("处理中");
await expect(workOrderList).toContainText("已完成");
await expect(workOrderList.getByText("暂无活动记录;不根据工单状态推测处置过程。", { exact: true })).toHaveCount(3);
await expect(workOrderList.getByText("工单报告", { exact: true })).toHaveCount(3);
await expect(workOrderList.getByText("暂无现场反馈", { exact: true })).toHaveCount(3);
await expect(workOrderList.getByText("活动记录", { exact: true })).toHaveCount(0);
expect(requestedPaths).toEqual(expect.arrayContaining([
"/api/sample/v1/context",
"/api/sample/v1/work-orders"
@@ -240,6 +242,7 @@ test.describe("外围工单测试", () => {
const projectId = "324ff7ee-c2d4-5184-a84b-48fb14a2933e";
let workOrders: Record<string, unknown>[] = [];
const writes: Record<string, unknown>[] = [];
let deleteRequests = 0;
await page.route("**/runtime-config.js", (route) => route.fulfill({
contentType: "text/javascript",
body: `globalThis.__TJWATER_CONFIG__=${JSON.stringify({ TJWATER_AUTH_MODE: "disabled", TJWATER_SERVER_API_BASE_URL: "http://server.test", TJWATER_PERIPHERAL_SAMPLE_API_BASE_URL: "http://peripheral.test" })}`
@@ -269,7 +272,7 @@ test.describe("外围工单测试", () => {
workOrders = [next];
return route.fulfill({ json: workOrders[0] });
}
if (path.includes("/work-orders/") && request.method() === "DELETE") { workOrders = []; return route.fulfill({ json: { resource_type: "work_order", resource_id: path.split("/").at(-1), revision: 3, deleted: true } }); }
if (path.includes("/work-orders/") && request.method() === "DELETE") { deleteRequests += 1; workOrders = []; return route.fulfill({ json: { resource_type: "work_order", resource_id: path.split("/").at(-1), revision: 3, deleted: true } }); }
return route.fulfill({ status: 404, json: { code: "NOT_FOUND", message: "not found", effect: "none", request_id: null } });
});
@@ -299,6 +302,12 @@ test.describe("外围工单测试", () => {
await panel.getByRole("button", { name: "编辑工单 合成预约维修" }).click();
editor = panel.getByRole("region", { name: "编辑工单 合成预约维修" });
await expect(panel.getByLabel("工单列表").getByRole("region", { name: "编辑工单 合成预约维修" })).toBeVisible();
await expect(editor.locator("xpath=ancestor::article")).toContainText("合成预约维修");
await expect(editor.getByRole("textbox", { name: "标题" })).toBeFocused();
await expect(panel.getByRole("button", { name: "编辑工单 合成预约维修" })).toHaveCount(0);
await expect(panel.getByRole("button", { name: "删除工单 合成预约维修" })).toHaveCount(0);
expect(await editor.getByRole("textbox", { name: "标题" }).evaluate((input) => getComputedStyle(input).outlineWidth)).toBe("0px");
await editor.getByRole("textbox", { name: "标题" }).fill("合成预约维修(已对齐)");
await editor.getByRole("textbox", { name: "负责人", exact: true }).fill("测试班组乙");
await editor.getByRole("textbox", { name: "位置", exact: true }).fill("联调测试点东侧");
@@ -323,13 +332,14 @@ test.describe("外围工单测试", () => {
await expect(panel.getByText("外部系统更新后的预约维修", { exact: true })).toBeVisible();
await panel.getByRole("button", { name: "编辑工单 外部系统更新后的预约维修" }).click();
const refreshedEditor = panel.getByRole("region", { name: "编辑工单 外部系统更新后的预约维修" });
await expect(refreshedEditor.locator("xpath=ancestor::article")).toContainText("外部系统更新后的预约维修");
await expect(refreshedEditor.getByRole("textbox", { name: "负责人", exact: true })).toHaveValue("测试班组乙");
await expect(refreshedEditor.getByRole("textbox", { name: "位置", exact: true })).toHaveValue("联调测试点东侧");
await expect(refreshedEditor.getByRole("textbox", { name: "回执要求(每行一项)" })).toHaveValue("回传现场处理结果\n上传照片");
for (const width of [1280, 375, 320]) {
await page.setViewportSize({ width, height: 780 });
await refreshedEditor.scrollIntoViewIfNeeded();
await refreshedEditor.locator("xpath=ancestor::article").evaluate((element) => element.scrollIntoView({ block: "start" }));
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(width);
await testInfo.attach(`peripheral-work-order-process-${width}`, {
body: await page.screenshot({ path: `/tmp/tjwater-operational-ui-review/peripheral-work-order-edit-${width}.png` }),
@@ -338,7 +348,34 @@ test.describe("外围工单测试", () => {
}
await page.setViewportSize({ width: 1280, height: 780 });
await refreshedEditor.getByRole("button", { name: "关闭编辑表单" }).click();
await expect(panel.getByRole("button", { name: "编辑工单 外部系统更新后的预约维修" })).toBeFocused();
await expect(panel.getByRole("button", { name: "删除工单 外部系统更新后的预约维修" })).toBeVisible();
await panel.getByRole("button", { name: "删除工单 外部系统更新后的预约维修" }).click();
const deletePrompt = panel.getByRole("group", { name: "确认删除工单 外部系统更新后的预约维修" });
await expect(deletePrompt).toBeVisible();
await expect(deletePrompt).toContainText("对应的工况任务将在后台同步后移出任务列表");
expect(deleteRequests).toBe(0);
await deletePrompt.getByRole("button", { name: "取消" }).click();
await expect(deletePrompt).toHaveCount(0);
expect(deleteRequests).toBe(0);
await panel.getByRole("button", { name: "删除工单 外部系统更新后的预约维修" }).click();
await page.keyboard.press("Escape");
await expect(deletePrompt).toHaveCount(0);
expect(deleteRequests).toBe(0);
await panel.getByRole("button", { name: "删除工单 外部系统更新后的预约维修" }).click();
for (const width of [1280, 375, 320]) {
await page.setViewportSize({ width, height: 780 });
await deletePrompt.scrollIntoViewIfNeeded();
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(width);
await testInfo.attach(`peripheral-work-order-delete-prompt-${width}`, {
body: await page.screenshot(),
contentType: "image/png"
});
}
await deletePrompt.getByRole("button", { name: "确认删除" }).click();
expect(deleteRequests).toBe(1);
await expect(panel.getByText("外部系统更新后的预约维修", { exact: true })).toBeHidden();
await page.route("http://server.test/api/v1/operational-tasks", (route) =>
@@ -526,8 +563,8 @@ test.describe("外围工单测试", () => {
await expect(conditionPanel.getByText("调查处理流程", { exact: true })).toBeVisible();
await expect(conditionPanel.getByText(/已完成 5 项 · 执行中 3 项 · 待人工或资料 3 项/)).toBeVisible();
await expect(conditionPanel.locator('[data-execution-node]')).toHaveCount(13);
await expect(conditionPanel.getByLabel("并行分枝 4 项")).toBeVisible();
await expect(conditionPanel.getByLabel("并行分枝 2 项")).toBeVisible();
await expect(conditionPanel.getByText(/历史流程按原始依赖展示/)).toBeVisible();
await expect(conditionPanel.getByText(/并行处理/)).toHaveCount(0);
await expect(conditionPanel.getByRole("button", { name: "完整工程图" })).toHaveCount(0);
await expect(conditionPanel.getByText("节点与执行记录", { exact: true })).toHaveCount(0);
await expect(conditionPanel.getByLabel("计划版本", { exact: true })).toHaveCount(0);
@@ -549,7 +586,7 @@ test.describe("外围工单测试", () => {
body: await conditionPanel.screenshot({ path: testInfo.outputPath("execution-plan-desktop.png") }),
contentType: "image/png"
});
await conditionPanel.getByLabel("并行分枝 2 项").scrollIntoViewIfNeeded();
await conditionPanel.locator('[data-execution-node="conclusion"]').scrollIntoViewIfNeeded();
await testInfo.attach("water-quality-condition-task-second-fork-1280", {
body: await conditionPanel.screenshot({ path: testInfo.outputPath("execution-plan-second-fork-desktop.png") }),
contentType: "image/png"
@@ -572,8 +609,7 @@ test.describe("外围工单测试", () => {
await expect(responsiveWorkflowStep).toBeVisible();
await responsiveConditionPanel.getByText("调查处理流程", { exact: true }).scrollIntoViewIfNeeded();
await expect(responsiveConditionPanel.locator('[data-execution-node]')).toHaveCount(13);
await expect(responsiveConditionPanel.getByLabel("并行分枝 4 项")).toBeVisible();
await expect(responsiveConditionPanel.getByLabel("并行分枝 2 项")).toBeVisible();
await expect(responsiveConditionPanel.getByText(/历史流程按原始依赖展示/)).toBeVisible();
await expect(responsiveConditionPanel.getByRole("button", { name: "放大流程图" })).toHaveCount(0);
await page.waitForTimeout(350);
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(width);
@@ -581,7 +617,7 @@ test.describe("外围工单测试", () => {
body: await page.screenshot({ path: testInfo.outputPath(`execution-plan-${width}.png`) }),
contentType: "image/png"
});
await responsiveConditionPanel.getByLabel("并行分枝 2 项").scrollIntoViewIfNeeded();
await responsiveConditionPanel.locator('[data-execution-node="conclusion"]').scrollIntoViewIfNeeded();
await page.evaluate(() => document.fonts.ready);
await testInfo.attach(`water-quality-condition-task-second-fork-${width}`, {
body: await page.screenshot({ path: testInfo.outputPath(`execution-plan-second-fork-${width}.png`) }),
@@ -618,8 +654,7 @@ test.describe("外围工单测试", () => {
await recovered.getByRole("button", { name: /调查任务/ }).click();
const taskRow = recovered.getByRole("button", { name: /水质投诉调查/ }).first();
await taskRow.click();
await expect(recovered.getByLabel("并行分枝 4 项")).toBeVisible();
await expect(recovered.getByLabel("并行分枝 2 项")).toBeVisible();
await expect(recovered.getByText(/历史流程按原始依赖展示/)).toBeVisible();
expect(agentRequests.filter((body) => "trigger_context" in body)).toHaveLength(0);
});
@@ -201,9 +201,10 @@ test("keeps execution spinners without breathing badges after selecting timeline
await expectRunningStatusAnimations(executingWorkOrder);
const detail = panel.getByTestId("scheduled-condition-detail-scroll");
await expect(detail.getByText("预约工单", { exact: true }).last()).toBeVisible();
await expect(detail.getByText("回执要求", { exact: true })).toBeVisible();
await expect(detail.getByText("复令要求", { exact: true })).toBeVisible();
await expect(detail.getByText(/上传阀门操作前后照片/).first()).toBeVisible();
await expect(detail.getByText("活动记录", { exact: true })).toBeVisible();
await expect(detail.getByText("工单报告", { exact: true })).toBeVisible();
await expect(detail.getByText("活动记录", { exact: true })).toHaveCount(0);
await expect(detail.locator("details")).toHaveCount(0);
});
@@ -236,7 +237,8 @@ test("shows completed field work as a static waiting-for-reply state", async ({
await awaitingReplyWorkOrder.click();
await expect(panel.getByText("确认模型边界已同步更新", { exact: true })).toBeVisible();
await expect(panel.getByText("活动记录", { exact: true })).toBeVisible();
await expect(panel.getByText("工单报告", { exact: true })).toBeVisible();
await expect(panel.getByText("活动记录", { exact: true })).toHaveCount(0);
await expect(
panel
.getByTestId("scheduled-condition-detail-scroll")
@@ -456,9 +456,9 @@ test("keeps mock conditions while using backend work orders and alerts", async (
await expect(panel).not.toContainText("工单演示回退");
await panel.getByRole("button", { name: /后端工单:北区阀门复核/ }).click();
const detail = panel.getByTestId("scheduled-condition-detail-scroll");
await expect(detail.getByText("预约工单", { exact: true }).last()).toBeVisible();
await expect(detail.getByText("工单报告", { exact: true })).toBeVisible();
await expect(detail.getByText("回传阀门开度", { exact: true })).toBeVisible();
await expect(detail.getByText("活动记录", { exact: true })).toBeVisible();
await expect(detail.getByText("活动记录", { exact: true })).toHaveCount(0);
await expect(detail.locator("details")).toHaveCount(0);
const alertButton = page.getByRole("button", {