import { describe, expect, test } from "bun:test"; import { statsPage, type StatsData } from "./stats"; function page(domains: string[], trafficAll?: StatsData["trafficAll"]): string { const trafficSources = domains.map((domain, i) => ({ domain, hits: 100 - i })); const d: StatsData = { total: 100, hiddenCount: 0, rows: [], totalSearches: 0, avgLatency: 0, avgCacheLatency: 0, handlePct: "0", avatarPct: "0", snapshots: [], trafficSources, trafficAll: trafficAll ?? { domains: trafficSources.length, hits: trafficSources.reduce((s, r) => s + r.hits, 0) }, trafficSeries: [], trafficSeries5m: [], trafficDaily: [], requestSeries: [], requestSeries5m: [], requestDaily: [], }; return statsPage(d); } /** The selection targets — one legend entry, one table row — as raw markup. */ function legendEntry(html: string, domain: string): string { const start = html.indexOf(``); if (start < 0) return ""; const next = html.indexOf(`.*?`, "s"))?.[0] ?? ""; } describe("traffic source selection targets", () => { // The split contract (2026-08-15): in the TABLE the domain name is a link to // the app's site — the row still selects via the rank/hits/% cells because // the click handler skips anchors. In the LEGEND the whole entry stays a // selection target (it has no other tappable surface), so the name there is // NOT a link. const html = page(["plyr.fm", "unknown"]); test("the legend entry is not a link", () => { const entry = legendEntry(html, "plyr.fm"); expect(entry).toContain("plyr.fm"); expect(entry).not.toContain(" { const cell = tableRow(html, "plyr.fm").match(/.*?<\/td>/s)?.[0] ?? ""; expect(cell).toContain('href="https://plyr.fm"'); expect(cell).toContain('target="_blank"'); }); test("the row keeps non-anchor cells to select by", () => { const row = tableRow(html, "plyr.fm"); expect(row).toContain('class="ts-hits"'); expect(row).toContain('class="ts-pct"'); }); test("bare app names (no dot) get no outbound link", () => { const html2 = page(["pckt"]); expect(tableRow(html2, "pckt")).not.toContain(" { // "unknown"/"homepage" are attribution buckets, not hostnames expect(tableRow(html, "unknown")).not.toContain(" { // Regression (stored XSS): domains arrive verbatim from the X-Client / Origin // request headers. The HTML paths escaped them, but the same values are also // inlined into a ", so one request with a crafted X-Client closed the element and // ran for every visitor to /stats. const hostile = 'a">'; const html = page([hostile]); test("no raw script tag survives into the document", () => { expect(html).not.toContain(""); }); test("the JSON payload cannot close the script element", () => { expect(html).not.toContain("