import { test } from "node:test"; import assert from "node:assert/strict"; import * as React from "react"; import { renderToStaticMarkup } from "react-dom/server"; import type { CitationView, PageCitationView, PostCitationView, SourceCitationView, SpanCitationView } from "../../lib/report/views"; import { CitationCard } from "./CitationCard"; import { CitedMarkdown, citeIdOf } from "./CitedMarkdown"; import { CitationsProvider } from "./CitationsContext"; import { InlineCite } from "./InlineCite"; import { ReferenceList } from "./ReferenceList"; import { QUOTE_SCORE_TOOLTIP, VerificationBadge, quoteScorePercent } from "./VerificationBadge"; // common's tsconfig has `jsx: "preserve"` (Next compiles it), so tsx falls // back to the classic transform — `React.createElement` on a free `React`. (globalThis as { React?: typeof React }).React = React; const h = React.createElement; const render = (el: Parameters[0]) => renderToStaticMarkup(el); const video: SpanCitationView = { kind: "video", id: "c01", number: 1, quote: "The bridge opened in the spring.", speaker: "The host", moment: "demo-channel/abc123/3126.00-3151.00", href: "/m/demo-channel/abc123/3126.00-3151.00/", start: 3126, end: 3151, record: { channel: "demo-channel", channelTitle: "Demo Channel", id: "abc123", title: "Demo stream", date: "2026-01-10", originalUrl: "https://media.example.org/demo-channel/abc123?t=3126", }, poster: "/poster.png", verification: { quoteScore: 0.974, quoteCheckedAt: "2026-10-04T12:00:00Z", voiceChecked: true, method: "cue-window v1" }, }; const audio: SpanCitationView = { ...video, kind: "audio", id: "au1", number: 2, poster: undefined, verification: undefined, speaker: undefined, moment: "demo-podcast/ep-042/610.50-628.00", href: "/m/demo-podcast/ep-042/610.50-628.00/", start: 610.5, end: 628, record: { channel: "demo-podcast", id: "ep-042", title: "Episode 42" }, }; const post: PostCitationView = { kind: "post", id: "p01", number: 3, quote: "Never said I would move.", moment: "demo-social/123", href: "/m/demo-social/123/", record: { channel: "demo-social", channelTitle: "Demo Social", id: "123", date: "2025-03-14" }, author: "@demo_account", text: "Never said I would move.\n\nPosted to settle it.", shot: "/media/posts/demo-social/123/shot.png", }; const source: SourceCitationView = { kind: "source", id: "a01", number: 4, quote: "He opened the bridge himself.", sourceId: "s0", sourceTitle: "An example article", image: "/reports/demo/stills/a01.png", href: "https://example.org/a", }; const page: PageCitationView = { kind: "page", id: "w01", number: 5, quote: "Opened to traffic: April 2019.", href: "https://example.org/city/history", title: "City records", archiveUrl: "https://web.archive.org/y", note: "The city's own record.", }; const all: CitationView[] = [video, audio, post, source, page]; const byId = Object.fromEntries(all.map((c) => [c.id, c])); test("a video card: title, channel, date, span, speaker, quote, poster and play", () => { const html = render(h(CitationCard, { citation: video })); for (const s of ["Demo stream", "Demo Channel", "2026-01-10", "52:06–52:31", "The host", "The bridge opened in the spring."]) { assert.ok(html.includes(s), s); } assert.match(html, /]*>.*Play 52:06–52:31/); assert.match(html, /href="https:\/\/media.example.org\/demo-channel\/abc123\?t=3126" target="_blank"/); assert.match(html, /data-citation-kind="video"/); }); test("an audio card has no picture without a poster, and still plays its span", () => { const html = render(h(CitationCard, { citation: audio })); assert.ok(!html.includes(" { const html = render(h(CitationCard, { citation: post })); for (const s of ["@demo_account", "Demo Social", "2025-03-14", "Never said I would move.", "Posted to settle it.", "Open post"]) { assert.ok(html.includes(s), s); } assert.match(html, / { const html = render(h(CitationCard, { citation: source })); assert.match(html, /The sentence as An example article shows it: He opened the bridge himself.]*>from An example article<\/a>/); assert.ok(!html.includes("archive")); assert.ok(!render(h(CitationCard, { citation: source, variant: "preview" })).includes("archive")); }); test("a page card: title, host, the archived copy, the note", () => { const html = render(h(CitationCard, { citation: page })); for (const s of ["City records", "example.org", "Archived copy", "The city's own record."]) assert.ok(html.includes(s), s); }); test("the preview density keeps the facts, clamps the quote and drops the note and post text", () => { const html = render(h(CitationCard, { citation: post, variant: "preview" })); assert.ok(html.includes("line-clamp-4")); assert.ok(!html.includes("Posted to settle it.")); assert.ok(!render(h(CitationCard, { citation: page, variant: "preview" })).includes("own record")); // the number shows on a card and a preview, never on a reference entry (the list's) assert.match(html, />3<\/span>/); assert.ok(!render(h(CitationCard, { citation: video, variant: "reference" })).includes(" { const html = render(h(VerificationBadge, { verification: video.verification })); assert.match(html, /data-quote-score="97"/); assert.ok(html.includes("Quote 97%")); assert.ok(html.includes("Voice checked")); assert.ok(html.includes("checked 2026-10-04")); assert.ok(html.includes("checked against the transcript")); assert.ok(html.includes("Method: cue-window v1.")); assert.ok(QUOTE_SCORE_TOOLTIP.includes("checked against the transcript")); assert.equal(render(h(VerificationBadge, { verification: undefined })), ""); assert.equal(render(h(VerificationBadge, { verification: { method: "x" } })), ""); assert.equal(quoteScorePercent(1.2), 100); assert.equal(quoteScorePercent(-1), 0); }); test("an inline cite: the label links to what it cites, the number to its reference entry", () => { const html = render(h(InlineCite, { citation: video, children: "on stream" })); assert.match(html, /]*>on stream<\/a>/); assert.match(html, /]*>]*href="https:\/\/example.org\/z" target="_blank" rel="noopener noreferrer"[^>]*>elsewhere<\/a>/); assert.match(html, /]*>\[x\]\(cite:c01\)<\/code>/); // a label holding an editorial [insertion] is still a cite, with its number const bracketed = render(h(CitedMarkdown, { citations: byId, children: "She said [“told [the mayor] so”](cite:c01)." })); assert.match(bracketed, /data-inline-cite="c01"[^]*?>“told \[the mayor\] so”<\/a>[^]*?\[1\]/); // a link to a place on the same page stays in the tab const jump = render(h(CitedMarkdown, { citations: {}, children: "[the claims](#claims)" })); assert.match(jump, /]*href="#claims"[^>]*>the claims<\/a>/); assert.doesNotMatch(jump, /target=/); // an id the map lacks renders as its label assert.ok(render(h(CitedMarkdown, { citations: {}, children: "[gone](cite:zz)" })).includes("gone")); assert.equal(citeIdOf("cite:c01"), "c01"); assert.equal(citeIdOf("https://x"), null); }); test("the reference list: number order as given, anchors c-, none when asked not to", () => { const html = render(h(ReferenceList, { citations: all })); const anchors = [...html.matchAll(/
  • [m[1], m[2]]); assert.deepEqual(anchors, [ ["c-c01", "1"], ["c-au1", "2"], ["c-p01", "3"], ["c-a01", "4"], ["c-w01", "5"], ]); assert.ok(!render(h(ReferenceList, { citations: all, anchors: false })).includes('id="c-')); assert.equal(render(h(ReferenceList, { citations: [] })), ""); });