# SkillPatch skill: detox-skill

This skill enables agents to generate Detox end-to-end tests for React Native applications in JavaScript/TypeScript. It provides comprehensive patterns for element matching, actions, expectations, and device control using the Detox gray-box testing framework. It also covers configuration, anti-patterns, and supports both local simulators/emulators and TestMu AI cloud environments.

You (the agent) were given this URL and asked to use this skill. This is a **public** skill — no SkillPatch account, API key, or prior setup is required. Two ways to use it:

**1. Use it right now** — the skill's complete file tree (SKILL.md + all reference files) is inlined below; read `SKILL.md` first, then follow it, consulting the other files as it directs.

**2. Install the exact package onto disk** (recommended if you can run a shell — this reproduces the skill byte-for-byte, including any binary assets that can't be inlined):

```bash
mkdir -p .claude/skills/detox-skill
curl -sSL https://skillpatch.dev/install_skill/detox-skill | tar -xz -C .claude/skills/
```

(`.claude/skills/` is Claude Code's convention; use whatever directory your agent loads skills from.)


---

## Skill files (3)

- `SKILL.md`
- `reference/advanced-patterns.md`
- `reference/playbook.md`


### `SKILL.md`

````markdown
---
name: detox-skill
description: >
  Generates Detox E2E tests for React Native apps in JavaScript. Gray-box testing
  framework with automatic synchronization. Supports local simulators/emulators
  and TestMu AI cloud. Use when user mentions "Detox", "React Native test",
  "element(by.id())", "device.launchApp". Triggers on: "Detox", "React Native E2E",
  "React Native test", "element(by.id)", "device.launchApp".
languages:
  - JavaScript
  - TypeScript
category: mobile-testing
license: MIT
metadata:
  author: TestMu AI
  version: "1.0"
---

# Detox Automation Skill

You are a senior React Native QA engineer specializing in Detox.

## Core Patterns

### Basic Test

```javascript
describe('Login', () => {
  beforeAll(async () => {
    await device.launchApp();
  });

  beforeEach(async () => {
    await device.reloadReactNative();
  });

  it('should login with valid credentials', async () => {
    await element(by.id('emailInput')).typeText('user@test.com');
    await element(by.id('passwordInput')).typeText('password123');
    await element(by.id('loginButton')).tap();
    await expect(element(by.id('dashboardTitle'))).toBeVisible();
    await expect(element(by.text('Welcome'))).toBeVisible();
  });

  it('should show error for invalid credentials', async () => {
    await element(by.id('emailInput')).typeText('wrong@test.com');
    await element(by.id('passwordInput')).typeText('wrong');
    await element(by.id('loginButton')).tap();
    await expect(element(by.id('errorMessage'))).toBeVisible();
  });
});
```

### Matchers (Finding Elements)

```javascript
element(by.id('uniqueId'))                    // testID prop (best)
element(by.text('Login'))                      // Text content
element(by.label('Submit'))                    // Accessibility label
element(by.type('RCTTextInput'))              // Native type
element(by.traits(['button']))                 // iOS traits

// Combined
element(by.id('list').withDescendant(by.text('Item 1')))
element(by.id('item').withAncestor(by.id('list')))

// At index (multiple matches)
element(by.text('Delete')).atIndex(0)
```

### Actions

```javascript
await element(by.id('btn')).tap();
await element(by.id('btn')).longPress();
await element(by.id('btn')).multiTap(3);
await element(by.id('input')).typeText('hello');
await element(by.id('input')).replaceText('new text');
await element(by.id('input')).clearText();
await element(by.id('scrollView')).scroll(200, 'down');
await element(by.id('scrollView')).scrollTo('bottom');
await element(by.id('item')).swipe('left', 'fast');
await element(by.id('input')).tapReturnKey();
```

### Expectations

```javascript
await expect(element(by.id('title'))).toBeVisible();
await expect(element(by.id('title'))).not.toBeVisible();
await expect(element(by.id('title'))).toExist();
await expect(element(by.id('title'))).toHaveText('Welcome');
await expect(element(by.id('toggle'))).toHaveToggleValue(true);
await expect(element(by.id('input'))).toHaveValue('hello');
```

### Device Control

```javascript
await device.launchApp({ newInstance: true });
await device.reloadReactNative();
await device.sendToHome();
await device.terminateApp();
await device.installApp();
await device.shake();                       // Shake gesture
await device.setLocation(37.7749, -122.4194); // GPS
await device.setURLBlacklist(['.*cdn.example.*']); // Block URLs
```

### Anti-Patterns

| Bad | Good | Why |
|-----|------|-----|
| `waitFor().withTimeout()` everywhere | Trust Detox auto-sync | Detox waits automatically |
| No `testID` on components | Add `testID` prop | Stable selectors |
| `device.launchApp()` in every test | `device.reloadReactNative()` | Faster |
| Manual delays | Detox synchronization | Built-in waiting |

### .detoxrc.js Configuration

```javascript
module.exports = {
  testRunner: { args: { config: 'e2e/jest.config.js' } },
  apps: {
    'ios.debug': {
      type: 'ios.app',
      binaryPath: 'ios/build/MyApp.app',
      build: 'xcodebuild -workspace ios/MyApp.xcworkspace -scheme MyApp -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build',
    },
    'android.debug': {
      type: 'android.apk',
      binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk',
      build: 'cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug',
      reversePorts: [8081],
    },
  },
  devices: {
    simulator: { type: 'ios.simulator', device: { type: 'iPhone 16' } },
    emulator: { type: 'android.emulator', device: { avdName: 'Pixel_7_API_34' } },
  },
  configurations: {
    'ios.sim.debug': { device: 'simulator', app: 'ios.debug' },
    'android.emu.debug': { device: 'emulator', app: 'android.debug' },
  },
};
```

## Cloud Execution on TestMu AI

Detox generates native test runners (Espresso for Android, XCUITest for iOS). Run on TestMu AI cloud by uploading the built artifacts and triggering a framework build.

### Android (Espresso) Cloud Run

```bash
# 1. Build the app and test APKs
detox build --configuration android.emu.debug

# 2. Upload app APK
curl -u "$LT_USERNAME:$LT_ACCESS_KEY" \
  -X POST "https://manual-api.lambdatest.com/app/upload/realDevice" \
  -F "appFile=@android/app/build/outputs/apk/debug/app-debug.apk" \
  -F "name=DetoxApp"
# Response: {"app_url": "lt://APP_ID", ...}

# 3. Upload test APK
curl -u "$LT_USERNAME:$LT_ACCESS_KEY" \
  -X POST "https://manual-api.lambdatest.com/app/upload/realDevice" \
  -F "appFile=@android/app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk" \
  -F "name=DetoxTestSuite"
# Response: {"app_url": "lt://TEST_SUITE_ID", ...}

# 4. Trigger Espresso build
curl -X POST "https://mobile-api.lambdatest.com/framework/v1/espresso/build" \
  -H "Authorization: Basic <BASE64_AUTH>" \
  -H "Content-Type: application/json" \
  -d '{
    "app": "lt://APP_ID",
    "testSuite": "lt://TEST_SUITE_ID",
    "device": ["Galaxy S21 5G-12", "Pixel 6-12"],
    "build": "Detox-Android",
    "deviceLog": true,
    "video": true
  }'
```

### iOS (XCUITest) Cloud Run

```bash
# 1. Build the iOS app and test runner
detox build --configuration ios.sim.release

# 2. Create .ipa from the build artifacts, then upload
curl -u "$LT_USERNAME:$LT_ACCESS_KEY" \
  -X POST "https://manual-api.lambdatest.com/app/upload/realDevice" \
  -F "appFile=@MyApp.ipa" -F "name=DetoxApp"

# 3. Upload test runner .ipa
curl -u "$LT_USERNAME:$LT_ACCESS_KEY" \
  -X POST "https://manual-api.lambdatest.com/app/upload/realDevice" \
  -F "appFile=@MyAppTests-Runner.ipa" -F "name=DetoxTestSuite"

# 4. Trigger XCUITest build
curl -X POST "https://mobile-api.lambdatest.com/framework/v1/xcui/build" \
  -H "Authorization: Basic <BASE64_AUTH>" \
  -H "Content-Type: application/json" \
  -d '{
    "app": "lt://APP_ID",
    "testSuite": "lt://TEST_SUITE_ID",
    "device": ["iPhone 14-16", "iPhone 13-15"],
    "build": "Detox-iOS",
    "devicelog": true,
    "video": true
  }'
```

## Quick Reference

| Task | Command |
|------|---------|
| Build iOS | `detox build --configuration ios.sim.debug` |
| Test iOS | `detox test --configuration ios.sim.debug` |
| Build Android | `detox build --configuration android.emu.debug` |
| Test Android | `detox test --configuration android.emu.debug` |
| Specific test | `detox test --configuration ios.sim.debug e2e/login.test.js` |
| Record video | `detox test --record-videos all` |
| Take screenshot | `await device.takeScreenshot('login-page')` |

## Setup

```bash
npm install detox --save-dev
npm install jest jest-circus --save-dev
detox init
```

## React Native Component Setup

```jsx
// Add testID to components for Detox
<TextInput testID="emailInput" placeholder="Email" />
<TextInput testID="passwordInput" placeholder="Password" secureTextEntry />
<Button testID="loginButton" title="Login" onPress={handleLogin} />
<Text testID="errorMessage">{error}</Text>
```

## Deep Patterns

For advanced patterns, debugging guides, CI/CD integration, and best practices,
see `reference/playbook.md`.

````


### `reference/advanced-patterns.md`

````markdown
# Detox (React Native) — Advanced Patterns & Playbook

## Screen Object Pattern

```javascript
class LoginScreen {
  get emailInput() { return element(by.id('email-input')); }
  get passwordInput() { return element(by.id('password-input')); }
  get submitBtn() { return element(by.id('login-button')); }
  get errorText() { return element(by.id('error-message')); }

  async login(email, password) {
    await this.emailInput.replaceText(email);
    await this.passwordInput.replaceText(password);
    await this.submitBtn.tap();
  }

  async expectError(message) {
    await expect(this.errorText).toBeVisible();
    await expect(this.errorText).toHaveText(message);
  }
}

class HomeScreen {
  get welcomeText() { return element(by.id('welcome-text')); }
  get productList() { return element(by.id('product-list')); }

  async scrollToProduct(name) {
    await waitFor(element(by.text(name)))
      .toBeVisible()
      .whileElement(by.id('product-list'))
      .scroll(200, 'down');
  }
}
```

## Advanced Interactions

```javascript
describe('Gestures', () => {
  it('supports swipe to delete', async () => {
    await element(by.id('item-1')).swipe('left', 'fast', 0.75);
    await element(by.id('delete-confirm')).tap();
    await expect(element(by.id('item-1'))).not.toExist();
  });

  it('supports pull to refresh', async () => {
    await element(by.id('list')).swipe('down', 'slow', 0.5, 0.5, 0.1);
    await waitFor(element(by.id('refreshed-indicator'))).toBeVisible().withTimeout(5000);
  });

  it('supports long press', async () => {
    await element(by.id('item-1')).longPress();
    await expect(element(by.id('context-menu'))).toBeVisible();
  });
});
```

## Device Capabilities

```javascript
describe('System interactions', () => {
  it('handles permissions', async () => {
    await device.launchApp({ permissions: { notifications: 'YES', camera: 'YES' } });
  });

  it('handles deep links', async () => {
    await device.openURL({ url: 'myapp://product/123' });
    await expect(element(by.id('product-detail'))).toBeVisible();
  });

  it('handles background/foreground', async () => {
    await device.sendToHome();
    await device.launchApp({ newInstance: false });
    await expect(element(by.id('home-screen'))).toBeVisible();
  });

  it('handles orientation', async () => {
    await device.setOrientation('landscape');
    await expect(element(by.id('landscape-layout'))).toBeVisible();
  });
});
```

## Configuration

```javascript
// .detoxrc.js
module.exports = {
  testRunner: { args: { $0: 'jest', config: 'e2e/jest.config.js' }, jest: { setupTimeout: 120000 } },
  apps: {
    'ios.debug': { type: 'ios.app', binaryPath: 'ios/build/Debug/MyApp.app', build: 'xcodebuild ...' },
    'android.debug': { type: 'android.apk', binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk' }
  },
  devices: {
    simulator: { type: 'ios.simulator', device: { type: 'iPhone 15' } },
    emulator: { type: 'android.emulator', device: { avdName: 'Pixel_6_API_34' } }
  },
  configurations: {
    'ios.sim.debug': { device: 'simulator', app: 'ios.debug' },
    'android.emu.debug': { device: 'emulator', app: 'android.debug' }
  }
};
```

## Anti-Patterns

- ❌ `await new Promise(r => setTimeout(r, 3000))` — use `waitFor().toBeVisible().withTimeout()`
- ❌ Matching by text for dynamic content — use `testID` props and `by.id()`
- ❌ Missing `device.reloadReactNative()` between unrelated test suites
- ❌ Not using `replaceText` — `typeText` appends, which causes issues with pre-filled fields

````


### `reference/playbook.md`

````markdown
# Detox — Advanced Implementation Playbook

## §1 Project Setup & Configuration

### .detoxrc.js — Production Config
```javascript
/** @type {Detox.DetoxConfig} */
module.exports = {
  testRunner: {
    args: {
      config: 'e2e/jest.config.js',
      maxWorkers: 1,
      _: ['e2e'],
    },
    jest: {
      setupTimeout: 120000,
      teardownTimeout: 30000,
    },
  },
  apps: {
    'ios.debug': {
      type: 'ios.app',
      binaryPath: 'ios/build/Build/Products/Debug-iphonesimulator/MyApp.app',
      build: 'xcodebuild -workspace ios/MyApp.xcworkspace -scheme MyApp -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build',
    },
    'ios.release': {
      type: 'ios.app',
      binaryPath: 'ios/build/Build/Products/Release-iphonesimulator/MyApp.app',
      build: 'xcodebuild -workspace ios/MyApp.xcworkspace -scheme MyApp -configuration Release -sdk iphonesimulator -derivedDataPath ios/build',
    },
    'android.debug': {
      type: 'android.apk',
      binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk',
      build: 'cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug',
      reversePorts: [8081],
    },
    'android.release': {
      type: 'android.apk',
      binaryPath: 'android/app/build/outputs/apk/release/app-release.apk',
      build: 'cd android && ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release',
    },
  },
  devices: {
    simulator: { type: 'ios.simulator', device: { type: 'iPhone 15' } },
    emulator: { type: 'android.emulator', device: { avdName: 'Pixel_6_API_34' } },
    'attached.android': { type: 'android.attached', device: { adbName: '.*' } },
  },
  configurations: {
    'ios.sim.debug': { device: 'simulator', app: 'ios.debug' },
    'ios.sim.release': { device: 'simulator', app: 'ios.release' },
    'android.emu.debug': { device: 'emulator', app: 'android.debug' },
    'android.emu.release': { device: 'emulator', app: 'android.release' },
    'android.att.debug': { device: 'attached.android', app: 'android.debug' },
  },
  artifacts: {
    rootDir: './e2e/artifacts',
    plugins: {
      screenshot: { shouldTakeAutomaticSnapshots: true, keepOnlyFailedTestsArtifacts: true },
      video: { enabled: true },
      log: { enabled: true },
    },
  },
  behavior: {
    init: { exposeGlobals: true },
    cleanup: { shutdownDevice: false },
  },
};
```

### e2e/jest.config.js
```javascript
module.exports = {
  maxWorkers: 1,
  testTimeout: 120000,
  rootDir: '..',
  testMatch: ['<rootDir>/e2e/**/*.test.js'],
  verbose: true,
  reporters: [
    'default',
    ['jest-junit', { outputDirectory: './e2e/artifacts', outputName: 'junit.xml' }],
  ],
};
```

---

## §2 Page Object Pattern

### BasePage
```javascript
class BasePage {
  async waitForVisible(testID, timeout = 5000) {
    await waitFor(element(by.id(testID)))
      .toBeVisible()
      .withTimeout(timeout);
  }

  async tapById(testID) {
    await element(by.id(testID)).tap();
  }

  async typeById(testID, text) {
    await element(by.id(testID)).clearText();
    await element(by.id(testID)).typeText(text);
  }

  async takeScreenshot(name) {
    await device.takeScreenshot(name);
  }
}
```

### LoginPage
```javascript
class LoginPage extends BasePage {
  get emailInput() { return element(by.id('email_input')); }
  get passwordInput() { return element(by.id('password_input')); }
  get loginButton() { return element(by.id('login_button')); }
  get errorMessage() { return element(by.id('error_message')); }

  async enterEmail(email) {
    await this.emailInput.clearText();
    await this.emailInput.typeText(email);
  }

  async enterPassword(password) {
    await this.passwordInput.clearText();
    await this.passwordInput.typeText(password);
  }

  async tapLogin() {
    await this.loginButton.tap();
  }

  async login(email, password) {
    await this.enterEmail(email);
    await this.enterPassword(password);
    await this.tapLogin();
    return new DashboardPage();
  }

  async expectError(message) {
    await expect(this.errorMessage).toBeVisible();
    await expect(this.errorMessage).toHaveText(message);
  }
}

module.exports = { LoginPage };
```

### DashboardPage
```javascript
class DashboardPage extends BasePage {
  get welcomeText() { return element(by.id('welcome_text')); }
  get productList() { return element(by.id('product_list')); }
  get cartBadge() { return element(by.id('cart_badge')); }

  async verifyOnScreen() {
    await waitFor(this.welcomeText).toBeVisible().withTimeout(10000);
  }

  async selectProduct(index) {
    await element(by.id(`product_item_${index}`)).tap();
    return new ProductDetailPage();
  }
}

module.exports = { DashboardPage };
```

---

## §3 Test Patterns

### Complete Flow Test
```javascript
const { LoginPage } = require('./pages/LoginPage');
const { DashboardPage } = require('./pages/DashboardPage');

describe('Purchase Flow', () => {
  beforeAll(async () => {
    await device.launchApp({ newInstance: true, permissions: { notifications: 'YES' } });
  });

  beforeEach(async () => {
    await device.reloadReactNative();
  });

  it('should complete login → browse → purchase', async () => {
    const loginPage = new LoginPage();
    const dashboard = await loginPage.login('user@test.com', 'password123');

    await dashboard.verifyOnScreen();
    await device.takeScreenshot('dashboard_loaded');

    const productPage = await dashboard.selectProduct(0);
    await productPage.addToCart();
    await expect(dashboard.cartBadge).toBeVisible();

    await productPage.goToCart();
    await element(by.id('checkout_button')).tap();
    await waitFor(element(by.text('Order Confirmed')))
      .toBeVisible()
      .withTimeout(15000);
  });

  it('should show error for invalid credentials', async () => {
    const loginPage = new LoginPage();
    await loginPage.enterEmail('wrong@test.com');
    await loginPage.enterPassword('wrong');
    await loginPage.tapLogin();
    await loginPage.expectError('Invalid credentials');
  });
});
```

---

## §4 Advanced Interactions

### Scroll & Search
```javascript
// Scroll until element is visible
await waitFor(element(by.text('Load More')))
  .toBeVisible()
  .whileElement(by.id('scroll_view'))
  .scroll(200, 'down');

// Scroll to edge
await element(by.id('scroll_view')).scrollTo('bottom');
await element(by.id('scroll_view')).scrollTo('top');

// FlatList — scroll by index
await element(by.id('product_list')).scrollTo('bottom');
```

### Gestures
```javascript
// Swipe
await element(by.id('card')).swipe('left', 'fast', 0.75);
await element(by.id('card')).swipe('right', 'slow', 0.5);

// Long press
await element(by.id('item')).longPress(2000);

// Multi-tap
await element(by.id('zoom_target')).multiTap(2); // double-tap

// Pinch (iOS only)
await element(by.id('map_view')).pinch(1.5); // zoom in
await element(by.id('map_view')).pinch(0.5); // zoom out
```

### Device Operations
```javascript
// Location
await device.setLocation(37.7749, -122.4194);

// Deep links
await device.openURL('myapp://products/123');

// Shake device
await device.shake();

// Background & foreground
await device.sendToHome();
await device.launchApp({ newInstance: false });

// Biometric (iOS simulator)
await device.matchFace(); // or device.unmatchFace()
await device.matchFinger(); // or device.unmatchFinger()

// Set orientation
await device.setOrientation('landscape');
await device.setOrientation('portrait');
```

### Synchronization Control
```javascript
// Disable for animations that never settle
await device.disableSynchronization();
await element(by.id('lottie_animation')).tap();
await device.enableSynchronization();

// Wait with timeout for async elements
await waitFor(element(by.id('loaded_content')))
  .toBeVisible()
  .withTimeout(15000);
```

---

## §5 Matchers & Assertions

```javascript
// Visibility
await expect(element(by.id('header'))).toBeVisible();
await expect(element(by.id('hidden'))).not.toBeVisible();
await expect(element(by.id('deleted'))).not.toExist();

// Text
await expect(element(by.id('title'))).toHaveText('Welcome');
await expect(element(by.id('label'))).toHaveLabel('Submit');

// Toggles
await expect(element(by.id('switch'))).toHaveToggleValue(true);

// Slider
await expect(element(by.id('slider'))).toHaveSliderPosition(0.5, 0.1);

// Multiple matching elements
await expect(element(by.text('Item')).atIndex(0)).toBeVisible();
await expect(element(by.text('Item')).atIndex(2)).toBeVisible();

// Compound matchers
await element(by.id('save').and(by.text('Save'))).tap();
await element(by.id('cell').withAncestor(by.id('list_section_1'))).tap();
await element(by.id('icon').withDescendant(by.text('Edit'))).tap();
```

---

## §6 Network & Mock Server Integration

```javascript
// In your React Native app, use launch arguments for mock mode:
// if (__DEV__ && process.argv.includes('--mock-api')) { useMockServer(); }

// Detox can launch with arguments:
await device.launchApp({
  newInstance: true,
  launchArgs: {
    mockServerPort: '3001',
    detoxEnableSync: 0, // if needed for certain animations
  },
});

// Set up mock server with beforeAll
const { MockServer } = require('./helpers/mockServer');

beforeAll(async () => {
  MockServer.start(3001);
  MockServer.stub('GET', '/api/products', {
    status: 200,
    body: [{ id: 1, name: 'Test Product', price: 9.99 }],
  });
});

afterAll(async () => {
  MockServer.stop();
});
```

---

## §7 CI/CD Integration

### GitHub Actions (iOS)
```yaml
name: Detox E2E
on:
  push: { branches: [main] }
  pull_request: { branches: [main] }

jobs:
  ios-e2e:
    runs-on: macos-14
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with: { node-version: 20 }

      - name: Install deps
        run: |
          npm ci
          cd ios && pod install

      - name: Boot Simulator
        run: |
          xcrun simctl boot "iPhone 15" || true
          xcrun simctl status_bar "iPhone 15" override --time "9:41"

      - name: Build for Detox
        run: npx detox build --configuration ios.sim.debug

      - name: Run E2E tests
        run: npx detox test --configuration ios.sim.debug --cleanup --headless

      - name: Upload artifacts
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: detox-artifacts
          path: e2e/artifacts/

  android-e2e:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20 }
      - run: npm ci

      - name: Android E2E
        uses: reactivecircus/android-emulator-runner@v2
        with:
          api-level: 34
          arch: x86_64
          profile: Pixel 6
          script: |
            npx detox build --configuration android.emu.debug
            npx detox test --configuration android.emu.debug --headless
```

---

## §8 Debugging Table

| # | Problem | Cause | Fix |
|---|---------|-------|-----|
| 1 | `element(by.id(...))` not found | `testID` prop not set | Add `testID="identifier"` to React Native component |
| 2 | `pumpAndSettle`-style timeout | Infinite animation (Lottie, ActivityIndicator) | Use `device.disableSynchronization()` around animated sections |
| 3 | `device.reloadReactNative()` crashes | Metro bundler not running | Start Metro: `npx react-native start` before running tests |
| 4 | Build fails for Detox | CocoaPods or Gradle cache stale | `cd ios && pod install --repo-update`; `cd android && ./gradlew clean` |
| 5 | Test passes locally, fails on CI | Timing differences | Increase `withTimeout()` values; use explicit `waitFor` |
| 6 | Keyboard covers input field | Auto-scroll not triggered | Append `\n` to `typeText()` to dismiss; or scroll manually |
| 7 | `clearText()` doesn't clear | Focus not on field | Call `tap()` before `clearText()` |
| 8 | Android emulator extremely slow | No hardware acceleration | Use `-gpu host` and `x86_64` image; enable KVM on CI |
| 9 | `scrollTo` scrolls past element | Scroll offset too large | Use `whileElement().scroll(100, 'down')` with smaller increments |
| 10 | Multiple elements match | Non-unique `testID` | Use unique IDs; or use `.atIndex(n)` or compound matchers |
| 11 | `launchApp` hangs | Previous app instance stuck | Use `{ newInstance: true, delete: true }` to force fresh start |
| 12 | Artifacts not generated | Missing artifacts config | Add `artifacts.plugins` in `.detoxrc.js` with screenshot/video enabled |

---

## §9 Best Practices Checklist

1. ✅ Use `testID` prop on all interactive/assertable React Native elements
2. ✅ Use `device.reloadReactNative()` in `beforeEach` for clean state
3. ✅ Use `waitFor().toBeVisible().withTimeout()` — never `sleep` or fixed delays
4. ✅ Disable synchronization only for animations, re-enable immediately
5. ✅ Use `--reuse` flag during development to skip app rebuild
6. ✅ Use page objects for readable, maintainable tests
7. ✅ Use `device.takeScreenshot()` at key points for debugging and CI evidence
8. ✅ Handle keyboard by appending `\n` to `typeText()` or dismissing explicitly
9. ✅ Use compound matchers for elements inside lists: `by.id().withAncestor()`
10. ✅ Configure artifact collection (screenshots, videos, logs) for CI
11. ✅ Run iOS tests on macOS runners; Android tests with hardware acceleration
12. ✅ Use launch arguments for mock mode: `launchArgs: { mockServerPort: '3001' }`
13. ✅ Test on both platforms — matcher behavior differs between iOS and Android

````
