import { expect, test } from "./helpers"; // A REPORT'S TIMELINE (report.json `entries`): dated entries newest first, // after In brief and before what the check found, each its own anchor; and, // on a site with a public URL, its feeds — linked from the header and named // in the page's metadata, served beside the page with their own media types // (the `_headers` compose writes, which serve-out applies as Pages does). // // The fixture's timeline, newest first: // update-mail 2026-10-04 "A reader asked about another year" // update-opening 2026-10-02 "The opening date, said on air" (updated 2026-10-04) // Both cite only c01, the summary's first citation, so no number moves. const REPORT = "/reports/demo-factcheck/"; const SITE = "https://reports.example.org"; const RSS = `${SITE}${REPORT}feed.xml`; const JSON_FEED = `${SITE}${REPORT}feed.json`; test("the Timeline: after In brief, before what the check found, newest first, each dated and anchored", async ({ page }) => { await page.goto(REPORT); const timeline = page.locator("[data-report-timeline]"); await expect(timeline.locator("h2")).toHaveText("Timeline"); const order = await page.evaluate(() => ["#in-brief", "[data-report-timeline]", "#found", "#claims"].map((s) => { const el = document.querySelector(s)!; return [...document.querySelectorAll("#in-brief, [data-report-timeline], #found, #claims")].indexOf(el); }), ); expect(order).toEqual([0, 1, 2, 3]); const entries = timeline.locator("li[data-entry]"); expect(await entries.evaluateAll((els) => els.map((e) => e.id))).toEqual(["update-mail", "update-opening"]); const newest = entries.nth(0); await expect(newest.locator("[data-entry-date]")).toHaveAttribute("href", "#update-mail"); await expect(newest.locator("[data-entry-date] time")).toHaveText("2026-10-04"); await expect(newest.locator("[data-entry-date] time")).toHaveAttribute("datetime", "2026-10-04"); await expect(newest.locator("h3")).toHaveText("A reader asked about another year"); // Its body cites as a section's does: c01 keeps its number. await expect(newest.locator('[data-inline-cite="c01"] [data-cite-number]')).toHaveText("[1]"); await expect(entries.nth(1)).toContainText("updated 2026-10-04"); // The header's update is the report's own: no entry is newer. await expect(page.locator("[data-report-dates]")).toHaveText("2026-10-01 · updated 2026-10-04 · revision 2"); // An entry's date is its permalink. await entries.nth(1).locator("[data-entry-date]").click(); await expect(page).toHaveURL(`${REPORT}#update-opening`); await expect(page.locator("#update-opening")).toBeInViewport(); }); test("Subscribe: RSS and JSON in the header, and the page's alternates name both", async ({ page }) => { await page.goto(REPORT); const feeds = page.locator("header [data-report-feeds]"); await expect(feeds).toContainText("Subscribe:"); await expect(feeds.locator('a[data-feed="rss"]')).toHaveAttribute("href", RSS); await expect(feeds.locator('a[data-feed="rss"]')).toHaveAttribute("type", "application/rss+xml"); await expect(feeds.locator('a[data-feed="json"]')).toHaveAttribute("href", JSON_FEED); await expect(feeds.locator('a[data-feed="json"]')).toHaveAttribute("type", "application/feed+json"); await expect(page.locator('head link[rel="alternate"][type="application/rss+xml"]')).toHaveAttribute("href", RSS); await expect(page.locator('head link[rel="alternate"][type="application/feed+json"]')).toHaveAttribute("href", JSON_FEED); }); test("feed.xml: RSS 2.0, served as application/rss+xml, one item per entry newest first", async ({ request }) => { const res = await request.get(`${REPORT}feed.xml`); expect(res.status()).toBe(200); expect(res.headers()["content-type"]).toMatch(/^application\/rss\+xml\b/); expect(res.headers()["access-control-allow-origin"]).toBe("*"); const xml = await res.text(); expect(xml).toMatch(/^<\?xml version="1\.0" encoding="UTF-8"\?>\n`); const links = [...xml.matchAll(/([^<]+)<\/guid>/g)].map((m) => m[1]); expect(links).toEqual([`${SITE}${REPORT}#update-mail`, `${SITE}${REPORT}#update-opening`]); expect(xml).toContain("Sun, 04 Oct 2026 00:00:00 GMT"); // A citation's marker links its reference on the page, absolutely. expect(xml).toContain(`<a href="${SITE}${REPORT}#c-c01">[1]`); }); test("feed.json: JSON Feed 1.1, served as application/feed+json, the same items", async ({ request }) => { const res = await request.get(`${REPORT}feed.json`); expect(res.status()).toBe(200); expect(res.headers()["content-type"]).toMatch(/^application\/feed\+json\b/); expect(res.headers()["access-control-allow-origin"]).toBe("*"); const feed = (await res.json()) as { version: string; home_page_url: string; feed_url: string; items: { id: string; title: string; date_published: string; date_modified?: string }[]; }; expect(feed.version).toBe("https://jsonfeed.org/version/1.1"); expect(feed.home_page_url).toBe(`${SITE}${REPORT}`); expect(feed.feed_url).toBe(JSON_FEED); expect(feed.items.map((i) => [i.id, i.title, i.date_published, i.date_modified])).toEqual([ [`${SITE}${REPORT}#update-mail`, "A reader asked about another year", "2026-10-04T00:00:00.000Z", undefined], [`${SITE}${REPORT}#update-opening`, "The opening date, said on air", "2026-10-02T00:00:00.000Z", "2026-10-04T00:00:00.000Z"], ]); }); test("the timeline in the other shapes: a slide per entry, and an entry's link opens its slide", async ({ page }) => { await page.goto(`${REPORT}?rv=slides#update-opening`); await expect(page).toHaveURL(`${REPORT}?rv=slides#s-4`); const slide = page.locator('[data-report-slides] [data-slide="entry:update-opening"]'); await expect(slide).toBeVisible(); await expect(slide.locator("time")).toHaveText("2026-10-02"); await expect(slide.locator("h2")).toHaveText("The opening date, said on air"); }); test("a phone: the Timeline reads in the column; nothing scrolls sideways", async ({ page }) => { await page.setViewportSize({ width: 375, height: 800 }); await page.goto(`${REPORT}#update-mail`); const entry = (await page.locator("#update-mail").boundingBox())!; expect(entry.x).toBeGreaterThanOrEqual(0); expect(entry.x + entry.width).toBeLessThanOrEqual(375); expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(375); });