Skip to content

Test: visual-diff GHAs after merge + new post coverage - #6

Open
RisingOrange wants to merge 245 commits into
mainfrom
visual-diff
Open

Test: visual-diff GHAs after merge + new post coverage#6
RisingOrange wants to merge 245 commits into
mainfrom
visual-diff

Conversation

@RisingOrange

Copy link
Copy Markdown
Owner

Throwaway PR to exercise the visual-diff GitHub Actions on the merged state of visual-diff (incl. main merged in and the new donate/pdoom/faq opt-ins).

Not for merging — corresponds to upstream PR PauseAI#785.

RisingOrange and others added 29 commits April 24, 2026 17:22
Captures HTML+asset archives of 15 key routes × 2 viewports on each PR
and uploads to Chromatic for server-side rendering and diffing against
baseline. Posts a non-blocking "UI Review" check on the PR.

- Routes auto-discovered from src/routes/, plus 5 markdown-post
  representatives; form/admin/content-churn routes explicitly excluded
  with WHY comments
- Deterministic snapshots: /api/news mocked with a fixture, Tally
  iframes on /statement and /join aborted to avoid async-height churn
- Chromium-only, non-blocking by design (exitZeroOnChanges)
- Fork PRs supported via plaintext project token in the workflow
  (Chromatic's recommended pattern)
- Playwright report (per-route screenshots) uploaded as GHA artifact
Without production secrets, Notion / Airtable / Substack calls during
`pnpm build` (prerender, remote prerender fns) and `pnpm preview`
(on-demand SSR, client-side fetches to /api/*) either fail or fall
back to a couple of placeholder records — leaving snapshots too sparse
to catch layout regressions in populated list/card components.

Boot MSW in the Node process via `NPM_CONFIG_NODE_OPTIONS=--import`
(the only way to propagate `--import` through pnpm, which otherwise
overwrites NODE_OPTIONS with `--experimental-global-webcrypto`; see
pnpm/pnpm#6210). The setup file is a no-op unless `VISUAL_TEST=1`.

Handlers cover the two Notion databases (press, funding), the three
Airtable tables (people, signatories, national-groups), the Substack
RSS feed hit by /api/news, plus catch-all empty-result responses for
any un-fixtured endpoint. Fake API keys are set in the workflow so
the SDKs actually make HTTP requests (they short-circuit to a "no
key" error otherwise, bypassing MSW entirely).

All fixture data lives under a single `tests/visual/fixtures/`
directory — the earlier `tests/visual/fixtures/news.json`, which was
intercepted at the browser boundary via `page.route().fulfill()`, is
replaced by `substack-feed.xml` served by MSW upstream of the
/api/news server code. `page.route()` in smoke.spec.ts is now
reserved for aborting third-party widgets (Tally, Mapbox, Luma)
whose requests never reach our Node process. Clean split: MSW for
fixture data, page.route for cross-origin aborts.

Fixtures use realistic-shaped but unambiguously-fake data: first
names paired with Example / Sample / Fixture surnames, Lorem-ipsum
bios, example.org URLs, Example Foundation / Example News. Chromatic
snapshots persist in its UI, so the content leaves no room to
mistake a fixture attribution for a real endorsement if an image
escapes context.

Adds `msw` as a dev dependency and entry-points tests/visual/msw-
setup.mjs in knip.config.ts so the indirect import is recognized.
Previously excluded because its chronological listing churns on any new
post, same mechanism as the home page's LatestNews section — but the
home page is included (its news churn is accepted as one re-approval
per news-flagged post).

Include /posts on the same trade-off: the cost is a re-approval in
Chromatic per post add, which is trivial. Removes an inconsistency
in which pages opt out vs which accept content-driven churn.
Four mobile snapshots blew past Chromatic's 25M-pixel snapshot cap with
the default Pixel 7 DPR of 2.625: /dear-sir-demis-2025 (52M, 2.1× over),
/posts (47M, 1.9× over), /learn (25.8M), /write (25.2M).

Override deviceScaleFactor to 1 on the mobile project. The viewport is
still 412 CSS px (the layout-relevant dimension); we just capture at
1× instead of 2.6× physical pixels. Mobile layout diffs don't need the
hi-DPI oversampling — a layout regression is visible at either DPR.

After the change, the largest mobile snapshot is 412 × 18468 ≈ 7.6M,
well under the cap.
Addresses the reviewer concern that a green Chromatic check could
silently imply coverage we don't actually have. On every PR, post a
sticky comment summarizing what the run snapshotted: covered/excluded
ratios for pages and posts, what "covered" doesn't mean (fixtures,
widget aborts, en-only), and a conditional warning when un-fixtured
requests fall through to MSW catch-alls / bypass.

Plumbing:
- tests/visual/scope-comment.ts renders the comment body from repo
  state (routes, posts, annotations) + the MSW warning log.
- msw-setup.mjs listens for request:unhandled and msw-handlers.ts's
  catch-alls log to MSW_WARN_LOG when set, so the warning section
  has data to render.
- visual-diff.yml generates the artifact (body.md + pr-number.txt)
  on every PR run.
- visual-diff-comment.yml (new) triggers on workflow_run, runs in the
  base-repo context with pull-requests: write, downloads the artifact,
  posts or updates a sticky comment via marocchino/sticky-pull-request-
  comment. Works for fork PRs without granting forks write permissions.
- README updated with a disclaimer about browser-side third-party
  embeds being out of scope for the MSW-based warning.
The previous page.route setup aborted three specific hosts (tally.so,
lu.ma, api.mapbox.com). A new third-party widget added in a future PR
would have loaded normally in CI, potentially leaking live state into
snapshots with no signal.

Replace with a generic rule: abort any cross-origin request whose
resourceType is `document` (iframe), `xhr`, or `fetch`. Pass through
cross-origin resources — images (Cloudinary), fonts, scripts (inlang
CDN), stylesheets — since those are embedded assets whose content is
part of the rendered design we want to capture. Server-side external
calls remain handled by MSW-node.

All three previous specific aborts fall under the generic rule:
- tally.so iframes → `document` → aborted
- lu.ma iframes → `document` → aborted
- api.mapbox.com tiles (via mapbox-gl.js) → `xhr` → aborted

Also:
- Derive the "same origin" from Playwright's `baseURL` fixture rather
  than hardcoding `http://localhost:4173`, so a port change in the
  config doesn't silently invert the rule.
- Update the "Not exercised" caveat to reflect the new behavior:
  containers render empty and reviewers should check the Netlify
  preview deploy for real rendering.
Codex review (gpt-5.5 high) flagged that the PR-number resolver
filters on head_branch and head_repository.id but not base_ref, so a
fork branch opened as two PRs against different base branches
simultaneously could route the comment to the wrong PR. Fix options
were all ugly (iterate + post to each, fail on >1 match); the case is
rare enough that documenting the limit is the pragmatic call.
`dotenv.config({ override: true })` was clobbering the fake
AIRTABLE_API_KEY / NOTION_API_KEY that the visual-diff workflow sets,
with the empty values from template.env. The SDKs then short-circuited
on empty key before ever making a request, so MSW had nothing to
intercept and /about, /statement, /communities snapshots rendered their
hardcoded "[FALLBACK DATA]" fallback paths instead of fixture data.
Previously the three `PauseAI Example North/South/East` records didn't
match any country name in the static `communities` list, so
`nationalGroups.find()` returned undefined for every card and the
fixture had no visible effect — snapshots were identical whether the
fixture returned records or an empty list. Now one record matches
'France' and carries an inline SVG tricolor as its image, so France's
card shows a fixture-provided flag while the rest keep the default
icon. The populated-card render path is now covered.
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
* Update australia-progress.md

* Add files via upload

* Update australia-progress.md

* Tweaked settings of introductory video in australia-progress.md

Added an introductory video for PauseAI Australia.

* Revise Australia progress report with new updates

Updated the Australia progress report with recent activities and milestones, including volunteer roles, newsletters, and outreach efforts.

* Misc updates mainly Dec 2025

* Update Australia progress with recent AI developments

* Add files via upload

* Update Australia progress with incorporation news

Added updates about incorporation and petition efforts.

* Assorted activity updates

* style: format code with prettier

---------

Co-authored-by: Wituareard <87208350+Wituareard@users.noreply.github.com>
…OU (PauseAI#812)

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
* Restructure /press to be a Markdown post

Same pattern as /funding: move to src/posts/press.md, fetch coverage via
a prerender remote function, keep the existing PressCoveragePanel as a
legacy leaf component wrapped by a tiny loader that does the await.

Also inherits the l10n pipeline for the intro/materials copy.

* Add PressCoveragePanelLoader to knip entry points

It's imported only from a Markdown file, which knip doesn't scan.

* Restore press page polish styles

Preserve .contact-clarification (1.1rem, bottom margin) and tighter
h2 margin under 'Media Coverage', both lost when converting from the
bespoke route.

* Use {#await} block for press coverage loader

Same fix as was applied to the funding donor list: top-level await in
PressCoveragePanelLoader.svelte created an unscoped async boundary that
would blank the whole /press page subtree during hydration.

Switched to an {#await} template block and dropped the now-unused
compilerOptions.experimental.async flag.

* Drop dead press coverage scaffolding

Remove temporary 'publication' property support, stale .logo-materials CSS rule (class no longer exists after markdown migration), redundant try/catch in the remote function, and the unused PressCoverageData interface.

* Switch press coverage to client-side fetching with loading skeletons

---------

Co-authored-by: Wituareard <87208350+Wituareard@users.noreply.github.com>
* Adjust iframe width and update donation milestone

Updated iframe width and donation milestone statement.

* Add files via upload

* Update australia-progress.md

* Add files via upload

* Add files via upload

* Add files via upload

* Update australia-progress.md

* Add files via upload

* Replace YouTube iframe with no-cookie version

Updated the video embed to use a no-cookie version for privacy.

* Remove video iframe from australia-progress.md

Removed embedded video iframe from Australia progress post.

* Enhance Australia page with action buttons

Added buttons for more details and progress updates.

* Create australia-story.md for AI story sharing

Added a new markdown file for sharing stories about AI in Australia.

* style: format code with prettier

* Replace iframe with Tally component

---------

Co-authored-by: Wituareard <87208350+Wituareard@users.noreply.github.com>
Adds @visualDiffEnabled: true to three posts whose embedded components
or layout aren't represented by the existing covered set: Donate
(donate.md), Doomers (pdoom.md), and the auto-generated SimpleToc
(faq.md).
# Conflicts:
#	knip.config.ts
#	pnpm-lock.yaml
When a commit lives on the repo's default branch, GitHub's
listPullRequestsAssociatedWithCommit endpoint only returns merged PRs;
open PRs with that commit at their head are filtered out. This is
unlikely in production (the feature branch is never main), but it
silently breaks the scope comment on any fork where the feature branch
happens to be default.

Switch to pulls?state=open&head={owner}:{branch}, which has no such
quirk. Trusted-inputs safety property is unchanged: head owner/branch
come from workflow_run, and we still verify head.repo.id and head.sha
against the trusted payload before posting.
@github-actions

github-actions Bot commented Apr 26, 2026

Copy link
Copy Markdown

Visual diff coverage
commit 8aef4d1 · run #29646323025

Review snapshots in Chromatic

  • Pages: 18/21 covered. Included by default.
  • Posts: 9/109 covered. Excluded by default (posts share a layout).
Route breakdown
  • Pages excluded:
    • /quotes: ~28M-pixel desktop snapshot is over Chromatic's 25M cap
    • /submitted: post-form state
    • /verify: token-dependent
  • Posts covered:
    • /action
    • /donate
    • /faq
    • /funding
    • /if-anyone-builds-it-campaign
    • /join
    • /learn
    • /pdoom
    • /values
Not exercised
  • Pages that fetch from external APIs render against pinned fixtures, not live data.
  • Cross-origin iframes and API calls (Tally, Luma, Mapbox, analytics, newly added widgets) are aborted at the browser boundary. Containers render as empty. Check the Netlify preview deploy to verify their real rendering.
  • Only en locale is built.

tests/visual/README.md explains the annotation syntax and the coverage model.

Wituareard and others added 30 commits July 14, 2026 16:29
The upsert-by-email logic from PauseAI#973 allowed anyone who knew another
person's email to overwrite their Airtable record by submitting the
onboarding form with that address. Remove findRecordByEmail and restore
the original create-only flow on step 2. The Discovery method string
fix from the same commit is retained.
…n permissions (PauseAI#974)

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
…in permissions (PauseAI#975)

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
* Add paying member opt-in to volunteer onboarding step

* Open Stripe popup synchronously to satisfy iOS gesture requirements

* Add /close route for Stripe success redirect and update docs

* Add Stripe and /close to the state diagram and submission flowchart

* Add paying-member disclaimer under volunteer submit button
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
# Conflicts:
#	.gitignore
#	package.json
#	pnpm-lock.yaml
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…I#948)

* feat: restructure top nav into grouped dropdowns (desktop + mobile)

Replace the flat top-menu link row with grouped navigation so content is
easier to discover, per the new-website working doc.

- About · Learn ▾ · Community ▾ · Get involved ▾ · Donate
  - Learn: Why this matters, Proposal, Our Strategy, FAQ
  - Community: Find your local group, Join an event (Luma)
  - Get involved: Take Action/Join, Start a local group
- Desktop: click/hover dropdowns (NavDropdown), close on Escape & click-outside,
  active-section highlight — modeled on the existing LanguageSwitcher pattern.
- Mobile (≤600px): hamburger toggles a panel with <details> accordion groups;
  tapping a link navigates and auto-closes (afterNavigate).
- Menu is now data-driven via navItems.ts, shared by the wide & narrow navbars.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: proper Search row in the mobile panel

The mobile panel's bottom section rendered as a bare divider with a lone
unlabeled search icon, which read as a rendering glitch (PR review feedback).

- Replace the shared `extras` snippet in the narrow panel with a dedicated
  `panelExtras` snippet: a full-width "Search" row (icon + label) styled like
  the other panel rows.
- Tapping Search now closes the panel before opening the search modal
  (the snippet receives a close callback).
- Anchor the language switcher's dropdown to the extras section
  (position: relative) so it opens in place on multi-locale builds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
"Our Strategy" -> "Our strategy", "Take Action/Join" -> "Take action/Join",
matching the sentence casing of the other dropdown items.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Adds fr locale to the hardcoded onboarding message table alongside
en/de. No code changes needed elsewhere since locale is picked up
from the existing ?locale= query param mechanism.


Claude-Session: https://claude.ai/code/session_01GQoVEaVPHQUVTZ71SoEjd6

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Conflicts resolved: package.json (keep msw dev-dep, take main's npm-run-all2
bump), pnpm-lock.yaml (regenerated).
Two regressions surfaced after the pnpm 10.29.2 -> 11.1.3 bump in the last
main merge:

1. MSW never booted. The preload was injected via NPM_CONFIG_NODE_OPTIONS,
   which pnpm 10 translated into the child NODE_OPTIONS but pnpm 11 ignores
   entirely, so build/preview hit live Airtable/Notion (401s) instead of
   fixtures. Set NODE_OPTIONS directly — pnpm 11 passes it through untouched.

2. The homepage Substack self-check waited for a fixture post title in the
   rendered first page. /api/news sorts merged internal + Substack items by
   date, and enough newer dated internal posts have since accrued to push the
   fixtures off page one. Assert the fixture is reachable via the /api/news
   API (paging through) instead of where it happens to sort.

Verified end-to-end on Node 24.2 (the .nvmrc pin): both home snapshots pass,
MSW warn log clean, no live API calls.
- smoke.spec.ts: rewrite the /api/news paging loop as a for-loop so the
  found/totalPages initialisers are read before reassignment (eslint
  no-useless-assignment was failing pnpm-check); guard res.ok and optional
  items/totalPages so an API error surfaces a targeted message; sharpen the
  failure wording (MSW handler broken *or* live feed leaked).
- visual-diff.yml: use an absolute --import path (github.workspace) since
  --import resolves relative specifiers per-process cwd.
- README.md, knip.config.ts: NPM_CONFIG_NODE_OPTIONS -> NODE_OPTIONS to match
  the workflow (the old mechanism is a no-op on pnpm 11).
From a full-PR review (Codex + Fable):

- QuotesCarousel: skip Glide autoplay when prefers-reduced-motion is set.
  The home page snapshot ran ~13s against a 10s autoplay interval, so the
  captured slide was nondeterministic; reduced-motion (which Playwright sets)
  now freezes it. Doubles as an a11y improvement for real visitors.
- airtable-people fixture: replace the live api.dicebear.com avatar URLs with
  inline data-URI SVGs. Chromatic doesn't archive cross-origin assets (no
  assetDomains configured), so /about was re-fetching them live on every diff
  — an availability/drift flake on the pipeline meant to remove live deps.
- SHA-pin the two third-party actions that hold credentials: chromaui/action
  (project token) and marocchino/sticky-pull-request-comment (pull-requests:
  write). A moved/compromised mutable tag would otherwise run with those.

Verified on Node 24.2: home + about snapshots pass, MSW warn log clean (no
dicebear or other live requests).
The data-URI avatars pushed the inline "large" objects past print-width, so
prettier expands them to multi-line. Pure formatting; no data change.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.