Integrate your Storybook React Native components with App Percy
Run App Percy visual tests on your Storybook React Native components using the @percy/storybook-react-native SDK, captured on real devices through BrowserStack App Automate.
Use the @percy/storybook-react-native SDK to add App Percy visual testing to your Storybook React Native components. The SDK discovers the stories in your React Native project, captures one snapshot per story, and uploads them to your App Percy project.
You can run your visual tests in either of two modes. The SDK is the same in both. The difference is where the tests run and who manages the device:
- BrowserStack App Automate, or library mode: You run your components on a real device in a BrowserStack App Automate session. You provision a Storybook host app, and BrowserStack manages the device.
- Local, or CLI mode: You run your components on a local emulator or simulator that you manage.
Choose the tab that matches where you want to run your tests:
In library mode, the SDK runs against BrowserStack App Automate from inside your existing WebdriverIO Appium tests. You build a Storybook-enabled app, upload it to App Automate, and run a test that discovers your stories and captures a snapshot of each on a real device.
Prerequisites
Before you begin, ensure you have the following:
- A Percy account with an app-type App Percy project. The project token has an
app_prefix. - Node.js version 20.19 or later, or 22.12 or later.
- A React Native project that uses Storybook for React Native, built as a Storybook-enabled app.
- An existing WebdriverIO Appium test suite.
- Your BrowserStack username and access key.
Install the SDK
Install the SDK, the App Percy Appium SDK, and the Percy CLI as development dependencies:
Set your environment variables
Export your BrowserStack credentials and your App Percy token. The token has an app_ prefix:
Build a Storybook app
Build a Storybook-enabled release .apk for Android. This app is separate from your production app. For the recommended Expo and bare React Native build setup, see the STORYBOOK_HOST_APP.md guide in the SDK repository.
Build with assembleRelease, not assembleDebug. A debug build expects Metro to serve the JavaScript bundle from localhost:8081. A BrowserStack cloud device has no Metro server, so a debug build fails with a loadJSBundleFromAssets redbox. A release build embeds the JavaScript bundle in the .apk.
For iOS, App Automate needs a distribution-signed .ipa, which the SDK cannot produce for you. See the Build a signed .ipa for iOS section below.
Upload the app to App Automate
Call provisionApp with the path to your built app. It uploads the app to App Automate and returns a bs:// reference:
provisionApp computes a content hash as the BrowserStack custom_id, so re-running with the same app returns the existing bs:// reference instead of uploading again. Set the reference as an environment variable for your test to read:
Point the SDK at your project
The SDK discovers stories from your React Native project’s .rnstorybook/main.ts configuration and .stories files. Set the project root so discoverStories can read them:
Add the snapshot calls to your test
In your WebdriverIO spec, start an App Automate session, then wrap your story iteration in runSession. percyStorybookSnapshot is the default export, and discoverStories and runSession are named exports:
runSession(driver, fn) runs driver.deleteSession() even when the test throws, so a failed run doesn’t leave a dangling App Automate session that burns your quota.
Run your tests
Run your test through the Percy CLI using the command:
App Percy starts, creates a build, captures one snapshot per story on a real BrowserStack device, and uploads them to your project. When the run finishes, the CLI prints the build URL:
Open the build URL to see one snapshot per story, captured on the device.
Snapshot options
Use percyStorybookSnapshot(driver, story, options) to control how the SDK captures each story. Pass options in a flat object. The SDK handles the navigation keys itself and forwards every other key to the @percy/appium-app percyScreenshot call, so you can use the full App Percy Appium option surface:
Performance options
These navigation options control how long the SDK waits at each stage. Override them per snapshot call:
| Option | Default | Purpose |
|---|---|---|
coldBootMaxMs |
30000 |
Real BrowserStack Android devices cold-boot between 4 and 20 seconds under load. |
globalNavigationBudgetMs |
8000 |
Hard ceiling on the total time the SDK spends navigating to a story per snapshot call. |
renderMs |
1500 |
Fallback delay the SDK waits after the stability poll runs out. |
stabilitySettleMs |
200 |
Defers the first stability capture to avoid catching the pre-render frame as stable. |
cacheNavigatorState |
false |
When true, the SDK keeps the drawer open across stories, saving about 500 ms per intra-component story. |
Navigation strategies
The SDK navigates to each story before it captures a snapshot. Set the navigationStrategy option to choose how it navigates:
-
ui-tap: Navigates by tapping through the Storybook UI. This is the default and works on all supported platforms with no extra setup. -
deeplink: Opens each story through a deep link, which is faster at about 500 ms per story compared to about 2 seconds. Your app must register a URL scheme, and you passappSchemeandappPackagein the snapshot options. For the scheme setup on Expo and bare React Native, see theAPP_AUTOMATE.mdguide in the SDK repository.
Deep-link navigation is unreliable below iOS 16.4. On earlier versions, the SDK fails with deep_link_unsupported_platform. Drop navigationStrategy: 'deeplink' to use the default UI-tap path, which works on all iOS versions.
Build a signed .ipa for iOS
App Automate installs a distribution-signed .ipa on iOS devices. The SDK cannot sign the app for you, because Apple’s signing flow needs your Apple Developer account, distribution certificate, and provisioning profile. After you have a signed .ipa, pass it to provisionApp the same way as an Android .apk:
For the full signing walkthrough, which covers the distribution certificate, the Ad Hoc provisioning profile, the xcodebuild archive and export steps, and CI integration, see the IOS_SIGNING.md guide in the SDK repository.
Configuration
Set your BrowserStack credentials, Percy token, and app details through environment variables:
| Variable | Mode | Purpose |
|---|---|---|
PERCY_TOKEN |
Both | App-type Percy project token. Starts with app_. |
BROWSERSTACK_USERNAME |
Library | App Automate authentication for provisionApp and the driver session. |
BROWSERSTACK_ACCESS_KEY |
Library | App Automate authentication for provisionApp and the driver session. |
PERCY_APP_URL |
Library | The bs:// reference returned by provisionApp. |
PERCY_RN_PROJECT_DIR |
Library | React Native project root for story discovery, where .rnstorybook/ lives. |
PERCY_APP_SCHEME |
Library | URL scheme for navigationStrategy: 'deeplink'. |
PERCY_APP_PACKAGE |
Library | Android package ID for navigationStrategy: 'deeplink'. |
DEBUG |
Both | Set to 1 for verbose per-story logging. |
For CLI mode, the SDK also reads a storybook-rn: block in .percy.yml. The following keys are available:
| Key | Default | Notes |
|---|---|---|
appium.server |
http://localhost:4723 |
Must be an http: or https: URL, or the run fails with invalid_config. |
appium.capabilities |
{} |
The SDK merges these over its per-platform defaults. Set platformName here. |
storybook.websocketHost |
localhost |
Pin to 127.0.0.1 on macOS to avoid the IPv6 mismatch. |
storybook.websocketPort |
7007 |
Storybook React Native channel server port. |
storybook.waitForReadyMs |
4000 |
Backstop wait for the device render acknowledgment. Raise for stories with slow data or image loads. |
storybook.settleMs |
250 |
Extra delay after the render acknowledgment, for animations or image decode. |
include |
['**/*'] |
Story-ID globs to keep. The CLI --include flag overrides this. |
skip |
[] |
Story-ID globs to drop. Wins over include. |
Troubleshooting
Every failure produces a structured error code in the message. Look up the code in the following table:
| Error code | What it means | What to do |
|---|---|---|
bs_credentials_missing |
BROWSERSTACK_USERNAME or BROWSERSTACK_ACCESS_KEY is not set. |
Add the environment variables, or pass credentials to provisionApp(path, { credentials }). |
bs_upload_failed |
BrowserStack rejects the upload. | Check the response body in the error message. A 401 or 403 means bad credentials. |
bs_upload_too_large |
The app exceeds your BrowserStack plan’s size limit, which defaults to 1 GB. | Strip native modules from your Storybook .apk, or contact BrowserStack support. |
bs_app_not_ready |
The first Appium session install fails because BrowserStack is still processing the upload. | The SDK retries once after a 5-second pause. If you see this repeatedly, file a Percy issue. |
nav_navigator_not_detected |
The SDK can’t find the Storybook React Native navigator within coldBootMaxMs. |
Confirm your .apk is a Storybook-enabled build, not the production app. Check the session video. |
nav_element_not_found |
A story’s component group or leaf isn’t tappable. | Verify the component title and story name match the on-device sidebar exactly. |
nav_render_timeout |
The story renders but the screenshot-stability poll doesn’t converge within 8 seconds. | Likely a permanent animation. Set parameters.percy.waitFor per story. |
app_cold_boot_timeout |
The cold-boot poll exceeds 30 seconds. | The device is slow. Increase coldBootMaxMs in the snapshot options. |
bs_app_reference_stale |
The bs:// reference points to an upload that BrowserStack no longer has. Uploads expire after about 30 days. |
Re-run provisionApp to get a fresh bs:// URL. |
bs_quota_exhausted |
Your BrowserStack plan’s App Automate session quota is used up. | Wait for running sessions to finish, or raise the plan limit. |
invalid_app_reference |
The app reference does not start with bs://. |
Pass the bs:// URL returned by provisionApp, not a local file path. |
build_is_debug_variant |
The uploaded app is a debug build, which expects Metro on localhost:8081 and fails on a cloud device. |
Build the release variant with ./gradlew assembleRelease, which embeds the JavaScript bundle. |
build_failed |
The SDK-driven Gradle or EAS build command exits with a non-zero code. | Read the build output above the error, fix the native build failure, and re-run. |
build_toolchain_missing |
The build command isn’t available on this machine. | Install the missing toolchain, or build the artifact yourself and pass its path to provisionApp. |
build_artifact_not_found |
The build succeeds but no .apk is at the expected output path. |
Locate the artifact and pass it to provisionApp directly. |
apple_signing_required |
iOS on App Automate needs a distribution-signed .ipa, which the SDK cannot produce. |
Follow the iOS signing guide, then call provisionApp('./MyApp.ipa'). |
unsupported_project_type |
The project path is neither an Expo nor a bare React Native root. | Pass the React Native project root, the directory whose package.json lists react-native. |
unsupported_platform |
The Appium session’s platform is neither Android nor iOS. | Set platformName to Android or iOS in your capabilities. |
deep_link_unsupported_platform |
Deep-link navigation doesn’t settle in time on this platform. | Verify the URL scheme is registered, or drop navigationStrategy: 'deeplink' to use the default UI-tap path. |
url_scheme_silent_failure |
The SDK sends the deep link but the app doesn’t surface the story. | Check that the scheme in app.json, AndroidManifest.xml, or Info.plist matches appScheme exactly. |
nav_state_diverged |
The on-device navigator ends up somewhere other than the requested story. | Usually a duplicate story title. Make the CSF title and export names unique across the project. |
no_stories_found |
Story discovery finds a .rnstorybook config but no .stories files match its glob. |
Check the stories glob in .rnstorybook/main.ts, set PERCY_RN_PROJECT_DIR, or pass --stories explicitly. |
include_zero_match |
The include or skip patterns filter out every discovered story. |
Loosen the include and skip globs in .percy.yml. |
invalid_descriptor |
The Appium session has no platformName capability. |
Set platformName to iOS or Android in the .percy.yml appium.capabilities. |
invalid_config |
A .percy.yml storybook-rn: value fails validation. |
Fix the value named in the message, for example appium.server: http://localhost:4723. |
appium_unreachable |
There is no Appium session, or the server is down. | Start the Appium server and check appium.server. In library mode, pass a live driver. |
percy_appium_app_missing |
The @percy/appium-app peer dependency is not installed. |
Run npm install --save-dev @percy/appium-app. |
percy_cli_unreachable |
The Percy CLI server is not running. | Wrap the run with npx percy app:exec -- <command> and set PERCY_TOKEN. |
token_missing |
PERCY_TOKEN is not set. |
Export the app-type project token, which starts with app_, from your Percy project settings. |
screenshot_failed |
Appium screenshot capture fails mid-run. | The device lost focus or the session expired. Check the session video and re-run with DEBUG=1. |
story_render_timeout |
The story doesn’t acknowledge render. | Confirm Metro is running and the device is connected. Increase storybook.waitForReadyMs for slow stories. |
storybook_ws_unreachable |
Storybook’s channel server is up but WebSockets are disabled. | Add enableWebsockets: true to getStorybookUI() in .rnstorybook/index. |
all_snapshots_failed |
Every story in the run fails to capture. | Read the per-story errors above, and re-run with DEBUG=1 for full detail. |
In CLI mode, the npx percy storybook-rn command drives a local iOS Simulator or Android emulator through Appium. The SDK discovers your stories, renders each on the device, and uploads one snapshot per story to your App Percy project.
Prerequisites
Before you begin, ensure you have the following:
- A Percy account with an app-type App Percy project. The project token has an
app_prefix. - Node.js version 20.19 or later, or 22.12 or later.
-
Appium 2.x, with the
uiautomator2driver for Android or thexcuitestdriver for iOS. - For iOS, Xcode and CocoaPods on macOS.
- For Android, Android Studio with an emulator.
- A React Native project that uses Storybook for React Native.
Enable WebSockets in Storybook
@storybook/react-native version 10 ships with the on-device WebSocket connection disabled. Add enableWebsockets: true to your getStorybookUI() call so the SDK can tell the device which story to render:
Build a native dev binary
Build a native dev binary rather than running in Expo Go. Storybook React Native needs native modules that Expo Go doesn’t include. On iOS, the build boots a Simulator with your app:
The first build takes 5 to 15 minutes. Later builds use Xcode’s cache and finish in seconds. When it completes, the Simulator shows your app with Storybook’s component picker.
On Android, the emulator doesn’t share the host’s localhost. After the app boots, forward the channel and Metro ports so the device can reach them:
adb reverse tcp:7007 tcp:7007
adb reverse tcp:8081 tcp:8081
Run Percy
Set your App Percy token, then run the health check and the snapshot command:
The command auto-discovers your stories, renders each on the device, and uploads the snapshots. A build URL prints when the run finishes. Open it to see one snapshot per story.
Filter stories
Use these flags to control which stories run:
Configuration
The SDK reads a storybook-rn: block in .percy.yml. The following keys are available:
| Key | Default | Notes |
|---|---|---|
appium.server |
http://localhost:4723 |
Must be an http: or https: URL, or the run fails with invalid_config. |
appium.capabilities |
{} |
The SDK merges these over its per-platform defaults. Set platformName here. |
storybook.websocketHost |
localhost |
Pin to 127.0.0.1 on macOS to avoid the IPv6 mismatch. |
storybook.websocketPort |
7007 |
Storybook React Native channel server port. |
storybook.waitForReadyMs |
4000 |
Backstop wait for the device render acknowledgment. Raise for stories with slow data or image loads. |
storybook.settleMs |
250 |
Extra delay after the render acknowledgment, for animations or image decode. |
include |
['**/*'] |
Story-ID globs to keep. The CLI --include flag overrides this. |
skip |
[] |
Story-ID globs to drop. Wins over include. |
Troubleshooting
Every CLI failure produces a structured error code in the message. Look up the code in the following table:
| Error code | What it means | What to do |
|---|---|---|
no_stories_found |
The Storybook config is missing, or no .stories files match the glob. |
Run npx storybook init --type react_native, or check the stories glob in main.ts. |
appium_unreachable |
The Appium server doesn’t accept a connection. | Start Appium with npx appium --port 4723, and verify with curl http://localhost:4723/status. |
storybook_ws_unreachable |
The channel server on port 7007 isn’t responding. | Verify withStorybook pins host: '127.0.0.1' and port: 7007 in metro.config.js, and check the Metro log. |
percy_cli_unreachable |
The Percy CLI’s local server on port 5338 isn’t running. | Wrap your command with npx percy exec -- <command>. |
token_missing |
PERCY_TOKEN isn’t set. |
Run export PERCY_TOKEN=app_... with a token from your Percy project settings. |
story_render_timeout |
The device doesn’t acknowledge render within waitForReadyMs. |
Increase waitForReadyMs to 6000 to 8000, or verify enableWebsockets: true in getStorybookUI(). |
screenshot_failed |
Appium’s screenshot capture returns an error. | The device lost focus or the session expired. Re-run with DEBUG=1 for details. |
include_zero_match |
Your include or skip patterns match no stories. |
Run with --dry-run to see all discovered stories, then adjust the patterns. |
Related topics
We're sorry to hear that. Please share your feedback so we can do better
Contact our Support team for immediate help while we work on improving our docs.
We're continuously improving our docs. We'd love to know what you liked
We're sorry to hear that. Please share your feedback so we can do better
Contact our Support team for immediate help while we work on improving our docs.
We're continuously improving our docs. We'd love to know what you liked
Thank you for your valuable feedback!