Files
next-tjwater-frontend/tests/browser/startup-screens.e2e.ts
T

162 lines
8.9 KiB
TypeScript

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<void>((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(); }
});
for (const width of [1440, 375]) {
test(`project timeout shows readiness diagnosis and can retry at ${width}px`, async ({ page }, testInfo) => {
test.setTimeout(40_000);
let fail = true;
const pending = gate();
await page.setViewportSize({ width, height: width === 1440 ? 900 : 740 });
await mockWorkbenchContext(page);
await mockAgentApi(page);
await page.route("**/ready", (route) => route.fulfill({ status: 503, headers, json: {
status: "not_ready", checks: { startup: "ready", metadata_database: "unavailable", mcp_transport: "ready" }
} }));
await page.route("**/api/v1/projects?**", async (route) => {
if (route.request().method() === "OPTIONS") return route.fallback();
if (fail) {
await pending.ready;
return route.abort().catch(() => undefined);
}
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: "应用启动" });
const error = startup.getByRole("alert");
await expect(error).toContainText("读取授权项目超时(已等待 15 秒)", { timeout: 20_000 });
await expect(error).toContainText("项目元数据库暂不可用");
await expect(error).not.toContainText("signal timed out");
await expect(startup.locator('[data-activity="loading"]')).toHaveCount(0);
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
await testInfo.attach(`project-timeout-${width}`, {
body: await page.screenshot({ path: testInfo.outputPath("project-timeout.png") }), contentType: "image/png"
});
fail = false;
pending.release();
await startup.getByRole("button", { name: "重新加载" }).click();
await expect(page.getByTestId("workbench-main-frame")).toBeVisible();
} finally { pending.release(); }
});
}