Files
pikasTech-HWLAB/web/hwlab-cloud-web/tests/workbench-e2e/specs/admin-hwpod-performance.spec.ts
T

231 lines
14 KiB
TypeScript

import type { Locator, Page } from "@playwright/test";
import { expect, fakeServerState, test } from "../fixtures/test";
type RequestLedgerEntry = { path?: string; query?: Record<string, string> };
const performanceErrorStatuses = [400, 401, 403, 404, 409, 500] as const;
async function performanceSummaryRequests(page: Page): Promise<RequestLedgerEntry[]> {
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<void> {
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<void> {
const toggle = diagnostic.getByRole("button", { name: "诊断详情" });
await toggle.click();
await expect(toggle).toHaveAttribute("aria-expanded", "true");
}