Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Gray-box end-to-end testing for React Native apps with Detox. Covers .detoxrc.js configuration, build and test commands, matchers, device.launchApp control, automatic synchronization, and macOS CI pipelines.
.claude/skills/pramoddutta-detox-mobile-testing/SKILL.md| Model | Eval pass | Runs |
|---|---|---|
| gemini-3.6-flash | 100% | 9 |
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-05 | ✗→✓ | ▲ Improved | 63% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 100% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 64% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 44% | 0% |
| case-13 | ✗→✓ | ▲ Improved | 57% | 0% |
This skill makes an AI agent write and run Detox gray-box E2E tests for React Native apps: configure .detoxrc.js for iOS simulators and Android emulators, build test binaries, write tests with element(by.id(...)) matchers, control the app lifecycle with device.launchApp, and lean on Detox's automatic synchronization instead of sleeps. Trigger it in React Native repositories containing an e2e/ directory, detox in package.json, or when the user asks for end-to-end tests on iOS/Android simulators.
sleep() in a Detox suite is a bug.testID, never by text or traversal. Text changes with copy edits and localization; view hierarchy changes with refactors. Add testID="login-button" props in the app code as part of writing the test.assembleRelease / -configuration Release binaries.device.launchApp({ newInstance: true }) or device.reloadReactNative() in beforeEach; tests that depend on the previous test's screen are unmaintainable.device.launchApp({ permissions: { notifications: 'YES', location: 'inuse' } }) sets iOS permissions deterministically; tapping system dialogs is flaky and Detox cannot see them anyway.device.disableSynchronization() to the smallest possible window.bashnpm install --save-dev detox jest @types/jest # iOS dependency for simulator control brew tap wix/brew brew install applesimutils # Scaffold e2e/ folder and config npx detox init
js// .detoxrc.js /** @type {Detox.DetoxConfig} */ module.exports = { testRunner: { args: { config: 'e2e/jest.config.js', _: ['e2e'], }, jest: { setupTimeout: 120000 }, }, apps: { 'ios.release': { type: 'ios.app', binaryPath: 'ios/build/Build/Products/Release-iphonesimulator/ShopApp.app', build: 'xcodebuild -workspace ios/ShopApp.xcworkspace -scheme ShopApp -configuration Release -sdk iphonesimulator -derivedDataPath ios/build', }, 'android.release': { type: 'android.apk', binaryPath: 'android/app/build/outputs/apk/release/app-release.apk', build: 'cd android && ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release && cd ..', }, }, devices: { simulator: { type: 'ios.simulator', device: { type: 'iPhone 15' } }, emulator: { type: 'android.emulator', device: { avdName: 'Pixel_7_API_34' } }, }, configurations: { 'ios.sim.release': { device: 'simulator', app: 'ios.release' }, 'android.emu.release': { device: 'emulator', app: 'android.release' }, }, };
bashnpx detox build --configuration ios.sim.release npx detox test --configuration ios.sim.release --cleanup npx detox build --configuration android.emu.release npx detox test --configuration android.emu.release --headless --record-logs failing
js// e2e/login.test.js describe('Login', () => { beforeAll(async () => { await device.launchApp({ newInstance: true, permissions: { notifications: 'YES' }, }); }); beforeEach(async () => { await device.reloadReactNative(); }); it('logs in with valid credentials', async () => { await element(by.id('email-input')).typeText('qa@example.com'); await element(by.id('password-input')).typeText('Str0ngPass!'); await element(by.id('login-button')).tap(); await expect(element(by.id('home-screen'))).toBeVisible(); await expect(element(by.text('Welcome back'))).toBeVisible(); }); it('shows a validation error for a bad password', async () => { await element(by.id('email-input')).typeText('qa@example.com'); await element(by.id('password-input')).typeText('nope'); await element(by.id('login-button')).tap(); await expect(element(by.id('login-error'))).toHaveText('Invalid email or password'); await expect(element(by.id('home-screen'))).not.toBeVisible(); }); });
js// e2e/orders.test.js it('renders orders fetched from the API', async () => { await element(by.id('tab-orders')).tap(); // Wait for async content beyond the automatic idle sync await waitFor(element(by.id('orders-list'))) .toBeVisible() .withTimeout(10000); // Scroll inside the list until a row appears await waitFor(element(by.text('Order #1042'))) .toBeVisible() .whileElement(by.id('orders-list')) .scroll(250, 'down'); await element(by.text('Order #1042')).tap(); await expect(element(by.id('order-detail-screen'))).toBeVisible(); });
js// e2e/deeplink.test.js it('opens a product from a deep link', async () => { await device.launchApp({ newInstance: true, url: 'shopapp://products/SKU-1042', }); await expect(element(by.id('product-screen'))).toBeVisible(); await expect(element(by.id('product-sku'))).toHaveText('SKU-1042'); }); it('survives backgrounding mid-checkout', async () => { await element(by.id('checkout-button')).tap(); await device.sendToHome(); await device.launchApp({ newInstance: false }); await expect(element(by.id('checkout-screen'))).toBeVisible(); });
yaml# .github/workflows/detox-ios.yml name: detox-ios on: [pull_request] jobs: ios-e2e: runs-on: macos-14 timeout-minutes: 45 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: npm - run: npm ci - run: cd ios && pod install && cd .. - name: Install simulator utils run: brew tap wix/brew && brew install applesimutils - name: Build app for Detox run: npx detox build --configuration ios.sim.release - name: Run Detox tests run: npx detox test --configuration ios.sim.release --cleanup --record-videos failing --take-screenshots failing - name: Upload failure artifacts if: failure() uses: actions/upload-artifact@v4 with: name: detox-artifacts path: artifacts
testID props during feature development, not retroactively; treat a missing testID as a review comment.e2e/jest.config.js separate from the unit-test Jest config (maxWorkers: 1, longer timeouts, Detox environment).--record-videos failing --take-screenshots failing in CI so every red test ships with visual evidence.detoxEnableMockServer flag) rather than tapping through logout flows in every test.--headless) and pin the AVD image version; emulator image drift is a top source of "works locally" failures.device.disableSynchronization() plus waitFor(...).withTimeout(...), then device.enableSynchronization() in a finally block.await new Promise(r => setTimeout(r, 5000)) between steps: Detox's synchronization already waits for idle; sleeps only slow the suite and mask real sync bugs.by.text() for anything that will be localized or copy-edited.--cleanup, leaving zombie simulators that exhaust CI runner disk and memory.detox in devDependencies, a .detoxrc.js, or an e2e/ folder with Detox tests.| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | pass→pass | 23,309 | 16,004 | -31% | 1 | 1 | 0% | 3,322 | 4,686 | +41% | 0 | 0 | — |
case-02 | pass→pass | 24,365 | 10,426 | -57% | 1 | 1 | 0% | 3,336 | 4,265 | +28% | 0 | 0 | — |
case-03 | pass→pass | 20,446 | 15,416 | -25% | 1 | 1 | 0% | 2,162 | 4,099 | +90% | 0 | 0 | — |
case-04 | pass→pass | 16,839 | 11,829 | -30% | 1 | 1 | 0% | 1,726 | 3,520 | +104% | 0 | 0 | — |
case-05 | fail→pass | 17,092 | 12,095 | -29% | 1 | 1 | 0% | 2,422 | 3,942 | +63% | 0 | 0 | — |
case-06 | fail→pass | 14,487 | 16,393 | +13% | 1 | 1 | 0% | 2,083 | 4,162 | +100% | 0 | 0 | — |
case-07 | pass→pass | 14,581 | 11,923 | -18% | 1 | 1 | 0% | 2,355 | 3,473 | +47% | 0 | 0 | — |
case-08 | fail→pass | 16,161 | 14,480 | -10% | 1 | 1 | 0% | 2,482 | 4,075 | +64% | 0 | 0 | — |
case-09 | pass→pass | 14,610 | 17,214 | +18% | 1 | 1 | 0% | 2,512 | 4,400 | +75% | 0 | 0 | — |
case-10 | fail→pass | 24,643 | 21,160 | -14% | 1 | 1 | 0% | 3,421 | 4,923 | +44% | 0 | 0 | — |
case-11 | fail→fail | 18,337 | 9,315 | -49% | 1 | 1 | 0% | 1,862 | 3,889 | +109% | 0 | 0 | — |
case-12 | pass→pass | 13,934 | 8,184 | -41% | 1 | 1 | 0% | 1,301 | 2,835 | +118% | 0 | 0 | — |
case-13 | fail→pass | 17,188 | 10,033 | -42% | 1 | 1 | 0% | 2,602 | 4,085 | +57% | 0 | 0 | — |
case-14 | pass→pass | 13,766 | 6,580 | -52% | 1 | 1 | 0% | 1,844 | 3,363 | +82% | 0 | 0 | — |
case-15 | pass→pass | 21,300 | 17,215 | -19% | 1 | 1 | 0% | 2,398 | 4,359 | +82% | 0 | 0 | — |
case-16 | pass→pass | 19,960 | 15,948 | -20% | 1 | 1 | 0% | 2,402 | 4,222 | +76% | 0 | 0 | — |
case-17 | fail→pass | 15,166 | 10,206 | -33% | 1 | 1 | 0% | 1,590 | 3,227 | +103% | 0 | 0 | — |
case-18 | pass→pass | 16,990 | 10,614 | -38% | 1 | 1 | 0% | 1,856 | 4,108 | +121% | 0 | 0 | — |
case-19 | pass→pass | 23,800 | 9,689 | -59% | 1 | 1 | 0% | 2,787 | 4,155 | +49% | 0 | 0 | — |
case-20 | fail→pass | 23,163 | 11,002 | -53% | 1 | 1 | 0% | 2,725 | 4,167 | +53% | 0 | 0 | — |
case-21 | pass→pass | 17,780 | 15,619 | -12% | 1 | 1 | 0% | 2,481 | 4,357 | +76% | 0 | 0 | — |
case-22 | pass→pass | 26,376 | 22,428 | -15% | 1 | 1 | 0% | 3,133 | 5,182 | +65% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +32 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.