import type { Locator, Page } from "@playwright/test"; import { expect, fakeServerState, test } from "../fixtures/test"; type RequestLedgerEntry = { path?: string; query?: Record }; const performanceErrorStatuses = [400, 401, 403, 404, 409, 500] as const; async function performanceSummaryRequests(page: Page): Promise { const state = await fakeServerState(page); const ledger = Array.isArray(state.requestLedger) ? state.requestLedger : []; return ledger.filter((entry): entry is RequestLedgerEntry => Boolean(entry && typeof entry === "object" && (entry as RequestLedgerEntry).path === "/v1/web-performance/summary")); } function countWindowRequests(entries: RequestLedgerEntry[], windowId: string): number { return entries.filter((entry) => entry.query?.window === windowId).length; } test("admin HWPOD groups renders node-ops as structured UI before raw JSON is requested", async ({ page }) => { await page.goto("/admin/hwpod-groups"); await expect(page.getByRole("heading", { name: "HWPOD 分组" })).toBeVisible(); await expect(page.getByText("hwpod-node-ops-v1")).toBeVisible(); await expect(page.getByText("/v1/hwpod-node-ops")).toBeVisible(); await expect(page.getByText("node.health")).toBeVisible(); await expect(page.getByText("node-d601-f103-v2")).toBeVisible(); await expect(page.locator("[data-testid='hwpod-raw-json']")).toHaveCount(0); await expect(page.locator("body")).not.toContainText('"contractVersion"'); await page.getByTestId("hwpod-raw-json-toggle").click(); await expect(page.getByTestId("hwpod-raw-json")).toContainText('"contractVersion"'); }); test("performance dashboard renders Chinese charts and keeps drill-down reachable", async ({ page }) => { await page.goto("/performance"); await expect(page.getByRole("heading", { name: "性能监控", exact: true })).toBeVisible(); await expect(page.locator(".performance-contract-item")).toHaveCount(5); await expect(page.locator(".performance-contract-item").filter({ hasText: "观测窗口" })).toContainText("最近 15 分钟"); await expect(page.locator(".performance-contract-item").filter({ hasText: "生成时间" })).toContainText("fake-server"); await expect(page.locator(".performance-contract-item").filter({ hasText: "样本覆盖" })).toContainText("路由"); await expect(page.locator(".performance-contract-item").filter({ hasText: "数据质量" })).toContainText("低样本阈值 5 个样本"); await expect(page.locator(".performance-contract-item").filter({ hasText: "统计口径" })).toContainText("精确窗口分位"); await expect(page.locator(".performance-collection-item")).toHaveCount(4); await expect(page.locator(".performance-collection-item").filter({ hasText: "最近样本" })).toContainText("归因字段不完整"); await expect(page.locator(".performance-collection-item").filter({ hasText: "当前窗口样本" })).toContainText("保留"); await expect(page.locator(".performance-collection-item").filter({ hasText: "来源组成" })).toContainText("工作台"); await expect(page.locator(".performance-collection-item").filter({ hasText: "归因完整性" })).toContainText("不完整"); await expect(page.getByText("工作台归因不完整")).toBeVisible(); await expect(page.getByText("工作台体感趋势")).toBeVisible(); await expect(page.getByText("状态分布")).toBeVisible(); await expect(page.getByText("慢 API TopN")).toBeVisible(); await expect(page.locator("svg.performance-line-chart")).toHaveCount(3); await expect(page.locator(".chart-point")).not.toHaveCount(0); await expect(page.locator(".performance-row-contract").filter({ hasText: "桶化估算" }).first()).toBeVisible(); await expect(page.locator(".performance-row-contract").filter({ hasText: "低样本" }).first()).toBeVisible(); await expect(page.locator(".performance-row-contract").filter({ hasText: "后端证据" }).first()).toContainText("已匹配后端"); await expect(page.locator(".performance-row-contract").filter({ hasText: "RUM 慢于后端" }).first()).toBeVisible(); await expect(page.locator("body")).not.toContainText(/Metric|Route|Count|Status|Samples|Problems|Workbench|Web Vitals|API Timing|Long Tasks/u); await expect(page.locator("body")).not.toContainText(/sessionId|traceId|runId|stdout|stderr|Secret/u); const windowChecks = [ { tab: "5 分钟", windowId: "5m", label: "最近 5 分钟" }, { tab: "15 分钟", windowId: "15m", label: "最近 15 分钟" }, { tab: "1 小时", windowId: "1h", label: "最近 1 小时" }, { tab: "6 小时", windowId: "6h", label: "最近 6 小时" }, { tab: "24 小时", windowId: "24h", label: "最近 24 小时" }, { tab: "全部", windowId: "all", label: "全部累计" } ]; for (const item of windowChecks) { await page.getByRole("tab", { name: item.tab, exact: true }).click(); await expect(page.locator(".performance-contract-item").filter({ hasText: "观测窗口" })).toContainText(item.label); } const summaryRequests = await performanceSummaryRequests(page); for (const item of windowChecks) expect(countWindowRequests(summaryRequests, item.windowId)).toBeGreaterThan(0); const allRequestsBeforeRefresh = countWindowRequests(summaryRequests, "all"); await page.getByLabel("采集新鲜度").getByRole("button", { name: "刷新" }).click(); await expect.poll(async () => countWindowRequests(await performanceSummaryRequests(page), "all")).toBeGreaterThan(allRequestsBeforeRefresh); await expect(page.locator(".performance-contract-item").filter({ hasText: "观测窗口" })).toContainText("全部累计"); await page.screenshot({ path: ".state/workbench-e2e/performance-dashboard-desktop.png", fullPage: true }); await page.setViewportSize({ width: 390, height: 844 }); await page.goto("/performance"); await expect(page.getByRole("heading", { name: "性能明细" })).toBeVisible(); await expect(page.locator("svg.performance-line-chart")).toHaveCount(3); await page.screenshot({ path: ".state/workbench-e2e/performance-dashboard-mobile.png", fullPage: true }); const scrollers = await page.locator(".performance-page .data-table-wrap").evaluateAll((items) => items.map((item) => ({ clientWidth: Math.round(item.clientWidth), scrollWidth: Math.round(item.scrollWidth), overflowX: getComputedStyle(item).overflowX }))); expect(scrollers.length).toBeGreaterThanOrEqual(1); expect(scrollers.every((item) => item.clientWidth <= 390)).toBeTruthy(); expect(scrollers.some((item) => item.scrollWidth > item.clientWidth + 120)).toBeTruthy(); expect(scrollers.every((item) => item.overflowX === "auto" || item.overflowX === "scroll")).toBeTruthy(); const documentScrollWidth = await page.evaluate(() => Math.round(document.documentElement.scrollWidth)); expect(documentScrollWidth).toBeLessThanOrEqual(390); }); test.describe("performance dashboard empty collection", () => { test.use({ scenarioId: "performance-empty" }); test("renders the empty summary contract without invented rows", async ({ page }) => { await page.goto("/performance"); await expect(page.getByRole("heading", { name: "性能监控", exact: true })).toBeVisible(); await expect(page.locator(".performance-contract-item")).toHaveCount(5); await expect(page.locator(".performance-contract-item").filter({ hasText: "样本覆盖" })).toContainText("0 样本"); await expect(page.locator(".performance-collection-item")).toHaveCount(4); await expect(page.locator(".performance-collection-item").filter({ hasText: "最近样本" })).toContainText("采集器尚未上报样本"); await expect(page.locator(".performance-collection-item").filter({ hasText: "当前窗口样本" })).toContainText("0 样本"); await expect(page.getByText("暂无采集样本")).toBeVisible(); await expect(page.getByText("等待数据").first()).toBeVisible(); await expect(page.getByText("暂无性能样本。请先打开工作台或执行一次真实页面操作后刷新。")).toBeVisible(); await expect(page.locator(".performance-row-contract")).toHaveCount(0); }); }); test.describe("performance dashboard stale current window", () => { test.use({ scenarioId: "performance-stale-window" }); test("keeps retained samples separate from the empty current window", async ({ page }) => { await page.goto("/performance"); await expect(page.getByRole("heading", { name: "性能监控", exact: true })).toBeVisible(); await expect(page.locator(".performance-contract-item").filter({ hasText: "样本覆盖" })).toContainText("0 样本"); await expect(page.locator(".performance-collection-item").filter({ hasText: "最近样本" })).toContainText("当前窗口暂无样本"); await expect(page.locator(".performance-collection-item").filter({ hasText: "当前窗口样本" })).toContainText("0 样本"); await expect(page.locator(".performance-collection-item").filter({ hasText: "当前窗口样本" })).toContainText("保留 37 样本"); await expect(page.getByText("当前窗口无样本")).toBeVisible(); await expect(page.getByText("保留样本存在,但当前窗口没有性能样本。")).toBeVisible(); await expect(page.locator(".performance-row-contract")).toHaveCount(0); }); }); test.describe("performance dashboard summary error", () => { test.use({ scenarioId: "performance-summary-error" }); test("surfaces the summary read-model failure without stale dashboard data", async ({ page }) => { await page.goto("/performance"); await expect(page.getByRole("heading", { name: "性能监控" })).toBeVisible(); await expect(page.getByText("性能监控加载失败")).toBeVisible(); await expect(page.getByText("暂时无法连接上游。")).toBeVisible(); const diagnostic = page.locator(".api-error-diagnostic"); await expectDiagnosticSummary(diagnostic, "11111111111111111111111111111111"); await expect(diagnostic).not.toContainText("requestId"); await expandDiagnostic(diagnostic); await expect(diagnostic).toContainText("requestId"); await expect(diagnostic).toContainText("req_e2e_api_error_diagnostic"); await expect(diagnostic).toContainText("upstream_unavailable"); await expect(diagnostic).toContainText("/v1/web-performance/summary"); await expect(diagnostic).toContainText("valuesPrinted"); await expect(diagnostic).toContainText("false"); await expect(page.getByRole("button", { name: "重试" })).toBeVisible(); await expect(page.locator("body")).not.toContainText(/Code Agent|输入已保留|稍后重试/u); await expect(page.locator(".performance-contract-item")).toHaveCount(0); }); }); for (const status of performanceErrorStatuses) { test.describe(`performance dashboard HTTP ${status} diagnostic`, () => { test.use({ scenarioId: `performance-summary-error-${status}` }); test(`surfaces HTTP ${status} trace diagnostics without stale dashboard data`, async ({ page }) => { await page.goto("/performance"); await expect(page.getByRole("heading", { name: "性能监控", exact: true })).toBeVisible(); await expect(page.getByText("性能监控加载失败")).toBeVisible(); const diagnostic = page.locator(".api-error-diagnostic"); await expectDiagnosticSummary(diagnostic, statusTraceId(status)); await expect(diagnostic).not.toContainText("requestId"); await expandDiagnostic(diagnostic); await expect(diagnostic).toContainText("requestId"); await expect(diagnostic).toContainText(`req_e2e_api_status_${status}`); await expect(diagnostic).toContainText(`e2e_http_${status}`); await expect(diagnostic).toContainText("/v1/web-performance/summary"); await expect(diagnostic).toContainText(String(status)); await expect(diagnostic).toContainText("valuesPrinted"); await expect(diagnostic).toContainText("false"); await expect(page.getByRole("button", { name: "重试" })).toBeVisible(); await expect(page.locator(".performance-contract-item")).toHaveCount(0); }); }); } test.describe("performance dashboard browser network diagnostic", () => { test.use({ scenarioId: "performance-network-error" }); test("surfaces browser generated trace diagnostics when the request has no response", async ({ page }) => { await page.goto("/performance"); await expect(page.getByRole("heading", { name: "性能监控", exact: true })).toBeVisible(); await expect(page.getByText("性能监控加载失败")).toBeVisible(); const diagnostic = page.locator(".api-error-diagnostic"); await expect(diagnostic).toContainText("trace_id"); await expect(diagnostic.getByRole("button", { name: "诊断详情" })).toHaveAttribute("aria-expanded", "false"); await expect(diagnostic).not.toContainText("requestId"); await expandDiagnostic(diagnostic); await expect(diagnostic).toContainText("requestId"); await expect(diagnostic).toContainText("browser_network_error"); await expect(diagnostic).toContainText("web / network"); await expect(diagnostic).toContainText("/v1/web-performance/summary"); await expect(diagnostic).toContainText("browser"); await expect(diagnostic).toContainText("valuesPrinted"); await expect(diagnostic).toContainText("false"); await expect(page.locator(".performance-contract-item")).toHaveCount(0); }); }); function statusTraceId(status: number): string { return String(status).repeat(11).slice(0, 32); } async function expectDiagnosticSummary(diagnostic: Locator, traceId: string): Promise { await expect(diagnostic).toContainText(`trace_id=${traceId}`); const toggle = diagnostic.getByRole("button", { name: "诊断详情" }); await expect(toggle).toHaveAttribute("aria-expanded", "false"); await expect(toggle).toBeVisible(); } async function expandDiagnostic(diagnostic: Locator): Promise { const toggle = diagnostic.getByRole("button", { name: "诊断详情" }); await toggle.click(); await expect(toggle).toHaveAttribute("aria-expanded", "true"); }