import { expect, test } from "@playwright/test"; import { mockBackend } from "./support/mockBackend"; test.setTimeout(180_000); type CameraProbe = { active: string | null; position: number[]; target: number[]; up: number[]; }; type SceneProbe = { errors: string[]; contextVisible?: boolean; camera?: CameraProbe; appearance?: { contextOpacity: number }; networkStyle?: { style: { scale: number } }; }; test("三维页面默认展开场景工具并可调建筑透明度", async ({ page }) => { await mockBackend(page); await page.addInitScript(() => { localStorage.setItem("active_project", "playwright-project"); localStorage.setItem("MAP_WORKSPACE", "zjb"); localStorage.setItem("NETWORK_NAME", "zjb"); }); await page.goto("/three-dimensional-scene"); await expect(page.getByLabel("三维场景控制面板")).toBeVisible(); await expect(page.getByRole("tab", { name: "场景" })).toHaveAttribute( "aria-selected", "true", ); const opacitySlider = page.getByRole("slider", { name: /建筑背景透明度/, }); await expect(opacitySlider).toBeEnabled({ timeout: 60_000 }); await expect(opacitySlider).toHaveAccessibleName("建筑背景透明度 60%"); const backgroundToggle = page.getByRole("switch", { name: "建筑背景" }); await backgroundToggle.evaluate((element) => element.scrollIntoView({ block: "center" }), ); const panel = page.getByLabel("三维场景控制面板"); const panelBeforeToggle = await panel.boundingBox(); const panelScrollBeforeToggle = await panel.locator(".overflow-y-auto").evaluate( (element) => element.scrollTop, ); await backgroundToggle.click(); await expect(backgroundToggle).toHaveAttribute("aria-checked", "false"); const panelAfterToggle = await panel.boundingBox(); const panelScrollAfterToggle = await panel.locator(".overflow-y-auto").evaluate( (element) => element.scrollTop, ); expect(panelAfterToggle).toEqual(panelBeforeToggle); expect(panelScrollAfterToggle).toBe(panelScrollBeforeToggle); await page.setViewportSize({ width: 375, height: 812 }); const mobilePanelBeforeToggle = await panel.boundingBox(); await backgroundToggle.click(); await expect(backgroundToggle).toHaveAttribute("aria-checked", "true"); const mobilePanelAfterToggle = await panel.boundingBox(); expect(mobilePanelAfterToggle).toEqual(mobilePanelBeforeToggle); await expect(opacitySlider).toBeEnabled(); expect( await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth), ).toBe(true); }); test("全部预设使用稳定且紧凑的场景视角", async ({ page }) => { await page.goto( "/three-dimensional/zjb/v29/preview.html?host=platform2&v=32-context-opacity-test", ); await expect(page.locator("#boot")).toBeHidden({ timeout: 60_000 }); const sceneProbe = () => page.evaluate( () => (window as typeof window & { webQA: SceneProbe }).webQA, ); await expect.poll(async () => (await sceneProbe()).camera?.active).toBe("overview"); const initialProbe = await sceneProbe(); const overview = initialProbe.camera; expect(overview?.up[1]).toBeGreaterThan(0.8); expect(initialProbe.appearance?.contextOpacity).toBe(0.4); expect(initialProbe.networkStyle?.style.scale).toBe(6); await page.evaluate(() => window.postMessage( { channel: "tjwater:zjb-scene", version: 2, type: "command", projectCode: "zjb", modelId: "zjb-water-network-v23", command: { name: "set-appearance", patch: { contextOpacity: 0.35 } }, }, window.location.origin, ), ); await expect .poll(async () => (await sceneProbe()).appearance?.contextOpacity) .toBe(0.35); const cameraBeforeContextToggle = (await sceneProbe()).camera; await page.evaluate(() => window.postMessage( { channel: "tjwater:zjb-scene", version: 2, type: "command", projectCode: "zjb", modelId: "zjb-water-network-v23", command: { name: "toggle-context" }, }, window.location.origin, ), ); await expect.poll(async () => (await sceneProbe()).contextVisible).toBe(false); const cameraAfterContextToggle = (await sceneProbe()).camera; expect(cameraAfterContextToggle?.position).toEqual(cameraBeforeContextToggle?.position); expect(cameraAfterContextToggle?.target).toEqual(cameraBeforeContextToggle?.target); await page.evaluate(() => window.postMessage( { channel: "tjwater:zjb-scene", version: 2, type: "command", projectCode: "zjb", modelId: "zjb-water-network-v23", command: { name: "toggle-context" }, }, window.location.origin, ), ); await page.evaluate(() => window.postMessage( { channel: "tjwater:zjb-scene", version: 2, type: "command", projectCode: "zjb", modelId: "zjb-water-network-v23", command: { name: "visit-camera", viewId: "plan" }, }, window.location.origin, ), ); await expect.poll(async () => (await sceneProbe()).camera?.active).toBe("plan"); await expect .poll(async () => { const camera = (await sceneProbe()).camera; return Math.max( Math.abs((camera?.position[0] ?? 0) - (camera?.target[0] ?? 1)), Math.abs((camera?.position[2] ?? 0) - (camera?.target[2] ?? 1)), ); }) .toBeLessThan(0.01); const planProbe = await sceneProbe(); const plan = planProbe.camera; expect(planProbe.errors).toEqual([]); expect(Math.abs((plan?.position[0] ?? 0) - (plan?.target[0] ?? 1))).toBeLessThan(0.01); expect(Math.abs((plan?.position[2] ?? 0) - (plan?.target[2] ?? 1))).toBeLessThan(0.01); expect(plan?.up[0]).toBeCloseTo(0, 6); expect(plan?.up[1]).toBeCloseTo(0, 6); expect(plan?.up[2]).toBeCloseTo(-1, 6); for (const viewId of [ "pump", "hydraulic", "singlePump", "main", "meter", "crossing", "station", "riser", ]) { await page.evaluate((nextViewId) => window.postMessage( { channel: "tjwater:zjb-scene", version: 2, type: "command", projectCode: "zjb", modelId: "zjb-water-network-v23", command: { name: "visit-camera", viewId: nextViewId }, }, window.location.origin, ), viewId, ); await expect.poll(async () => (await sceneProbe()).camera?.active).toBe(viewId); const presetProbe = await sceneProbe(); expect(presetProbe.errors).toEqual([]); expect(presetProbe.camera?.position.every(Number.isFinite)).toBe(true); expect(presetProbe.camera?.target.every(Number.isFinite)).toBe(true); expect(presetProbe.camera?.up[1]).toBeGreaterThan(0.99); } const occupancy = await page.evaluate(async () => { const threeUrl = "/three-dimensional/zjb/v29/vendor/three/three.module.js"; const navigationUrl = "/three-dimensional/zjb/v29/camera-navigation.mjs?v=31-camera-presets-test"; const THREE = await import(threeUrl); const { framePose } = await import(navigationUrl); const aspect = 16 / 9; const box = new THREE.Box3( new THREE.Vector3(-330, 0, -205), new THREE.Vector3(330, 15, 205), ); const pose = framePose(box, aspect, { direction: [0, 1, 0], up: [0, 0, -1], padding: 1.04, }); const camera = new THREE.PerspectiveCamera(42, aspect, 0.1, 10_000); camera.position.fromArray(pose.position); camera.up.fromArray(pose.up); camera.lookAt(new THREE.Vector3(...pose.target)); camera.updateProjectionMatrix(); camera.updateMatrixWorld(); const points = [ [-330, 0, -205], [-330, 0, 205], [-330, 15, -205], [-330, 15, 205], [330, 0, -205], [330, 0, 205], [330, 15, -205], [330, 15, 205], ].map((point) => new THREE.Vector3(...point).project(camera)); return { x: Math.max(...points.map((point) => Math.abs(point.x))), y: Math.max(...points.map((point) => Math.abs(point.y))), }; }); expect(occupancy.x).toBeLessThanOrEqual(1); expect(occupancy.y).toBeLessThanOrEqual(1); expect(Math.max(occupancy.x, occupancy.y)).toBeGreaterThan(0.9); });