# React Native E2E Testing on Every Pull Request (Expo Included)

> How to run React Native and Expo end-to-end tests on every pull request: which build to test, when a JavaScript-only change still needs one, and how Detox, Maestro and AI agents compare.

Source: https://qa.tech/blog/react-native-e2e-testing-pull-requests · Published: 2026-11-06

---
React Native's own docs describe end-to-end testing as building your app in the release configuration and running tests against it on a device, simulator or emulator. They also say those tests are ["slower to run"](https://reactnative.dev/docs/testing-overview) and "more prone to flakiness". Both are true, and they're why many teams do React Native E2E testing only before a release and skip it on PRs.

Running them per PR works once you've settled two things: which binary each PR is tested on, and what the tests use to find things on the screen.

## Which build a React Native pull request needs

A React Native app is two layers: native code that gets compiled into the binary, and JavaScript that runs inside it. That split matters on PRs. An architect running several React Native apps in production pointed out to us that most of their changes never touch native code, so many PRs don't need a new iOS or Android build, and yet every PR still produces its own version of the app.

That gives you two kinds of PR:

- **Native changed** (a new native module, an SDK upgrade, a config plugin): you need a fresh native build.
- **Only JavaScript changed**: the native layer is the same as main's, but the test binary still has to contain the PR's JavaScript bundle.

Expo makes the first question answerable. Its fingerprint runtime version policy bumps the version ["whenever anything that may impact the native runtime changes"](https://docs.expo.dev/eas-update/runtime-versions/), and [Expo Workflows](https://docs.expo.dev/eas/workflows/introduction/) compares fingerprints to "skip redundant native builds". JavaScript, styling and images are the parts [EAS Update](https://docs.expo.dev/eas-update/introduction/) can ship over the air; anything native needs a new build. For a JavaScript-only PR, the Workflows [`repack` job](https://docs.expo.dev/eas/workflows/pre-packaged-jobs/) "repackages the app's metadata and JavaScript bundle without performing a full native rebuild", which gives you a test binary with the PR's JavaScript on main's native layer.

## Building the test binary

You want a simulator build for iOS and an APK for Android, in the release configuration React Native's docs recommend.

With EAS Build, add a profile for it. Expo's own [E2E example](https://docs.expo.dev/eas/workflows/examples/e2e-tests/) uses exactly this:

```json
{
  "build": {
    "e2e-test": {
      "withoutCredentials": true,
      "ios": { "simulator": true },
      "android": { "buildType": "apk" }
    }
  }
}
```

EAS builds Android as an `.aab` by default, and an `.aab` can't be installed directly, which is why the profile asks for `apk` (more on [which mobile build to use for automated testing](https://qa.tech/blog/mobile-build-for-automated-testing)). Simulator builds come out as a `.app` and need no Apple credentials.

Without EAS, generate the native projects in CI with `npx expo prebuild`, then build a release variant: `./gradlew assembleRelease` on a Linux runner, and `xcodebuild -configuration Release` for a simulator destination on a macOS runner. Debug builds expect a Metro dev server for their JavaScript, so they won't run on their own in a cloud emulator. Bare React Native skips the prebuild step.

## Detox, Maestro, or an agent

**Detox** is built for React Native. It's gray-box: it watches your app's async work, which cuts a real source of flakiness. It can match on text or labels, but its [matcher docs](https://wix.github.io/Detox/docs/api/matchers) recommend unique identifiers, which in React Native means `testID`, and its [guide](https://wix.github.io/Detox/docs/guide/test-id) has you forward each one through your own components down to a native view. React Native's docs add a cost of their own: setting `testID` ["disables the 'layout-only view removal' optimization"](https://reactnative.dev/docs/view) for that view. Detox [officially supports](https://github.com/wix/Detox) React Native 0.77 to 0.84, while [Expo SDK 58](https://docs.expo.dev/versions/latest/) already targets React Native 0.88.

**Maestro** writes tests as YAML flows and matches on visible text or IDs. Expo's E2E example uses it on Expo Workflows, triggered on every pull request. It's quicker to start with than Detox, but the flows are still a suite someone has to keep current, and we've met Expo teams whose Maestro suite had quietly broken with nobody watching it. Both tools, and the rest of the field, are compared in our [Appium alternatives](https://qa.tech/compare/appium-alternatives) roundup.

**An AI agent** works from a goal and reads the screen. It needs no `testID`s, and a redesigned login page doesn't mean rewriting the test, because "log in with the test account" still describes what to do.

The SDK cycle tips this decision more than anything. Expo releases ["three times each year"](https://docs.expo.dev/versions/latest/), and each SDK targets a single React Native version, a newer one each time. One mobile engineer described what an upgrade did to their app: "the definition of our style sheets hadn't changed, it's just that the way in which Expo renders buttons, for example, had changed." Every upgrade is a regression run across both platforms, and with selector-based tests it's also a maintenance pass.

## A pull request workflow that works

1. On every PR, check the fingerprint. If it matches main's, repack main's build with the PR's JavaScript. If it doesn't, build the `e2e-test` profile (or prebuild and build in CI). Do it for both platforms.
2. Upload both binaries, and keep the IDs.
3. Run the tests for the flows the change can reach, on both platforms, pinned to those builds.
4. Post the result on the PR. Block the merge once the team trusts it.
5. On merge to main, upload that build as the new default, and run a short critical-path suite on a schedule.
6. After every Expo SDK or React Native upgrade, run the critical-path suite on both platforms before anything else merges.

Step 3 is where most teams cut corners, because running the whole suite on every PR is slow. Scope it to the change instead. Our post on [risk-based testing for pull requests](https://qa.tech/blog/risk-based-testing-pull-requests) covers how deep to go.

## Where QA.tech fits

QA.tech tests web apps, native and React Native mobile apps, and APIs from one platform, with an agent that reviews each pull request before merge and regression test plans that run on main after it. For React Native teams it cuts the two costs above, on cloud iOS Simulators and Android Emulators (hardware checks stay in your device pass).

Nothing in your components needs a `testID`, since the agent finds a button by looking at the rendered screen, so native, React Native, Expo and Flutter apps all look the same to it. Shared iOS and Android flows don't need two suites either. When both apps are builds of the same product, each goes in as its own environment and the same test cases run on both.

On a pull request, your CI uploads both platform builds and starts a review pinned to them. From the PR and its diff, the agent works out which flows the change touches, reuses the matching test cases, fills coverage gaps with new ones, and posts pass, fail, or unable to verify back on the PR. Expo users can upload an EAS `e2e-test` or preview build, or a release build from prebuild and Gradle. The CI setup is in [our mobile PR docs](https://docs.qa.tech/pr-testing/agents/mobile), and [mobile pull request testing](https://qa.tech/blog/mobile-pull-request-testing) walks through the same pipeline for native apps.

![A pull request list with a QA.tech result tooltip: four tests passed, no regressions found](https://qa.tech/blog-assets/react-native-e2e-testing-pull-requests-pr-result.jpg)
*The PR list tooltip: four tests passed, no regressions found. This one is from a web repo; a React Native PR shows its result there once CI has uploaded the build.*

To run this on your own React Native builds, book a demo of [mobile testing](https://qa.tech/product/mobile-testing), which comes with Growth and Enterprise. For the broader picture of agents on mobile, see [mobile test automation with AI agents](https://qa.tech/blog/mobile-test-automation-with-ai-agents).

## FAQ

**How do you run React Native E2E tests on pull requests?**
Build a simulator `.app` and an `.apk` from the PR's commit in CI, using a release-configuration profile such as Expo's `e2e-test`. Run the tests against those builds and post the result on the PR.

**Does a JavaScript-only change need a new build for testing?**
The native layer can be reused, but the binary you test must contain the PR's JavaScript. On Expo Workflows, a `repack` job does that without a full native rebuild. Testing main's last build only tells you main still works.

**Do I need testIDs for React Native E2E tests?**
For Detox, in practice yes: it can match text and labels, but its docs recommend unique IDs on the elements you interact with. Maestro can match visible text, though its docs recommend IDs for icons and dynamic content. A goal-based agent needs none.

**Detox or Maestro?**
Detox suits teams who want gray-box synchronisation and are happy to maintain `testID`s within its supported React Native range. Maestro is quicker to adopt and runs on Expo Workflows. Both leave you a scripted suite to maintain.

**Can I test an Expo app with Expo Go?**
No. Expo Go runs your JavaScript inside Expo's own native shell, so it isn't the binary you ship. For automated E2E, test a real build of your app, from EAS Build or from `expo prebuild`.
