548 lines
27 KiB
TypeScript
548 lines
27 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
||
import { mockAgentApi } from "./support/mock-agent-api";
|
||
|
||
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 observationQueries: Record<string, string>[] = [];
|
||
const writeRequests: 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",
|
||
TJWATER_PERIPHERAL_SAMPLE_DEMO_START: "2026-04-30T08:00:00Z",
|
||
TJWATER_PERIPHERAL_SAMPLE_DEMO_END: "2026-04-30T16:00:00Z"
|
||
})};`
|
||
})
|
||
);
|
||
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 feedback = {
|
||
feedback_id: "40000000-0000-4000-8000-000000000001",
|
||
task_id: "10000000-0000-4000-8000-000000000001",
|
||
recorded_by: "operator",
|
||
recorded_at: "2026-04-30T09:10:00Z",
|
||
occurred_at: "2026-04-30T09:00:00Z",
|
||
applicable_time: { start: "2026-04-30T08:00:00Z", end: "2026-04-30T16:00:00Z" },
|
||
asset_ids: ["DEMO-P001"],
|
||
finding: "instrument_fault",
|
||
notes: "压力变送器零点漂移",
|
||
attachment_refs: [],
|
||
origin: "synthetic"
|
||
};
|
||
const task = {
|
||
task_id: feedback.task_id,
|
||
project_id: "324ff7ee-c2d4-5184-a84b-48fb14a2933e",
|
||
revision: 3,
|
||
title: "DEMO-P001 测点偏差核查",
|
||
summary: "校验记录外围核查",
|
||
source: {
|
||
system: "next",
|
||
task_id: "20000000-0000-4000-8000-000000000001",
|
||
object_type: "validation",
|
||
object_id: "30000000-0000-4000-8000-000000000001",
|
||
revision: 2
|
||
},
|
||
asset_ids: ["DEMO-P001"],
|
||
analysis_window: { start: "2026-04-30T08:00:00Z", end: "2026-04-30T16:00:00Z" },
|
||
evidence_refs: [],
|
||
requested_check: "核查仪表状态",
|
||
status: "pending_review",
|
||
origin: "synthetic",
|
||
created_at: "2026-04-30T08:05:00Z",
|
||
updated_at: "2026-04-30T09:10:00Z",
|
||
created_by: "operator",
|
||
history: [
|
||
{
|
||
sequence: 1,
|
||
from_status: null,
|
||
to_status: "pending",
|
||
actor: "operator",
|
||
at: "2026-04-30T08:05:00Z",
|
||
reason: "提交样板核查"
|
||
},
|
||
{
|
||
sequence: 2,
|
||
from_status: "processing",
|
||
to_status: "pending_review",
|
||
actor: "operator",
|
||
at: "2026-04-30T09:10:00Z",
|
||
reason: "等待审核"
|
||
}
|
||
],
|
||
feedback: [feedback]
|
||
};
|
||
await page.route("http://peripheral.test/api/sample/v1/**", async (route) => {
|
||
const url = new URL(route.request().url());
|
||
if (route.request().method() !== "GET") writeRequests.push(route.request().url());
|
||
if (url.pathname.endsWith("/monitoring/observations"))
|
||
observationQueries.push(Object.fromEntries(url.searchParams));
|
||
if (url.pathname.endsWith("/context"))
|
||
return route.fulfill({
|
||
json: {
|
||
protocol_version: "tjwater-peripheral-sample@1",
|
||
origin: "synthetic",
|
||
project_id: task.project_id,
|
||
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("/scenario"))
|
||
return route.fulfill({ json: { scenario: "normal", revision: 1 } });
|
||
if (url.pathname.endsWith("/tasks"))
|
||
return route.fulfill({ json: { items: [task], next_cursor: null } });
|
||
if (url.pathname.endsWith("/work-orders"))
|
||
return route.fulfill({ json: { items: createSourceWorkOrders(task.project_id), next_cursor: null } });
|
||
if (url.pathname.endsWith("/test-data"))
|
||
return route.fulfill({ json: { items: [], next_cursor: null } });
|
||
if (url.pathname.endsWith("/monitoring/points"))
|
||
return route.fulfill({
|
||
json: {
|
||
items: Array.from({ length: 118 }, (_, index) => ({
|
||
point_id: `SCADA-${index + 1}`,
|
||
label: `压力测点 ${index + 1}`,
|
||
unit: "m",
|
||
status: "active",
|
||
origin: "historical_replay"
|
||
})),
|
||
next_cursor: null,
|
||
origin: "historical_replay",
|
||
source_id: "tjwater_v2"
|
||
}
|
||
});
|
||
if (url.pathname.endsWith("/monitoring/alerts"))
|
||
return route.fulfill({
|
||
json: {
|
||
availability: "not_configured",
|
||
items: [],
|
||
origin: "historical_replay",
|
||
reason: "来源未配置",
|
||
source_id: "tjwater_v2"
|
||
}
|
||
});
|
||
if (url.pathname.endsWith("/monitoring/observations"))
|
||
return route.fulfill({
|
||
json: {
|
||
origin: "historical_replay",
|
||
source_id: "tjwater_v2",
|
||
source_revision: "2026-04-30",
|
||
truncated: false,
|
||
items: [
|
||
{
|
||
record_id: "reading-1",
|
||
point_id: "SCADA-1",
|
||
observed_at: "2026-04-30T08:15:00Z",
|
||
value: 31.42,
|
||
quality: "valid",
|
||
unit: "m"
|
||
},
|
||
{
|
||
record_id: "reading-2",
|
||
point_id: "SCADA-1",
|
||
observed_at: "2026-04-30T08:30:00Z",
|
||
value: null,
|
||
quality: "missing",
|
||
unit: "m"
|
||
}
|
||
]
|
||
}
|
||
});
|
||
if (url.pathname.endsWith("/demo-data"))
|
||
return route.fulfill({ json: createDemoDataset(task.project_id) });
|
||
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();
|
||
await expect(page.getByText(/118 个测点 · 告警来源未配置 · 真实项目样本回放/)).toBeVisible();
|
||
const hierarchy = page.getByLabel("任务层级");
|
||
await expect(hierarchy).toContainText("外围合成数据核查");
|
||
await expect(hierarchy).toContainText("3 个子任务");
|
||
await expect(page.getByLabel("子任务阶段 合成通信核查")).toContainText(/派发.*当前阶段.*执行.*待开始.*复令.*待开始/);
|
||
await expect(page.getByLabel("子任务阶段 合成压力偏差复核")).toContainText(/派发.*已完成.*执行.*当前阶段.*复令.*待开始/);
|
||
await expect(page.getByLabel("子任务阶段 合成零流量样本确认")).toContainText(/派发.*已完成.*执行.*已完成.*复令.*已完成/);
|
||
const scenarioSelect = page.getByRole("combobox", { name: "场景" });
|
||
await scenarioSelect.focus();
|
||
await page.keyboard.press("Enter");
|
||
await expect(page.getByRole("listbox")).toBeVisible();
|
||
await page.keyboard.press("Escape");
|
||
await expect(page.getByRole("listbox")).toBeHidden();
|
||
await expect(scenarioSelect).toBeFocused();
|
||
|
||
const pointSelect = page.getByRole("combobox", { name: "测点", exact: true });
|
||
await pointSelect.click();
|
||
await page.getByRole("option", { name: "压力测点 2 · m" }).click();
|
||
await expect(pointSelect).toContainText("压力测点 2 · m");
|
||
const historyMode = page.getByRole("combobox", { name: "时间模式", exact: true });
|
||
const historyStart = page.getByRole("button", { name: "开始(UTC)", exact: true });
|
||
const historyEnd = page.getByRole("button", { name: "结束(UTC)", exact: true });
|
||
await expect(historyStart).toContainText("2026-04-30 08:00");
|
||
await historyMode.click();
|
||
await page.getByRole("option", { name: "最近 8 小时", exact: true }).click();
|
||
await expect(page.getByText(/UTC 历史窗口 · 最近 8 小时/)).toBeVisible();
|
||
await expect(historyStart).not.toContainText("2026-04-30 08:00");
|
||
await historyMode.click();
|
||
await page.getByRole("option", { name: "演示历史窗口", exact: true }).click();
|
||
await expect(historyStart).toContainText("2026-04-30 08:00");
|
||
await expect(historyEnd).toContainText("2026-04-30 16:00");
|
||
await historyStart.click();
|
||
const startDialog = page.getByRole("dialog", { name: "选择开始(UTC)" });
|
||
await startDialog.getByRole("gridcell", { name: "2026年4月29日", exact: true }).click();
|
||
await startDialog.getByRole("combobox", { name: "分钟" }).click();
|
||
await page.getByRole("option", { name: "15", exact: true }).click();
|
||
await startDialog.getByRole("button", { name: "确认", exact: true }).click();
|
||
await expect(historyStart).toContainText("2026-04-29 08:15");
|
||
await historyEnd.click();
|
||
const endDialog = page.getByRole("dialog", { name: "选择结束(UTC)" });
|
||
await endDialog.getByRole("combobox", { name: "分钟" }).click();
|
||
await page.getByRole("option", { name: "45", exact: true }).click();
|
||
await endDialog.getByRole("button", { name: "确认", exact: true }).click();
|
||
await expect(historyEnd).toContainText("2026-04-30 16:45");
|
||
expect(observationQueries).toEqual([]);
|
||
await page.getByRole("button", { name: "读取历史读数" }).click();
|
||
await expect(page.getByText("31.42 m")).toBeVisible();
|
||
await expect(page.getByText("缺失")).toBeVisible();
|
||
expect(observationQueries).toEqual([
|
||
{ point_id: "SCADA-2", start: "2026-04-29T08:15:00Z", end: "2026-04-30T16:45:00Z" }
|
||
]);
|
||
await page.getByText("合成演示样本", { exact: true }).click();
|
||
await page.getByRole("button", { name: "手动读取合成样本" }).click();
|
||
await expect(
|
||
page.getByText(/5 个虚拟测点,240 条读数,9 条状态,2 条告警,4 条运行事件/)
|
||
).toBeVisible();
|
||
await expect(page.getByText("0 m", { exact: true }).first()).toBeVisible();
|
||
await expect(page.getByText("缺失", { exact: true }).first()).toBeVisible();
|
||
|
||
for (const width of [320, 375, 1280]) {
|
||
await page.setViewportSize({ width, height: 720 });
|
||
await pointSelect.scrollIntoViewIfNeeded();
|
||
await pointSelect.click();
|
||
await expect(page.getByRole("listbox")).toBeVisible();
|
||
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(
|
||
width
|
||
);
|
||
await testInfo.attach(`peripheral-select-${width}px`, {
|
||
body: await page.screenshot(),
|
||
contentType: "image/png"
|
||
});
|
||
await page.keyboard.press("Escape");
|
||
await historyStart.click();
|
||
await expect(startDialog).toBeVisible();
|
||
await expect(
|
||
startDialog.locator(
|
||
'input[type="datetime-local"], input[type="date"], input[type="time"], select'
|
||
)
|
||
).toHaveCount(0);
|
||
await startDialog.getByRole("gridcell", { name: "2026年4月28日", exact: true }).click();
|
||
const calendarBox = await startDialog.boundingBox();
|
||
expect(calendarBox!.x).toBeGreaterThanOrEqual(0);
|
||
expect(calendarBox!.x + calendarBox!.width).toBeLessThanOrEqual(width);
|
||
expect(
|
||
await startDialog.evaluate((element) => element.scrollWidth <= element.clientWidth)
|
||
).toBe(true);
|
||
await page.screenshot({ path: `test-results/peripheral-datetime/${width}-calendar.png` });
|
||
await page.keyboard.press("Escape");
|
||
await expect(startDialog).toBeHidden();
|
||
await expect(historyStart).toBeFocused();
|
||
await expect(historyStart).toContainText("2026-04-29 08:15");
|
||
}
|
||
expect(observationQueries).toHaveLength(1);
|
||
expect(writeRequests).toEqual([]);
|
||
});
|
||
|
||
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: "外围样板管理" }).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("增量存储未供给时保留原有外围能力且不请求缺表资源", 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: "synthetic", 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 })).toBeVisible();
|
||
await expect(panel.getByText("连接与演练场景", { exact: true })).toBeVisible();
|
||
expect(managedRequests).toEqual([]);
|
||
});
|
||
|
||
test("创建、修改并软删除测试工单和测试数据", async ({ page }) => {
|
||
const projectId = "324ff7ee-c2d4-5184-a84b-48fb14a2933e";
|
||
let workOrders: Record<string, unknown>[] = [];
|
||
let testData: 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 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: "synthetic", 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("/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: "test" } });
|
||
if (path.endsWith("/monitoring/alerts")) return route.fulfill({ json: { items: [], availability: "not_configured", reason: "未配置", origin: "historical_replay", source_id: "test" } });
|
||
if (path.endsWith("/demo-data")) return route.fulfill({ json: createDemoDataset(projectId) });
|
||
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();
|
||
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" };
|
||
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(); workOrders = [{ ...workOrders[0], ...body, revision: 2 }]; delete workOrders[0].request_id; delete workOrders[0].expected_revision;
|
||
return route.fulfill({ json: workOrders[0] });
|
||
}
|
||
if (path.includes("/work-orders/") && request.method() === "DELETE") { workOrders = []; return route.fulfill({ json: { resource_type: "work_order", resource_id: path.split("/").at(-1), revision: 3, deleted: true } }); }
|
||
if (path.endsWith("/test-data") && request.method() === "GET") return route.fulfill({ json: { items: testData, next_cursor: null } });
|
||
if (path.endsWith("/test-data") && request.method() === "POST") {
|
||
const body = request.postDataJSON(); const item = { ...body, record_id: "60000000-0000-4000-8000-000000000001", project_id: projectId, revision: 1, origin: "synthetic", created_at: now, updated_at: now, created_by: "tester", updated_by: "tester" };
|
||
delete item.request_id; testData = [item]; return route.fulfill({ status: 201, json: item });
|
||
}
|
||
if (path.includes("/test-data/") && request.method() === "PATCH") { const body = request.postDataJSON(); testData = [{ ...testData[0], ...body, revision: 2 }]; delete testData[0].request_id; delete testData[0].expected_revision; return route.fulfill({ json: testData[0] }); }
|
||
if (path.includes("/test-data/") && request.method() === "DELETE") { testData = []; return route.fulfill({ json: { resource_type: "test_data_record", 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.getByLabel("任务层级")).toContainText("外围合成数据核查");
|
||
await expect(panel.getByRole("textbox", { name: "标题" })).toBeHidden();
|
||
await panel.getByRole("button", { name: "新增子任务" }).click();
|
||
await panel.getByRole("textbox", { name: "标题" }).fill("演示 测试工单");
|
||
await panel.getByRole("textbox", { name: "说明" }).fill("外围 CRUD 联调");
|
||
await panel.getByRole("button", { name: "创建子任务" }).click();
|
||
await expect(panel.getByText("演示 测试工单", { exact: true })).toBeVisible();
|
||
await panel.getByRole("button", { name: "编辑子任务 演示 测试工单" }).click();
|
||
await panel.getByRole("textbox", { name: "标题" }).fill("演示 测试工单(已改)");
|
||
await panel.getByRole("button", { name: "保存修改" }).click();
|
||
await expect(panel.getByText("演示 测试工单(已改)", { exact: true })).toBeVisible();
|
||
await panel.getByRole("button", { name: "删除子任务 演示 测试工单(已改)" }).click();
|
||
await expect(panel.getByText("演示 测试工单(已改)", { exact: true })).toBeHidden();
|
||
|
||
await panel.getByRole("group", { name: "外围样板资源类型" }).getByRole("button", { name: /测试数据/ }).click();
|
||
await panel.getByRole("button", { name: "新增测试数据" }).click();
|
||
await panel.getByRole("textbox", { name: "名称" }).fill("演示 压力样本");
|
||
await panel.getByRole("textbox", { name: "数值" }).fill("31.2");
|
||
await panel.getByRole("button", { name: "创建数据" }).click();
|
||
await expect(panel.getByText("演示 压力样本", { exact: true })).toBeVisible();
|
||
await panel.getByRole("button", { name: "编辑数据 演示 压力样本" }).click();
|
||
await panel.getByRole("textbox", { name: "数值" }).fill("30.8");
|
||
await panel.getByRole("button", { name: "保存修改" }).click();
|
||
await expect(panel.getByText(/30.8 m/)).toBeVisible();
|
||
await panel.getByRole("button", { name: "删除数据 演示 压力样本" }).click();
|
||
await expect(panel.getByText("演示 压力样本", { exact: true })).toBeHidden();
|
||
});
|
||
|
||
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) {
|
||
return [
|
||
["合成通信核查", "核查监测点通信和数据上送状态", "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",
|
||
origin: "synthetic",
|
||
created_at: "2026-09-17T00:00:00Z",
|
||
updated_at: "2026-09-17T00:00:00Z",
|
||
created_by: "synthetic-generator",
|
||
updated_by: "synthetic-generator"
|
||
}));
|
||
}
|