Testing accessibility is crucial, and while you can definitely write UI tests it's very easy to forget about testing smaller parts of the app. If you have no tests starting from scratch, writing a decent amount can be time-consuming. Manual testing is prone to inconsistencies and can take even more time.
AccessibilitySnapshot is a tool that visually represents your components’ accessibility properties, which is great for spotting areas to improve. It also helps to catch regressions as you develop your app. With the right setup, you can test the majority of your app very quickly.
In this article, I will describe the steps taken to get AccessibilitySnapshot to work in my React Native project. Also, I will take some extra steps to simplify creating new tests in just one line of code.
Step 1: Add the dependency
Open your app's xcworkspace. In the Project Navigator select your app's project. In the xcodeproj tab, select your App project and click on Package Dependencies.
After clicking the Add button paste the AccessibilitySnapshot repo URL in the search bar. On the “Add to Project” menu select your app’s project and click “Add Package”.
After a bit, a dialog with products will appear. We only need AccessibilitySnapshot (the first one), however, in the "Add to Target" column we have to change it to the tests target.
AccessibilitySnapshot requires iOS 13 so we will need to go to our test target and change the deployment target.
Step 2: Creating a test file
The default React Native template has a single tests Objective-C file with a single test method - (void)testRendersWelcomeScreen. You can delete that file.
Right click on the tests folder and select "New File…". Create a new Swift file and call it AccessibilitySnapshotTests.
Xcode will ask you to configure a bridging header, we don’t need it so click "Don't Create".
In the newly created file paste the following code:
The first argument of AppRegistry.registryComponent and the moduleName on line 14 in AccessibilitySnapshotTests.swift must match.
Step 3: Running the test
Let's look at this library in action. Make sure the packager is running and in Xcode's menu bar select "Product" and then "Test" or use the keyboard shortcut ⌘U. After a little while the test should fail.
And if we look at the error message we can see that a snapshot was recorded. Now all we need to do is rerun the tests, and they should pass!
Ok, but what if we want to refactor our component, and the snapshot becomes invalid? Just temporarily add isRecording = true before the assertSnapshot line and run the tests again.
Step 4: Continuous Integration
Before we automate the snapshot testing we need to do a couple of small changes. The images produced on Apple Silicon and Intel processors differ so our tests would fail if our CI's architecture doesn't match. The difference is so little that you can't even see it. For that we need to change the version of AccessibilitySnapshot that we use.
Navigate to "Package Dependencies" again and double click on AccessbilitySnapshot.
In the dialog window, switch to the "Brach" rule, paste entin/perceptual-precision in the text field, and click "Done".
Next, we need to add a new argument perceptualPrecision to .accessibleImage:
Currently our tests will load the JavaScript bundle from the packager but on CI it won't be running so let's make a quick modification to the AccessibilitySnapshotTests.swift file:
- let bridge = RCTBridge(bundleURL: RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")!, moduleProvider: nil)!
+ let bridge = RCTBridge(bundleURL: ProcessInfo.processInfo.environment["USE_LOCAL_BUNDLE"]?.isEmpty ?? true
And then we need to add the USE_LOCAL_BUNDLE environment variable in the Xcode test scheme. In the menu bar select "Product", then go to "Scheme", and click "Edit Scheme…".
Now select "Test" from the sidebar. Switch to the "Arguments" tab. Deselect "Use the Run action's arguments and environment variables". Click on the plus button under the environment variables section. Set the name to USE_LOCAL_BUNDLE and value to $(USE_LOCAL_BUNDLE). Also, let's add one more variable called SNAPSHOT_ARTIFACTS with a value of /tmp/iOS-Snapshots. In the "Expand Variables Based On" dropdown select your project (not the tests target). Click "Close". In Git you will see changes to an xcscheme file; make sure to commit them.
Time for the workflow file. I will be using GitHub Actions:
A quick summary of the workflow: it adds FORCE_BUNDLING=1 to ios/.xcode.env.local and runs xcodebuild USE_LOCAL_BUNDLE=1 test on a specific simulator configuration.
I also wanted to be able to see the snapshots that were generated in case the CI job fails so I added a step to upload the contents of /tmp/iOS-Snapshots as an artifact.
All that is left is to open a pull request and wait for the CI job to pass.
Sanity checking
I want to make sure that my workflow fails when it should. After merging the PR with a passing CI I made a fake refactor of my profile component where I delete the accessibility props. As expected, the workflow failed. The results are in:
We can easily see that I completely forgot to test if my component is accessible after the refactor. Changes were requested!
Step 5: Improving the developer experience
Testing multiple components
You can copy the testAccessibilitySnapshot method for each component that you want to test but for me modifying native files this often is cumbersome. Instead I would prefer if I can register every component in JavaScript code and have the native code run a snapshot test on every registered component without the need of touching Swift files. We can create a native module to aid us with this task. Create one more Swift file in our tests target. Call it SnapshotTests and paste the following:
To make it very simple to add snapshot tests, let's create a registerAccessibilitySnapshotNode function in JavaScript. It will call AppRegistry.registerComponent and SnapshotTests.registerName:
Now we need to modify the AccessibilitySnapshotTests.swift file. It will become quite a bit more complex, but we wouldn’t need to modify it every time we want to make an accessibility snapshot of a new component:
importReact
importAccessibilitySnapshot
importSnapshotTesting
importXCTest
classAccessibilitySnapshotTests:XCTestCase{
functestAccessibilitySnapshot()throws{
letbridgeDelegate=BridgeDelegate()
letexpectationForJavaScriptDidLoadNotification=expectation(description:"Wait for JavaScript to load")
Our code waits for the JavaScript bundle to be loaded and executed by listening to RCTJavaScriptDidLoadNotification, every component that we want to test should be registered by then since we specified registerName: as synchronous
Loop through the registrations array and assert the snapshot for each one
In my demo app, I proceeded with splitting the profile screen in separate components so that I can create a separate snapshot for each. All I had to do was add a call to registerAccessibilitySnapshotNode for each component.
Before generating/testing snapshots make sure that global settings such as dark mode and text size have been set to default (or simply use a new Simulator). As of right now, there is no way to specify those settings per snapshot.
Ideally, you want always to run those tests on the same Simulator configuration. If your components depend on the screen size, then you can expect that tests may fail when run on a different configuration.
Important notes on the article
The code in this article is compatible with React Native 0.71, it might not work if you're on a different version.
We looked into the process of adding and using AccessibilitySnapshot in our React Native project. We covered how to set up GitHub Actions to run the tests automatically. We also improved the process of writing new tests to be as simple as possible. Now, we are ready to start testing!
The quickest way to get accessibility tests for the majority of your app will be to write 1 test per screen. Follow that with covering dialogs, popups, and menus that require an extra tap to show up. And then try to cover the UI that is displayed only when your components are in some non-default state (e.g. when a text input is displayed only after a checkbox is checked). Good luck!
Need help improving your app’s accessibility?
We help ensure your app meets accessibility standards and reaches a wider audience.
Tell us where testing slows your team down today. We'll come back with a plan to wire the QA agent into your delivery flow and what to prove out first.
What is accessibility, why do you need it, and how to implement it in your app? Read our article to find out.
Sep 12·Article
React Native Android Accessibility Tips
In this article Luke Walczak, software developer at Callstack, shares many valuable React Native Android Accessibility tips.
Mar 19·Tutorial
The Hard Way vs. The React Native AI SDK Way: Stop Writing Custom Modules for Every Model
Learn how React Native AI uses a unified provider model to switch between cloud and on-device LLMs without rewriting application logic, enabling reliable AI experiences online and offline.
Mar 12·Tutorial
Build Smarter Apps: Tool Calling & AI Orchestration Explained
Learn how on-device models in React Native can call tools, interact with external APIs, and return structured outputs you can use directly in application logic.
Mar 5·Tutorial
MLC LLM + React Native: On-Device AI Without the Pain
Learn how to run third-party on-device LLMs in React Native using MLC LLM. Choose your models, run them efficiently across platforms, and keep a unified JavaScript API with React Native AI.
Feb 24·Tutorial
Intelligent Fallbacks with Apple Intelligence in React Native
Learn how to use Apple’s built-in Foundation Models in React Native apps, reduce memory usage, and enable fast, fully on-device AI with React Native AI Apple.
Feb 10·Tutorial
What Is the React Native AI SDK? A Complete Intro & Quickstart
Learn why on-device AI matters for React Native apps, how local LLMs behave offline, and what problems React Native AI is designed to solve from day one.
Jan 29·Tutorial
Implementing an Android TurboModule with Kotlin
Learn how to implement a React Native TurboModule on Android, from the TypeScript spec and Codegen to the Kotlin implementation and package registration.
Jan 19·Tutorial
Working with Different Threads in Swift TurboModules
Learn how JavaScript, native module, background, and main threads interact in React Native TurboModules, and how to use GCD queues to run async work safely in Swift.
Jan 9·Tutorial
How to Add Type-Safe Constants to Swift TurboModules
Learn how to expose typed, immutable constants from a Swift TurboModule using Codegen, and get full end-to-end type safety between native and JavaScript.
Dec 30·Tutorial
Adding Event Emitters to Your TurboModule in Swift
Learn how to emit native events from a Swift TurboModule and subscribe to them in JavaScript, including proper memory management and Codegen integration.
Dec 22·Tutorial
Writing Your First TurboModule in Swift
Learn how to build a fully working TurboModule in Swift, integrate it with Codegen, and bridge it through Objective‑C inside an Expo app.