Repository navigation
Conversation
Adds screenshot comparison for RNTester in public CI. An untrusted workflow captures cropped screenshots with Maestro, and a trusted workflow running from main compares them with baselines stored in a GCS bucket, reports on the PR, and publishes new baselines on pushes to main. Maintainers approve intentional changes with the visual-change-approved label. Changelog: [Internal]
robhogan
marked this pull request as ready for review
October 15, 2026 15:15
cortinico
reviewed
Oct 15, 2026
cortinico
left a comment
Contributor
There was a problem hiding this comment.
Thats' great - can we add some markdown in the repos to explain how to use this?
cortinico
reviewed
Oct 15, 2026
| VISUAL_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} | ||
|
|
||
| jobs: | ||
| build_android: |
Contributor
There was a problem hiding this comment.
We already build rn_tester in previous workflows, no? Can't we reuse the APK from that jobs?
Comment on lines
+94
to
+102
| api-level: 35 | ||
| target: google_apis | ||
| arch: x86_64 | ||
| ram-size: '8192M' | ||
| heap-size: '4096M' | ||
| disk-size: '10G' | ||
| cores: '4' | ||
| disable-animations: true | ||
| avd-name: visual_emulator |
Contributor
There was a problem hiding this comment.
I'm a little concerned that we don't have an explicit resolution configured here and this could make the screenshots drift quite a lot once we bump this rule. Potentially we should add like profile: 'pixel' or so?
This was referenced Oct 16, 2026
Visual capture now runs as a reusable workflow called from Test All, after build_android and test_ios_rntester, and downloads their RNTester Release artifacts instead of building RNTester again. Visual Compare RNTester runs when Test All completes, including when unrelated jobs fail, and only if the run uploaded a capture. The Android emulator uses the pixel_7 device profile, so the screen size and density no longer depend on the emulator action's defaults. The emulator version is pinned too, and is part of the environment key, since emulator updates can change rendering. The comparison downloads each platform's capture by name, since download-artifact extracts a single pattern match without its own directory, which left a one-platform run with nothing compared. A platform with no capture is now reported as such rather than omitted. The capture also dismisses a Pixel Launcher ANR dialog raised during boot, which hid the app in about a third of runs. Adds packages/rn-tester/visual-tests/README.md on adding tests, local runs, approvals and tolerances. Changelog: [Internal]
Contributor
|
@robhogan I'm having a harder time in reviewing this. Can you ask your agent to split it in smaller meaningful commits (doens't need to ne separate PRs), so it would be easier for me to do a pass? Also my browser is unhappy with the diff size 😅 |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
RNTester's rendering coverage, particularly text, currently lives in Meta's internal jest-e2e suite. To move it into public CI we need screenshot comparison that works for fork PRs and doesn't add PNGs to the repo. This adds that, as a separate set of visual flows alongside the existing Maestro E2E.
How it works
Visual tests are Maestro flows in
packages/rn-tester/visual-tests/flows/. Each one deep-links to an RNTester example, waits for it to be ready and captures a screenshot cropped to atestID. Maestro only captures - it never decides pass or fail.The work is split across a trust boundary, so a fork PR never runs with credentials:
test_visual_android_rntesterafterbuild_androidandtest_visual_ios_rntesteraftertest_ios_rntester. Each runs every flow in one Maestro invocation, on a Pixel 7 API 35 emulator (1080x2400, 420dpi) or an iPhone 17 Pro on iOS 26.2, and uploads the PNGs and a manifest recording the commit and device environment as an artifact. They have no secrets.workflow_runon Test All, frommain) treats that artifact as data - it's validated before any image is decoded and nothing in it is executed. It finds the baseline, compares with pixelmatch, posts a check run per platform and a single PR comment with expected, captured and diff images, and on pushes tomainpublishes the captures as a new baseline. It runs once Test All completes, including when unrelated jobs fail, so the report arrives with the rest of CI rather than as soon as the captures are ready.pull_request_target, on thevisual-change-approvedlabel) records a maintainer's approval of an intentional change. The approval is tied to the exact set of differences, so a later push that changes them removes the label and needs a new review.Comparison is strict by default: any changed pixel is a difference. A test can be loosened in
visual-tests/config.json, with evidence. Captures have been byte-identical across separate builds and runners so far, so I'd expect that to be rare.What defines the baseline
A baseline is the full set of captures from a commit on
main, keyed by commit and by device environment. Every push tomainpublishes one, and nothing is ever overwritten.A PR is compared with the baseline for the commit it's based on (the first parent of the merge commit CI tests), or the nearest earlier commit on
mainthat has one, and the report says which. So a PR based on last week'smainis compared with last week's rendering, not with whatever has landed since. Approving a PR doesn't promote its images -mainis captured again after merge and that becomes the baseline.The environment key covers everything that affects rendering (OS runtime, device, screen size, density, font scale, locale, appearance, animations, system image and, on Android, the emulator version, which is pinned). If the CI image drifts, the key changes and a fresh baseline is published rather than every PR showing a spurious diff.
Settings
Storage is any GCS bucket, configured through repository variables. Writes use GitHub's OIDC token, so there's no stored secret, and only the two trusted workflows on
maincan authenticate. They need create-only access (roles/storage.objectCreator). Reads are public.VISUAL_BUCKETVISUAL_PUBLIC_BASE_URLVISUAL_GCP_PROJECTVISUAL_WORKLOAD_IDENTITY_PROVIDERvisual-compare-rntester.ymlandvisual-approve-rntester.ymlonmainExpo can host the bucket and a CDN for this. Reports and approvals expire after 30 days by lifecycle rule. Baselines are small (8-175 KB per image), and at the current rate of pushes to
mainthe cost is cents a month.packages/rn-tester/visual-tests/README.mdcovers adding a test and handling a failure. This starts with two text tests and runs non-blocking. The plan is to port high-value jest-e2e text and rendering tests, then make the checks required once they've proven reliable.Changelog:
[INTERNAL] [ADDED] - Visual regression testing for RNTester in CI
Test Plan:
Ran the full flow in a fork (https://github.com/robhogan/react-native), with Test All trimmed to the jobs this depends on, against a personal GCS bucket:
visual-change-approvedpasses the approval check. Pushing a further, different change removed the label with a comment and failed the check again.Not yet tested: a PR from a third-party fork, to confirm it can't reach storage credentials.