Files
next-tjwater-frontend/tests/browser/support/mock-scada-api.ts
T
jiang 78481903d7 test: migrate workbench fixtures to persistent event streams
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.
2026-11-02 18:56:57 +08:00

163 lines
5.0 KiB
TypeScript

import type { Page } from "@playwright/test";
import { mockWorkbenchEventStream } from "./mock-workbench-event-stream";
const emptyRuntimeSnapshot = {
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-17T02:30:00Z",
conditions: [],
alerts: [],
scenarios: [],
latest_analysis: null,
work_orders: []
};
export const TEST_PROJECT_ID = "324ff7ee-c2d4-5184-a84b-48fb14a2933e";
export async function mockScadaApi(
page: Page,
devices: unknown[] = [],
runtimeSnapshot: unknown = emptyRuntimeSnapshot,
readings: unknown[] = [],
projectPresentation?: { metadata: unknown; accessContext: unknown },
analysisRuns: Record<string, unknown> = {},
analysisHistory: unknown[] = []
) {
await mockWorkbenchEventStream(page);
const corsHeaders = { "Access-Control-Allow-Origin": "*" };
await page.route("https://tjwater-api.invalid/**", async (route) => {
if (route.request().method() === "OPTIONS") {
await route.fulfill({
status: 204,
headers: {
...corsHeaders,
"Access-Control-Allow-Headers": "Authorization, Content-Type, Last-Event-ID, X-Project-Id",
"Access-Control-Allow-Methods": "DELETE, GET, PATCH, POST, OPTIONS"
}
});
return;
}
const url = new URL(route.request().url());
if (url.pathname === "/api/v1/projects") {
await route.fulfill({
contentType: "application/json",
headers: corsHeaders,
body: JSON.stringify({
items: [{ project_id: TEST_PROJECT_ID, gs_workspace: "tjwater_next", status: "active" }],
total: 1,
limit: 1000,
offset: 0
})
});
return;
}
if (url.pathname === "/api/v1/scada-devices") {
await route.fulfill({
contentType: "application/json",
headers: corsHeaders,
body: JSON.stringify({ items: devices, total: devices.length, limit: 1000, offset: 0 })
});
return;
}
if (url.pathname === "/api/v1/projects/current/metadata" && projectPresentation) {
await route.fulfill({
contentType: "application/json",
headers: corsHeaders,
body: JSON.stringify(projectPresentation.metadata)
});
return;
}
if (url.pathname === "/api/v1/access-context" && projectPresentation) {
await route.fulfill({
contentType: "application/json",
headers: corsHeaders,
body: JSON.stringify(projectPresentation.accessContext)
});
return;
}
if (url.pathname === "/api/v1/timeseries/scada-readings") {
await route.fulfill({
contentType: "application/json",
headers: corsHeaders,
body: JSON.stringify(readings)
});
return;
}
if (url.pathname === "/api/v1/workbench/runtime") {
await route.fulfill({
contentType: "application/json",
headers: corsHeaders,
body: JSON.stringify(runtimeSnapshot)
});
return;
}
if (url.pathname === "/api/v1/workbench/sync-snapshot") {
await route.fulfill({
contentType: "application/json",
headers: corsHeaders,
body: JSON.stringify({
schema: "workbench-sync-snapshot@1",
event_cursor: 0,
runtime: runtimeSnapshot,
tasks: [],
notifications: [],
generated_at: "2026-09-17T02:30:00Z"
})
});
return;
}
if (url.pathname === "/api/v1/workbench/events") {
await route.fallback();
return;
}
if (url.pathname === "/api/v1/workbench/analysis-runs") {
const limit = Number(url.searchParams.get("limit") ?? 10);
const offset = Number(url.searchParams.get("offset") ?? 0);
const items = analysisHistory.slice(offset, offset + limit);
await route.fulfill({
contentType: "application/json",
headers: corsHeaders,
body: JSON.stringify({
items,
limit,
offset,
next_offset: offset + limit < analysisHistory.length ? offset + limit : null
})
});
return;
}
const analysisRunMatch = url.pathname.match(/^\/api\/v1\/workbench\/analysis-runs\/([^/]+)$/);
if (analysisRunMatch) {
const runId = decodeURIComponent(analysisRunMatch[1]);
const analysisRun = analysisRuns[runId];
if (analysisRun) {
await route.fulfill({
contentType: "application/json",
headers: corsHeaders,
body: JSON.stringify(analysisRun)
});
} else {
await route.fulfill({
status: 404,
contentType: "application/json",
headers: corsHeaders,
body: JSON.stringify({ detail: "Analysis run not found" })
});
}
return;
}
await route.fulfill({
status: 404,
contentType: "application/json",
headers: corsHeaders,
body: "{}"
});
});
}