feat: extend supply workbench demo and map navigation
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { mockWorkbenchContext } from "./support/mock-workbench-context";
|
||||
|
||||
const EMPTY_RASTER_TILE = Buffer.from(
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=",
|
||||
"base64"
|
||||
);
|
||||
|
||||
// Uses generated local building data, while unrelated backend services are isolated.
|
||||
test("3D adds opaque buildings without enabling duplicate GIS layers", async ({ page }) => {
|
||||
test.skip(
|
||||
process.env.TJWATER_CITY_SCENE_TEST !== "1",
|
||||
"Requires the generated local city-model scene"
|
||||
);
|
||||
test.setTimeout(60000);
|
||||
await mockWorkbenchContext(page, undefined, undefined, {}, [], {
|
||||
tiandituToken: "city-scene-tianditu-token"
|
||||
});
|
||||
await page.route("https://t0.tianditu.gov.cn/**", (route) =>
|
||||
route.fulfill({ contentType: "image/png", body: EMPTY_RASTER_TILE })
|
||||
);
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
const toggle = page
|
||||
.getByRole("group", { name: "地图缩放", exact: true })
|
||||
.locator("button[aria-pressed]");
|
||||
await expect(toggle).toBeEnabled({ timeout: 15000 });
|
||||
const businessLayers = await page.evaluate(() =>
|
||||
window.__waterNetworkMap!.getStyle().layers.map((layer) => ({
|
||||
id: layer.id,
|
||||
visibility: window.__waterNetworkMap!.getLayoutProperty(layer.id, "visibility") ?? "visible"
|
||||
}))
|
||||
);
|
||||
await toggle.click();
|
||||
await expect(toggle).toHaveAttribute("aria-pressed", "true");
|
||||
await page.evaluate(() =>
|
||||
window.__waterNetworkMap?.jumpTo({ center: [121.464, 30.916], zoom: 16, pitch: 55 })
|
||||
);
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
page.evaluate(
|
||||
() =>
|
||||
window.__waterNetworkMap?.queryRenderedFeatures({ layers: ["fengxian-city-buildings"] })
|
||||
.length ?? 0
|
||||
),
|
||||
{ timeout: 30000 }
|
||||
)
|
||||
.toBeGreaterThan(20);
|
||||
const buildingPoint = await page.evaluate(() => {
|
||||
const map = window.__waterNetworkMap!;
|
||||
const canvas = map.getCanvas();
|
||||
for (let y = 20; y < canvas.clientHeight; y += 20) {
|
||||
for (let x = 20; x < canvas.clientWidth; x += 20) {
|
||||
if (map.queryRenderedFeatures([x, y], { layers: ["fengxian-city-buildings"] }).length > 0) {
|
||||
return { x, y };
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
expect(buildingPoint).not.toBeNull();
|
||||
const mapCanvasBounds = await page.locator(".maplibregl-canvas").boundingBox();
|
||||
expect(mapCanvasBounds).not.toBeNull();
|
||||
await page.mouse.click(
|
||||
mapCanvasBounds!.x + buildingPoint!.x,
|
||||
mapCanvasBounds!.y + buildingPoint!.y
|
||||
);
|
||||
await expect(page.locator(".maplibregl-popup")).toHaveCount(0);
|
||||
await expect(page.getByText("建筑参考模型", { exact: true })).toHaveCount(0);
|
||||
const scene = await page.evaluate(() => ({
|
||||
layers: window
|
||||
.__waterNetworkMap!.getStyle()
|
||||
.layers.filter((layer) => layer.id.startsWith("fengxian-city-"))
|
||||
.map((layer) => layer.id),
|
||||
sources: Object.keys(window.__waterNetworkMap!.getStyle().sources).filter((id) =>
|
||||
id.startsWith("fengxian-city-")
|
||||
),
|
||||
opacity: window.__waterNetworkMap!.getPaintProperty(
|
||||
"fengxian-city-buildings",
|
||||
"fill-extrusion-opacity"
|
||||
),
|
||||
layerOrder: window.__waterNetworkMap!.getStyle().layers.map((layer) => layer.id)
|
||||
}));
|
||||
expect(scene.layers).toEqual(["fengxian-city-buildings"]);
|
||||
expect(scene.sources).toEqual(["fengxian-city-buildings"]);
|
||||
expect(scene.opacity).toBe(1);
|
||||
const buildingIndex = scene.layerOrder.indexOf("fengxian-city-buildings");
|
||||
expect(buildingIndex).toBeGreaterThan(scene.layerOrder.indexOf("tianditu-vector-annotation"));
|
||||
expect(buildingIndex).toBeLessThan(
|
||||
scene.layerOrder.findIndex((layerId) => layerId.startsWith("pipes-"))
|
||||
);
|
||||
for (const layer of businessLayers) {
|
||||
expect(
|
||||
await page.evaluate(
|
||||
(id) => window.__waterNetworkMap!.getLayoutProperty(id, "visibility") ?? "visible",
|
||||
layer.id
|
||||
)
|
||||
).toBe(layer.visibility);
|
||||
}
|
||||
await page.evaluate(() => window.__waterNetworkMap?.rotateTo(90, { duration: 0 }));
|
||||
await expect(page.locator("[data-map-compass]")).toHaveAttribute("data-bearing", "90.0");
|
||||
await page.evaluate(() => window.__waterNetworkMap?.rotateTo(0, { duration: 0 }));
|
||||
await page.screenshot({ path: "../city-model-fengxian/reports/browser-nanqiao.png" });
|
||||
await toggle.click();
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.evaluate(() =>
|
||||
window.__waterNetworkMap?.getLayoutProperty("fengxian-city-buildings", "visibility")
|
||||
)
|
||||
)
|
||||
.toBe("none");
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await toggle.click();
|
||||
await expect(toggle).toHaveAttribute("aria-pressed", "true");
|
||||
const box = await toggle.boundingBox();
|
||||
expect(box?.width).toBe(40);
|
||||
expect(box?.height).toBe(40);
|
||||
await page.screenshot({ path: "../city-model-fengxian/reports/browser-mobile.png" });
|
||||
});
|
||||
@@ -0,0 +1,111 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { mockWorkbenchContext } from "./support/mock-workbench-context";
|
||||
|
||||
test("combined compass switches 2D and 3D with north reset first", async ({ page }) => {
|
||||
await mockWorkbenchContext(page);
|
||||
await page.route("**/city-model/manifest.json", (route) => route.fulfill({ status: 404 }));
|
||||
|
||||
for (const viewport of [
|
||||
{ width: 1280, height: 800 },
|
||||
{ width: 375, height: 812 }
|
||||
]) {
|
||||
await page.setViewportSize(viewport);
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
|
||||
const zoomControls = page.getByRole("group", { name: "地图缩放", exact: true });
|
||||
const modeButton = zoomControls.locator("button[aria-pressed]");
|
||||
const compass = modeButton.locator("[data-map-compass]");
|
||||
await expect(zoomControls).toBeVisible();
|
||||
await expect(zoomControls.getByRole("button", { name: "放大", exact: true })).toBeVisible();
|
||||
await expect(zoomControls.getByRole("button", { name: "缩小", exact: true })).toBeVisible();
|
||||
await expect(
|
||||
zoomControls.getByRole("button", { name: "切换到三维", exact: true })
|
||||
).toBeVisible();
|
||||
await expect(modeButton).toHaveAttribute("aria-pressed", "false");
|
||||
await expect(compass).toHaveAttribute("data-map-mode", "2d");
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.evaluate(() => ({
|
||||
dragRotate: window.__waterNetworkMap!.dragRotate.isEnabled(),
|
||||
touchPitch: window.__waterNetworkMap!.touchPitch.isEnabled()
|
||||
}))
|
||||
)
|
||||
.toEqual({ dragRotate: false, touchPitch: false });
|
||||
|
||||
await modeButton.click();
|
||||
await expect
|
||||
.poll(() => page.evaluate(() => window.__waterNetworkMap!.getPitch()))
|
||||
.toBe(viewport.width < 640 ? 45 : 50);
|
||||
await expect(modeButton).toHaveAttribute("aria-pressed", "true");
|
||||
await expect(modeButton).toHaveAttribute("aria-label", "切换到二维");
|
||||
await expect(compass).toHaveAttribute("data-map-mode", "3d");
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.evaluate(() => ({
|
||||
dragRotate: window.__waterNetworkMap!.dragRotate.isEnabled(),
|
||||
touchPitch: window.__waterNetworkMap!.touchPitch.isEnabled()
|
||||
}))
|
||||
)
|
||||
.toEqual({ dragRotate: true, touchPitch: true });
|
||||
|
||||
await page.evaluate(() => window.__waterNetworkMap!.jumpTo({ bearing: 60 }));
|
||||
await expect(modeButton).toHaveAttribute("aria-label", "恢复北向");
|
||||
await expect(compass).toHaveAttribute("data-bearing", "60.0");
|
||||
await expect(compass.locator("svg")).toHaveAttribute(
|
||||
"style",
|
||||
/rotateX\([^)]*\) rotateZ\(-60deg\)/
|
||||
);
|
||||
|
||||
await modeButton.click();
|
||||
await expect.poll(() => page.evaluate(() => window.__waterNetworkMap!.getBearing())).toBe(0);
|
||||
await expect(modeButton).toHaveAttribute("aria-label", "切换到二维");
|
||||
expect(await page.evaluate(() => window.__waterNetworkMap!.getPitch())).toBeGreaterThan(0);
|
||||
|
||||
await modeButton.click();
|
||||
await expect.poll(() => page.evaluate(() => window.__waterNetworkMap!.getPitch())).toBe(0);
|
||||
await expect(modeButton).toHaveAttribute("aria-pressed", "false");
|
||||
await expect(modeButton).toHaveAttribute("aria-label", "切换到三维");
|
||||
await expect(compass).toHaveAttribute("data-map-mode", "2d");
|
||||
await expect(
|
||||
zoomControls.getByRole("button", { name: "缩放到全局", exact: true })
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "调整视角", exact: true })).toHaveCount(0);
|
||||
await expect(page.getByRole("region", { name: "地图视角" })).toHaveCount(0);
|
||||
}
|
||||
});
|
||||
|
||||
test("3D navigation overrides the browser context menu for right-drag rotation", async ({
|
||||
page
|
||||
}) => {
|
||||
await mockWorkbenchContext(page);
|
||||
await page.route("**/city-model/manifest.json", (route) => route.fulfill({ status: 404 }));
|
||||
await page.setViewportSize({ width: 1280, height: 800 });
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
|
||||
const modeButton = page
|
||||
.getByRole("group", { name: "地图缩放", exact: true })
|
||||
.locator("button[aria-pressed]");
|
||||
await modeButton.click();
|
||||
await expect.poll(() => page.evaluate(() => window.__waterNetworkMap!.getPitch())).toBe(50);
|
||||
|
||||
const canvas = page.locator(".maplibregl-canvas");
|
||||
const bounds = await canvas.boundingBox();
|
||||
expect(bounds).not.toBeNull();
|
||||
const startX = bounds!.x + bounds!.width / 2;
|
||||
const startY = bounds!.y + bounds!.height / 2;
|
||||
await page.mouse.move(startX, startY);
|
||||
await page.mouse.down({ button: "right" });
|
||||
await page.mouse.move(startX + 80, startY, { steps: 4 });
|
||||
await page.mouse.up({ button: "right" });
|
||||
|
||||
await expect
|
||||
.poll(() => page.evaluate(() => Math.abs(window.__waterNetworkMap!.getBearing())))
|
||||
.toBeGreaterThan(10);
|
||||
expect(
|
||||
await canvas.evaluate((element) =>
|
||||
element.dispatchEvent(
|
||||
new MouseEvent("contextmenu", { bubbles: true, cancelable: true, button: 2 })
|
||||
)
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
@@ -217,12 +217,34 @@ test.describe("外围样板管理", () => {
|
||||
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 workOrderList = page.getByLabel("工单列表");
|
||||
await expect(workOrderList).toContainText("合成通信核查");
|
||||
await expect(workOrderList).toContainText("合成压力偏差复核");
|
||||
await expect(workOrderList).toContainText("合成零流量样本确认");
|
||||
await expect(workOrderList).toContainText("待处理");
|
||||
await expect(workOrderList).toContainText("处理中");
|
||||
await expect(workOrderList).toContainText("已完成");
|
||||
await expect(page.getByLabel("工单步骤 合成通信核查")).toContainText(/派发.*当前步骤.*执行.*待开始.*复令.*待开始/);
|
||||
await expect(page.getByLabel("工单步骤 合成压力偏差复核")).toContainText(/派发.*已完成.*执行.*当前步骤.*复令.*待开始/);
|
||||
await expect(page.getByLabel("工单步骤 合成零流量样本确认")).toContainText(/派发.*已完成.*执行.*已完成.*复令.*已完成/);
|
||||
await expect(workOrderList).not.toContainText("外围合成数据核查");
|
||||
await expect(workOrderList).not.toContainText("子任务");
|
||||
await testInfo.attach("peripheral-work-orders-1280", {
|
||||
body: await workOrderList.screenshot(),
|
||||
contentType: "image/png"
|
||||
});
|
||||
for (const width of [375, 320]) {
|
||||
await page.setViewportSize({ width, height: 720 });
|
||||
const firstOrderSteps = page.getByLabel("工单步骤 合成通信核查");
|
||||
await firstOrderSteps.scrollIntoViewIfNeeded();
|
||||
await expect(firstOrderSteps).toBeVisible();
|
||||
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(width);
|
||||
await testInfo.attach(`peripheral-work-orders-${width}`, {
|
||||
body: await page.screenshot(),
|
||||
contentType: "image/png"
|
||||
});
|
||||
}
|
||||
await page.setViewportSize({ width: 1280, height: 720 });
|
||||
const scenarioSelect = page.getByRole("combobox", { name: "场景" });
|
||||
await scenarioSelect.focus();
|
||||
await page.keyboard.press("Enter");
|
||||
@@ -375,7 +397,7 @@ test.describe("外围样板管理", () => {
|
||||
expect(managedRequests).toEqual([]);
|
||||
});
|
||||
|
||||
test("创建、修改并软删除测试工单和测试数据", async ({ page }) => {
|
||||
test("创建、修改并软删除测试工单和测试数据", async ({ page }, testInfo) => {
|
||||
const projectId = "324ff7ee-c2d4-5184-a84b-48fb14a2933e";
|
||||
let workOrders: Record<string, unknown>[] = [];
|
||||
let testData: Record<string, unknown>[] = [];
|
||||
@@ -418,19 +440,33 @@ test.describe("外围样板管理", () => {
|
||||
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.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("button", { name: "新增工单" }).click();
|
||||
const workOrderEditor = panel.getByRole("textbox", { name: "标题" }).locator("xpath=../..");
|
||||
await expect(workOrderEditor).toHaveCSS("background-color", "rgb(255, 255, 255)");
|
||||
await testInfo.attach("work-order-editor-1280", {
|
||||
body: await workOrderEditor.screenshot(),
|
||||
contentType: "image/png"
|
||||
});
|
||||
await page.setViewportSize({ width: 375, height: 720 });
|
||||
await workOrderEditor.scrollIntoViewIfNeeded();
|
||||
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(375);
|
||||
await testInfo.attach("work-order-editor-375", {
|
||||
body: await page.screenshot(),
|
||||
contentType: "image/png"
|
||||
});
|
||||
await page.setViewportSize({ width: 1280, height: 720 });
|
||||
await panel.getByRole("textbox", { name: "标题" }).fill("演示 测试工单");
|
||||
await panel.getByRole("textbox", { name: "说明" }).fill("外围 CRUD 联调");
|
||||
await panel.getByRole("button", { name: "创建子任务" }).click();
|
||||
await panel.getByRole("button", { name: "创建工单" }).click();
|
||||
await expect(panel.getByText("演示 测试工单", { exact: true })).toBeVisible();
|
||||
await panel.getByRole("button", { name: "编辑子任务 演示 测试工单" }).click();
|
||||
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 panel.getByRole("button", { name: "删除工单 演示 测试工单(已改)" }).click();
|
||||
await expect(panel.getByText("演示 测试工单(已改)", { exact: true })).toBeHidden();
|
||||
|
||||
await panel.getByRole("group", { name: "外围样板资源类型" }).getByRole("button", { name: /测试数据/ }).click();
|
||||
@@ -447,6 +483,133 @@ test.describe("外围样板管理", () => {
|
||||
await expect(panel.getByText("演示 压力样本", { exact: true })).toBeHidden();
|
||||
});
|
||||
|
||||
test("创建水质投诉工单后进入结构化工况任务并由用户打开 Agent", async ({ page }, testInfo) => {
|
||||
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>[] = [];
|
||||
page.on("request", (request) => {
|
||||
if (
|
||||
request.method() === "POST" &&
|
||||
request.url().includes("/api/v1/agent/chat/") &&
|
||||
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_API_BASE_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: "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: [], 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: "synthetic",
|
||||
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.goto("/");
|
||||
await page.getByRole("button", { name: "外围样板管理" }).click();
|
||||
const panel = page.getByRole("complementary", { name: "外围样板管理" });
|
||||
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 expect(conditionPanel.getByRole("heading", { name: "水质投诉调查", exact: true })).toBeVisible();
|
||||
await expect(conditionPanel.getByText("演示执行流程 · 水质投诉调查流程", { exact: true })).toBeVisible();
|
||||
await expect(conditionPanel.getByText("接收投诉工单", { exact: true }).last()).toBeVisible();
|
||||
await expect(conditionPanel.getByText("校验上游水质监测", { exact: true }).last()).toBeVisible();
|
||||
await expect(conditionPanel.getByText("研判供水链路", { exact: true }).last()).toBeVisible();
|
||||
await expect(conditionPanel.getByText("核查二次供水", { exact: true }).last()).toBeVisible();
|
||||
await expect(conditionPanel.getByText("汇总结论与处置", { exact: true }).last()).toBeVisible();
|
||||
await testInfo.attach("water-quality-condition-task-1280", {
|
||||
body: await conditionPanel.screenshot(),
|
||||
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 expect(responsiveConditionPanel.getByRole("heading", { name: "水质投诉调查" })).toBeVisible();
|
||||
const responsiveWorkflowStep = responsiveConditionPanel
|
||||
.getByText("校验上游水质监测", { exact: true })
|
||||
.last();
|
||||
await responsiveWorkflowStep.scrollIntoViewIfNeeded();
|
||||
await expect(responsiveWorkflowStep).toBeVisible();
|
||||
await page.waitForTimeout(350);
|
||||
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(width);
|
||||
await testInfo.attach(`water-quality-condition-task-${width}`, {
|
||||
body: await page.screenshot(),
|
||||
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()) {
|
||||
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: "WQ-SECONDARY-SUPPLY-001",
|
||||
status: "open"
|
||||
});
|
||||
const triggerRequests = agentRequests.filter((body) => "trigger_context" in body);
|
||||
expect(triggerRequests).toHaveLength(2);
|
||||
expect(new Set(triggerRequests.map((body) =>
|
||||
(body.trigger_context as { trigger_id: string }).trigger_id
|
||||
))).toEqual(new Set([
|
||||
`peripheral.work_order.created:${projectId}:${workOrderId}:1`
|
||||
]));
|
||||
});
|
||||
|
||||
test("在 375px 视口内完整呈现面板", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 375, height: 720 });
|
||||
await page.goto("/");
|
||||
|
||||
@@ -17,9 +17,51 @@ export async function mockAgentApi(page: Page, options: MockAgentApiOptions = {}
|
||||
description: "Playwright 浏览器测试",
|
||||
icon: "bolt" as const
|
||||
};
|
||||
const triggeredSessions = new Map<string, { sessionId: string; triggerId: string }>();
|
||||
await page.route("**/api/v1/agent/chat/**", async (route) => {
|
||||
const pathname = new URL(route.request().url()).pathname;
|
||||
|
||||
if (pathname.endsWith("/session") && route.request().method() === "POST") {
|
||||
const body = route.request().postDataJSON() as {
|
||||
trigger_context?: { trigger_id?: string };
|
||||
};
|
||||
const triggerId = body.trigger_context?.trigger_id;
|
||||
const existing = triggerId ? triggeredSessions.get(triggerId) : undefined;
|
||||
const sessionId = existing?.sessionId ?? `browser-session-${triggeredSessions.size + 1}`;
|
||||
if (triggerId && !existing) triggeredSessions.set(triggerId, { sessionId, triggerId });
|
||||
await route.fulfill({
|
||||
status: existing ? 200 : 201,
|
||||
json: {
|
||||
session_id: sessionId,
|
||||
title: "水质投诉事件调查",
|
||||
status: "active",
|
||||
created_at: "2026-09-22T02:00:00Z",
|
||||
updated_at: "2026-09-22T02:00:00Z",
|
||||
created: !existing,
|
||||
event_duplicate: Boolean(existing),
|
||||
trigger_id: triggerId
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const sessionMatch = pathname.match(/\/session\/([^/]+)$/);
|
||||
if (sessionMatch && route.request().method() === "GET") {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
id: sessionMatch[1],
|
||||
session_id: sessionMatch[1],
|
||||
title: "水质投诉事件调查",
|
||||
status: "active",
|
||||
created_at: "2026-09-22T02:00:00Z",
|
||||
updated_at: "2026-09-22T02:00:00Z",
|
||||
messages: [],
|
||||
is_streaming: false
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathname.endsWith("/stream")) {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
|
||||
@@ -8,7 +8,10 @@ export async function mockWorkbenchContext(
|
||||
projectPresentation?: { metadata: unknown; accessContext: unknown },
|
||||
analysisRuns: Record<string, unknown> = {},
|
||||
analysisHistory: unknown[] = [],
|
||||
options: { runtimeMode?: "auto" | "demo" | "history" | "backend" } = {}
|
||||
options: {
|
||||
runtimeMode?: "auto" | "demo" | "history" | "backend";
|
||||
tiandituToken?: string;
|
||||
} = {}
|
||||
) {
|
||||
await mockScadaApi(
|
||||
page,
|
||||
@@ -29,6 +32,7 @@ export async function mockWorkbenchContext(
|
||||
TJWATER_MAP_URL: "https://workbench-map.invalid/geoserver",
|
||||
TJWATER_GEOSERVER_WORKSPACE: "tjwater_next",
|
||||
TJWATER_MAPBOX_ACCESS_TOKEN: "",
|
||||
TJWATER_TIANDITU_TOKEN: options.tiandituToken ?? "",
|
||||
TJWATER_WORKBENCH_RUNTIME_MODE: options.runtimeMode ?? "auto",
|
||||
TJWATER_ENABLE_MSW: "false"
|
||||
})};`
|
||||
|
||||
@@ -44,6 +44,35 @@ test.describe("Main Frame workbench", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("desktop selector switches Tianditu vector and imagery layer pairs", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1280, height: 800 });
|
||||
await openWorkbench(page);
|
||||
|
||||
await selectBasemap(page, "天地图矢量");
|
||||
await expectBaseLayerVisibility(page, {
|
||||
"tianditu-vector": "visible",
|
||||
"tianditu-vector-annotation": "visible",
|
||||
"tianditu-image": "none",
|
||||
"tianditu-image-annotation": "none"
|
||||
});
|
||||
|
||||
await selectBasemap(page, "天地图影像");
|
||||
await expectBaseLayerVisibility(page, {
|
||||
"tianditu-vector": "none",
|
||||
"tianditu-vector-annotation": "none",
|
||||
"tianditu-image": "visible",
|
||||
"tianditu-image-annotation": "visible"
|
||||
});
|
||||
});
|
||||
|
||||
test("mobile layer panel keeps Tianditu choices usable", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 375, height: 812 });
|
||||
await openWorkbench(page);
|
||||
await selectBasemap(page, "天地图矢量");
|
||||
|
||||
await expect(page.locator("main")).toHaveAttribute("data-basemap-tone", "light");
|
||||
});
|
||||
|
||||
test("wide desktop expands the map document when Agent is collapsed", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1920, height: 1080 });
|
||||
await openWorkbench(page);
|
||||
@@ -58,7 +87,9 @@ test.describe("Main Frame workbench", () => {
|
||||
await page.getByRole("button", { name: "折叠 Agent 面板" }).click();
|
||||
await expect(page.getByLabel("Agent 折叠栏")).toBeVisible();
|
||||
await expect.poll(async () => Math.round((await workspace.boundingBox())?.x ?? 0)).toBe(0);
|
||||
await expect.poll(async () => Math.round((await workspace.boundingBox())?.width ?? 0)).toBe(1920);
|
||||
await expect
|
||||
.poll(async () => Math.round((await workspace.boundingBox())?.width ?? 0))
|
||||
.toBe(1920);
|
||||
expect((await workspace.boundingBox())!.width).toBeGreaterThan(expandedWorkspace!.width);
|
||||
await expect(page).toHaveScreenshot("workbench-desktop-wide-max-agent.png", {
|
||||
animations: "disabled",
|
||||
@@ -123,6 +154,7 @@ async function prepareDeterministicWorkbench(page: Page) {
|
||||
body: `globalThis.__TJWATER_CONFIG__ = {
|
||||
TJWATER_AUTH_MODE: "disabled",
|
||||
TJWATER_MAPBOX_ACCESS_TOKEN: "visual-test-token",
|
||||
TJWATER_TIANDITU_TOKEN: "visual-test-tianditu-token",
|
||||
TJWATER_MAP_URL: "https://visual-map.invalid/geoserver",
|
||||
TJWATER_GEOSERVER_WORKSPACE: "tjwater_next",
|
||||
TJWATER_SERVER_API_BASE_URL: "https://tjwater-api.invalid",
|
||||
@@ -135,6 +167,9 @@ async function prepareDeterministicWorkbench(page: Page) {
|
||||
await page.route("https://api.mapbox.com/**", async (route) => {
|
||||
await route.fulfill({ contentType: "image/png", body: EMPTY_RASTER_TILE });
|
||||
});
|
||||
await page.route("https://t0.tianditu.gov.cn/**", async (route) => {
|
||||
await route.fulfill({ contentType: "image/png", body: EMPTY_RASTER_TILE });
|
||||
});
|
||||
await page.route("https://visual-map.invalid/**", async (route) => {
|
||||
await route.fulfill({
|
||||
contentType: "application/vnd.mapbox-vector-tile",
|
||||
@@ -182,19 +217,39 @@ async function openWorkbench(page: Page) {
|
||||
await page.evaluate(async () => {
|
||||
await document.fonts.ready;
|
||||
});
|
||||
await page.waitForFunction(() => Boolean(window.__waterNetworkMap?.getLayer("scada-pressure-hit")));
|
||||
await page.waitForFunction(() =>
|
||||
Boolean(window.__waterNetworkMap?.getLayer("scada-pressure-hit"))
|
||||
);
|
||||
}
|
||||
|
||||
async function selectBasemap(page: Page, label: "浅色" | "影像") {
|
||||
type BasemapLabel = "浅色" | "影像" | "天地图矢量" | "天地图影像";
|
||||
|
||||
async function selectBasemap(page: Page, label: BasemapLabel) {
|
||||
await page.getByRole("button", { name: /图层:管理地图图层/ }).click();
|
||||
await page.getByRole("button", { name: label, exact: true }).click();
|
||||
await page.getByRole("button", { name: /图层:管理地图图层/ }).click();
|
||||
await expect(page.locator("main")).toHaveAttribute(
|
||||
"data-basemap-tone",
|
||||
label === "影像" ? "satellite" : "light"
|
||||
["影像", "天地图影像"].includes(label) ? "satellite" : "light"
|
||||
);
|
||||
}
|
||||
|
||||
async function expectBaseLayerVisibility(page: Page, expected: Record<string, "visible" | "none">) {
|
||||
await expect
|
||||
.poll(async () =>
|
||||
page.evaluate((layerIds) => {
|
||||
const map = window.__waterNetworkMap;
|
||||
return Object.fromEntries(
|
||||
layerIds.map((layerId) => [
|
||||
layerId,
|
||||
map?.getLayoutProperty(layerId, "visibility") ?? "visible"
|
||||
])
|
||||
);
|
||||
}, Object.keys(expected))
|
||||
)
|
||||
.toEqual(expected);
|
||||
}
|
||||
|
||||
async function openDesktopAgent(page: Page) {
|
||||
await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click();
|
||||
await expect(page.getByRole("complementary", { name: "Agent 命令面板" })).toBeVisible();
|
||||
|
||||
Reference in New Issue
Block a user