From 9a096362f329619ecd11d0a4413518e4c99f1d38 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 6 Nov 2026 07:45:50 -0500 Subject: [PATCH 1/6] Show slide 12's live tracker only after its counts settle The deck swapped the still for the tracker once the snapshot line printed and fonts loaded, and a WebKit run on production showed the frame while three of the four count cards still read opacity 0. It now also waits for the four cards and their ancestors to reach full opacity and size, and it polls only while the slide shows, since a hidden frame's cards read as at rest before they fade in. A reload inside the frame brings the still back until the new page loads and passes the check. A page the browser keeps for Back keeps its frame, since it returns whole and fires no new load. A preview's cross-origin frame stays on the still. Keys pressed on the year menu now reach the deck, so only text fields keep the arrows. The notes' stage direction and the ledger's tracker entry say so. Co-Authored-By: Claude Opus 5.5 --- slideshows/appam-2026/EVIDENCE.md | 2 +- slideshows/appam-2026/SPEAKER-NOTES.md | 2 +- .../appam-2026/slides/CoverageEmbed.test.tsx | 285 +++++++++++++++++- .../appam-2026/slides/CoverageEmbed.tsx | 60 +++- 4 files changed, 321 insertions(+), 28 deletions(-) diff --git a/slideshows/appam-2026/EVIDENCE.md b/slideshows/appam-2026/EVIDENCE.md index c18bf669..8ba05072 100644 --- a/slideshows/appam-2026/EVIDENCE.md +++ b/slideshows/appam-2026/EVIDENCE.md @@ -195,7 +195,7 @@ We quote these on slide 7 and in the notes' Questions section, and we model none ## The web apps -We captured slide 12's coverage tracker from www.policyengine.org/us/model/rules/coverage on November 5, 2026. It shows 109 total programs, 87 complete, 18 partial and 4 in progress, and its top line reads "Coverage reflects policyengine-us 2.72.3 (main @ 4c65c82, fetched 2026-10-27). The production API served 2.2.1 when the snapshot was fetched." The deck's own runs use 2.90.2. Slide 12 loads that page in a live frame at the still's crop, and the still shows until the tracker renders and stands in for it in the PDF. +We captured slide 12's coverage tracker from www.policyengine.org/us/model/rules/coverage on November 5, 2026. It shows 109 total programs, 87 complete, 18 partial and 4 in progress, and its top line reads "Coverage reflects policyengine-us 2.72.3 (main @ 4c65c82, fetched 2026-10-27). The production API served 2.2.1 when the snapshot was fetched." The deck's own runs use 2.90.2. We load that page in a live frame on slide 12 at the still's crop, and print the still alone in an export. We show the still until the tracker prints that snapshot line and its four count cards reach full opacity and size. We captured slide 11's CliffWatch screenshot on November 4, 2026, and Build's report on November 5, 2026, from the live sites. We scripted the recording, built it from screenshots and drew its cursor and address bar. CliffWatch drew that run's chart in under a second. Other runs took longer. diff --git a/slideshows/appam-2026/SPEAKER-NOTES.md b/slideshows/appam-2026/SPEAKER-NOTES.md index 4273e126..0b2bfc24 100644 --- a/slideshows/appam-2026/SPEAKER-NOTES.md +++ b/slideshows/appam-2026/SPEAKER-NOTES.md @@ -96,7 +96,7 @@ In Build, you can open the same Massachusetts credit, set it to $880, pick Massa We model federal income and payroll taxes and credits and every state income tax, in 44 jurisdictions. We model TANF and child care subsidies in all 50 states and DC, plus SNAP, WIC, SSI, school meals, ACA premium tax credits, Medicaid and CHIP eligibility and local taxes such as Philadelphia's wage tax. Our coverage tracker lists 109 programs and marks 87 complete, 18 partial and 4 in progress. -*The tracker runs live in the frame, so you can click and scroll in it, and the arrows keep moving the deck. If you click its search box, click the slide outside the frame, which also moves to the next step.* +*Once the tracker loads, you can click and scroll in the frame, and the left and right arrow keys still move the deck. After you click in its search box, they move the box's cursor until you click elsewhere in the frame. If you pick a year in its menu, the tracker counts only the programs we mark verified for that year.* ## 13. We build Microcosm, our microdata, from surveys and official totals (9:50, 50 seconds) diff --git a/slideshows/appam-2026/slides/CoverageEmbed.test.tsx b/slideshows/appam-2026/slides/CoverageEmbed.test.tsx index 4ee9df29..2f667e9e 100644 --- a/slideshows/appam-2026/slides/CoverageEmbed.test.tsx +++ b/slideshows/appam-2026/slides/CoverageEmbed.test.tsx @@ -3,6 +3,8 @@ import { createRoot, type Root } from 'react-dom/client'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import CoverageEmbed, { TRACKER_URL, forwardKey, trackerAgrees } from './CoverageEmbed'; +(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; + let container: HTMLDivElement; let root: Root; @@ -28,7 +30,7 @@ describe('the coverage tracker on the rules slide', () => { expect(container.querySelector('img')?.getAttribute('alt')).toBe('The tracker'); }); - it('keeps the frame hidden until the tracker renders', async () => { + it('keeps the frame hidden on first render', async () => { window.history.replaceState(null, '', '/slides/appam-2026?slide=13'); await renderEmbed(); expect(container.querySelector('iframe')?.style.opacity).toBe('0'); @@ -43,31 +45,251 @@ describe('the coverage tracker on the rules slide', () => { }); }); +const SNAPSHOT = 'Coverage reflects policyengine-us 2.72.3 (main @ 4c65c82, fetched 2026-10-27). 109 Total programs'; +const FALLBACK = 'Coverage reflects the bundled fallback registry. 64 Total programs'; + +const card = (label: string, style = '') => `

${label}

`; +/** The legend after the tracker's cards, which repeats three of their labels and never fades. */ +const LEGEND = ['Complete', 'Partial', 'In progress', 'Not started'].map((label) => `${label}`).join(''); +/** The tracker's four cards and its legend, with the given attributes on the last card. */ +const cards = (lastStyle = '') => + [card('Total programs'), card('Complete'), card('Partial'), card('In progress', lastStyle), LEGEND].join(''); +const CARD_STATES = { + 'at rest': { html: cards(), rests: true }, + 'at rest under an identity matrix': { html: cards('data-transform="matrix(1, 0, 0, 1, 0, 0)"'), rests: true }, + 'still fading in': { html: cards('data-opacity="0"'), rests: false }, + 'nearly faded in': { html: cards('data-opacity="0.99"'), rests: false }, + 'still growing': { html: cards('data-transform="matrix(0.9, 0, 0, 0.9, 0, 0)"'), rests: false }, + 'missing a card': { html: [card('Total programs'), card('Complete'), card('Partial')].join(''), rests: false }, + 'not drawn yet': { html: '', rests: false }, +}; + +/** A page whose computed styles come from data attributes, since jsdom computes neither property. */ +const stubPage = (innerText: string, status = 'loaded', html = cards()) => { + const body = document.createElement('div'); + body.innerHTML = html; + Object.defineProperty(body, 'innerText', { value: innerText }); + return { + body, + fonts: { status }, + defaultView: { + getComputedStyle: (element: HTMLElement) => ({ + opacity: element.dataset.opacity ?? '1', + transform: element.dataset.transform ?? 'none', + }), + }, + } as unknown as Document; +}; + describe('the check that shows the frame over the still', () => { - const page = (innerText: string, status = 'loaded') => - ({ body: { innerText }, fonts: { status } }) as unknown as Document; - const snapshot = 'Coverage reflects policyengine-us 2.72.3 (main @ 4c65c82, fetched 2026-10-27). 109 Total programs'; + const TEXTS = { + 'the still’s snapshot': { text: SNAPSHOT, matches: true }, + 'the fallback counts': { text: FALLBACK, matches: false }, + 'another release': { text: 'Coverage reflects policyengine-us 2.95.0 (main @ abc1234). 120 Total programs', matches: false }, + 'another commit of the same release': { + text: 'Coverage reflects policyengine-us 2.72.3 (main @ abc1234, fetched 2026-11-01). 110 Total programs', + matches: false, + }, + 'nothing yet': { text: '', matches: false }, + }; - it('keeps the still while the tracker prints its fallback counts', () => { - expect(trackerAgrees(page('Coverage reflects the bundled fallback registry. 64 Total programs'))).toBe(false); + it('passes for every page that prints the snapshot, with fonts loaded and cards at rest, and no other', () => { + for (const [textName, { text, matches }] of Object.entries(TEXTS)) { + for (const status of ['loaded', 'loading']) { + for (const [cardName, { html, rests }] of Object.entries(CARD_STATES)) { + const expected = matches && status === 'loaded' && rests; + expect(trackerAgrees(stubPage(text, status, html)), `${textName}, ${status}, ${cardName}`).toBe(expected); + } + } + } }); - it('keeps the still when the tracker moves to another snapshot', () => { - expect(trackerAgrees(page('Coverage reflects policyengine-us 2.95.0 (main @ abc1234). 120 Total programs'))).toBe(false); + it('holds the still for a fading ancestor of a card', () => { + const html = `
${cards()}
`; + expect(trackerAgrees(stubPage(SNAPSHOT, 'loaded', html))).toBe(false); }); - it('keeps the still on an empty or unreadable page', () => { - expect(trackerAgrees(page(''))).toBe(false); + it('keeps the still when it can’t read the page', () => { expect(trackerAgrees(null)).toBe(false); expect(trackerAgrees(undefined)).toBe(false); }); - it('keeps the still while the page loads its fonts', () => { - expect(trackerAgrees(page(snapshot, 'loading'))).toBe(false); + it('keeps the still when the page has no window to read styles from', () => { + const page = { ...stubPage(SNAPSHOT), defaultView: null } as unknown as Document; + expect(trackerAgrees(page)).toBe(false); + }); +}); + +describe('when the frame shows', () => { + let resize: (width: number) => void; + + beforeEach(() => { + vi.useFakeTimers({ toFake: ['setInterval', 'clearInterval'] }); + vi.stubGlobal( + 'ResizeObserver', + class { + constructor(callback: (entries: { contentRect: { width: number } }[]) => void) { + resize = (width) => callback([{ contentRect: { width } }]); + } + observe() {} + disconnect() {} + }, + ); + }); + + afterEach(() => { + vi.useRealTimers(); + vi.unstubAllGlobals(); + }); + + const mount = async (page: Document | null) => { + window.history.replaceState(null, '', '/slides/appam-2026?slide=13'); + await renderEmbed(); + const frame = container.querySelector('iframe') as HTMLIFrameElement; + setPage(frame, page); + return frame; + }; + const setPage = (frame: HTMLIFrameElement, page: Document | null) => + Object.defineProperty(frame, 'contentDocument', { value: page, configurable: true }); + const show = (width: number) => act(async () => resize(width)); + const load = (frame: HTMLIFrameElement) => act(async () => void frame.dispatchEvent(new Event('load'))); + const poll = () => act(async () => void vi.advanceTimersByTime(1200)); + const shown = (frame: HTMLIFrameElement) => frame.style.opacity === '1' && frame.style.pointerEvents === 'auto'; + const hidden = (frame: HTMLIFrameElement) => frame.style.opacity === '0' && frame.style.pointerEvents === 'none'; + + it('needs the frame loaded, the slide showing and a page that agrees, in every combination', async () => { + const pages = { agreeing: true, fallback: false, unreadable: false }; + for (const loaded of [false, true]) { + for (const showing of [false, true]) { + for (const [name, agrees] of Object.entries(pages)) { + const page = name === 'agreeing' ? stubPage(SNAPSHOT) : name === 'fallback' ? stubPage(FALLBACK) : null; + const frame = await mount(page); + if (showing) await show(1171.5); + if (loaded) await load(frame); + await poll(); + const label = `loaded ${loaded}, showing ${showing}, ${name}`; + expect(shown(frame), label).toBe(loaded && showing && agrees); + expect(hidden(frame) || shown(frame), label).toBe(true); + await act(async () => root.unmount()); + root = createRoot(container); + } + } + } + }); + + it('waits for the slide to show even when the hidden page already agrees', async () => { + const frame = await mount(stubPage(SNAPSHOT)); + await load(frame); + await poll(); + expect(hidden(frame)).toBe(true); + await show(1171.5); + expect(hidden(frame)).toBe(true); + await poll(); + expect(shown(frame)).toBe(true); + }); + + it('holds the still until the cards come to rest', async () => { + const frame = await mount(stubPage(SNAPSHOT, 'loaded', cards('data-opacity="0"'))); + await show(1171.5); + await load(frame); + await poll(); + expect(hidden(frame)).toBe(true); + setPage(frame, stubPage(SNAPSHOT)); + await poll(); + expect(shown(frame)).toBe(true); + }); + + it('brings the still back when the frame’s page goes away, until the check passes again', async () => { + const frame = await mount(stubPage(SNAPSHOT)); + await show(1171.5); + await load(frame); + await poll(); + expect(shown(frame)).toBe(true); + + setPage(frame, stubPage(FALLBACK)); + await act(async () => void frame.contentWindow?.dispatchEvent(new Event('pagehide'))); + expect(hidden(frame)).toBe(true); + await load(frame); + await poll(); + expect(hidden(frame)).toBe(true); + + setPage(frame, stubPage(SNAPSHOT)); + await poll(); + expect(shown(frame)).toBe(true); + }); + + it('waits for the new page to load before it checks again', async () => { + const frame = await mount(stubPage(SNAPSHOT)); + await show(1171.5); + await load(frame); + await poll(); + await act(async () => void frame.contentWindow?.dispatchEvent(new Event('pagehide'))); + await poll(); + expect(hidden(frame)).toBe(true); + await load(frame); + await poll(); + expect(shown(frame)).toBe(true); + }); + + it('keeps the frame when the browser keeps the page for Back', async () => { + const frame = await mount(stubPage(SNAPSHOT)); + await show(1171.5); + await load(frame); + await poll(); + const leaving = Object.assign(new Event('pagehide'), { persisted: true }); + await act(async () => void frame.contentWindow?.dispatchEvent(leaving)); + await poll(); + expect(shown(frame)).toBe(true); }); - it('shows the frame once the tracker matches the still', () => { - expect(trackerAgrees(page(snapshot))).toBe(true); + it('first checks the page 400 ms after the slide shows', async () => { + const frame = await mount(stubPage(SNAPSHOT)); + await load(frame); + await show(1171.5); + await act(async () => void vi.advanceTimersByTime(399)); + expect(hidden(frame)).toBe(true); + await act(async () => void vi.advanceTimersByTime(1)); + expect(shown(frame)).toBe(true); + }); + + it('hands a key pressed in the frame to the deck once, however many times the frame loads', async () => { + const frame = await mount(stubPage(SNAPSHOT)); + await load(frame); + await load(frame); + const deck = vi.fn(); + window.addEventListener('keydown', deck); + frame.contentWindow?.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', cancelable: true })); + window.removeEventListener('keydown', deck); + expect(deck).toHaveBeenCalledOnce(); + expect(deck.mock.calls[0][0].key).toBe('ArrowRight'); + }); + + it('stays on the still across origins, where the browser denies the frame’s window and document', async () => { + const frame = await mount(null); + const denied = new Proxy( + {}, + { + get() { + throw new DOMException('Blocked a frame with another origin', 'SecurityError'); + }, + }, + ); + Object.defineProperty(frame, 'contentWindow', { value: denied, configurable: true }); + await show(1171.5); + await load(frame); + await poll(); + expect(hidden(frame)).toBe(true); + }); + + it('keeps showing the frame when the slide leaves and comes back', async () => { + const frame = await mount(stubPage(SNAPSHOT)); + await show(1171.5); + await load(frame); + await poll(); + await show(0); + expect(frame.style.opacity).toBe('0'); + await show(1171.5); + expect(shown(frame)).toBe(true); }); }); @@ -79,6 +301,41 @@ describe('a key pressed inside the frame', () => { return event; }; + it('reaches the deck from every element except a text field', () => { + const editable = document.createElement('div'); + Object.defineProperty(editable, 'isContentEditable', { value: true }); + const targets: [HTMLElement, boolean][] = [ + [document.createElement('div'), true], + [document.createElement('select'), true], + [document.createElement('button'), true], + [document.createElement('a'), true], + [document.createElement('input'), false], + [document.createElement('textarea'), false], + [editable, false], + ]; + for (const key of ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', ' ', 'PageDown', 'Home', 'f']) { + for (const [target, reaches] of targets) { + const deck = vi.fn(); + window.addEventListener('keydown', deck); + press(key, target); + window.removeEventListener('keydown', deck); + expect(deck.mock.calls.length, `${key} on ${target.tagName}`).toBe(reaches ? 1 : 0); + } + } + }); + + it('moves the deck from the year menu, and leaves the menu the keys the deck ignores', () => { + const deck = vi.fn((event: KeyboardEvent) => { + if (event.key === 'ArrowRight') event.preventDefault(); + }); + window.addEventListener('keydown', deck); + const right = press('ArrowRight', document.createElement('select')); + const down = press('ArrowDown', document.createElement('select')); + window.removeEventListener('keydown', deck); + expect(right.defaultPrevented).toBe(true); + expect(down.defaultPrevented).toBe(false); + }); + it('reaches the deck, and the frame leaves a key the deck uses alone', () => { const deck = vi.fn((event: KeyboardEvent) => event.preventDefault()); window.addEventListener('keydown', deck); diff --git a/slideshows/appam-2026/slides/CoverageEmbed.tsx b/slideshows/appam-2026/slides/CoverageEmbed.tsx index 5b69c2d8..0c047f01 100644 --- a/slideshows/appam-2026/slides/CoverageEmbed.tsx +++ b/slideshows/appam-2026/slides/CoverageEmbed.tsx @@ -19,17 +19,45 @@ const CROP = { left: 404.5, top: 105.5, width: STILL.width / 2, height: STILL.he * other counts under another line, and a loading or error page prints neither, * so the still stays until the tracker agrees with it. */ -const TRACKER_TEXT = 'Coverage reflects policyengine-us 2.72.3'; +const TRACKER_TEXT = 'Coverage reflects policyengine-us 2.72.3 (main @ 4c65c82'; +/** The labels on the still's four count cards, which the tracker fades in. */ +const COUNT_LABELS = ['Total programs', 'Complete', 'Partial', 'In progress']; +const IDENTITY = 'matrix(1, 0, 0, 1, 0, 0)'; -/** Whether the page in the frame shows the still's snapshot in its own fonts. */ +/** Whether an element and everything around it have reached full opacity and size. */ +function atRest(element: Element, view: Window) { + for (let node: Element | null = element; node; node = node.parentElement) { + const { opacity, transform } = view.getComputedStyle(node); + if (Number(opacity) < 1 || (transform !== 'none' && transform !== IDENTITY)) return false; + } + return true; +} + +/** Whether the tracker has finished drawing the four count cards the still shows. */ +function countsAtRest(page: Document) { + const view = page.defaultView; + if (!view) return false; + const leaves = [...page.body.querySelectorAll('*')].filter((element) => element.children.length === 0); + return COUNT_LABELS.every((label) => { + const card = leaves.find((leaf) => leaf.textContent?.trim() === label); + return Boolean(card) && atRest(card as Element, view); + }); +} + +/** + * Whether the page in the frame prints the still's snapshot, with no font still + * loading and its count cards at rest. + */ export function trackerAgrees(page: Document | null | undefined) { if (!page?.body?.innerText?.includes(TRACKER_TEXT)) return false; - return (page.fonts?.status ?? 'loaded') === 'loaded'; + if ((page.fonts?.status ?? 'loaded') !== 'loaded') return false; + return countsAtRest(page); } +/** Only a text field keeps the arrows, so the year menu still lets them drive the deck. */ const isTyping = (target: EventTarget | null) => { const element = target as HTMLElement | null; - return Boolean(element?.isContentEditable) || ['INPUT', 'TEXTAREA', 'SELECT'].includes(element?.tagName ?? ''); + return Boolean(element?.isContentEditable) || ['INPUT', 'TEXTAREA'].includes(element?.tagName ?? ''); }; /** Hands a key pressed inside the frame to the deck, unless the key is typing. */ @@ -41,10 +69,11 @@ export function forwardKey(event: KeyboardEvent) { /** * The tracker, live on the slide, over a still of the same crop. The still - * shows until the tracker prints the still's snapshot, and stays if it never does. Clicks and - * scrolling go to the tracker. Arrow keys drive the deck after a click in the - * frame too, because the deck and the tracker share an origin in production; - * on a branch preview they stay with the frame until the slide is clicked. + * shows until the tracker prints the still's snapshot and its count cards come + * to rest, and it stays if they never do. It stays for good on a branch preview, + * where a cross-origin frame's document reads as null. Clicks and scrolling go + * to the tracker, and arrow keys still drive the deck after a click in the + * frame, unless the click lands in a text field. */ export default function CoverageEmbed({ alt }: { alt: string }) { const box = useRef(null); @@ -61,13 +90,12 @@ export default function CoverageEmbed({ alt }: { alt: string }) { return () => observer.disconnect(); }, [exporting]); - // A hidden frame starts no font loads, so the check waits for the slide to show. + // A hidden frame's cards read as at rest before they fade in, so the check waits for the slide to show. const visible = boxWidth > 0; useEffect(() => { if (!loaded || ready || !visible) return; - const sameOrigin = new URL(TRACKER_URL).origin === window.location.origin; const timer = setInterval(() => { - if (!sameOrigin || trackerAgrees(frame.current?.contentDocument)) setReady(true); + if (trackerAgrees(frame.current?.contentDocument)) setReady(true); }, 400); return () => clearInterval(timer); }, [loaded, ready, visible]); @@ -75,7 +103,15 @@ export default function CoverageEmbed({ alt }: { alt: string }) { const handleLoad = () => { setLoaded(true); try { - frame.current?.contentWindow?.addEventListener('keydown', forwardKey); + const page = frame.current?.contentWindow; + page?.addEventListener('keydown', forwardKey); + // A reload prints the fallback counts first, so the still returns until the new page loads and passes the check. + page?.addEventListener('pagehide', (event) => { + // A page the browser keeps for Back returns whole, and its frame fires no new load. + if (event.persisted) return; + setLoaded(false); + setReady(false); + }); } catch { // A cross-origin frame, on a branch preview. } From e55f2e536356e31a7b133c956bd35368fff9c189 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 6 Nov 2026 09:09:56 -0500 Subject: [PATCH 2/6] Pin the tracker's URL and each count card in its tests The fresh review of 9a09636 found gaps the string swaps left open: the test compared the frame's URL with the same exported constant, the card states varied only the last card, and nothing pinned the end of polling once the frame shows. The tests now use the literal URL, fade each of the four cards in turn, cover an unmount and a poll after the frame shows, and six more swaps cover those lines. The comments say any host but www.policyengine.org, since a preview isn't the only cross-origin host. The ledger's last sentence and the stage direction describe the fade and what a year in the tracker's menu does to the counts. Co-Authored-By: Claude Opus 5.5 --- slideshows/appam-2026/EVIDENCE.md | 2 +- slideshows/appam-2026/SPEAKER-NOTES.md | 2 +- .../appam-2026/slides/CoverageEmbed.test.tsx | 51 +++++++++++++++++-- .../appam-2026/slides/CoverageEmbed.tsx | 12 ++--- 4 files changed, 54 insertions(+), 13 deletions(-) diff --git a/slideshows/appam-2026/EVIDENCE.md b/slideshows/appam-2026/EVIDENCE.md index 8ba05072..da2faabf 100644 --- a/slideshows/appam-2026/EVIDENCE.md +++ b/slideshows/appam-2026/EVIDENCE.md @@ -195,7 +195,7 @@ We quote these on slide 7 and in the notes' Questions section, and we model none ## The web apps -We captured slide 12's coverage tracker from www.policyengine.org/us/model/rules/coverage on November 5, 2026. It shows 109 total programs, 87 complete, 18 partial and 4 in progress, and its top line reads "Coverage reflects policyengine-us 2.72.3 (main @ 4c65c82, fetched 2026-10-27). The production API served 2.2.1 when the snapshot was fetched." The deck's own runs use 2.90.2. We load that page in a live frame on slide 12 at the still's crop, and print the still alone in an export. We show the still until the tracker prints that snapshot line and its four count cards reach full opacity and size. +We captured slide 12's coverage tracker from www.policyengine.org/us/model/rules/coverage on November 5, 2026. It shows 109 total programs, 87 complete, 18 partial and 4 in progress, and its top line reads "Coverage reflects policyengine-us 2.72.3 (main @ 4c65c82, fetched 2026-10-27). The production API served 2.2.1 when the snapshot was fetched." The deck's own runs use 2.90.2. We load that page in a live frame on slide 12 at the still's crop, and print the still alone in an export. We show the still until the tracker prints that snapshot line and its four count cards finish fading in. We captured slide 11's CliffWatch screenshot on November 4, 2026, and Build's report on November 5, 2026, from the live sites. We scripted the recording, built it from screenshots and drew its cursor and address bar. CliffWatch drew that run's chart in under a second. Other runs took longer. diff --git a/slideshows/appam-2026/SPEAKER-NOTES.md b/slideshows/appam-2026/SPEAKER-NOTES.md index 0b2bfc24..64866f2d 100644 --- a/slideshows/appam-2026/SPEAKER-NOTES.md +++ b/slideshows/appam-2026/SPEAKER-NOTES.md @@ -96,7 +96,7 @@ In Build, you can open the same Massachusetts credit, set it to $880, pick Massa We model federal income and payroll taxes and credits and every state income tax, in 44 jurisdictions. We model TANF and child care subsidies in all 50 states and DC, plus SNAP, WIC, SSI, school meals, ACA premium tax credits, Medicaid and CHIP eligibility and local taxes such as Philadelphia's wage tax. Our coverage tracker lists 109 programs and marks 87 complete, 18 partial and 4 in progress. -*Once the tracker loads, you can click and scroll in the frame, and the left and right arrow keys still move the deck. After you click in its search box, they move the box's cursor until you click elsewhere in the frame. If you pick a year in its menu, the tracker counts only the programs we mark verified for that year.* +*Once the tracker loads, you can click and scroll in the frame, and the left and right arrow keys still move the deck. After you click in its search box, they move the box's cursor until you click elsewhere in the frame. If you pick a year in its menu, the four counts change, because the tracker counts only the programs we mark verified for that year.* ## 13. We build Microcosm, our microdata, from surveys and official totals (9:50, 50 seconds) diff --git a/slideshows/appam-2026/slides/CoverageEmbed.test.tsx b/slideshows/appam-2026/slides/CoverageEmbed.test.tsx index 2f667e9e..4114b393 100644 --- a/slideshows/appam-2026/slides/CoverageEmbed.test.tsx +++ b/slideshows/appam-2026/slides/CoverageEmbed.test.tsx @@ -1,7 +1,7 @@ import { act } from 'react'; import { createRoot, type Root } from 'react-dom/client'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import CoverageEmbed, { TRACKER_URL, forwardKey, trackerAgrees } from './CoverageEmbed'; +import CoverageEmbed, { forwardKey, trackerAgrees } from './CoverageEmbed'; (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; @@ -26,7 +26,9 @@ describe('the coverage tracker on the rules slide', () => { it('loads the live tracker over its still', async () => { window.history.replaceState(null, '', '/slides/appam-2026?slide=13'); await renderEmbed(); - expect(container.querySelector('iframe')?.getAttribute('src')).toBe(TRACKER_URL); + expect(container.querySelector('iframe')?.getAttribute('src')).toBe( + 'https://www.policyengine.org/us/model/rules/coverage', + ); expect(container.querySelector('img')?.getAttribute('alt')).toBe('The tracker'); }); @@ -51,9 +53,12 @@ const FALLBACK = 'Coverage reflects the bundled fallback registry. 64 Total prog const card = (label: string, style = '') => `

${label}

`; /** The legend after the tracker's cards, which repeats three of their labels and never fades. */ const LEGEND = ['Complete', 'Partial', 'In progress', 'Not started'].map((label) => `${label}`).join(''); +const COUNT_LABELS = ['Total programs', 'Complete', 'Partial', 'In progress']; +/** The tracker's four cards and its legend, with the given attributes on one card. */ +const cardsWith = (styled: string, style: string) => + [...COUNT_LABELS.map((label) => card(label, label === styled ? style : '')), LEGEND].join(''); /** The tracker's four cards and its legend, with the given attributes on the last card. */ -const cards = (lastStyle = '') => - [card('Total programs'), card('Complete'), card('Partial'), card('In progress', lastStyle), LEGEND].join(''); +const cards = (lastStyle = '') => cardsWith('In progress', lastStyle); const CARD_STATES = { 'at rest': { html: cards(), rests: true }, 'at rest under an identity matrix': { html: cards('data-transform="matrix(1, 0, 0, 1, 0, 0)"'), rests: true }, @@ -104,6 +109,14 @@ describe('the check that shows the frame over the still', () => { } }); + it('holds the still while any one of the four cards fades in or grows', () => { + for (const label of COUNT_LABELS) { + for (const style of ['data-opacity="0"', 'data-transform="matrix(0.9, 0, 0, 0.9, 0, 0)"']) { + expect(trackerAgrees(stubPage(SNAPSHOT, 'loaded', cardsWith(label, style))), `${label}, ${style}`).toBe(false); + } + } + }); + it('holds the still for a fading ancestor of a card', () => { const html = `
${cards()}
`; expect(trackerAgrees(stubPage(SNAPSHOT, 'loaded', html))).toBe(false); @@ -281,7 +294,7 @@ describe('when the frame shows', () => { expect(hidden(frame)).toBe(true); }); - it('keeps showing the frame when the slide leaves and comes back', async () => { + it('keeps showing the frame when the deck moves to a neighboring slide and back', async () => { const frame = await mount(stubPage(SNAPSHOT)); await show(1171.5); await load(frame); @@ -291,6 +304,34 @@ describe('when the frame shows', () => { await show(1171.5); expect(shown(frame)).toBe(true); }); + + it('starts again from the still when the deck unmounts the slide and returns', async () => { + const frame = await mount(stubPage(SNAPSHOT)); + await show(1171.5); + await load(frame); + await poll(); + expect(shown(frame)).toBe(true); + await act(async () => root.unmount()); + root = createRoot(container); + const returned = await mount(stubPage(SNAPSHOT)); + await show(1171.5); + expect(hidden(returned)).toBe(true); + await poll(); + expect(hidden(returned)).toBe(true); + }); + + it('stops reading the frame’s page once the frame shows', async () => { + const frame = await mount(stubPage(SNAPSHOT)); + await show(1171.5); + await load(frame); + await poll(); + expect(shown(frame)).toBe(true); + const read = vi.fn(() => stubPage(FALLBACK)); + Object.defineProperty(frame, 'contentDocument', { get: read, configurable: true }); + await poll(); + expect(read).not.toHaveBeenCalled(); + expect(shown(frame)).toBe(true); + }); }); describe('a key pressed inside the frame', () => { diff --git a/slideshows/appam-2026/slides/CoverageEmbed.tsx b/slideshows/appam-2026/slides/CoverageEmbed.tsx index 0c047f01..06c7f74b 100644 --- a/slideshows/appam-2026/slides/CoverageEmbed.tsx +++ b/slideshows/appam-2026/slides/CoverageEmbed.tsx @@ -4,7 +4,7 @@ import { useEffect, useRef, useState } from 'react'; import Image from '@/components/core/BasePathImage'; import { isExportMode } from '@/lib/slide-url'; -export const TRACKER_URL = 'https://www.policyengine.org/us/model/rules/coverage'; +const TRACKER_URL = 'https://www.policyengine.org/us/model/rules/coverage'; const STILL = { width: 2343, height: 721 }; /** The frame lays the tracker out at the width of the still's capture. */ @@ -70,10 +70,10 @@ export function forwardKey(event: KeyboardEvent) { /** * The tracker, live on the slide, over a still of the same crop. The still * shows until the tracker prints the still's snapshot and its count cards come - * to rest, and it stays if they never do. It stays for good on a branch preview, - * where a cross-origin frame's document reads as null. Clicks and scrolling go - * to the tracker, and arrow keys still drive the deck after a click in the - * frame, unless the click lands in a text field. + * to rest, and it stays if they never do. On any host but www.policyengine.org, + * a cross-origin frame's document reads as null, so the still stays for good. + * Clicks and scrolling go to the tracker, and arrow keys still drive the deck + * after a click in the frame, unless the click lands in a text field. */ export default function CoverageEmbed({ alt }: { alt: string }) { const box = useRef(null); @@ -113,7 +113,7 @@ export default function CoverageEmbed({ alt }: { alt: string }) { setReady(false); }); } catch { - // A cross-origin frame, on a branch preview. + // A cross-origin frame, on any host but www.policyengine.org. } }; From c67b1fc71bf6b2873b29d8fa2813d10750b4c133 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 6 Nov 2026 09:56:38 -0500 Subject: [PATCH 3/6] Pin the viewer's mounting and a same-commit refresh in tests The review of e55f2e5 found two behaviors the description states with no test: the viewer keeps a neighboring slide mounted and hidden and unmounts one two or more away, and a snapshot refreshed on the same commit still passes the check. Four more string swaps cover those lines. Co-Authored-By: Claude Opus 5.5 --- components/core/SlideshowViewer.test.tsx | 20 +++++++++++++++++++ .../appam-2026/slides/CoverageEmbed.test.tsx | 4 ++++ 2 files changed, 24 insertions(+) diff --git a/components/core/SlideshowViewer.test.tsx b/components/core/SlideshowViewer.test.tsx index c43253d9..b70d4e9b 100644 --- a/components/core/SlideshowViewer.test.tsx +++ b/components/core/SlideshowViewer.test.tsx @@ -177,6 +177,26 @@ describe("SlideshowViewer clicker keys", () => { }); }); +describe("SlideshowViewer mounting", () => { + const numbered = (n: number) => + function NumberedSlide() { + return

; + }; + const deck = { ...config, slides: [0, 1, 2, 3, 4].map(numbered) }; + const mounted = () => + [...container.querySelectorAll("[data-slide]")].map((slide) => [ + slide.dataset.slide, + slide.parentElement?.style.display, + ]); + + it("keeps each neighbor mounted and hidden, and unmounts a slide two or more away", async () => { + await renderAt("/slides/demo?slide=2", deck); + expect(mounted()).toEqual([["1", "none"], ["2", "block"], ["3", "none"]]); + await press("End"); + expect(mounted()).toEqual([["3", "none"], ["4", "block"]]); + }); +}); + describe("SlideshowViewer exportBuilds", () => { const seen: (number | undefined)[] = []; const Probe = ({ buildStep }: { buildStep?: number }) => { diff --git a/slideshows/appam-2026/slides/CoverageEmbed.test.tsx b/slideshows/appam-2026/slides/CoverageEmbed.test.tsx index 4114b393..14f709c0 100644 --- a/slideshows/appam-2026/slides/CoverageEmbed.test.tsx +++ b/slideshows/appam-2026/slides/CoverageEmbed.test.tsx @@ -89,6 +89,10 @@ const stubPage = (innerText: string, status = 'loaded', html = cards()) => { describe('the check that shows the frame over the still', () => { const TEXTS = { 'the still’s snapshot': { text: SNAPSHOT, matches: true }, + 'the same commit, fetched later': { + text: 'Coverage reflects policyengine-us 2.72.3 (main @ 4c65c82, fetched 2026-11-01). 109 Total programs', + matches: true, + }, 'the fallback counts': { text: FALLBACK, matches: false }, 'another release': { text: 'Coverage reflects policyengine-us 2.95.0 (main @ abc1234). 120 Total programs', matches: false }, 'another commit of the same release': { From aaa263346f03da2288d299b7d972ff6a77ea449c Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 6 Nov 2026 10:10:58 -0500 Subject: [PATCH 4/6] Check that a neighboring slide survives a move in the viewer The mounting test read which slides sat in the DOM at two stops, so a viewer that remounted a neighbor on every move still passed it. That remount would cost the tracker's frame its state and restart it from the still. Co-Authored-By: Claude Opus 5.5 --- components/core/SlideshowViewer.test.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/components/core/SlideshowViewer.test.tsx b/components/core/SlideshowViewer.test.tsx index b70d4e9b..b20a97d7 100644 --- a/components/core/SlideshowViewer.test.tsx +++ b/components/core/SlideshowViewer.test.tsx @@ -192,8 +192,10 @@ describe("SlideshowViewer mounting", () => { it("keeps each neighbor mounted and hidden, and unmounts a slide two or more away", async () => { await renderAt("/slides/demo?slide=2", deck); expect(mounted()).toEqual([["1", "none"], ["2", "block"], ["3", "none"]]); + const neighbor = container.querySelector("[data-slide='3']"); await press("End"); expect(mounted()).toEqual([["3", "none"], ["4", "block"]]); + expect(container.querySelector("[data-slide='3']")).toBe(neighbor); }); }); From 1ceb8321fc50515d94f3b41542b3d13cadceb5ff Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 6 Nov 2026 10:30:34 -0500 Subject: [PATCH 5/6] Check that the shown slide survives a move to its neighbor and back The mounting test followed a slide that stayed a neighbor, so a viewer that remounted a slide whenever it stopped showing still passed it. That remount would cost the tracker's frame its state on every move away and back. Co-Authored-By: Claude Opus 5.5 --- components/core/SlideshowViewer.test.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/components/core/SlideshowViewer.test.tsx b/components/core/SlideshowViewer.test.tsx index b20a97d7..5e7e8f7b 100644 --- a/components/core/SlideshowViewer.test.tsx +++ b/components/core/SlideshowViewer.test.tsx @@ -189,9 +189,15 @@ describe("SlideshowViewer mounting", () => { slide.parentElement?.style.display, ]); - it("keeps each neighbor mounted and hidden, and unmounts a slide two or more away", async () => { + it("keeps each neighbor mounted and hidden across moves, and unmounts a slide two or more away", async () => { await renderAt("/slides/demo?slide=2", deck); expect(mounted()).toEqual([["1", "none"], ["2", "block"], ["3", "none"]]); + // The same node means React kept the slide's instance, so its state survives the move. + const shown = container.querySelector("[data-slide='2']"); + await press("ArrowRight"); + expect(container.querySelector("[data-slide='2']")).toBe(shown); + await press("ArrowLeft"); + expect(container.querySelector("[data-slide='2']")).toBe(shown); const neighbor = container.querySelector("[data-slide='3']"); await press("End"); expect(mounted()).toEqual([["3", "none"], ["4", "block"]]); From a774459af04e09fa89575063bf5fe2c13fc7fc80 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 6 Nov 2026 15:15:33 -0500 Subject: [PATCH 6/6] Check that slide 12's frame survives builds and its slide's renders - SlideshowViewer.test.tsx: a slide and its neighbor keep their instances while the shown slide steps through its builds and back. - deck-layout.test.tsx: in the real deck, slide 12's frame keeps its node through both of slide 11's builds and a move to slide 13 and back, and unmounts two slides away. Co-Authored-By: Claude Opus 5.5 --- components/core/SlideshowViewer.test.tsx | 13 ++++++++++++ .../appam-2026/slides/deck-layout.test.tsx | 21 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/components/core/SlideshowViewer.test.tsx b/components/core/SlideshowViewer.test.tsx index 5e7e8f7b..4a6fdf20 100644 --- a/components/core/SlideshowViewer.test.tsx +++ b/components/core/SlideshowViewer.test.tsx @@ -203,6 +203,19 @@ describe("SlideshowViewer mounting", () => { expect(mounted()).toEqual([["3", "none"], ["4", "block"]]); expect(container.querySelector("[data-slide='3']")).toBe(neighbor); }); + + it("keeps each slide's instance while it or its neighbor steps through builds", async () => { + const built = { ...config, slides: [{ component: numbered(0), builds: 2 }, numbered(1)] }; + await renderAt("/slides/demo", built); + expect(mounted()).toEqual([["0", "block"], ["1", "none"]]); + const first = container.querySelector("[data-slide='0']"); + const second = container.querySelector("[data-slide='1']"); + for (const key of ["ArrowRight", "ArrowRight", "ArrowLeft", "ArrowLeft"]) { + await press(key); + expect(container.querySelector("[data-slide='0']")).toBe(first); + expect(container.querySelector("[data-slide='1']")).toBe(second); + } + }); }); describe("SlideshowViewer exportBuilds", () => { diff --git a/slideshows/appam-2026/slides/deck-layout.test.tsx b/slideshows/appam-2026/slides/deck-layout.test.tsx index d0132a4d..406c554d 100644 --- a/slideshows/appam-2026/slides/deck-layout.test.tsx +++ b/slideshows/appam-2026/slides/deck-layout.test.tsx @@ -343,5 +343,26 @@ describe('links', () => { await click(main.querySelector('.slide-active h1')!); expect(main.dataset.currentStep).toBe('2'); }); + + it("keeps slide 12's frame through every move that leaves the slide shown or a neighbor, and drops it two slides away", async () => { + const press = (key: string) => + act(async () => { + window.dispatchEvent(new KeyboardEvent('keydown', { key })); + }); + const frame = () => container.querySelector("iframe[title*='coverage tracker']"); + window.history.replaceState(null, '', '/slides/appam-2026?slide=13'); + await act(async () => { + root.render(); + }); + const shown = frame(); + expect(shown).not.toBeNull(); + // Back through both of slide 11's builds, then on to slide 13 and back. + for (const key of ['ArrowLeft', 'ArrowLeft', 'ArrowRight', 'ArrowRight', 'ArrowRight', 'ArrowLeft']) { + await press(key); + expect(frame()).toBe(shown); + } + for (const key of ['ArrowLeft', 'ArrowLeft', 'ArrowLeft']) await press(key); + expect(frame()).toBeNull(); + }); }); });