import { test, expect, type Page } from "@playwright/test"; import { FIXTURE_SITES } from "./fixture-summary"; // The /stats/ dashboard's cross-site chart: the three site-based presets, plus // the power-user Customize toolbox (chart types, scale, exclude-outlier, // indexed) and its guard rails. Asserts against the live recharts SVG. // // The e2e server reads the synthetic summary (fixture-summary.ts), so the // dashboard is always there — in a fresh clone too, where these used to skip // on "no corpus data" — and its numbers are known: six sites, the first with // a 12,000-transcript week in May (the All-time axis reaches 10K) and every // site's recent weeks in the hundreds. const pressedPreset = (page: Page) => page.locator('[aria-label="Chart preset"] button[aria-pressed="true"]'); const yTicks = (page: Page) => page .locator(".recharts-yAxis .recharts-cartesian-axis-tick-value") .allTextContents(); const fullLines = (page: Page) => // Lines that actually span the data (guards against the scale-undefined glitch // that collapsed lines to 1-2 points). page.evaluate( () => [...document.querySelectorAll(".recharts-line-curve")].filter( (p) => (p.getAttribute("d") || "").length > 30, ).length, ); // The fixture's public sites, one line each. const siteCount = FIXTURE_SITES.length; test.beforeEach(async ({ page }) => { await page.goto("/stats/"); // Never the no-data panel: the e2e always serves the fixture summary. await expect(page.getByText("This build shipped without dashboard data")).toHaveCount(0); await page.waitForSelector(".recharts-surface"); await expect(page.locator(".recharts-line-curve")).toHaveCount(siteCount); }); test("Recent is the default: every site line is full, on a linear axis", async ({ page, }) => { await expect(pressedPreset(page)).toHaveText("Recent"); expect(await fullLines(page)).toBe(siteCount); // Linear (not symlog): no "1K"/"10K" decade ticks. const ticks = (await yTicks(page)).map((t) => t.trim()); expect(ticks).not.toContain("10K"); }); test("all three presets draw site lines; All-time is symlog", async ({ page }) => { for (const name of ["Recent", "Cumulative", "All-time"]) { await page.getByRole("button", { name, exact: true }).click(); await expect(pressedPreset(page)).toHaveText(name); await expect(page.locator(".recharts-line-curve")).toHaveCount(siteCount); expect(await fullLines(page)).toBe(siteCount); } // All-time is the tamed symlog view. const ticks = (await yTicks(page)).map((t) => t.trim()); for (const t of ["0", "10", "100", "1K", "10K"]) expect(ticks).toContain(t); }); test("Customize still reaches the full toolbox (ranked / small / channel)", async ({ page, }) => { await page.getByRole("button", { name: "Customize" }).click(); await page.getByRole("button", { name: "By channel", exact: true }).click(); await page.getByRole("button", { name: "Ranked", exact: true }).click(); await expect(page.locator(".recharts-bar-rectangle").first()).toBeVisible(); await page.getByRole("button", { name: "Small", exact: true }).click(); await expect(page.locator(".recharts-responsive-container").first()).toBeVisible(); }); test("Customize greys invalid option combinations", async ({ page }) => { await page.getByRole("button", { name: "Customize" }).click(); // Default Recent is a line chart: Share invalid, Symlog valid. await expect(page.getByRole("button", { name: "Share", exact: true })).toBeDisabled(); await expect(page.getByRole("button", { name: "Symlog", exact: true })).toBeEnabled(); // Switch to a stacked Area: Symlog invalid, Share valid. await page.getByRole("button", { name: "Area", exact: true }).click(); await expect(page.getByRole("button", { name: "Symlog", exact: true })).toBeDisabled(); await expect(page.getByRole("button", { name: "Share", exact: true })).toBeEnabled(); }); test("exclude-outlier drops a site line", async ({ page }) => { await page.getByRole("button", { name: "Customize" }).click(); await expect(page.locator(".recharts-line-curve")).toHaveCount(siteCount); await page.getByRole("button", { name: /^Exclude / }).click(); await expect(page.locator(".recharts-line-curve")).toHaveCount(siteCount - 1); }); test("Indexed rebases every series to a shared baseline", async ({ page }) => { await page.getByRole("button", { name: "Customize" }).click(); await page.getByRole("button", { name: "Indexed", exact: true }).click(); await expect(pressedPreset(page)).toHaveCount(0); // custom const ys = await page.evaluate(() => [...document.querySelectorAll(".recharts-line-curve")] .map((p) => { const m = (p.getAttribute("d") || "").match( /^M\s*[-\d.]+[, ]\s*([-\d.]+)/, ); return m ? parseFloat(m[1]) : null; }) .filter((v): v is number => v != null), ); expect(ys.length).toBeGreaterThanOrEqual(2); expect(Math.max(...ys) - Math.min(...ys)).toBeLessThan(2); }); test("mobile: small multiples stack to one column", async ({ page }) => { await page.setViewportSize({ width: 375, height: 800 }); await page.getByRole("button", { name: "Customize" }).click(); await page.getByRole("button", { name: "Small", exact: true }).click(); await expect(page.locator(".recharts-responsive-container")).toHaveCount( siteCount, ); });