Static fulfilled SSE bodies closed immediately and made ordinary UI cases reconnect. Serve persistent fixture streams, retain outage and cursor recovery coverage, and update legacy approval and investigation assertions for current disclosures and SVG diagrams. Validation: 12 related browser scenarios, 877 unit tests, type checking, scoped ESLint, and diff checks. Unrelated SCADA replay changes remain outside this commit.
1055 lines
60 KiB
TypeScript
1055 lines
60 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
import { mockWorkbenchEventStream } from "./support/mock-workbench-event-stream";
|
|
import { mockAgentApi } from "./support/mock-agent-api";
|
|
import { expectThemeColor, readRenderedContrast } from "./support/theme-colors";
|
|
|
|
test.describe("外围系统", () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
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: "324ff7ee-c2d4-5184-a84b-48fb14a2933e",
|
|
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 }));
|
|
});
|
|
|
|
test("外围面板只读取工单能力,不再请求监测回放与测试数据", async ({ page }, testInfo) => {
|
|
const requestedPaths: string[] = [];
|
|
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",
|
|
TJWATER_PERIPHERAL_SAMPLE_TIME_MODE: "demo"
|
|
})};`
|
|
})
|
|
);
|
|
await page.route("http://server.test/api/v1/projects**", (route) =>
|
|
route.fulfill({
|
|
json: {
|
|
items: [
|
|
{
|
|
project_id: "324ff7ee-c2d4-5184-a84b-48fb14a2933e",
|
|
gs_workspace: "tjwater_next",
|
|
status: "active"
|
|
}
|
|
],
|
|
total: 1,
|
|
limit: 1000,
|
|
offset: 0
|
|
}
|
|
})
|
|
);
|
|
const projectId = "324ff7ee-c2d4-5184-a84b-48fb14a2933e";
|
|
await page.route("http://peripheral.test/api/sample/v1/**", async (route) => {
|
|
const url = new URL(route.request().url());
|
|
requestedPaths.push(url.pathname);
|
|
if (url.pathname.endsWith("/context"))
|
|
return route.fulfill({
|
|
json: {
|
|
protocol_version: "tjwater-peripheral-sample@1",
|
|
origin: "system_data",
|
|
project_id: projectId,
|
|
user_id: "50000000-0000-4000-8000-000000000001",
|
|
can_write: true,
|
|
scenarios: ["normal"],
|
|
managed_resources: { work_orders: "available", test_data: "available" },
|
|
state_transitions: {
|
|
pending: ["processing"],
|
|
processing: ["pending_review"],
|
|
pending_review: ["processing", "completed"],
|
|
completed: []
|
|
}
|
|
}
|
|
});
|
|
if (url.pathname.endsWith("/work-orders"))
|
|
return route.fulfill({ json: { items: createSourceWorkOrders(projectId), next_cursor: null } });
|
|
return route.fulfill({
|
|
status: 404,
|
|
json: { code: "NOT_FOUND", message: "not found", effect: "none", request_id: null }
|
|
});
|
|
});
|
|
await page.goto("/");
|
|
await page.getByRole("button", { name: "外围系统", exact: true }).click();
|
|
const panel = page.getByRole("complementary", { name: "外围系统" });
|
|
await expect(panel).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.getByText("历史监测回放", { exact: true })).toHaveCount(0);
|
|
await expect(panel.getByText("测试数据", { exact: true })).toHaveCount(0);
|
|
const appointmentList = panel.getByLabel("预约工单列表");
|
|
const complaintList = panel.getByLabel("投诉工单列表");
|
|
const burstList = panel.getByLabel("爆管上报工单列表");
|
|
await expect(appointmentList).toContainText("合成通信核查");
|
|
await page.getByRole("button", { name: "关闭通知", exact: true }).evaluateAll((buttons) => buttons.forEach((button) => (button as HTMLButtonElement).click()));
|
|
await testInfo.attach("peripheral-work-orders", {
|
|
body: await page.screenshot({ path: testInfo.outputPath("work-orders.png") }), contentType: "image/png"
|
|
});
|
|
await expect(appointmentList).toContainText("合成压力偏差复核");
|
|
await expect(appointmentList).toContainText("合成零流量样本确认");
|
|
await expect(appointmentList).toContainText("待处理");
|
|
await expect(appointmentList).toContainText("处理中");
|
|
await expect(appointmentList).toContainText("已完成");
|
|
await expect(appointmentList.getByText("工单报告", { exact: true })).toHaveCount(3);
|
|
await expect(appointmentList.getByText("暂无现场反馈", { exact: true })).toHaveCount(3);
|
|
const appointmentDetails = appointmentList.getByLabel("工单详情 合成通信核查", { exact: true });
|
|
await expect(appointmentDetails.getByText("工单报告", { exact: true })).toBeHidden();
|
|
await appointmentDetails.locator(":scope > summary").click();
|
|
await expect(appointmentDetails.getByText("工单报告", { exact: true })).toBeVisible();
|
|
await appointmentDetails.locator(":scope > summary").click();
|
|
await expect(complaintList).toContainText("水质投诉:演示小区");
|
|
await expect(complaintList.getByRole("region", { name: "投诉工单信息" })).toContainText("发生时间");
|
|
await expect(complaintList.getByText("工单报告", { exact: true })).toHaveCount(0);
|
|
await expect(burstList).toContainText("爆管上报:演示路口");
|
|
await expect(burstList.getByRole("region", { name: "爆管上报工单信息" })).toContainText("发生时间");
|
|
await expect(complaintList).toHaveAttribute("open", "");
|
|
await complaintList.locator("summary").click();
|
|
await expect(complaintList).not.toHaveAttribute("open", "");
|
|
await expect(complaintList.getByText("水质投诉:演示小区")).toBeHidden();
|
|
await complaintList.locator("summary").click();
|
|
await expect(complaintList.getByText("水质投诉:演示小区")).toBeVisible();
|
|
expect(requestedPaths).toEqual(expect.arrayContaining([
|
|
"/api/sample/v1/context",
|
|
"/api/sample/v1/work-orders"
|
|
]));
|
|
expect(requestedPaths.some((path) => path.includes("/monitoring/") || path.endsWith("/test-data") || path.endsWith("/demo-data"))).toBe(false);
|
|
});
|
|
|
|
test("未配置时从顶栏进入且不暗连外围服务", async ({ page }) => {
|
|
const peripheralRequests: string[] = [];
|
|
page.on("request", (request) => {
|
|
if (request.url().includes("/api/sample/v1")) peripheralRequests.push(request.url());
|
|
});
|
|
await page.goto("/");
|
|
await page.getByRole("button", { name: "外围系统", exact: true }).click();
|
|
await expect(page.getByRole("complementary", { name: "外围系统" })).toBeVisible();
|
|
await expect(
|
|
page
|
|
.getByRole("complementary", { name: "外围系统" })
|
|
.getByText("录入业务工单并跟踪处理反馈,事件调查依据实际查询和现场记录开展。", { exact: true })
|
|
.last()
|
|
).toBeVisible();
|
|
await expect(page.getByText(/当前不会连接外围工单服务/)).toBeVisible();
|
|
expect(peripheralRequests).toEqual([]);
|
|
});
|
|
|
|
test("SCADA 系统按当前项目读取最新记录并设定模拟异常", async ({ page }, testInfo) => {
|
|
const latestRequests: string[] = [];
|
|
const simulatedWrites: Array<Record<string, unknown>> = [];
|
|
const projectId = "324ff7ee-c2d4-5184-a84b-48fb14a2933e";
|
|
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"
|
|
})};`
|
|
})
|
|
);
|
|
await page.route("http://peripheral.test/api/sample/v1/**", (route) => {
|
|
const request = route.request();
|
|
const path = new URL(request.url()).pathname;
|
|
if (path.endsWith("/context")) return route.fulfill({ json: {
|
|
protocol_version: "tjwater-peripheral-sample@1", origin: "system_data", project_id: projectId,
|
|
user_id: "50000000-0000-4000-8000-000000000001", can_write: true,
|
|
scenarios: ["normal"], managed_resources: { work_orders: "available", test_data: "available" },
|
|
state_transitions: { pending: [], processing: [], pending_review: [], completed: [] }
|
|
} });
|
|
if (path.endsWith("/test-data") && request.method() === "GET") {
|
|
return route.fulfill({ json: { items: [], next_cursor: null } });
|
|
}
|
|
if (path.endsWith("/test-data") && request.method() === "POST") {
|
|
const body = request.postDataJSON() as Record<string, unknown>;
|
|
simulatedWrites.push(body);
|
|
return route.fulfill({ status: 201, json: {
|
|
...body,
|
|
record_id: "50000000-0000-4000-8000-000000000017",
|
|
project_id: projectId,
|
|
revision: 1,
|
|
origin: body.origin,
|
|
created_at: "2026-10-22T02:03:01Z",
|
|
updated_at: "2026-10-22T02:03:01Z",
|
|
created_by: "tester",
|
|
updated_by: "tester"
|
|
} });
|
|
}
|
|
if (path.endsWith("/work-orders")) {
|
|
return route.fulfill({ json: { items: [], next_cursor: null } });
|
|
}
|
|
return route.fulfill({ status: 404, json: {} });
|
|
});
|
|
await page.route("http://server.test/api/v1/timeseries/scada-readings/latest?**", (route) => {
|
|
latestRequests.push(route.request().headers()["x-project-id"] ?? "");
|
|
return route.fulfill({
|
|
json: {
|
|
items: [
|
|
{
|
|
device_id: "PRESSURE-17",
|
|
device_type: "pressure",
|
|
measurement_unit: "MPa",
|
|
transmission_mode: "realtime",
|
|
transmission_frequency: "15min",
|
|
observed_at: "2026-10-22T01:02:03Z",
|
|
monitored_value: 0.3172,
|
|
cleaned_value: 0.315
|
|
},
|
|
{
|
|
device_id: "FLOW-09",
|
|
device_type: "pipe_flow",
|
|
measurement_unit: "m³/h",
|
|
transmission_mode: "realtime",
|
|
transmission_frequency: "15min",
|
|
observed_at: null,
|
|
monitored_value: null,
|
|
cleaned_value: null
|
|
}
|
|
],
|
|
total: 2,
|
|
limit: 500,
|
|
offset: 0,
|
|
observed_count: 1,
|
|
latest_observed_at: "2026-10-22T01:02:03Z"
|
|
}
|
|
});
|
|
});
|
|
|
|
await page.goto("/");
|
|
await page.getByRole("button", { name: "外围系统", exact: true }).click();
|
|
const panel = page.getByRole("complementary", { name: "外围系统" });
|
|
await panel.getByRole("tab", { name: "SCADA 系统" }).click();
|
|
|
|
await expect(panel.getByRole("table", { name: "SCADA 最新数据表" })).toBeVisible();
|
|
await expect(panel.getByText("PRESSURE-17", { exact: true })).toBeVisible();
|
|
await expect(panel.getByText("0.3172 MPa", { exact: true })).toBeVisible();
|
|
await expect(panel.getByText("0.315 MPa", { exact: true })).toBeVisible();
|
|
await expect(panel.getByText("FLOW-09", { exact: true })).toBeVisible();
|
|
await expect(panel.getByText("无记录", { exact: true })).toBeVisible();
|
|
await expect(panel.getByLabel("SCADA 最新数据概览")).toContainText(/1\s*\/\s*2 个设备/);
|
|
expect(latestRequests).toEqual(["324ff7ee-c2d4-5184-a84b-48fb14a2933e"]);
|
|
|
|
await expect(panel.getByPlaceholder("输入需要演练的异常值")).toHaveCount(0);
|
|
await testInfo.attach("scada-overview", {
|
|
body: await page.screenshot({ path: testInfo.outputPath("scada-overview.png") }), contentType: "image/png"
|
|
});
|
|
|
|
await panel.getByRole("textbox", { name: "筛选 SCADA 设备" }).fill("pressure");
|
|
await expect(panel.getByText("PRESSURE-17", { exact: true })).toBeVisible();
|
|
await expect(panel.getByText("FLOW-09", { exact: true })).toHaveCount(0);
|
|
await panel.getByRole("textbox", { name: "筛选 SCADA 设备" }).fill("");
|
|
|
|
await panel.getByRole("button", { name: "设定读数" }).click();
|
|
for (const width of [1280, 375, 320]) {
|
|
await page.setViewportSize({ width, height: 900 });
|
|
await panel.getByPlaceholder("输入需要演练的异常值").scrollIntoViewIfNeeded();
|
|
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(width);
|
|
await testInfo.attach(`scada-peripheral-anomaly-${width}`, {
|
|
body: await page.screenshot({ path: testInfo.outputPath(`scada-${width}.png`) }),
|
|
contentType: "image/png"
|
|
});
|
|
}
|
|
await panel.getByPlaceholder("输入需要演练的异常值").fill("0.08");
|
|
await panel.getByPlaceholder("例如:压力突降,偏离近一小时稳定区间").fill("压力突降");
|
|
await expectThemeColor(panel.getByPlaceholder("输入需要演练的异常值"), "color", "--ink");
|
|
await expectThemeColor(panel.getByPlaceholder("例如:压力突降,偏离近一小时稳定区间"), "color", "--ink");
|
|
await panel.getByRole("button", { name: "保存模拟读数", exact: true }).click();
|
|
await expect(panel.getByLabel("SCADA 模拟异常列表")).toContainText("PRESSURE-17");
|
|
await expect(panel.getByLabel("SCADA 模拟异常列表")).toContainText("0.08 MPa");
|
|
expect(simulatedWrites[0]).toMatchObject({
|
|
point_id: "PRESSURE-17",
|
|
value: 0.08,
|
|
quality: "valid",
|
|
notes: "[SCADA_ANOMALY] 压力突降"
|
|
});
|
|
});
|
|
|
|
test("项目分析成果打开时保留地图遮挡", async ({ page }) => {
|
|
await page.goto("/");
|
|
|
|
await page.locator(".maplibregl-canvas-container").evaluate((element) => {
|
|
const probeWindow = window as Window & { __mapPointerEvents?: string[] };
|
|
probeWindow.__mapPointerEvents = [];
|
|
for (const type of ["pointerdown", "wheel"] as const) {
|
|
element.addEventListener(type, () => probeWindow.__mapPointerEvents?.push(type), true);
|
|
}
|
|
});
|
|
|
|
const expectMapInteractionBlocked = async (panelName: string) => {
|
|
const panel = page.getByRole("complementary", { name: panelName });
|
|
const [panelBox, canvasBox] = await Promise.all([
|
|
panel.boundingBox(),
|
|
page.locator(".maplibregl-canvas").boundingBox()
|
|
]);
|
|
expect(panelBox).not.toBeNull();
|
|
expect(canvasBox).not.toBeNull();
|
|
|
|
const x = panelBox!.x - 24;
|
|
const y = panelBox!.y + 120;
|
|
expect(x).toBeGreaterThan(canvasBox!.x);
|
|
expect(y).toBeLessThan(canvasBox!.y + canvasBox!.height);
|
|
await expect
|
|
.poll(() =>
|
|
page.evaluate(
|
|
({ x: pointX, y: pointY }) =>
|
|
document.elementFromPoint(pointX, pointY)?.hasAttribute("data-map-interaction-shield"),
|
|
{ x, y }
|
|
)
|
|
)
|
|
.toBe(true);
|
|
|
|
await page.mouse.click(x, y);
|
|
await page.mouse.wheel(0, 120);
|
|
await expect
|
|
.poll(() =>
|
|
page.evaluate(
|
|
() => (window as Window & { __mapPointerEvents?: string[] }).__mapPointerEvents
|
|
)
|
|
)
|
|
.toEqual([]);
|
|
};
|
|
|
|
await page.getByRole("button", { name: "项目分析成果" }).click();
|
|
await expect(page.getByRole("complementary", { name: "项目分析成果" })).toBeVisible();
|
|
await expectMapInteractionBlocked("项目分析成果");
|
|
});
|
|
|
|
test("增量存储未供给时保留原有外围能力且不请求缺表资源", async ({ page }) => {
|
|
const projectId = "324ff7ee-c2d4-5184-a84b-48fb14a2933e";
|
|
const managedRequests: string[] = [];
|
|
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"
|
|
})}`
|
|
}));
|
|
await page.route("http://peripheral.test/api/sample/v1/**", (route) => {
|
|
const path = new URL(route.request().url()).pathname;
|
|
if (path.includes("/work-orders") || path.includes("/test-data")) {
|
|
managedRequests.push(path);
|
|
return route.fulfill({ status: 503, json: {} });
|
|
}
|
|
if (path.endsWith("/context")) return route.fulfill({ json: {
|
|
protocol_version: "tjwater-peripheral-sample@1", origin: "system_data", project_id: projectId,
|
|
user_id: "50000000-0000-4000-8000-000000000001", can_write: true,
|
|
scenarios: ["normal"], managed_resources: {
|
|
work_orders: "not_configured", test_data: "not_configured"
|
|
}, state_transitions: { pending: [], processing: [], pending_review: [], completed: [] }
|
|
} });
|
|
if (path.endsWith("/scenario")) return route.fulfill({ json: { scenario: "normal", revision: 1 } });
|
|
if (path.endsWith("/tasks")) return route.fulfill({ json: { items: [], next_cursor: null } });
|
|
if (path.endsWith("/monitoring/points")) return route.fulfill({ json: {
|
|
items: [], next_cursor: null, origin: "historical_replay", source_id: "tjwater_v2"
|
|
} });
|
|
if (path.endsWith("/monitoring/alerts")) return route.fulfill({ json: {
|
|
items: [], availability: "not_configured", reason: "告警源未配置",
|
|
origin: "historical_replay", source_id: "tjwater_v2"
|
|
} });
|
|
return route.fulfill({ status: 404, json: {} });
|
|
});
|
|
|
|
await page.goto("/");
|
|
await page.getByRole("button", { name: "外围系统" }).click();
|
|
const panel = page.getByRole("complementary", { name: "外围系统" });
|
|
await expect(panel.getByText("工单服务未就绪", { exact: true })).toBeVisible();
|
|
await expect(panel.getByText("测试数据存储待供给", { exact: true })).toHaveCount(0);
|
|
await expect(panel.getByText("历史监测回放", { exact: true })).toHaveCount(0);
|
|
expect(managedRequests).toEqual([]);
|
|
});
|
|
|
|
test("创建编辑真实过程、刷新外部更新并软删除预约工单", async ({ page }, testInfo) => {
|
|
test.setTimeout(60_000);
|
|
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" })}`
|
|
}));
|
|
await page.route("http://peripheral.test/api/sample/v1/**", async (route) => {
|
|
const request = route.request();
|
|
const url = new URL(request.url());
|
|
const path = url.pathname;
|
|
const now = "2026-09-18T00:00:00Z";
|
|
if (path.endsWith("/context")) return route.fulfill({ json: { protocol_version: "tjwater-peripheral-sample@1", origin: "system_data", project_id: projectId, user_id: "50000000-0000-4000-8000-000000000001", can_write: true, scenarios: ["normal"], managed_resources: { work_orders: "available", test_data: "available" }, state_transitions: { pending: ["processing"], processing: ["pending_review"], pending_review: ["processing", "completed"], completed: [] } } });
|
|
if (path.endsWith("/work-orders") && request.method() === "GET") return route.fulfill({ json: { items: workOrders, next_cursor: null } });
|
|
if (path.endsWith("/work-orders") && request.method() === "POST") {
|
|
const body = request.postDataJSON();
|
|
writes.push(body);
|
|
const createdDetail = body.work_order_type === "water_quality_complaint"
|
|
? "投诉工单已创建。"
|
|
: body.work_order_type === "burst_report"
|
|
? "爆管上报工单已创建。"
|
|
: "预约工单已创建。";
|
|
const item = { ...body, work_order_id: "50000000-0000-4000-8000-000000000001", project_id: projectId, revision: 1, origin: body.origin, created_at: now, updated_at: now, created_by: "tester", updated_by: "tester", activity: [{ sequence: 1, occurred_at: now, actor: "tester", kind: "created", title: "工单创建", detail: createdDetail }] };
|
|
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", 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;
|
|
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] });
|
|
}
|
|
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 } });
|
|
});
|
|
|
|
await page.goto("/");
|
|
await page.getByRole("button", { name: "外围系统" }).click();
|
|
const panel = page.getByRole("complementary", { name: "外围系统" });
|
|
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.getByLabel("预约工单列表")).toContainText("暂无预约工单");
|
|
await expect(panel.getByLabel("投诉工单列表")).toContainText("暂无投诉工单");
|
|
await expect(panel.getByRole("textbox", { name: "标题" })).toBeHidden();
|
|
await panel.getByRole("button", { name: "新增预约工单" }).click();
|
|
let editor = panel.getByRole("region", { name: "新增预约工单" });
|
|
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: "位置", exact: true }).fill("联调测试点西侧");
|
|
await editor.getByRole("textbox", { name: "场景编号" }).fill("APPOINTMENT-TEST-001");
|
|
// Portaled calendar and select menus keep the solid panel theme and keyboard focus.
|
|
await editor.getByRole("button", { name: "预约时间", exact: true }).click();
|
|
const calendar = page.getByRole("dialog", { name: "选择预约时间", exact: true });
|
|
await expect(calendar).toBeVisible();
|
|
await expectThemeColor(calendar.getByRole("textbox", { name: "年份", exact: true }), "color", "--ink");
|
|
await expect(calendar).toHaveCSS("background-color", "rgb(255, 255, 255)");
|
|
await calendar.getByRole("combobox", { name: "月份", exact: true }).click();
|
|
await expect(page.getByRole("listbox")).toHaveCSS("background-color", "rgb(255, 255, 255)");
|
|
await page.keyboard.press("Escape");
|
|
await expect(calendar.getByRole("combobox", { name: "月份", exact: true })).toBeFocused();
|
|
await testInfo.attach("peripheral-calendar", {
|
|
body: await page.screenshot({ path: testInfo.outputPath("peripheral-calendar.png") }),
|
|
contentType: "image/png"
|
|
});
|
|
await page.keyboard.press("Escape");
|
|
await expect(editor.getByRole("button", { name: "预约时间", exact: true })).toBeFocused();
|
|
await editor.getByRole("textbox", { name: "回执要求(每行一项)" }).fill("回传现场处理结果\n上传照片");
|
|
await editor.getByRole("button", { name: "创建预约工单" }).click();
|
|
await expect(panel.getByText("合成预约维修", { exact: true })).toBeVisible();
|
|
expect(writes[0]).toMatchObject({
|
|
work_order_type: "general",
|
|
assignee: "测试班组甲",
|
|
location: "联调测试点西侧",
|
|
scenario_code: "APPOINTMENT-TEST-001",
|
|
feedback_requirements: ["回传现场处理结果", "上传照片"]
|
|
});
|
|
|
|
await panel.getByRole("button", { name: "编辑工单 合成预约维修" }).click();
|
|
editor = panel.getByRole("region", { name: "编辑工单 合成预约维修" });
|
|
await expect(panel.getByLabel("预约工单列表").getByRole("region", { name: "编辑工单 合成预约维修" })).toBeVisible();
|
|
await testInfo.attach("work-order-editor-text", {
|
|
body: await editor.screenshot({ path: testInfo.outputPath("work-order-editor.png") }), contentType: "image/png"
|
|
});
|
|
console.log("work-order-title-contrast", await readRenderedContrast(editor.getByRole("textbox", { name: "标题" })));
|
|
for (const field of await editor.getByRole("textbox").all()) {
|
|
await expectThemeColor(field, "color", "--ink");
|
|
await expect(field).toHaveCSS("opacity", "1");
|
|
}
|
|
await page.setViewportSize({ width: 375, height: 900 });
|
|
await testInfo.attach("work-order-editor-text-narrow", {
|
|
body: await editor.screenshot({ path: testInfo.outputPath("work-order-editor-375.png") }), contentType: "image/png"
|
|
});
|
|
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(375);
|
|
await page.setViewportSize({ width: 1440, height: 900 });
|
|
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("联调测试点东侧");
|
|
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 expect(panel.getByText("合成预约维修(已对齐)", { exact: true })).toBeVisible();
|
|
expect(writes[1]).toMatchObject({
|
|
status: "awaiting_reply",
|
|
assignee: "测试班组乙",
|
|
location: "联调测试点东侧",
|
|
clear_assignee: false,
|
|
clear_location: false,
|
|
progress_note: "班组乙已确认东侧作业窗口,等待复核。",
|
|
feedback_requirements: ["回传现场处理结果", "上传照片"]
|
|
});
|
|
expect(workOrders[0]).toMatchObject({ work_order_type: "general", scenario_code: "APPOINTMENT-TEST-001" });
|
|
|
|
workOrders = [{ ...workOrders[0], title: "外部系统更新后的预约维修", revision: 3 }];
|
|
await panel.getByRole("button", { name: "刷新工单" }).click();
|
|
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.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: testInfo.outputPath(`peripheral-work-order-edit-${width}.png`) }),
|
|
contentType: "image/png"
|
|
});
|
|
}
|
|
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) =>
|
|
route.fulfill({ status: 503, json: { detail: "operational task test stub" } })
|
|
);
|
|
await panel.getByRole("button", { name: "投诉与爆管上报" }).click();
|
|
await panel.getByRole("combobox", { name: "事件类型" }).click();
|
|
await page.getByRole("option", { name: "爆管上报", exact: true }).click();
|
|
await panel.getByRole("textbox", { name: "事件位置" }).fill("演示路与样板路交叉口");
|
|
await panel.getByRole("textbox", { name: "现场描述" }).fill("路面积水并伴随压力突降");
|
|
await expectThemeColor(panel.getByRole("textbox", { name: "事件位置" }), "color", "--ink");
|
|
await expectThemeColor(panel.getByRole("textbox", { name: "现场描述" }), "color", "--ink");
|
|
await panel.getByRole("button", { name: "创建事件工单" }).click();
|
|
await expect(panel.getByText("爆管上报工单", { exact: true }).last()).toBeVisible();
|
|
await expect(panel.getByLabel("爆管上报工单列表")).toContainText("爆管上报:演示路与样板路交叉口");
|
|
await expect(panel.getByLabel("爆管上报工单列表")).toContainText("爆管上报工单已创建。");
|
|
expect(workOrders[0]).toMatchObject({
|
|
work_order_type: "burst_report",
|
|
scenario_code: null,
|
|
origin: "system_data",
|
|
title: "爆管上报:演示路与样板路交叉口",
|
|
status: "open",
|
|
priority: "urgent"
|
|
});
|
|
|
|
});
|
|
|
|
for (const [kind, label] of [["water_quality_complaint", "水质投诉"], ["burst_report", "爆管上报"]]) {
|
|
test(`${label}业务录入使用真实来源且不附加演练场景`, async ({ page }) => {
|
|
const projectId = "324ff7ee-c2d4-5184-a84b-48fb14a2933e";
|
|
const orders: Record<string, unknown>[] = [];
|
|
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"
|
|
})}`
|
|
}));
|
|
await page.route("http://peripheral.test/api/sample/v1/**", async (route) => {
|
|
const request = route.request();
|
|
const path = new URL(request.url()).pathname;
|
|
if (path.endsWith("/context")) return route.fulfill({ json: {
|
|
protocol_version: "tjwater-peripheral-sample@1", origin: "system_data", project_id: projectId,
|
|
user_id: "50000000-0000-4000-8000-000000000001", can_write: true, scenarios: ["normal"],
|
|
managed_resources: { work_orders: "available", test_data: "available" }, state_transitions: {}
|
|
} });
|
|
if (path.endsWith("/work-orders") && request.method() === "POST") {
|
|
const body = request.postDataJSON() as Record<string, unknown>;
|
|
expect(body).toMatchObject({ work_order_type: kind, origin: "system_data", scenario_code: null });
|
|
const order = { ...body, work_order_id: "50000000-0000-4000-8000-000000000001",
|
|
project_id: projectId, revision: 1, created_at: "2026-10-29T01:00:00Z", updated_at: "2026-10-29T01:00:00Z",
|
|
created_by: "operator", updated_by: "operator", activity: [] };
|
|
delete order.request_id;
|
|
orders.push(order);
|
|
return route.fulfill({ status: 201, json: order });
|
|
}
|
|
if (path.endsWith("/work-orders")) return route.fulfill({ json: { items: orders, next_cursor: null } });
|
|
return route.fulfill({ status: 404, json: {} });
|
|
});
|
|
// Keep this check bounded to peripheral intake; investigation presentation has separate coverage.
|
|
await page.route("http://server.test/api/v1/operational-tasks", (route) =>
|
|
route.fulfill({ status: 503, json: { detail: "investigation unavailable in intake test" } }));
|
|
await page.goto("/");
|
|
await page.getByRole("button", { name: "外围系统", exact: true }).click();
|
|
const panel = page.getByRole("complementary", { name: "外围系统" });
|
|
await panel.getByRole("button", { name: "投诉与爆管上报" }).click();
|
|
if (kind === "burst_report") {
|
|
await panel.getByRole("combobox", { name: "事件类型" }).click();
|
|
await page.getByRole("option", { name: label, exact: true }).click();
|
|
}
|
|
await panel.getByRole("textbox", { name: "事件位置" }).fill("供水小区");
|
|
await panel.getByRole("textbox", { name: "现场描述" }).fill("用户上报异常,待实际数据核查");
|
|
await panel.getByRole("button", { name: "创建事件工单" }).click();
|
|
await expect(panel.getByRole("heading", { name: `${label}:供水小区`, exact: true })).toBeVisible();
|
|
await expect(panel.getByText("工单已创建,调查任务待触发", { exact: true })).toBeVisible();
|
|
await expect(panel.getByText("合成工单环境", { exact: true })).toHaveCount(0);
|
|
expect(orders).toHaveLength(1);
|
|
});
|
|
}
|
|
|
|
test("创建水质投诉工单后进入结构化工况任务并由用户打开 Agent", async ({ page }, testInfo) => {
|
|
test.setTimeout(90_000);
|
|
const projectId = "324ff7ee-c2d4-5184-a84b-48fb14a2933e";
|
|
const workOrderId = "50000000-0000-4000-8000-000000000099";
|
|
const agentRequests: Array<Record<string, unknown>> = [];
|
|
let workOrders: Record<string, unknown>[] = [];
|
|
const plan = {
|
|
schema: "execution-plan@1", title: "水质投诉调查计划", reason: "先完成四路并行核查,汇合后再分两路核实",
|
|
nodes: [
|
|
{ id: "accept", title: "接收投诉工单", status: "completed", owner: "agent", detail: "已关联合成工单", evidence_refs: ["e-1"], action_refs: [] },
|
|
{ id: "upstream", title: "校验上游水质监测", status: "completed", owner: "agent", detail: "上游三个点位指标正常", evidence_refs: ["e-1"], action_refs: [] },
|
|
{ id: "upstream-review", title: "复核上游趋势", status: "completed", owner: "agent", detail: "过去两小时趋势稳定", evidence_refs: ["e-1"], action_refs: [] },
|
|
{ id: "nearby", title: "核对邻近投诉", status: "running", owner: "agent", detail: "检查投诉空间分布", evidence_refs: [], action_refs: [] },
|
|
{ id: "pressure", title: "比对压力波动", status: "completed", owner: "agent", detail: "泵房出水压力稳定", evidence_refs: [], action_refs: [] },
|
|
{ id: "hydraulic", title: "校核水力路径", status: "running", owner: "agent", detail: "追踪供水路径", evidence_refs: [], action_refs: [] },
|
|
{ id: "valve", title: "排查阀门操作", status: "pending", owner: "agent", detail: "等待水力路径结果", evidence_refs: [], action_refs: [] },
|
|
{ id: "maintenance", title: "核对运维记录", status: "waiting", owner: "human", detail: "等待近期冲洗记录", evidence_refs: [], action_refs: [] },
|
|
{ id: "stage-judgment", title: "阶段性判断", status: "completed", owner: "agent", detail: "疑似二次供水环节问题,待现场核验。", evidence_refs: ["e-1"], action_refs: [] },
|
|
{ id: "secondary", title: "核查二次供水", status: "running", owner: "agent", detail: "核对二次供水设施", evidence_refs: [], action_refs: [] },
|
|
{ id: "tank-record", title: "核对水箱记录", status: "waiting", owner: "human", detail: "等待水箱清洗记录", evidence_refs: [], action_refs: [] },
|
|
{ id: "sampling", title: "现场采样", status: "waiting", owner: "human", detail: "等待现场采样结果", evidence_refs: [], action_refs: [] },
|
|
{ id: "conclusion", title: "形成调查结论", status: "pending", owner: "agent", detail: "两路核验完成后汇总", evidence_refs: [], action_refs: [] }
|
|
],
|
|
edges: [
|
|
{ from: "accept", to: "upstream", condition: "上游监测" }, { from: "upstream", to: "upstream-review", condition: null }, { from: "upstream-review", to: "stage-judgment", condition: null },
|
|
{ from: "accept", to: "nearby", condition: "邻近投诉" }, { from: "nearby", to: "stage-judgment", condition: null },
|
|
{ from: "accept", to: "pressure", condition: "压力路径" }, { from: "pressure", to: "hydraulic", condition: null }, { from: "hydraulic", to: "valve", condition: null }, { from: "valve", to: "stage-judgment", condition: null },
|
|
{ from: "accept", to: "maintenance", condition: "运维记录" }, { from: "maintenance", to: "stage-judgment", condition: null },
|
|
{ from: "stage-judgment", to: "secondary", condition: "设施核验" }, { from: "secondary", to: "tank-record", condition: null }, { from: "tank-record", to: "conclusion", condition: null },
|
|
{ from: "stage-judgment", to: "sampling", condition: "现场采样" }, { from: "sampling", to: "conclusion", condition: null }
|
|
]
|
|
};
|
|
let operationalTask: Record<string, unknown> | null = null;
|
|
const summarizeTask = () => operationalTask
|
|
? Object.fromEntries(Object.entries(operationalTask).filter(([key]) => ![
|
|
"latest_plan", "latest_report", "evidence", "actions", "permissions", "work_order_snapshot"
|
|
].includes(key)))
|
|
: null;
|
|
page.on("request", (request) => {
|
|
if (
|
|
request.method() === "POST" &&
|
|
request.url().includes("/api/v1/") &&
|
|
request.postData()
|
|
) {
|
|
agentRequests.push(request.postDataJSON() as Record<string, unknown>);
|
|
}
|
|
});
|
|
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_AGENT_URL: "http://127.0.0.1:8787",
|
|
TJWATER_PERIPHERAL_SAMPLE_API_BASE_URL: "http://peripheral.test"
|
|
})}`
|
|
}));
|
|
await page.route("http://peripheral.test/api/sample/v1/**", async (route) => {
|
|
const request = route.request();
|
|
const path = new URL(request.url()).pathname;
|
|
if (path.endsWith("/context")) return route.fulfill({ json: {
|
|
protocol_version: "tjwater-peripheral-sample@1", origin: "system_data",
|
|
project_id: projectId, user_id: "50000000-0000-4000-8000-000000000001",
|
|
can_write: true, scenarios: ["normal"],
|
|
managed_resources: { work_orders: "available", test_data: "available" },
|
|
state_transitions: { pending: [], processing: [], pending_review: [], completed: [] }
|
|
} });
|
|
if (path.endsWith("/scenario")) return route.fulfill({ json: { scenario: "normal", revision: 1 } });
|
|
if (path.endsWith("/monitoring/points")) return route.fulfill({ json: { items: [], next_cursor: null, origin: "historical_replay", source_id: "test" } });
|
|
if (path.endsWith("/monitoring/alerts")) return route.fulfill({ json: { items: [], availability: "not_configured", reason: "未配置", origin: "historical_replay", source_id: "test" } });
|
|
if (path.endsWith("/work-orders") && request.method() === "GET") return route.fulfill({ json: { items: workOrders, next_cursor: null } });
|
|
if (path.endsWith("/work-orders") && request.method() === "POST") {
|
|
const body = request.postDataJSON() as Record<string, unknown>;
|
|
const item = {
|
|
...body,
|
|
work_order_id: workOrderId,
|
|
project_id: projectId,
|
|
revision: 1,
|
|
origin: body.origin,
|
|
created_at: "2026-09-22T02:00:00Z",
|
|
updated_at: "2026-09-22T02:00:00Z",
|
|
created_by: "tester",
|
|
updated_by: "tester"
|
|
};
|
|
delete item.request_id;
|
|
workOrders = [item];
|
|
return route.fulfill({ status: 201, json: item });
|
|
}
|
|
if (path.endsWith("/test-data")) return route.fulfill({ json: { items: [], next_cursor: null } });
|
|
return route.fulfill({ status: 404, json: { code: "NOT_FOUND", message: "not found", effect: "none", request_id: null } });
|
|
});
|
|
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/schedules", (route) =>
|
|
route.fulfill({ json: { items: [] } })
|
|
);
|
|
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: "2026-09-23T08:30:00Z",
|
|
conditions: [], alerts: [], scenarios: [], latest_analysis: null, work_orders: []
|
|
},
|
|
tasks: summarizeTask() ? [summarizeTask()] : [],
|
|
notifications: [],
|
|
generated_at: "2026-09-23T08:30:00Z"
|
|
} })
|
|
);
|
|
await mockWorkbenchEventStream(page);
|
|
await page.route("http://server.test/api/v1/operational-tasks**", async (route) => {
|
|
const request = route.request();
|
|
const url = new URL(request.url());
|
|
if (request.method() === "GET" && url.pathname.endsWith("/operational-tasks/schedules")) {
|
|
return route.fulfill({ json: { items: [] } });
|
|
}
|
|
if (request.method() === "POST" && url.pathname.endsWith("/operational-tasks")) {
|
|
const input = request.postDataJSON() as Record<string, unknown>;
|
|
operationalTask = {
|
|
project_id: projectId, task_id: "70000000-0000-4000-8000-000000000001",
|
|
task_type: "event_investigation", workflow: "water_quality_complaint",
|
|
lifecycle_status: "waiting_human", active_run: null,
|
|
status: "waiting_human", priority: "normal",
|
|
revision: 2, execution_generation: 1, execution_request_id: "71000000-0000-4000-8000-000000000001",
|
|
title: "水质投诉调查", summary: input.summary, source: input.source, trigger: input.trigger,
|
|
scheduled_for: null, agent_session_id: "browser-session-1", agent_lease_expires_at: null,
|
|
current_plan_revision: 1, current_report_revision: 1,
|
|
created_at: "2026-09-23T08:20:00Z", updated_at: "2026-09-23T08:25:00Z",
|
|
latest_plan: { revision: 1, execution_generation: 1, source_session_id: "browser-session-1", published_at: "2026-09-23T01:08:00Z", published_by: "agent", plan },
|
|
latest_report: { revision: 1, execution_generation: 1, source_session_id: "browser-session-1", published_at: "2026-09-23T01:09:00Z", published_by: "agent", report: { title: "水质投诉调查报告", current_conclusion: "疑似二次供水环节问题,待现场核验。", conclusion_boundary: "现场采样和水箱清洗记录尚未返回。", evidence_refs: ["e-1"], recommendations: ["安排现场采样"], pending_items: ["确认水箱清洗记录"] } },
|
|
evidence: [{ evidence_id: "e-1", kind: "synthetic", summary: "上游余氯处于正常范围", source_label: "外围合成场景", source_ref: "WQ-DEMO-01", observed_at: "2026-09-23T00:55:00Z", recorded_at: "2026-09-23T01:08:00Z", recorded_by: "agent", has_snapshot: false }],
|
|
actions: [], 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 }
|
|
};
|
|
return route.fulfill({ status: 201, json: operationalTask });
|
|
}
|
|
if (request.method() === "GET" && url.pathname.endsWith("/operational-tasks")) {
|
|
const summary = summarizeTask();
|
|
return route.fulfill({ json: { items: summary ? [summary] : [], limit: 50, offset: 0, next_offset: null } });
|
|
}
|
|
if (request.method() === "GET" && operationalTask) return route.fulfill({ json: operationalTask });
|
|
return route.fulfill({ status: 404, json: {} });
|
|
});
|
|
|
|
await page.goto("/");
|
|
await page.getByRole("button", { name: "外围系统" }).click();
|
|
const panel = page.getByRole("complementary", { name: "外围系统" });
|
|
await panel.getByRole("button", { name: "投诉与爆管上报" }).click();
|
|
await panel.getByRole("textbox", { name: "事件位置" }).fill("演示小区");
|
|
await panel.getByRole("textbox", { name: "现场描述" }).fill("多户居民反映晨间用水有异味");
|
|
await panel.getByRole("button", { name: "创建事件工单" }).click();
|
|
|
|
await expect(panel).toBeHidden();
|
|
await expect(page.getByRole("complementary", { name: "Agent 命令面板" })).toBeHidden();
|
|
const conditionPanel = page.getByRole("region", { name: "工况任务", exact: true });
|
|
await expect(conditionPanel).toBeVisible();
|
|
await conditionPanel.getByRole("button", { name: /调查任务/ }).click();
|
|
await expect(conditionPanel.getByRole("heading", { name: "水质投诉调查", exact: true })).toBeVisible();
|
|
const expectSavedPlan = async (taskPanel: typeof conditionPanel) => {
|
|
const diagram = taskPanel.getByRole("region", { name: "调查脉络", exact: true });
|
|
await diagram.scrollIntoViewIfNeeded();
|
|
await expect(diagram.locator("svg g.node")).toHaveCount(plan.nodes.length);
|
|
await expect(diagram.locator("svg .flowchart-link")).toHaveCount(plan.edges.length);
|
|
await expect(diagram.getByLabel("图示状态说明")).toContainText("未标注状态");
|
|
await expect(diagram.locator("svg g.node").filter({ hasText: "形成调查结论" })).toBeVisible();
|
|
return diagram;
|
|
};
|
|
const diagram = await expectSavedPlan(conditionPanel);
|
|
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);
|
|
await expect(diagram.locator("svg g.node").filter({ hasText: "核对水箱记录" })).toBeVisible();
|
|
const report = conditionPanel.getByRole("region", { name: "任务报告" });
|
|
await expect(report.getByText(/疑似二次供水环节问题,待现场核验/)).toBeVisible();
|
|
await expect(report.getByText("完整调查报告", { exact: true })).toHaveCount(0);
|
|
await expect(report.getByText(/结论边界/)).toBeVisible();
|
|
await expect(conditionPanel.getByText("TJWater operational task", { exact: true })).toHaveCount(0);
|
|
await expect(conditionPanel.getByText("browser-session-1", { exact: true })).toHaveCount(0);
|
|
await diagram.scrollIntoViewIfNeeded();
|
|
await expect(conditionPanel.getByText("前置工作与条件", { exact: true })).toHaveCount(0);
|
|
await diagram.scrollIntoViewIfNeeded();
|
|
await expect(page.getByText("已建立水质投诉调查", { exact: true })).toBeHidden({ timeout: 10_000 });
|
|
await page.evaluate(() => document.fonts.ready);
|
|
await testInfo.attach("water-quality-condition-task-1280", {
|
|
body: await conditionPanel.screenshot({ path: testInfo.outputPath("execution-plan-desktop.png") }),
|
|
contentType: "image/png"
|
|
});
|
|
await diagram.locator("svg g.node").filter({ hasText: "形成调查结论" }).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"
|
|
});
|
|
for (const width of [375, 320]) {
|
|
await page.setViewportSize({ width, height: 720 });
|
|
const responsiveConditionPanel = page.getByRole("region", { name: "工况任务", exact: true });
|
|
if (!await responsiveConditionPanel.isVisible()) {
|
|
await page.getByRole("button", { name: "打开工况任务", exact: true }).click();
|
|
}
|
|
await expect(responsiveConditionPanel).toBeVisible();
|
|
const expandResponsiveTask = responsiveConditionPanel.getByRole("button", { name: "展开工况任务" });
|
|
if (await expandResponsiveTask.isVisible()) {
|
|
await expandResponsiveTask.click();
|
|
}
|
|
await responsiveConditionPanel.getByRole("button", { name: /调查任务/ }).click();
|
|
await expect(responsiveConditionPanel.getByRole("heading", { name: "水质投诉调查" })).toBeVisible();
|
|
const responsiveDiagram = await expectSavedPlan(responsiveConditionPanel);
|
|
const responsiveWorkflowStep = responsiveDiagram.locator("svg g.node").filter({ hasText: "校验上游水质监测" });
|
|
await responsiveWorkflowStep.scrollIntoViewIfNeeded();
|
|
await expect(responsiveWorkflowStep).toBeVisible();
|
|
await responsiveDiagram.scrollIntoViewIfNeeded();
|
|
await expect(responsiveConditionPanel.getByRole("button", { name: "放大流程图" })).toHaveCount(0);
|
|
await expect.poll(() => page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(width);
|
|
await testInfo.attach(`water-quality-condition-task-${width}`, {
|
|
body: await page.screenshot({ path: testInfo.outputPath(`execution-plan-${width}.png`) }),
|
|
contentType: "image/png"
|
|
});
|
|
await responsiveDiagram.locator("svg g.node").filter({ hasText: "形成调查结论" }).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`) }),
|
|
contentType: "image/png"
|
|
});
|
|
}
|
|
await page.setViewportSize({ width: 1280, height: 720 });
|
|
const restoredConditionPanel = page.getByRole("region", { name: "工况任务", exact: true });
|
|
if (!await restoredConditionPanel.isVisible()) {
|
|
await page.getByRole("button", { name: "工况任务", exact: true }).click();
|
|
}
|
|
const agentCollaborationButton = restoredConditionPanel.getByRole("button", { name: "Agent 协作" });
|
|
if (!await agentCollaborationButton.isVisible()) {
|
|
const expandRestoredTask = restoredConditionPanel.getByRole("button", { name: "展开工况任务" });
|
|
if (await expandRestoredTask.isVisible()) await expandRestoredTask.click();
|
|
await restoredConditionPanel.getByRole("button", { name: /调查任务/ }).click();
|
|
}
|
|
await agentCollaborationButton.click();
|
|
await expect(page.getByRole("complementary", { name: "Agent 命令面板" })).toBeVisible();
|
|
expect(workOrders[0]).toMatchObject({
|
|
work_order_type: "water_quality_complaint",
|
|
scenario_code: null,
|
|
origin: "system_data",
|
|
status: "open"
|
|
});
|
|
expect(agentRequests.filter((body) => "trigger_context" in body)).toHaveLength(0);
|
|
await page.route("**/api/v1/sessions", (route) => route.fulfill({ json: { sessions: [{
|
|
id: "browser-session-1", title: "水质投诉调查", created_at: "2026-09-23T01:00:00Z", updated_at: "2026-09-23T01:00:00Z",
|
|
trigger: { subject_id: workOrderId, workflow: "water_quality_complaint", occurred_at: "2026-09-23T01:00:00Z" }
|
|
}] } }));
|
|
await page.reload();
|
|
await page.getByRole("button", { name: "工况任务", exact: true }).click();
|
|
const recovered = page.getByRole("region", { name: "工况任务", exact: true });
|
|
await recovered.getByRole("button", { name: "展开工况任务" }).click();
|
|
await recovered.getByRole("button", { name: /调查任务/ }).click();
|
|
const taskRow = recovered.getByRole("button", { name: /水质投诉调查/ }).first();
|
|
await taskRow.click();
|
|
await expectSavedPlan(recovered);
|
|
expect(agentRequests.filter((body) => "trigger_context" in body)).toHaveLength(0);
|
|
});
|
|
|
|
test("在 375px 视口内完整呈现面板", async ({ page }) => {
|
|
await page.setViewportSize({ width: 375, height: 720 });
|
|
await page.goto("/");
|
|
const launcher = page.getByRole("button", { name: "外围系统" });
|
|
await launcher.focus();
|
|
await page.keyboard.press("Enter");
|
|
const panel = page.getByRole("complementary", { name: "外围系统" });
|
|
await expect(panel).toBeVisible();
|
|
const box = await panel.boundingBox();
|
|
expect(box).not.toBeNull();
|
|
expect(box!.x).toBeGreaterThanOrEqual(0);
|
|
expect(box!.x + box!.width).toBeLessThanOrEqual(375);
|
|
});
|
|
});
|
|
|
|
function createDemoDataset(projectId: string) {
|
|
const points = Array.from({ length: 5 }, (_, index) => ({
|
|
point_id: `DEMO-${index + 1}`,
|
|
label: `虚拟压力测点 ${index + 1}`,
|
|
unit: "m",
|
|
status: "active",
|
|
origin: "synthetic"
|
|
}));
|
|
return {
|
|
generator_version: "water-demo@1",
|
|
seed: "water-demo-seed-20260917",
|
|
origin: "synthetic",
|
|
window: { start: "2026-09-17T00:00:00Z", end: "2026-09-17T12:00:00Z" },
|
|
project_id: projectId,
|
|
source_id: "peripheral-demo",
|
|
source_revision: "demo-r1",
|
|
points,
|
|
observations: Array.from({ length: 240 }, (_, index) => ({
|
|
record_id: `demo-reading-${index}`,
|
|
point_id: `DEMO-${(index % 5) + 1}`,
|
|
observed_at: `2026-09-17T${String(Math.floor(index / 20)).padStart(2, "0")}:${String((index % 4) * 15).padStart(2, "0")}:00Z`,
|
|
value: index === 0 ? 0 : index === 5 ? null : 31 + index / 100,
|
|
quality: index === 5 ? "missing" : "valid",
|
|
unit: "m"
|
|
})),
|
|
device_states: Array.from({ length: 9 }, (_, index) => ({
|
|
record_id: `60000000-0000-4000-8000-${String(index).padStart(12, "0")}`,
|
|
asset_id: `VIRTUAL-${index + 1}`,
|
|
asset_type: "pressure_sensor",
|
|
observed_at: "2026-09-17T01:00:00Z",
|
|
state: index === 1 ? "offline" : "online"
|
|
})),
|
|
alerts: [
|
|
{
|
|
alert_id: "70000000-0000-4000-8000-000000000001",
|
|
asset_id: "VIRTUAL-2",
|
|
code: "communication_lost",
|
|
message: "演示通信中断",
|
|
occurred_at: "2026-09-17T02:00:00Z",
|
|
cleared_at: "2026-09-17T02:15:00Z"
|
|
},
|
|
{
|
|
alert_id: "70000000-0000-4000-8000-000000000002",
|
|
asset_id: "VIRTUAL-3",
|
|
code: "low_level",
|
|
message: "演示低水位",
|
|
occurred_at: "2026-09-17T05:00:00Z",
|
|
cleared_at: null
|
|
}
|
|
],
|
|
operations: Array.from({ length: 4 }, (_, index) => ({
|
|
record_id: `80000000-0000-4000-8000-${String(index).padStart(12, "0")}`,
|
|
actor: "synthetic-generator",
|
|
asset_id: `VIRTUAL-${index + 1}`,
|
|
operation: ["pump_start", "pump_stop", "valve_open", "valve_close"][index],
|
|
note: "演示事件,不是控制指令",
|
|
occurred_at: `2026-09-17T0${index + 1}:30:00Z`
|
|
}))
|
|
};
|
|
}
|
|
|
|
function createSourceWorkOrders(projectId: string) {
|
|
const appointments = [
|
|
["合成通信核查", "核查监测点通信和数据上送状态", "open", "high"],
|
|
["合成压力偏差复核", "复核压力偏差与历史回放证据", "in_progress", "urgent"],
|
|
["合成零流量样本确认", "确认零流量样本及质量标记", "completed", "normal"]
|
|
].map(([title, summary, status, priority], index) => ({
|
|
work_order_id: `50000000-0000-4000-8000-${String(index + 1).padStart(12, "0")}`,
|
|
project_id: projectId,
|
|
revision: 1,
|
|
title,
|
|
summary,
|
|
status,
|
|
priority,
|
|
assignee: null,
|
|
scheduled_at: "2026-09-17T08:00:00Z",
|
|
work_order_type: "general",
|
|
location: null,
|
|
scenario_code: null,
|
|
feedback_requirements: [],
|
|
activity: [],
|
|
origin: "synthetic",
|
|
created_at: "2026-09-17T00:00:00Z",
|
|
updated_at: "2026-09-17T00:00:00Z",
|
|
created_by: "synthetic-generator",
|
|
updated_by: "synthetic-generator"
|
|
}));
|
|
const events = [
|
|
{
|
|
work_order_id: "50000000-0000-4000-8000-000000000004",
|
|
title: "水质投诉:演示小区",
|
|
summary: "居民反映晨间用水有异味",
|
|
priority: "high",
|
|
work_order_type: "water_quality_complaint",
|
|
location: "演示小区",
|
|
scenario_code: "WQ-SECONDARY-SUPPLY-001",
|
|
activity_detail: "投诉工单已创建。"
|
|
},
|
|
{
|
|
work_order_id: "50000000-0000-4000-8000-000000000005",
|
|
title: "爆管上报:演示路口",
|
|
summary: "路面积水并伴随压力突降",
|
|
priority: "urgent",
|
|
work_order_type: "burst_report",
|
|
location: "演示路口",
|
|
scenario_code: "BURST-REPORT-001",
|
|
activity_detail: "爆管上报工单已创建。"
|
|
}
|
|
].map(({ activity_detail, ...event }) => ({
|
|
...event,
|
|
project_id: projectId,
|
|
revision: 1,
|
|
status: "open",
|
|
assignee: null,
|
|
scheduled_at: "2026-09-17T08:30:00Z",
|
|
feedback_requirements: [],
|
|
activity: [{
|
|
sequence: 1,
|
|
occurred_at: "2026-09-17T08:30:00Z",
|
|
actor: "synthetic-generator",
|
|
kind: "created",
|
|
title: "工单创建",
|
|
detail: activity_detail
|
|
}],
|
|
origin: "synthetic",
|
|
created_at: "2026-09-17T08:30:00Z",
|
|
updated_at: "2026-09-17T08:30:00Z",
|
|
created_by: "synthetic-generator",
|
|
updated_by: "synthetic-generator"
|
|
}));
|
|
return [...appointments, ...events];
|
|
}
|