refactor(workbench): simplify appointment tasks

This commit is contained in:
2026-09-24 12:01:11 +08:00
parent 436d5500f1
commit ddf953b641
28 changed files with 443 additions and 1384 deletions
+5 -4
View File
@@ -1,5 +1,5 @@
import { expect, test } from "@playwright/test";
import { mockHierarchicalWorkOrder } from "./support/mock-hierarchical-work-order";
import { mockFlatWorkOrder } from "./support/mock-hierarchical-work-order";
test.describe("mobile workbench sheet", () => {
test.use({ viewport: { width: 390, height: 844 } });
@@ -138,12 +138,13 @@ test.describe("mobile workbench sheet", () => {
});
test("keeps the waiting-for-reply state readable in the narrow sheet", async ({ page }) => {
await mockHierarchicalWorkOrder(page, "reply");
await mockFlatWorkOrder(page, "reply");
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.getByRole("button", { name: "打开工况任务" }).click();
const sheet = page.getByRole("region", { name: "工况任务抽屉" });
await sheet.getByRole("button", { name: "展开工况任务" }).click();
await sheet.getByRole("button", { name: /预约任务/ }).click();
await expect(sheet.getByText("最近工况", { exact: true })).toHaveCount(1);
await sheet.getByRole("button", { name: "收起工况任务" }).click();
const awaitingReplyWorkOrder = sheet
.locator('button[aria-pressed]')
@@ -152,8 +153,8 @@ test.describe("mobile workbench sheet", () => {
await expect(awaitingReplyWorkOrder.getByText("待复令", { exact: true })).toBeVisible();
await awaitingReplyWorkOrder.click();
await expect(sheet.getByText("2/3 完成", { exact: true })).toBeVisible();
await expect(sheet.getByText("确认模型边界已同步更新 · 待复令", { exact: true })).toBeVisible();
await expect(sheet.getByText("确认模型边界已同步更新", { exact: true })).toBeVisible();
await expect(sheet.getByText("活动记录", { exact: true })).toBeVisible();
});
test("clears mobile sheet state when crossing into the desktop layout", async ({ page }) => {
+5 -20
View File
@@ -167,23 +167,8 @@ test("审批动作展示中文参数和证据,viewer 不可审批,冲突后
work_order_type: "general",
location: "联调测试点东侧",
scenario_code: "APPOINTMENT-TEST-001",
subtasks: [{
id: "field-service",
title: "阀井现场维修",
summary: "按预约窗口完成现场维修并回填结果。",
status: "pending",
scheduled_at: "2026-09-23T10:00:00Z",
location: "联调测试点东侧",
assignee: "测试班组乙",
duration_minutes: 60,
reply_window_minutes: 30,
stages: [
{ id: "dispatch", sequence: 1, title: "派发", status: "pending", owner: "调度岗", time_label: "待安排", description: "确认预约窗口。" },
{ id: "execution", sequence: 2, title: "执行", status: "pending", owner: "测试班组乙", time_label: "待安排", description: "完成现场维修。" },
{ id: "reply", sequence: 3, title: "复令", status: "pending", owner: "调度岗", time_label: "待安排", description: "核对回执并闭环。" }
],
reply_requirements: ["回填现场处理结果"]
}],
feedback_requirements: ["回填现场处理结果"],
activity: [],
origin: "synthetic",
updated_at: "2026-09-23T08:30:00Z"
}
@@ -282,17 +267,17 @@ test("审批动作展示中文参数和证据,viewer 不可审批,冲突后
await expect(conditionPanel.getByRole("button", { name: /定时任务/ })).toContainText("模拟数据");
await expect(conditionPanel.getByRole("button", { name: /调查任务/ })).toContainText("后端");
await expect(conditionPanel.getByRole("button", { name: /预约任务/ })).toContainText("后端");
await expect(conditionPanel.getByRole("button", { name: /预约任务/ })).toContainText("演示");
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 conditionPanel.getByRole("button", { name: /预约任务/ }).click();
await expect(conditionPanel.getByText("最近工况", { exact: true })).toHaveCount(1);
await expect(conditionPanel.getByText("城北阀井预约维修", { exact: true }).first()).toBeVisible();
await expect(conditionPanel.getByText(/^演示工单:/).first()).toBeVisible();
await conditionPanel.getByText("城北阀井预约维修", { exact: true }).first().click();
await expect(conditionPanel.getByText(/阀井现场维修.*待处理/)).toBeVisible();
await expect(conditionPanel.getByText("回执要求", { exact: true })).toBeVisible();
await expect(conditionPanel.getByText("回填现场处理结果", { exact: true })).toBeVisible();
await expect(conditionPanel.getByText("测试班组乙", { exact: true }).first()).toBeVisible();
await expect(conditionPanel.getByText(/位置:联调测试点东侧/)).toBeVisible();
await expect(conditionPanel.getByText("编辑后的预约维修说明", { exact: true })).toBeVisible();
+14 -26
View File
@@ -109,7 +109,7 @@ 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);
expect(requestedPaths).toEqual(expect.arrayContaining([
"/api/sample/v1/context",
"/api/sample/v1/work-orders"
@@ -254,19 +254,18 @@ test.describe("外围工单测试", () => {
if (path.endsWith("/work-orders") && request.method() === "POST") {
const body = request.postDataJSON();
writes.push(body);
const item = { ...body, work_order_id: "50000000-0000-4000-8000-000000000001", project_id: projectId, revision: 1, origin: "synthetic", created_at: now, updated_at: now, created_by: "tester", updated_by: "tester" };
const item = { ...body, work_order_id: "50000000-0000-4000-8000-000000000001", project_id: projectId, revision: 1, origin: "synthetic", created_at: now, updated_at: now, created_by: "tester", updated_by: "tester", activity: [{ sequence: 1, occurred_at: now, actor: "tester", kind: "created", title: "工单创建", detail: "预约工单已创建。" }] };
delete item.request_id; workOrders = [item]; return route.fulfill({ status: 201, json: item });
}
if (path.includes("/work-orders/") && request.method() === "PATCH") {
const body = request.postDataJSON();
writes.push(body);
const next = { ...workOrders[0], ...body, revision: 2, updated_at: "2026-09-18T01:00:00Z" };
const next = { ...workOrders[0], ...body, revision: 2, updated_at: "2026-09-18T01:00:00Z", activity: [...workOrders[0].activity, { sequence: 2, occurred_at: "2026-09-18T01:00:00Z", actor: "tester", kind: "feedback", title: "现场反馈", detail: body.progress_note }] };
if (body.clear_assignee) next.assignee = null;
if (body.clear_location) next.location = null;
if (body.clear_scenario_code) next.scenario_code = null;
if (body.clear_scheduled_at) next.scheduled_at = null;
if (body.clear_subtasks) next.subtasks = null;
for (const key of ["request_id", "expected_revision", "clear_assignee", "clear_location", "clear_scenario_code", "clear_scheduled_at", "clear_subtasks"]) delete next[key];
for (const key of ["request_id", "expected_revision", "clear_assignee", "clear_location", "clear_scenario_code", "clear_scheduled_at", "progress_note"]) delete next[key];
workOrders = [next];
return route.fulfill({ json: workOrders[0] });
}
@@ -287,9 +286,7 @@ test.describe("外围工单测试", () => {
await editor.getByRole("textbox", { name: "负责人", exact: true }).fill("测试班组甲");
await editor.getByRole("textbox", { name: "位置", exact: true }).fill("联调测试点西侧");
await editor.getByRole("textbox", { name: "场景编号" }).fill("APPOINTMENT-TEST-001");
await editor.getByRole("textbox", { name: "责任班组" }).fill("测试班组甲");
await editor.getByRole("textbox", { name: "处置位置" }).fill("联调测试点西侧");
await editor.getByRole("textbox", { name: "阶段说明", exact: true }).first().fill("确认班组甲和西侧作业窗口。");
await editor.getByRole("textbox", { name: "回执要求(每行一项)" }).fill("回传现场处理结果\n上传照片");
await editor.getByRole("button", { name: "创建预约工单" }).click();
await expect(panel.getByText("合成预约维修", { exact: true })).toBeVisible();
expect(writes[0]).toMatchObject({
@@ -297,11 +294,7 @@ test.describe("外围工单测试", () => {
assignee: "测试班组甲",
location: "联调测试点西侧",
scenario_code: "APPOINTMENT-TEST-001",
subtasks: expect.arrayContaining([expect.objectContaining({
assignee: "测试班组甲",
location: "联调测试点西侧",
stages: expect.arrayContaining([expect.objectContaining({ description: "确认班组甲和西侧作业窗口。" })])
})])
feedback_requirements: ["回传现场处理结果", "上传照片"]
});
await panel.getByRole("button", { name: "编辑工单 合成预约维修" }).click();
@@ -309,12 +302,10 @@ test.describe("外围工单测试", () => {
await editor.getByRole("textbox", { name: "标题" }).fill("合成预约维修(已对齐)");
await editor.getByRole("textbox", { name: "负责人", exact: true }).fill("测试班组乙");
await editor.getByRole("textbox", { name: "位置", exact: true }).fill("联调测试点东侧");
await editor.getByRole("textbox", { name: "责任班组" }).fill("测试班组乙");
await editor.getByRole("textbox", { name: "处置位置" }).fill("联调测试点东侧");
await editor.getByRole("textbox", { name: "阶段说明", exact: true }).first().fill("确认班组乙和东侧作业窗口,等待真实反馈。");
await editor.getByRole("textbox", { name: "现场反馈(可选)" }).fill("班组乙已确认东侧作业窗口,等待复核。");
await editor.getByRole("combobox", { name: "工单状态" }).click();
await page.getByRole("option", { name: "待复令", exact: true }).click();
await editor.getByRole("button", { name: "保存工单与过程" }).click();
await editor.getByRole("button", { name: "保存工单" }).click();
await expect(panel.getByText("合成预约维修(已对齐)", { exact: true })).toBeVisible();
expect(writes[1]).toMatchObject({
status: "awaiting_reply",
@@ -322,12 +313,8 @@ test.describe("外围工单测试", () => {
location: "联调测试点东侧",
clear_assignee: false,
clear_location: false,
clear_subtasks: false,
subtasks: expect.arrayContaining([expect.objectContaining({
assignee: "测试班组乙",
location: "联调测试点东侧",
stages: expect.arrayContaining([expect.objectContaining({ description: "确认班组乙和东侧作业窗口,等待真实反馈。" })])
})])
progress_note: "班组乙已确认东侧作业窗口,等待复核。",
feedback_requirements: ["回传现场处理结果", "上传照片"]
});
expect(workOrders[0]).toMatchObject({ work_order_type: "general", scenario_code: "APPOINTMENT-TEST-001" });
@@ -337,8 +324,8 @@ test.describe("外围工单测试", () => {
await panel.getByRole("button", { name: "编辑工单 外部系统更新后的预约维修" }).click();
const refreshedEditor = panel.getByRole("region", { name: "编辑工单 外部系统更新后的预约维修" });
await expect(refreshedEditor.getByRole("textbox", { name: "负责人", exact: true })).toHaveValue("测试班组乙");
await expect(refreshedEditor.getByRole("textbox", { name: "处置位置" })).toHaveValue("联调测试点东侧");
await expect(refreshedEditor.getByRole("textbox", { name: "阶段说明", exact: true }).first()).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 });
@@ -730,7 +717,8 @@ function createSourceWorkOrders(projectId: string) {
work_order_type: "general",
location: null,
scenario_code: null,
subtasks: null,
feedback_requirements: [],
activity: [],
origin: "synthetic",
created_at: "2026-09-17T00:00:00Z",
updated_at: "2026-09-17T00:00:00Z",
@@ -1,5 +1,5 @@
import { expect, test, type Locator, type Page } from "@playwright/test";
import { mockHierarchicalWorkOrder } from "./support/mock-hierarchical-work-order";
import { mockFlatWorkOrder } from "./support/mock-hierarchical-work-order";
test("scheduled conditions keep acrylic blur outside transformed ancestors", async ({ page }) => {
await page.goto("/", { waitUntil: "domcontentloaded" });
@@ -168,7 +168,7 @@ test("switching conditions keeps the detail viewport fully expanded", async ({ p
test("keeps execution spinners without breathing badges after selecting timeline tasks", async ({
page
}) => {
await mockHierarchicalWorkOrder(page, "execution");
await mockFlatWorkOrder(page, "execution");
await page.goto("/", { waitUntil: "domcontentloaded" });
await openConditionFeed(page);
@@ -200,16 +200,15 @@ test("keeps execution spinners without breathing badges after selecting timeline
await executingWorkOrder.click();
await expectRunningStatusAnimations(executingWorkOrder);
const detail = panel.getByTestId("scheduled-condition-detail-scroll");
await expect(detail.getByText("子任务", { exact: true })).toBeVisible();
await expect(detail.getByText("1/3 完成", { exact: true })).toBeVisible();
await expect(detail.getByText("预约工单", { exact: true }).last()).toBeVisible();
await expect(detail.getByText("回执要求", { exact: true })).toBeVisible();
await expect(detail.getByText(/上传阀门操作前后照片/).first()).toBeVisible();
const activeSubtask = detail.locator("details").filter({ hasText: "记录最终开度" });
await expect(activeSubtask).toHaveAttribute("open", "");
await expect(activeSubtask.getByText("执行中", { exact: true })).toBeVisible();
await expect(detail.getByText("活动记录", { exact: true })).toBeVisible();
await expect(detail.locator("details")).toHaveCount(0);
});
test("shows completed field work as a static waiting-for-reply state", async ({ page }) => {
await mockHierarchicalWorkOrder(page, "reply");
await mockFlatWorkOrder(page, "reply");
await page.goto("/", { waitUntil: "domcontentloaded" });
await openConditionFeed(page);
@@ -236,8 +235,8 @@ test("shows completed field work as a static waiting-for-reply state", async ({
);
await awaitingReplyWorkOrder.click();
await expect(panel.getByText("2/3 完成", { exact: true })).toBeVisible();
await expect(panel.getByText("确认模型边界已同步更新 · 待复令", { exact: true })).toBeVisible();
await expect(panel.getByText("确认模型边界已同步更新", { exact: true })).toBeVisible();
await expect(panel.getByText("活动记录", { exact: true })).toBeVisible();
await expect(
panel
.getByTestId("scheduled-condition-detail-scroll")
@@ -3,7 +3,7 @@ import { mockWorkbenchContext } from "./mock-workbench-context";
type WorkOrderPhase = "execution" | "reply";
export async function mockHierarchicalWorkOrder(page: Page, phase: WorkOrderPhase) {
export async function mockFlatWorkOrder(page: Page, phase: WorkOrderPhase) {
const parentTitle = phase === "execution"
? "演示工单:北辰分区阀门开度调整"
: "演示工单:泵站边界复核复令";
@@ -11,7 +11,6 @@ export async function mockHierarchicalWorkOrder(page: Page, phase: WorkOrderPhas
const subtaskTitles = phase === "execution"
? ["上传阀门操作前后照片", "记录最终开度", "复核北辰末端压力不低于 0.22MPa"]
: ["上传泵站运行截图", "填写出水压力复核结果", "确认模型边界已同步更新"];
const activeIndex = phase === "execution" ? 1 : 2;
const scheduledAt = shanghaiTimestamp(10, 30);
const workOrderId = `mock-${phase}-work-order`;
@@ -40,39 +39,16 @@ export async function mockHierarchicalWorkOrder(page: Page, phase: WorkOrderPhas
risk_level: phase === "execution" ? "attention" : "normal",
updated_at: scheduledAt,
detail: "浏览器测试使用的后端工单投影。",
recommendation: "按子任务逐项完成并回传。",
recommendation: "按回执要求完成并回传。",
duration_minutes: 45,
source: "生产工单系统",
dispatcher: "调度中心",
priority: phase === "execution" ? "urgent" : "normal",
subtasks: subtaskTitles.map((title, index) => {
const position = index < activeIndex ? "before" : index > activeIndex ? "after" : "active";
const stageStatuses = phase === "execution" && position === "active"
? ["done", "current", "pending"]
: phase === "reply" && position === "active"
? ["done", "done", "current"]
: position === "before"
? ["done", "done", "done"]
: ["pending", "pending", "pending"];
return {
id: `${workOrderId}-subtask-${index + 1}`,
title,
summary: `完成“${title}”并保留阶段记录。`,
status: position === "before" ? "completed" : position === "active" ? "running" : "pending",
scheduled_at: shanghaiTimestamp(10, 30 + index * 15),
location: phase === "execution" ? "北辰分区边界阀门 BV-07" : "北辰二级泵站 P-02",
assignee: phase === "execution" ? "北辰抢修二组" : "泵站运行班组",
duration_minutes: 15,
reply_window_minutes: 20,
stages: [
{ id: "dispatch", sequence: 1, title: "派发", status: stageStatuses[0], owner: "调度中心", time_label: "10:20", description: "确认范围并派发。" },
{ id: "execution", sequence: 2, title: "执行中", status: stageStatuses[1], owner: "现场班组", time_label: "10:30-10:45", description: "现场执行本项任务。" },
{ id: "reply", sequence: 3, title: "待复令", status: stageStatuses[2], owner: "现场班组", time_label: "10:45-11:05", description: "回传结果并等待复令。" }
],
reply_requirements: [`回传“${title}”的执行结果`]
};
})
location: phase === "execution" ? "北辰分区边界阀门 BV-07" : "北辰二级泵站 P-02",
assignee: phase === "execution" ? "北辰抢修二组" : "泵站运行班组",
feedback_requirements: subtaskTitles,
activity: [{ sequence: 1, occurred_at: scheduledAt, actor: "调度中心", kind: "created",
title: "演示工单创建", detail: "预约任务已创建。" }]
}]
});
}
+13 -40
View File
@@ -53,23 +53,10 @@ test("history mode opens on the latest day with backend work orders", async ({ p
source: "工单系统",
dispatcher: "调度中心",
priority: "normal",
subtasks: [{
id: "wo-history-1-subtask-1",
title: "阀门状态复核",
summary: "现场复核阀门状态并回传。",
status: "running",
scheduled_at: "2026-08-17T09:40:00Z",
location: "北区阀门 V-17",
assignee: "北区班组",
duration_minutes: 30,
reply_window_minutes: 15,
stages: [
{ id: "dispatch", sequence: 1, title: "派发", status: "done", owner: "调度中心", time_label: "17:20", description: "已派发。" },
{ id: "execution", sequence: 2, title: "执行", status: "current", owner: "北区班组", time_label: "17:40-18:10", description: "现场复核中。" },
{ id: "reply", sequence: 3, title: "复令", status: "pending", owner: "北区班组", time_label: "18:10-18:25", description: "等待结果回传。" }
],
reply_requirements: ["回传阀门状态"]
}]
location: "北区阀门 V-17",
assignee: "北区班组",
feedback_requirements: ["回传阀门状态"],
activity: []
}]
}, undefined, {}, [], { runtimeMode: "history" });
@@ -450,23 +437,11 @@ test("keeps mock conditions while using backend work orders and alerts", async (
source: "生产工单系统",
dispatcher: "调度中心",
priority: "urgent",
subtasks: [{
id: "wo-backend-1-subtask-1",
title: "阀门开度复核",
summary: "现场复核阀门开度。",
status: "running",
scheduled_at: TODAY_11_00,
location: "北区阀门 V-1",
assignee: "北区班组",
duration_minutes: 45,
reply_window_minutes: 20,
stages: [
{ id: "dispatch", sequence: 1, title: "派发", status: "done", owner: "调度中心", time_label: "10:50", description: "已派发。" },
{ id: "execution", sequence: 2, title: "执行中", status: "current", owner: "北区班组", time_label: "11:00—11:45", description: "现场执行中。" },
{ id: "reply", sequence: 3, title: "待复令", status: "pending", owner: "北区班组", time_label: "11:45—12:05", description: "等待复令。" }
],
reply_requirements: ["回传阀门开度"]
}]
location: "北区阀门 V-1",
assignee: "北区班组",
feedback_requirements: ["回传阀门开度"],
activity: [{ sequence: 1, occurred_at: TODAY, actor: "调度中心", kind: "created",
title: "工单创建", detail: "现场复核阀门开度并回传。" }]
}]
});
await page.goto("/", { waitUntil: "domcontentloaded" });
@@ -481,12 +456,10 @@ 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 })).toBeVisible();
await expect(detail.getByText(/阀门开度复核/).first()).toBeVisible();
await expect(detail.getByText("0/1 完成", { exact: true })).toBeVisible();
await expect(
detail.locator("details").filter({ hasText: "阀门开度复核" }).getByText("执行中", { exact: true })
).toBeVisible();
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.locator("details")).toHaveCount(0);
const alertButton = page.getByRole("button", {
name: "查看异常处置面板,当前 1 条待复核工况"