import { expect, test } from "@playwright/test"; import { mockAgentApi } from "./support/mock-agent-api"; import { mockWorkbenchContext } from "./support/mock-workbench-context"; import { TEST_PROJECT_ID } from "./support/mock-scada-api"; const project = { project_id: TEST_PROJECT_ID, name: "奉贤供水项目", code: "tjwater_v2", gs_workspace: "tjwater_next", status: "active" }; const headers = { "Access-Control-Allow-Origin": "*" }; const gate = () => { let release = () => {}; const ready = new Promise((resolve) => { release = resolve; }); return { ready, release: () => release() }; }; for (const width of [1440, 375, 320]) { test(`project startup follows real loading stages at ${width}px`, async ({ page }, testInfo) => { const projects = gate(); const context = gate(); await page.setViewportSize({ width, height: width === 1440 ? 900 : 740 }); await page.emulateMedia({ reducedMotion: "reduce" }); await mockWorkbenchContext(page); await mockAgentApi(page); await page.route("**/api/v1/projects?**", async (route) => { if (route.request().method() === "OPTIONS") return route.fallback(); await projects.ready; await route.fulfill({ headers, json: { items: [project], total: 1, limit: 1000, offset: 0 } }); }); for (const path of ["projects/current/metadata", "access-context"]) { await page.route(`**/api/v1/${path}`, async (route) => { if (route.request().method() === "OPTIONS") return route.fallback(); await context.ready; await route.fallback(); }); } try { await page.goto("/", { waitUntil: "domcontentloaded" }); const startup = page.getByRole("main", { name: "应用启动" }); await expect(startup.getByRole("heading", { name: "正在加载授权项目" })).toBeVisible(); const steps = startup.getByRole("list", { name: "项目加载进度" }).getByRole("listitem"); await expect(steps.nth(0)).toHaveAttribute("aria-current", "step"); await expect(steps.nth(1)).toContainText("等待项目列表返回"); await expect(startup.locator('[data-activity="loading"]')).toHaveCount(1); await expect(startup.locator('[data-activity="loading"]')).toHaveCSS("animation-name", "none"); const panel = await startup.locator('[data-slot="startup-panel"]').boundingBox(); expect(panel!.x).toBeGreaterThanOrEqual(16); expect(panel!.x + panel!.width).toBeLessThanOrEqual(width - 16); await page.screenshot({ path: testInfo.outputPath(`projects-loading-${width}.png`), animations: "disabled" }); projects.release(); await expect(startup.getByRole("heading", { name: "正在准备项目环境" })).toBeVisible(); await expect(steps.nth(0)).toContainText("已完成"); await expect(steps.nth(1)).toHaveAttribute("aria-current", "step"); await expect(steps.nth(1)).toContainText("奉贤供水项目"); await expect(page.getByTestId("workbench-main-frame")).toHaveCount(0); expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true); await page.screenshot({ path: testInfo.outputPath(`project-preparing-${width}.png`), animations: "disabled" }); context.release(); await expect(startup).toHaveCount(0); await expect(page.getByTestId("workbench-main-frame")).toBeVisible(); } finally { projects.release(); context.release(); } }); test(`authentication startup and reconnect share the same surface at ${width}px`, async ({ page }, testInfo) => { const authentication = gate(); let fail = true; await page.setViewportSize({ width, height: width === 1440 ? 900 : 740 }); await page.emulateMedia({ reducedMotion: "no-preference" }); await mockWorkbenchContext(page); await mockAgentApi(page); await page.route("**/src/app/app.tsx*", async (route) => { await authentication.ready; if (fail) return route.abort("failed"); return route.fallback(); }); try { await page.goto("/", { waitUntil: "domcontentloaded" }); const startup = page.getByRole("main", { name: "应用启动" }); await expect(startup.getByRole("heading", { name: "正在连接统一身份认证" })).toBeVisible(); await expect(startup.getByRole("list", { name: "身份认证进度" })).toContainText("检查登录状态"); await expect(startup.locator('[data-activity="loading"]')).not.toHaveCSS("animation-name", "none"); await page.screenshot({ path: testInfo.outputPath(`authentication-loading-${width}.png`), animations: "disabled" }); await page.emulateMedia({ reducedMotion: "reduce" }); await expect(startup.locator('[data-activity="loading"]')).toHaveCSS("animation-name", "none"); authentication.release(); await expect(startup.getByRole("heading", { name: "统一身份认证连接失败" })).toBeVisible(); await expect(startup.getByRole("alert")).toContainText("请检查网络连接后重试"); await expect(startup.locator('[data-activity="loading"]')).toHaveCount(0); expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true); await page.screenshot({ path: testInfo.outputPath(`authentication-error-${width}.png`), animations: "disabled" }); fail = false; await startup.getByRole("button", { name: "重新连接" }).focus(); await page.keyboard.press("Enter"); await expect(page.getByTestId("workbench-main-frame")).toBeVisible(); } finally { authentication.release(); } }); } test("project loading failure can retry without retaining a loading animation", async ({ page }, testInfo) => { let fail = true; const retry = gate(); await page.emulateMedia({ reducedMotion: "reduce" }); await mockWorkbenchContext(page); await mockAgentApi(page); await page.route("**/api/v1/projects?**", async (route) => { if (route.request().method() === "OPTIONS") return route.fallback(); if (fail) return route.fulfill({ status: 503, headers, json: { detail: "项目服务暂时不可用,请稍后重试。" } }); await retry.ready; return route.fulfill({ headers, json: { items: [project], total: 1, limit: 1000, offset: 0 } }); }); try { await page.goto("/", { waitUntil: "domcontentloaded" }); const startup = page.getByRole("main", { name: "应用启动" }); await expect(startup.getByRole("heading", { name: "暂时无法进入工作台" })).toBeVisible(); await expect(startup.getByRole("alert")).toContainText("项目服务暂时不可用"); await expect(startup.locator('[data-activity="loading"]')).toHaveCount(0); await page.screenshot({ path: testInfo.outputPath("project-loading-error.png"), animations: "disabled" }); fail = false; await startup.getByRole("button", { name: "重新加载" }).press("Enter"); await expect(startup.getByRole("heading", { name: "正在加载授权项目" })).toBeVisible(); await expect(startup.getByRole("alert")).toHaveCount(0); retry.release(); await expect(page.getByTestId("workbench-main-frame")).toBeVisible(); } finally { retry.release(); } });