import { expect, test } from "./helpers"; import type { Page } from "@playwright/test"; // ONE ARTICLE, THREE SHAPES (release 22): the report read as the article and // as slides (the overview: overview.spec.ts), over the cited fixture site // (stage.ts). The // fixture's deck, from its report.json and its slide fields: // // 1 title 2 In brief // 3 update-mail 4 update-opening (the timeline, newest first: timeline.spec.ts) // 5 What the check found // 6 The bridge (points, the city's record as its evidence) // 7 claim-1 (evidence: the stream) 8 claim-2 ("Every year?", points) // 9 Moving away (no body: its claims) // 10 claim-3 (quote: the post) 11 claim-4 (statement) 12 claim-5 // 13 sources (the closing line) // // Every test also holds the page's requests to the cited promise (helpers.ts): // the slides are built from the report's own page.json, nothing else. const REPORT = "/reports/demo-factcheck/"; // The article's switch (sticky under the site's header) or the overlay's. const switchIn = (page: Page, where: "article" | "overlay") => where === "article" ? page.locator("[data-report-views] [data-view-switch]") : page.locator("[data-reader-overlay] [data-view-switch]"); const stage = (page: Page) => page.locator("[data-report-slides]"); test("the switch: Article pressed; every place a slide stands for has its id", async ({ page }) => { await page.goto(REPORT); const sw = switchIn(page, "article"); await expect(sw).toHaveAttribute("role", "group"); await expect(sw.getByRole("button")).toHaveText(["Article", "Slides", "Overview"]); await expect(sw.getByRole("button", { name: "Article" })).toHaveAttribute("aria-pressed", "true"); await expect(sw.getByRole("button", { name: "Slides" })).toHaveAttribute("aria-pressed", "false"); for (const id of ["report-head", "in-brief", "update-mail", "update-opening", "found", "bridge", "claim-1", "claim-2", "moving", "claim-3", "claim-4", "claim-5", "report-end"]) { await expect(page.locator(`[id="${id}"]`), id).toHaveCount(1); } }); test("slides: the switch opens the slide for the part in view, and Esc goes back there", async ({ page }) => { await page.goto(`${REPORT}#claim-3`); await expect(page.locator("article#claim-3")).toBeInViewport(); // The switch stays in reach, and the claim lands below it, uncovered. await expect(switchIn(page, "article")).toBeInViewport(); const bar = (await page.locator("[data-report-views]").boundingBox())!; const claim = (await page.locator("article#claim-3").boundingBox())!; expect(claim.y).toBeGreaterThanOrEqual(bar.y + bar.height); await switchIn(page, "article").getByRole("button", { name: "Slides" }).click(); await expect(page).toHaveURL(`${REPORT}?rv=slides#s-10`); await expect(stage(page)).toHaveAttribute("data-report-slides", "10"); await expect(stage(page).locator('[data-slide="claim:claim-3"]')).toBeVisible(); // A region with a live label; the focus is in it; the article behind is inert. await expect(stage(page)).toHaveAttribute("aria-roledescription", "slides"); await expect(stage(page)).toBeFocused(); await expect(page.locator("[data-slide-label]")).toHaveAttribute("aria-live", "polite"); await expect(page.locator("[data-slide-label]")).toContainText("Slide 10 of 13: Claim — He has said many times that he would move away."); await expect(page.locator("main")).toHaveAttribute("inert", ""); await expect(switchIn(page, "overlay").getByRole("button", { name: "Slides" })).toHaveAttribute("aria-pressed", "true"); // The quote layout: the post's own words, its marker opening its card. await expect(stage(page).locator('[data-slide-evidence="p01"] .rs-pull')).toHaveText("Never said I would move. Not once."); await page.keyboard.press("Escape"); await expect(page).toHaveURL(`${REPORT}#claim-3`); await expect(page.locator("[data-reader-overlay]")).toHaveCount(0); await expect(page.locator("article#claim-3")).toBeInViewport(); await expect(page.locator("article#claim-3")).toBeFocused(); await expect(page.locator("main")).not.toHaveAttribute("inert", /.*/); }); test("near the end, a place that cannot scroll up to the bars is still the one the switch opens", async ({ page }) => { await page.setViewportSize({ width: 1280, height: 2400 }); await page.goto(`${REPORT}#claim-5`); await expect(page.locator("article#claim-5")).toBeInViewport(); // The page has run out: claim-5 sits well below where a jump would land it. const bar = (await page.locator("[data-report-views]").boundingBox())!; const claim = (await page.locator("article#claim-5").boundingBox())!; expect(claim.y).toBeGreaterThan(bar.y + bar.height + 64); await switchIn(page, "article").getByRole("button", { name: "Slides" }).click(); await expect(page).toHaveURL(`${REPORT}?rv=slides#s-12`); await expect(stage(page).locator('[data-slide="claim:claim-5"]')).toBeVisible(); }); test("slides: ← → PgUp PgDn Home End step; the rail and the hash follow; Back undoes the switch", async ({ page }) => { await page.goto(REPORT); await switchIn(page, "article").getByRole("button", { name: "Slides" }).click(); await expect(page).toHaveURL(`${REPORT}?rv=slides#s-1`); await expect(stage(page).locator('[data-slide="title"]')).toContainText("Five claims, checked against what was said on air"); await page.keyboard.press("ArrowRight"); await expect(page).toHaveURL(`${REPORT}?rv=slides#s-2`); await page.keyboard.press("PageDown"); await expect(page).toHaveURL(`${REPORT}?rv=slides#s-3`); await expect(stage(page).locator('[data-slide="entry:update-mail"]')).toBeVisible(); await page.keyboard.press("ArrowRight"); await page.keyboard.press("ArrowRight"); await expect(page).toHaveURL(`${REPORT}?rv=slides#s-5`); await expect(stage(page).locator("[data-found-group]")).toHaveCount(4); await page.keyboard.press("End"); await expect(page).toHaveURL(`${REPORT}?rv=slides#s-13`); await expect(stage(page).locator('[data-slide="sources"]')).toContainText("Every citation opens the moment it quotes."); await page.keyboard.press("ArrowRight"); await expect(page).toHaveURL(`${REPORT}?rv=slides#s-13`); await page.keyboard.press("PageUp"); await expect(page).toHaveURL(`${REPORT}?rv=slides#s-12`); await page.keyboard.press("Home"); await expect(page).toHaveURL(`${REPORT}?rv=slides#s-1`); await expect(page.locator(".rs-rail button")).toHaveCount(13); await page.locator(".rs-rail button").nth(7).click(); await expect(page).toHaveURL(`${REPORT}?rv=slides#s-8`); await expect(stage(page).locator('[data-slide="claim:claim-2"]')).toContainText("Every year?"); await page.getByRole("button", { name: "Previous slide" }).click(); await expect(page).toHaveURL(`${REPORT}?rv=slides#s-7`); // Stepping replaces the entry: one Back leaves the slides. await page.goBack(); await expect(page).toHaveURL(REPORT); await expect(page.locator("[data-reader-overlay]")).toHaveCount(0); }); test("slides: a shared link opens the same slide; a section's link opens its slide", async ({ page }) => { await page.goto(`${REPORT}?rv=slides#s-6`); await expect(stage(page).locator('[data-slide="section:bridge"]')).toBeVisible(); await expect(stage(page).locator('[data-slide="section:bridge"] .rs-points li')).toHaveText([ "The article dates the opening to 2018", /He says 2019 on stream, and the city's record\s*\[4\]\s*agrees/, ]); await page.goto(`${REPORT}?rv=slides#moving`); await expect(page).toHaveURL(`${REPORT}?rv=slides#s-9`); await expect(stage(page).locator('[data-slide="section:moving"] .rs-claimlist li')).toHaveCount(3); }); test("slides: a citation's number opens its card; the evidence shows the clip's poster", async ({ page }) => { await page.goto(`${REPORT}?rv=slides#s-7`); const slide = stage(page).locator('[data-slide="claim:claim-1"]'); await expect(slide.locator('[data-verdict="CONTRADICTED"]')).toBeVisible(); const pic = slide.locator('[data-slide-evidence="c01"] img'); await expect(pic).toHaveAttribute("src", "/reports/demo-factcheck/stills/poster.png"); await expect.poll(() => pic.evaluate((el) => (el as HTMLImageElement).naturalWidth)).toBeGreaterThan(0); const number = slide.locator('[data-inline-cite="c01"] [data-cite-number]'); await number.click(); await expect(page.locator('[data-cite-preview="c01"]')).toBeVisible(); await expect(page.locator('[data-cite-preview="c01"]')).toContainText("The bridge opened in the spring of twenty nineteen"); // The click opened the card; it did not leave the slide. await expect(page).toHaveURL(`${REPORT}?rv=slides#s-7`); }); test("slides: Read this in the article opens the article at the slide's place", async ({ page }) => { await page.goto(`${REPORT}?rv=slides#s-8`); await stage(page).locator('[data-slide="claim:claim-2"] [data-slide-read]').click(); await expect(page).toHaveURL(`${REPORT}#claim-2`); await expect(page.locator("article#claim-2")).toBeInViewport(); await expect(page.locator("article#claim-2")).toBeFocused(); }); test("a phone: the slide reflows, its evidence under its words; nothing scrolls sideways", async ({ page }) => { await page.setViewportSize({ width: 375, height: 800 }); await page.goto(`${REPORT}?rv=slides#s-7`); const slide = stage(page).locator('[data-slide="claim:claim-1"]'); await expect(slide).toHaveAttribute("data-fit", "reflow"); const words = (await slide.locator("h2").boundingBox())!; const ev = (await slide.locator('[data-slide-evidence="c01"]').boundingBox())!; expect(ev.y).toBeGreaterThan(words.y + words.height); expect(ev.x + ev.width).toBeLessThanOrEqual(375); expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(375); }); test("light and dark: the slides follow the base and wear the site's accent", async ({ page }) => { await page.goto(`${REPORT}?rv=slides#s-6`); const slide = stage(page).locator('[data-slide="section:bridge"]'); const accent = await page.evaluate(() => getComputedStyle(document.documentElement).getPropertyValue("--brand").trim()); const band = await slide.locator(".rs-band").evaluate((el) => getComputedStyle(el).backgroundColor); const probe = await page.evaluate((c) => { const d = document.createElement("div"); d.style.color = c; document.body.append(d); const v = getComputedStyle(d).color; d.remove(); return v; }, accent); expect(band).toBe(probe); const lightBg = await slide.evaluate((el) => getComputedStyle(el).backgroundColor); await page.evaluate(() => { document.documentElement.classList.add("dark"); document.documentElement.setAttribute("data-base", "dark"); }); await expect.poll(() => slide.evaluate((el) => getComputedStyle(el).backgroundColor)).not.toBe(lightBg); }); test("slides.html opens with no network: one page per slide, its pictures inlined, the keys step", async ({ page, baseURL }) => { const url = `${baseURL}${REPORT}slides.html`; const asked: string[] = []; await page.route("**/*", (route) => { const u = route.request().url(); if (u === url) return route.continue(); asked.push(u); return route.abort(); }); await page.setViewportSize({ width: 1280, height: 720 }); await page.goto(url); await expect(page.locator(".rs-page")).toHaveCount(13); await expect(page.locator('#s-1 [data-slide="title"]')).toContainText("Checking an example article"); const imgs = page.locator("img"); expect(await imgs.count()).toBeGreaterThan(0); for (const img of await imgs.all()) { expect(await img.getAttribute("src")).toMatch(/^data:image\//); await expect.poll(() => img.evaluate((el) => (el as HTMLImageElement).naturalWidth)).toBeGreaterThan(0); } await page.keyboard.press("ArrowRight"); await expect(page).toHaveURL(`${url}#s-2`); await expect(page.locator("#s-2")).toBeInViewport(); await expect(page.locator("#s-13 [data-export-footer]")).toContainText("Revision 2"); expect(asked).toEqual([]); });