import { test } from "node:test"; import assert from "node:assert/strict"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import * as React from "react"; import { renderToStaticMarkup } from "react-dom/server"; // The report routes, rendered from the fixture report site's view JSON // (export/fixtures/report-site): the never-empty params, the report page, a // moment page per kind, the index, a cited site's home, and the header's nav. // // getPaths() is cached on first use, so the environment is set before any // module that reads it is imported — every import below is dynamic. (globalThis as { React?: typeof React }).React = React; const root = fs.mkdtempSync(path.join(os.tmpdir(), "export-reports-")); const publicDir = path.join(root, "public"); const sitesDir = path.join(root, "transcripts", "sites"); fs.mkdirSync(publicDir, { recursive: true }); fs.mkdirSync(path.join(sitesDir, "demo-cited"), { recursive: true }); fs.writeFileSync( path.join(sitesDir, "demo-cited", "site.json"), JSON.stringify({ siteTitle: "Demo Reports", search: false, reports: ["demo-factcheck"] }), ); process.env.EXPORT_PUBLIC_DIR = publicDir; process.env.TRANSCRIPTS_DIR = path.join(root, "transcripts"); process.env.SITES_DIR = sitesDir; process.env.SITE_ID = "demo-cited"; delete process.env.INSTANCE_MODE; const fixture = await import("../../fixtures/report-site/fixture"); const reportPage = await import("../reports/[reportId]/page"); const momentPage = await import("../m/[...moment]/page"); const reportsIndexPage = await import("../reports/page"); const home = await import("../(workspace)/page"); const { headerNavLinks } = await import("./nav"); const { default: ReportArticle } = await import("../components/reports/ReportArticle"); const render = (el: React.ReactElement) => renderToStaticMarkup(el); test("with no reports, each dynamic route builds its one placeholder page, which says so", async () => { assert.deepEqual(reportPage.generateStaticParams(), [{ reportId: "_none" }]); assert.deepEqual(momentPage.generateStaticParams(), [{ moment: ["_none"] }]); const r = render(await reportPage.default({ params: Promise.resolve({ reportId: "_none" }) })); assert.match(r, /No reports/); const m = render(await momentPage.default({ params: Promise.resolve({ moment: ["_none"] }) })); assert.match(m, /No moments/); assert.match(render(reportsIndexPage.default()), /No reports/); }); test("the fixture's view JSON is exactly what the builder makes of its report", () => { for (const [urlPath, value] of Object.entries(fixture.buildFixtureFiles())) { const onDisk = fs.readFileSync(fixture.fixtureFilePath(urlPath), "utf8"); assert.equal(onDisk, fixture.serializeFixture(value), `${urlPath} is stale: run fixtures/report-site/write.ts`); } }); test("the fixture report cites all five kinds", () => { const view = fixture.buildFixtureReportView(); assert.deepEqual( [...new Set(Object.values(view.citations).map((c) => c.kind))].sort(), ["audio", "page", "post", "source", "video"], ); }); // From here on the public dir holds the fixture. test("the fixture public dir is in place", () => { fs.cpSync(fixture.FIXTURE_PUBLIC_DIR, publicDir, { recursive: true }); }); test("params: every report and every moment, as the index files list them", () => { assert.deepEqual(reportPage.generateStaticParams(), [{ reportId: "demo-factcheck" }]); assert.deepEqual(momentPage.generateStaticParams(), [ { moment: ["demo-channel", "abc123", "3126.00-3151.00"] }, { moment: ["demo-podcast", "ep-042", "610.50-628.00"] }, { moment: ["demo-social", "1234567890"] }, ]); }); test("the report page: header, tally, sections and claims, inline cites, references, downloads", async () => { const params = Promise.resolve({ reportId: "demo-factcheck" }); const html = render(await reportPage.default({ params })); // the header: the series on one line and the title on the next, one muted // line of dates and the revision linked to its history, its timeline's // feeds (the fixture keeps a timeline), the document under // review as a card, the subtitle const header = html.slice(html.indexOf("")); assert.match( header, /]*>Demo Checks<\/span> Checking an example article<\/span><\/h1>/, ); assert.match( header, /<\/h1>

]*>2026-10-01 · updated 2026-10-04 · ]*>revision 2<\/a><\/p>

]*>[^]*?Subscribe:[^]*?<\/p>

"), html.indexOf("") + 2000); assert.match( views, /^<\/header>
`${m[2]}:${m[1]}`), ["article:true", "slides:false", "iso:false"], ); assert.match(header, /<\/div>]*>Four claims about a demo channel[^<]*<\/p>$/, "the subtitle, under the card"); assert.doesNotMatch(header, /data-report-attribution|data-report-byline|Fact-check|Under review|The article/); assert.doesNotMatch(html.slice(0, html.indexOf("/); assert.match(card, /]*>An example article about a demo channel<\/a>/); assert.match(card, /]*>A. Writer<\/span> · Example Gazette<\/span> · 2026-09-20<\/span><\/p>/); assert.match(card, /
]*>]*>1 archive link<\/summary>/); assert.ok(card.includes("as published on the day")); assert.ok(!html.includes("Read in the edition published on the day"), "the subject's note is not in the card"); // a claim's source sentence links to the document instead of repeating them // a sentence of the document under review sits on a rail in its colour, with // no link to the document: the rail says whose it is; its box wears the colour too const sentence = html.slice(html.indexOf('data-source-sentence="a01"'), html.indexOf("", html.indexOf('data-source-sentence="a01"'))); assert.match(sentence, /class="[^"]*border-l-4[^"]*" style="border-left-color:#8a6fb0"/); assert.doesNotMatch(sentence, /href="#source-/); assert.match(html, /
]*style="border-left-color:#8a6fb0"/); assert.match(html, /data-verdict-tally=""/); for (const v of ["CORROBORATED", "PARTLY", "CONTRADICTED", "UNTESTABLE"]) assert.match(html, new RegExp(`data-verdict="${v}"`)); assert.match(html, /
", html.indexOf('
]*>]*>“He opened it himself”<\/a><\/h3><\/div>
]*>He opened the bridge himself in 2018.]*>He opened the bridge|data-claim-text/); // a claim without one is headed by its text assert.match(html, /]*>]*>He went back to the bridge every year.<\/a>/); assert.match(html, /
`${m[2]}:${m[1]}`); assert.deepEqual(refs, ["1:c01", "2:au1", "3:a01", "4:w01", "5:a02", "6:p01"]); assert.match(html, /href="\/reports\/demo-factcheck\/citations.json" download=""/); assert.match(html, /href="\/reports\/demo-factcheck\/citations.csv" download=""/); const meta = await reportPage.generateMetadata({ params }); assert.equal(meta.title, "Demo Checks: Checking an example article"); // a claim's flag: a pill on its card; an unflagged claim carries none assert.match(html, /
]*>]*data-brand-mark=""[^]*?<\/svg>No source given<\/span>/); // a claim's evidence: what the report added first, marked; then evidence of // unknown origin, unmarked; what the article gave itself folded away, unmarked const claim1 = html.slice(html.indexOf('
", html.indexOf('
m[1]); assert.deepEqual(order, ["c01", "au1", "w01"]); // the added card: the mark alone, under its number box, the words its tooltip assert.match( claim1, /
]*>]*>]*>1<\/span>]*>]*data-brand-mark=""[^]*?<\/svg>Not in the article<\/span><\/span><\/span>/, ); assert.equal([...claim1.matchAll(/data-citation-added=/g)].length, 1, "only the added card is marked"); assert.match(claim1, /
]*>]*>In the article \(1\)<\/summary>[^]*?data-citation="w01"/); assert.doesNotMatch(claim1, /]* open/, "folded by default"); // the reference list marks the added citation with the mark alone const refList = html.slice(html.indexOf("data-reference-list")); assert.match(refList, /
  • ]*> m[1]); assert.deepEqual(tiers, ["1", "2", "3"]); const tier1 = html.slice(html.indexOf('data-report-tier="1"'), html.indexOf('data-report-tier="2"')); const tier2 = html.slice(html.indexOf('data-report-tier="2"'), html.indexOf('data-report-tier="3"')); const tier3 = html.slice(html.indexOf('data-report-tier="3"')); assert.doesNotMatch(tier1 + tier2, /\s*\d+ min\s*<|\d+ minutes?\b/, "no reading time"); assert.equal([...tier2.matchAll(/data-depth-dot="filled"/g)].length, 2); assert.match(tier1, /data-verdict-tally=""[^]*?The article makes four claims/); const jumpsAt = tier1.indexOf("data-report-jumps"); const jumps = [...tier1.slice(jumpsAt, tier1.indexOf("

    ", jumpsAt)).matchAll(/]*>([^<]+)<\/a>/g)].map((m) => `${m[1]} ${m[2]}`); assert.deepEqual(jumps, ["#found What the check found", "#claims Every claim", "#downloads Downloads"]); assert.match(html, /id="downloads"/); // what the check found: groups in a fixed order, every row a link to its claim assert.match(tier2, /

    ]*>What the check found<\/h2>/); const groups = [...tier2.matchAll(/data-found-group="([A-Z_]+)"/g)].map((m) => m[1]); assert.deepEqual(groups, ["CONTRADICTED", "PARTLY", "UNTESTABLE", "CORROBORATED"]); const rows = [...tier2.matchAll(/data-found-row="([^"]+)"/g)].map((m) => m[1]); assert.deepEqual(rows, ["claim-1", "claim-3", "claim-2", "claim-4", "claim-5"]); assert.match(tier2, /
  • ]*>]*>“He opened it himself”<\/a>]*> — It opened in 2019/); assert.match(tier2, /
  • ]*>]*>He was at the opening.<\/a><\/li>/, "no gist: the title alone"); assert.match(tier2, /
  • ]*>Every claim, with its evidence<\/h2>
    ]*>]*>How it was checked<\/h3>[^]*?searched for in the channel/); }); test("the report header: no series, the title alone; an edited report says so; no accent, the border's rail", () => { const view = fixture.buildFixtureReportView(); const s0 = view.sources.s0; const html = render( React.createElement(ReportArticle, { view: { ...view, series: undefined, updated: view.published, history: { ...view.history!, current: false }, sources: { ...view.sources, s0: { ...s0, accent: undefined, author: undefined, date: undefined } }, }, }), ); const header = html.slice(html.indexOf("")); // no series: the title alone is the heading assert.match(header, /]*>Checking an example article<\/h1>/); assert.doesNotMatch(header, /data-report-series/); // no 'updated' when it is the published date; the revision, edited since assert.match(header, /

    ]*>2026-10-01 · ]*data-report-revision="2"[^>]*>edited since revision 2<\/a><\/p>/); // without an accent the card's rail is the border colour; only the parts it has assert.match(header, /

    ]*>Example Gazette<\/span><\/p>/); }); test("a titled claim with no sentence of the document: its text follows plain, unquoted and unlabelled", () => { const view = fixture.buildFixtureReportView(); const sections = view.sections.map((s) => ({ ...s, claims: s.claims.map((c) => (c.id === "claim-1" ? { ...c, sourceQuote: undefined } : c)), })); const html = render(React.createElement(ReportArticle, { view: { ...view, sections } })); const claim = html.slice(html.indexOf('
    ", html.indexOf('
    <\/div>

    ]*>He opened the bridge himself in 2018.<\/p>/); assert.doesNotMatch(claim, /data-source-sentence|says:|]*>He opened/); }); test("the fixture's own timeline: newest first, its feeds on the site's URL", () => { const view = fixture.buildFixtureReportView(); assert.deepEqual(view.entries?.map((e) => e.id), ["update-mail", "update-opening"]); assert.equal(view.updated, "2026-10-04"); assert.deepEqual(view.feeds, { rss: `${fixture.FIXTURE_SITE_URL}/reports/demo-factcheck/feed.xml`, json: `${fixture.FIXTURE_SITE_URL}/reports/demo-factcheck/feed.json`, }); }); test("a report's timeline: after In brief, before the claims, newest first, dated, each its anchor; Subscribe and the alternates with feeds", async () => { const view = fixture.buildFixtureReportView(); const feeds = { rss: "https://reports.example.org/reports/demo-factcheck/feed.xml", json: "https://reports.example.org/reports/demo-factcheck/feed.json", }; const entries = [ { id: "update-2", date: "2026-10-06T09:30:00Z", title: "A second update", body: "More [on stream](cite:c01)." }, { id: "update-1", date: "2026-10-05", updated: "2026-10-06", title: "A first update", body: "Something new." }, ]; const html = render(React.createElement(ReportArticle, { view: { ...view, entries, feeds } })); const at = (s: string) => html.indexOf(s); assert.ok(at('id="in-brief"') < at("data-report-timeline")); assert.ok(at("data-report-timeline") < at('id="found"')); assert.ok(at('data-entry="update-2"') < at('data-entry="update-1"')); const newest = html.slice(at('

  • ", at('
  • ]*>2026-10-06<\/time>/); // The header: Subscribe, to both feeds. const header = html.slice(at("")); assert.match(header, /data-report-feeds=""[^]*?Subscribe:[^]*? { const html = render( await momentPage.default({ params: Promise.resolve({ moment: ["demo-channel", "abc123", "3126.00-3151.00"] }) }), ); assert.match(html, /
  • ")), /Fact-check/); const first = html.slice(html.indexOf('data-report-entry="demo-factcheck"')); assert.match(first.slice(0, first.indexOf("
  • ")), /Fact-check/, "no series: the kind's eyebrow"); } finally { fs.writeFileSync(file, original); } }); test("the header nav: Reports when the build has one; a cited site has Reports alone, or nothing with one report", () => { const full = headerNavLinks({ cited: false, reports: true, duplicates: false, downloads: true }); assert.deepEqual( full.map((l) => l.label), ["Search", "Ask AI", "Reports", "Downloads", "Use with AI"], ); assert.ok(!headerNavLinks({ cited: false, reports: false, duplicates: false, downloads: false }).some((l) => l.label === "Reports")); assert.deepEqual(headerNavLinks({ cited: true, reports: true, duplicates: true, downloads: true }), [ { href: "/reports/", label: "Reports" }, ]); // One report is the home page: nowhere else to go. assert.deepEqual(headerNavLinks({ cited: true, reports: true, duplicates: false, downloads: false, singleReport: true }), []); }); test.after(() => fs.rmSync(root, { recursive: true, force: true }));