485 lines
19 KiB
TypeScript
485 lines
19 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
||
import { mockWorkbenchContext } from "./support/mock-workbench-context";
|
||
|
||
const TODAY = shanghaiTimestamp(10, 30);
|
||
const TODAY_10_10 = shanghaiTimestamp(10, 10);
|
||
const TODAY_10_20 = shanghaiTimestamp(10, 20);
|
||
const TODAY_10_25 = shanghaiTimestamp(10, 25);
|
||
const TODAY_10_35 = shanghaiTimestamp(10, 35);
|
||
const TODAY_11_00 = shanghaiTimestamp(11, 0);
|
||
const YESTERDAY_10_10 = shanghaiTimestamp(10, 10, -1);
|
||
const YESTERDAY_10_20 = shanghaiTimestamp(10, 20, -1);
|
||
|
||
test("history mode opens on the latest day with backend work orders", async ({ page }) => {
|
||
await mockWorkbenchContext(page, {
|
||
availability: "ready",
|
||
source: "scheduled_tasks",
|
||
scenario_availability: "not_configured",
|
||
artifact_availability: "not_configured",
|
||
schedule_availability: "ready",
|
||
work_order_availability: "ready",
|
||
alert_availability: "ready",
|
||
generated_at: "2026-09-18T02:30:00Z",
|
||
conditions: [{
|
||
id: "backend-schedule-demo-day-next",
|
||
task_id: "backend-schedule",
|
||
session_id: "scheduled-condition-backend-schedule-demo-day",
|
||
title: "历史 SCADA 诊断计划",
|
||
summary: "来自后端持久化的目标明确的诊断计划。",
|
||
status: "running",
|
||
risk_level: "normal",
|
||
scheduled_at: "2026-08-17T09:40:00Z",
|
||
updated_at: "2026-08-17T09:40:00Z",
|
||
detail: "按计划复核 SCADA 缺测与越界记录。",
|
||
recommendation: null,
|
||
evidence: ["调度 ID:demo-day"],
|
||
model_name: "TJWater scheduler"
|
||
}],
|
||
alerts: [],
|
||
scenarios: [],
|
||
latest_analysis: null,
|
||
work_orders: [{
|
||
id: "wo-history-1",
|
||
code: "WO-20260817-001",
|
||
scheduled_at: "2026-08-17T09:40:00Z",
|
||
title: "历史工单:阀门复核",
|
||
summary: "来自后端工单投影的历史预约任务。",
|
||
status: "pending",
|
||
risk_level: "normal",
|
||
updated_at: "2026-08-17T09:35:00Z",
|
||
detail: "复核阀门状态并回传结果。",
|
||
recommendation: null,
|
||
duration_minutes: 30,
|
||
source: "工单系统",
|
||
dispatcher: "调度中心",
|
||
priority: "normal",
|
||
location: "北区阀门 V-17",
|
||
assignee: "北区班组",
|
||
feedback_requirements: ["回传阀门状态"],
|
||
activity: []
|
||
}]
|
||
}, undefined, {}, [], { runtimeMode: "history" });
|
||
|
||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||
|
||
await expect(page.getByText("历史 2026-08-17 17:40", { exact: true })).toBeVisible();
|
||
await expect(page.getByRole("region", { name: "运行时间轴" })).toContainText("2026.08.17");
|
||
await page.getByRole("button", { name: "工况任务", exact: true }).click();
|
||
const panel = page.getByRole("region", { name: "工况任务", exact: true });
|
||
await expect(panel).toContainText("SCADA 数据诊断");
|
||
await panel.getByRole("button", { name: "展开工况任务" }).click();
|
||
await panel.getByRole("button", { name: /预约任务/ }).click();
|
||
await expect(panel).toContainText("历史工单:阀门复核");
|
||
await expect(panel).not.toContainText("历史 SCADA 诊断计划");
|
||
});
|
||
|
||
test("keeps mock conditions while retaining explicit backend alerts", async ({ page }) => {
|
||
await mockWorkbenchContext(page, {
|
||
availability: "ready",
|
||
source: "scheduled_tasks",
|
||
scenario_availability: "not_configured",
|
||
artifact_availability: "not_configured",
|
||
schedule_availability: "not_configured",
|
||
work_order_availability: "not_configured",
|
||
alert_availability: "ready",
|
||
generated_at: TODAY,
|
||
conditions: [{
|
||
id: "schedule-run-test-1",
|
||
task_id: "backend-schedule",
|
||
session_id: "scheduled-condition-schedule-run-test-1",
|
||
title: "演示 合成低压调查",
|
||
summary: "来自后端持久化 Action Run 的 演示 沙箱测试状态。",
|
||
status: "error",
|
||
risk_level: "critical",
|
||
scheduled_at: TODAY_10_20,
|
||
updated_at: TODAY_10_25,
|
||
detail: "演示 测试运行状态:已失败。",
|
||
recommendation: "请在 演示 调查任务中检查失败步骤与运行日志。",
|
||
evidence: ["运行状态:failed"],
|
||
model_name: "TJWater scheduler"
|
||
}],
|
||
alerts: [{
|
||
id: "scheduled-condition-alert-schedule-run-test-1",
|
||
condition_id: "schedule-run-test-1",
|
||
source: {
|
||
task_type: "analysis_task",
|
||
task_id: "backend-schedule",
|
||
run_id: "schedule-run-test-1"
|
||
},
|
||
title: "演示 测试待复核:演示 合成低压调查",
|
||
description: "运行失败。演示 测试运行状态:已失败。",
|
||
occurred_at: TODAY_10_25,
|
||
severity: "critical"
|
||
}],
|
||
scenarios: [],
|
||
latest_analysis: null,
|
||
work_orders: []
|
||
});
|
||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||
|
||
await page.getByRole("button", { name: "工况任务", exact: true }).click();
|
||
const panel = page.getByRole("region", { name: "工况任务", exact: true });
|
||
await expect(panel).toContainText("SCADA 数据诊断");
|
||
await expect(panel).not.toContainText("演示 合成低压调查");
|
||
|
||
const alertButton = page.getByRole("button", {
|
||
name: "查看异常处置面板,当前 1 条待复核工况"
|
||
});
|
||
await alertButton.focus();
|
||
await page.keyboard.press("Enter");
|
||
await expect(page.getByText("演示 测试待复核:演示 合成低压调查")).toBeVisible();
|
||
});
|
||
|
||
test("opens the latest persisted analysis as the active visualization", async ({ page }) => {
|
||
const latestAnalysis = {
|
||
run_id: "analysis-1",
|
||
name: "北区爆管定位",
|
||
run_type: "burst_location",
|
||
run_type_label: "爆管定位",
|
||
status: "completed",
|
||
status_label: "已完成",
|
||
created_by: "alice",
|
||
created_at: TODAY_10_20,
|
||
started_at: TODAY_10_10,
|
||
results: [{
|
||
result_type: "burst_location",
|
||
created_at: TODAY_10_20,
|
||
facts: [{ label: "关联管段", value: "P-42" }]
|
||
}],
|
||
map_payload: {
|
||
type: "FeatureCollection",
|
||
features: [{
|
||
type: "Feature",
|
||
properties: { label: "后端爆管定位" },
|
||
geometry: { type: "Point", coordinates: [121.5, 30.9] }
|
||
}]
|
||
}
|
||
};
|
||
const historicalAnalysis = {
|
||
...latestAnalysis,
|
||
run_id: "analysis-2",
|
||
name: "南区历史定位",
|
||
created_at: YESTERDAY_10_20,
|
||
started_at: YESTERDAY_10_10,
|
||
results: [{
|
||
result_type: "burst_location",
|
||
created_at: YESTERDAY_10_20,
|
||
facts: [{ label: "关联管段", value: "P-older" }]
|
||
}],
|
||
map_payload: {
|
||
type: "FeatureCollection",
|
||
features: [{
|
||
type: "Feature",
|
||
properties: { label: "历史运行几何" },
|
||
geometry: { type: "Point", coordinates: [121.4, 30.8] }
|
||
}]
|
||
}
|
||
};
|
||
await mockWorkbenchContext(page, {
|
||
availability: "ready",
|
||
source: "analysis_runs",
|
||
scenario_availability: "ready",
|
||
artifact_availability: "ready",
|
||
schedule_availability: "ready",
|
||
work_order_availability: "not_configured",
|
||
alert_availability: "not_configured",
|
||
generated_at: TODAY,
|
||
conditions: [{
|
||
id: "analysis-run-analysis-1",
|
||
task_id: "backend-analysis",
|
||
session_id: "scheduled-condition-analysis-run-analysis-1",
|
||
title: "北区爆管定位",
|
||
summary: "来自项目后端持久化分析运行:爆管定位。",
|
||
status: "completed",
|
||
risk_level: "normal",
|
||
scheduled_at: TODAY_10_10,
|
||
updated_at: TODAY_10_20,
|
||
detail: "分析类型:爆管定位;后端状态:已完成。",
|
||
recommendation: null,
|
||
evidence: ["运行 ID:analysis-1"],
|
||
model_name: "TJWater analysis runtime"
|
||
}, {
|
||
id: "analysis-run-analysis-2",
|
||
task_id: "backend-analysis",
|
||
session_id: "scheduled-condition-analysis-run-analysis-2",
|
||
title: "南区历史定位",
|
||
summary: "来自项目后端持久化分析运行:爆管定位。",
|
||
status: "completed",
|
||
risk_level: "normal",
|
||
scheduled_at: YESTERDAY_10_10,
|
||
updated_at: YESTERDAY_10_20,
|
||
detail: "分析类型:爆管定位;后端状态:已完成。",
|
||
recommendation: null,
|
||
evidence: ["运行 ID:analysis-2"],
|
||
model_name: "TJWater analysis runtime"
|
||
}],
|
||
alerts: [],
|
||
scenarios: [
|
||
{
|
||
id: "analysis-run-analysis-1",
|
||
run_id: "analysis-1",
|
||
name: "北区爆管定位",
|
||
description: "爆管定位 · 已完成 · 创建人 alice",
|
||
status: "review",
|
||
run_type: "burst_location",
|
||
run_type_label: "爆管定位",
|
||
run_status: "completed",
|
||
created_at: TODAY_10_20
|
||
},
|
||
{
|
||
id: "analysis-run-analysis-2",
|
||
run_id: "analysis-2",
|
||
name: "南区历史定位",
|
||
description: "爆管定位 · 已完成 · 创建人 alice",
|
||
status: "review",
|
||
run_type: "burst_location",
|
||
run_type_label: "爆管定位",
|
||
run_status: "completed",
|
||
created_at: YESTERDAY_10_20
|
||
}
|
||
],
|
||
latest_analysis: latestAnalysis,
|
||
work_orders: []
|
||
}, undefined, {
|
||
"analysis-1": latestAnalysis,
|
||
"analysis-2": historicalAnalysis
|
||
}, [{
|
||
run_id: "analysis-1",
|
||
name: "北区爆管定位",
|
||
run_type: "burst_location",
|
||
run_type_label: "爆管定位",
|
||
status: "completed",
|
||
status_label: "已完成",
|
||
created_by: "alice",
|
||
created_at: TODAY_10_20,
|
||
started_at: TODAY_10_10,
|
||
has_results: true
|
||
}, {
|
||
run_id: "analysis-2",
|
||
name: "南区历史定位",
|
||
run_type: "burst_location",
|
||
run_type_label: "爆管定位",
|
||
status: "completed",
|
||
status_label: "已完成",
|
||
created_by: "alice",
|
||
created_at: YESTERDAY_10_20,
|
||
started_at: YESTERDAY_10_10,
|
||
has_results: true
|
||
}, ...Array.from({ length: 9 }, (_, index) => ({
|
||
run_id: `analysis-${index + 3}`,
|
||
name: `批次分析 ${index + 3}`,
|
||
run_type: "pressure_analysis",
|
||
run_type_label: "压力分析",
|
||
status: "completed",
|
||
status_label: "已完成",
|
||
created_by: "scheduler",
|
||
created_at: YESTERDAY_10_20,
|
||
started_at: YESTERDAY_10_10,
|
||
has_results: index % 2 === 0
|
||
}))]);
|
||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||
|
||
await expect(page.getByRole("button", { name: "切换模拟方案" })).toHaveCount(0);
|
||
await expect(page.getByRole("button", { name: /当前分析视图/ })).toHaveCount(0);
|
||
|
||
await page.getByRole("button", { name: "工况任务", exact: true }).click();
|
||
const conditionPanel = page.getByRole("region", { name: "工况任务", exact: true });
|
||
await expect(conditionPanel).not.toContainText("北区爆管定位");
|
||
await expect(conditionPanel).not.toContainText("南区历史定位");
|
||
await expect(conditionPanel).toContainText("SCADA 数据诊断");
|
||
await page.getByRole("button", { name: "工况任务", exact: true }).click();
|
||
await expect(page.getByRole("region", { name: "工况任务", exact: true })).toHaveCount(0);
|
||
|
||
await page.getByRole("button", { name: "打开最新后端分析成果" }).click();
|
||
await expect(page.getByRole("complementary", { name: "后端分析成果预览" })).toHaveCount(0);
|
||
await expect(page.getByRole("heading", { name: "北区爆管定位" })).toBeVisible();
|
||
await expect(page.getByText("Backend Analysis")).toBeVisible();
|
||
await expect(page.getByText("关联管段:P-42")).toBeVisible();
|
||
const analysisViewButton = page.getByRole("button", { name: "当前分析视图:北区爆管定位" });
|
||
await expect(analysisViewButton).toContainText("分析视图 · 北区爆管定位");
|
||
await expect.poll(() => page.evaluate(() =>
|
||
window.__waterNetworkMap
|
||
?.querySourceFeatures("simulation-backend-analysis")
|
||
.map((feature) => feature.properties?.label) ?? []
|
||
)).toContain("后端爆管定位");
|
||
|
||
await analysisViewButton.click();
|
||
await expect(page.getByText(/爆管定位 · 项目后端分析运行 · 后端分析运行/)).toBeVisible();
|
||
await page.getByText("关闭分析视图", { exact: true }).click();
|
||
await expect(page.getByRole("button", { name: /当前分析视图/ })).toHaveCount(0);
|
||
await expect.poll(() => page.evaluate(() =>
|
||
window.__waterNetworkMap
|
||
?.querySourceFeatures("simulation-backend-analysis").length ?? 0
|
||
)).toBe(0);
|
||
|
||
await page.getByRole("button", { name: "项目分析成果" }).click();
|
||
const resultsPanel = page.getByRole("complementary", { name: "项目分析成果" });
|
||
await resultsPanel.getByText("浏览项目分析成果", { exact: true }).click();
|
||
await expect(resultsPanel.getByRole("button", { name: "打开分析成果:北区爆管定位" })).toBeVisible();
|
||
await expect(resultsPanel.getByRole("button", { name: "打开分析成果:批次分析 11" })).toHaveCount(0);
|
||
await resultsPanel.getByRole("button", { name: "加载更多成果" }).click();
|
||
await expect(resultsPanel.getByRole("button", { name: "打开分析成果:批次分析 11" })).toBeVisible();
|
||
await resultsPanel.getByRole("button", { name: "打开分析成果:南区历史定位" }).click();
|
||
await expect(resultsPanel).toHaveCount(0);
|
||
await expect(page.getByRole("heading", { name: "南区历史定位" })).toBeVisible();
|
||
await expect(page.getByRole("button", { name: "当前分析视图:南区历史定位" })).toBeVisible();
|
||
await expect.poll(() => page.evaluate(() =>
|
||
window.__waterNetworkMap
|
||
?.querySourceFeatures("simulation-backend-analysis")
|
||
.map((feature) => feature.properties?.label) ?? []
|
||
)).toContain("历史运行几何");
|
||
});
|
||
|
||
test("keeps a non-spatial backend result in the analysis workspace without demo geometry", async ({ page }) => {
|
||
await page.setViewportSize({ width: 1280, height: 900 });
|
||
await mockWorkbenchContext(page, {
|
||
availability: "ready",
|
||
source: "analysis_runs",
|
||
scenario_availability: "ready",
|
||
artifact_availability: "ready",
|
||
schedule_availability: "not_configured",
|
||
work_order_availability: "not_configured",
|
||
alert_availability: "not_configured",
|
||
generated_at: TODAY,
|
||
conditions: [],
|
||
alerts: [],
|
||
scenarios: [],
|
||
latest_analysis: {
|
||
run_id: "analysis-table-only",
|
||
name: "压力统计结果",
|
||
run_type: "pressure_analysis",
|
||
run_type_label: "压力分析",
|
||
status: "completed",
|
||
status_label: "已完成",
|
||
created_by: "alice",
|
||
created_at: TODAY_10_20,
|
||
started_at: TODAY_10_10,
|
||
results: [{
|
||
result_type: "pressure_analysis",
|
||
created_at: TODAY_10_20,
|
||
facts: [{ label: "结果状态", value: "已完成" }]
|
||
}],
|
||
map_payload: null
|
||
},
|
||
work_orders: []
|
||
});
|
||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||
|
||
await page.getByRole("button", { name: "打开最新后端分析成果" }).click();
|
||
await expect(page.getByRole("heading", { name: "压力统计结果" })).toBeVisible();
|
||
await page.getByRole("button", { name: "当前分析视图:压力统计结果" }).click();
|
||
await expect(page.getByText("仅看地图", { exact: true })).toHaveCount(0);
|
||
await page.getByRole("menuitem", { name: /半屏显示/ }).click();
|
||
await expect(page.getByRole("menu")).toHaveCount(0);
|
||
await expect(page.locator("#main-workspace")).toHaveAttribute("data-layout-mode", "map_split");
|
||
await expect(page.locator("#workspace-map")).toBeVisible();
|
||
await expect(page.locator("#artifact-workspace")).toBeVisible();
|
||
await expect.poll(() => page.evaluate(() => ({
|
||
backend: window.__waterNetworkMap
|
||
?.querySourceFeatures("simulation-backend-analysis").length ?? 0,
|
||
demo: window.__waterNetworkMap
|
||
?.querySourceFeatures("simulation-impact-area").length ?? 0
|
||
}))).toEqual({ backend: 0, demo: 0 });
|
||
});
|
||
|
||
test("keeps mock conditions while using backend work orders and alerts", async ({ page }) => {
|
||
await mockWorkbenchContext(page, {
|
||
availability: "ready",
|
||
source: "scheduled_tasks",
|
||
scenario_availability: "not_configured",
|
||
artifact_availability: "not_configured",
|
||
schedule_availability: "ready",
|
||
work_order_availability: "ready",
|
||
alert_availability: "ready",
|
||
generated_at: TODAY,
|
||
conditions: [{
|
||
id: "backend-schedule-diagnosis-next",
|
||
task_id: "backend-schedule",
|
||
session_id: "scheduled-condition-backend-schedule-diagnosis",
|
||
title: "SCADA 后端定时诊断",
|
||
summary: "来自项目数据库的持久化调度定义。",
|
||
status: "pending",
|
||
risk_level: "normal",
|
||
scheduled_at: TODAY_10_35,
|
||
updated_at: TODAY,
|
||
detail: "每 5 分钟触发一次。",
|
||
recommendation: null,
|
||
evidence: ["调度 ID:diagnosis-5m"],
|
||
model_name: "TJWater scheduler"
|
||
}],
|
||
alerts: [{
|
||
id: "backend-alert-alarm-1",
|
||
condition_id: null,
|
||
source: {
|
||
task_type: "scheduled_task",
|
||
task_id: "diagnosis-5m",
|
||
run_id: "trigger-1"
|
||
},
|
||
title: "后端告警:泵站通信中断",
|
||
description: "由诊断任务明确推送到项目数据库。",
|
||
occurred_at: TODAY,
|
||
severity: "warning"
|
||
}],
|
||
scenarios: [],
|
||
latest_analysis: null,
|
||
work_orders: [{
|
||
id: "wo-backend-1",
|
||
code: "WO-20260917-001",
|
||
scheduled_at: TODAY_11_00,
|
||
title: "后端工单:北区阀门复核",
|
||
summary: "现场复核阀门开度并回传。",
|
||
status: "running",
|
||
risk_level: "attention",
|
||
updated_at: TODAY,
|
||
detail: "来自正式工单同步适配器。",
|
||
recommendation: "完成后复核压力。",
|
||
duration_minutes: 45,
|
||
source: "生产工单系统",
|
||
dispatcher: "调度中心",
|
||
priority: "urgent",
|
||
location: "北区阀门 V-1",
|
||
assignee: "北区班组",
|
||
feedback_requirements: ["回传阀门开度"],
|
||
activity: [{ sequence: 1, occurred_at: TODAY, actor: "调度中心", kind: "created",
|
||
title: "工单创建", detail: "现场复核阀门开度并回传。" }]
|
||
}]
|
||
});
|
||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||
|
||
await page.getByRole("button", { name: "工况任务", exact: true }).click();
|
||
const panel = page.getByRole("region", { name: "工况任务", exact: true });
|
||
await expect(panel).toContainText("SCADA 数据诊断");
|
||
await expect(panel).not.toContainText("SCADA 后端定时诊断");
|
||
await panel.getByRole("button", { name: "展开工况任务" }).click();
|
||
await panel.getByRole("button", { name: /预约任务/ }).click();
|
||
await expect(panel).toContainText("后端工单:北区阀门复核");
|
||
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.locator("details")).toHaveCount(0);
|
||
|
||
const alertButton = page.getByRole("button", {
|
||
name: "查看异常处置面板,当前 1 条待复核工况"
|
||
});
|
||
await alertButton.focus();
|
||
await page.keyboard.press("Enter");
|
||
await expect(page.getByText("来源:后端运行态", { exact: true })).toBeVisible();
|
||
await expect(page.getByText("后端告警:泵站通信中断")).toBeVisible();
|
||
});
|
||
|
||
function shanghaiTimestamp(hour: number, minute: number, dayOffset = 0) {
|
||
const target = new Date(Date.now() + dayOffset * 24 * 60 * 60 * 1000);
|
||
const parts = new Intl.DateTimeFormat("en-CA", {
|
||
timeZone: "Asia/Shanghai",
|
||
year: "numeric",
|
||
month: "2-digit",
|
||
day: "2-digit"
|
||
}).formatToParts(target);
|
||
const get = (type: Intl.DateTimeFormatPartTypes) =>
|
||
parts.find((part) => part.type === type)?.value ?? "";
|
||
return `${get("year")}-${get("month")}-${get("day")}T${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}:00+08:00`;
|
||
}
|