231 lines
14 KiB
TypeScript
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");
|
|
}
|