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);
});