> ## Documentation Index
> Fetch the complete documentation index at: https://e2e.tester.army/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# From Detox

> Use your existing app build and testIDs with screen, expect, and agent goals.

export const migrationPrompt = `Migrate this project's Detox tests to e2e, one spec at a time. Docs: \`https://e2e.tester.army/docs/migrate/detox.md\` and \`https://e2e.tester.army/docs/mobile.md\`

1. Read the migration guide above. It maps \`.detoxrc.js\` and the Detox API to e2e. The mobile guide covers devices and app builds.
2. If the project has no e2e config (\`e2e.config.ts\`, \`.mts\`, \`.js\`, or \`.mjs\`), run \`npx e2e init --yes --engine mobile\` (or the pnpm or bun equivalent). It writes a mobile config with one target for this machine's platform and an example test against the Settings app, and installs nothing yet. Rewrite \`tests/example.e2e.ts\` for your app or delete it.
3. Read the e2e skill (\`npx e2e guide\` prints it, \`npx e2e guide <topic>\` prints a topic) and follow it for every step below.
4. If you ran init, ask me which model to use for agent steps: a ChatGPT, GitHub Copilot, OpenCode Console, or SuperGrok subscription, an API key, or a local model. Configure it with its provider package. If it needs a sign-in, give me the \`npx e2e login\` command and wait until I confirm.
5. Port \`.detoxrc.js\` into the e2e config: one target per platform with \`mobile({ platform })\`, the app's \`bundleId\` and \`appPath\` on the target, and the device type as \`mobile({ device })\`. e2e uses the normal debug or release build, with no Detox framework.
6. Install dependencies, then run \`npx e2e doctor\` (e2e 0.22.1 or newer) and fix what it reports.
7. List the Detox specs and ask me which one to move first. Suggest a flow that needs frequent maintenance.
8. Rewrite that spec as a \`.e2e.ts\` file under \`tests/\` (e2e runs \`tests/**/*.e2e.ts\` by default), not in the Detox folder, with the guide's tables. Start it with \`app.open()\`, since nothing launches on its own. Narrow queries that match several nodes.
9. Keep the launch and the final assertions deterministic, with \`app.open()\` and locator assertions. Where a run of scripted interactions breaks often, propose an \`agent.act\` goal for it instead.
10. Build and install the app, run the new file with \`npx e2e run <file>\` on each platform target, and fix it until it passes. Keep the Detox spec until I confirm it can go.
11. Summarize which tests moved, which stayed in Detox, and why.`;

e2e tests a React Native app on a simulator or emulator with your normal
debug or release build, with no Detox framework in the app. Your `testID`
props work with `getByTestId` on both platforms.

<SetupPrompt>

<Prompt description={migrationPrompt} children={migrationPrompt} actions={["copy", "cursor"]} />

</SetupPrompt>

For a migration that spans several sessions, the [`migrate-to-e2e` skill](https://e2e.tester.army/docs/coding-agents#migrate-an-existing-suite) records what moved in a progress file and keeps the old suite running until you retire each test.

## Port the config

<Note>Set up a target with the [mobile guide](https://e2e.tester.army/docs/mobile) and a model with the [quickstart](https://e2e.tester.army/docs/quickstart). Both runners can use the same simulator while you move one spec at a time.</Note>

| Detox (`.detoxrc.js`) | e2e |
| --- | --- |
| `apps.<name>.binaryPath` | `app: { appPath }` on the target plus one `device.installApp()` per device in a fixture; the engine installs nothing on its own |
| `apps.<name>.bundleId` | `app: { bundleId: 'com.example.app' }` on the target, the app `app.open()` launches |
| `devices.<name>.device.type` | `mobile({ device: 'iPhone 17' })` |
| `configurations` | `targets`, one engine per platform |
| Jest config, `maxWorkers` | `workers`, one per device |
| `detox build` | Your normal build |
| `device.launchApp({ permissions })` | `app.permissions` on the target, set before every fresh launch, or `device.openApp(app, { relaunch: true, permissions })` for one; `grant`, `deny`, `reset` for `YES`, `NO`, `unset` |
| `device.launchApp({ launchArgs })` | `app.launchArguments` on the target, or `device.openApp(app, { relaunch: true, launchArguments })` for one; a list of strings the platform launch command takes, not a map |

```ts title="e2e.config.ts"

export default {
  targets: [
    {
      name: 'ios',
      engine: mobile({ platform: 'ios' }),
      app: { bundleId: 'com.example.app', appPath: 'ios/build/MyApp.app' },
    },
    {
      name: 'android',
      engine: mobile({ platform: 'android' }),
      app: { bundleId: 'com.example.app', appPath: 'android/app/build/outputs/apk/debug/app-debug.apk' },
    },
  ],
  workers: 1,
} satisfies E2EConfig;
```

## Rewrite one spec

Start with a flow that needs frequent maintenance. If its screens take time
to settle, set the engine's
[`settle` and `transition` options](https://e2e.tester.army/docs/reference/mobile#engine).

```js title="Detox"
describe('login', () => {
  beforeAll(async () => { await device.launchApp({ newInstance: true }); });

  it('shows the home screen', async () => {
    await element(by.id('email')).typeText('ada@example.test');
    await element(by.id('password')).typeText('hunter2');
    await element(by.text('Sign in')).tap();
    await waitFor(element(by.id('home'))).toBeVisible().withTimeout(10000);
    await expect(element(by.text('Welcome, Ada'))).toBeVisible();
  });
});
```

```ts title="e2e"

test('shows the home screen', async ({ agent, app, screen }) => {
  await app.open();
  const ada = credentials.user('ada');
  await agent.act('sign in as {username}', { params: { username: ada.username, password: ada.password } });
  await expect(screen.getByTestId('home')).toBeVisible({ timeout: 10_000 });
  await expect(screen.getByText('Welcome, Ada')).toBeVisible();
});
```

Nothing launches on its own, so the test starts with `app.open()`. The
password comes from [configured credentials](https://e2e.tester.army/docs/authentication).

## Map each Detox call

| Detox | e2e |
| --- | --- |
| `by.id('x')` | `screen.getByTestId('x')` |
| `by.text('t')` | `screen.getByText('t')`; both match the exact text |
| `by.label('l')` | `screen.getByLabel('l')` |
| `by.type('RCTImageView')` | `device.locator('role=image')`, agent-device's selector syntax |
| `by.id('x').withAncestor(by.id('row'))` | `screen.getByTestId('row').getByTestId('x')` |
| `.atIndex(n)` | `.nth(n)` |
| `.tap()`, `.multiTap(2)`, `.longPress()` | `.tap()`, `.doubleTap()`, `.longPress()` |
| `.tap({ x, y })` | `.tap({ position: { x, y } })` |
| `.typeText(t)`, `.replaceText(t)` | `.fill(t)` replaces the content |
| `.clearText()` | `.clear()` |
| `.tapReturnKey()` | `.press('Enter')` |
| `.swipe('up', 'fast')` | `.swipe({ direction: 'down', momentum: 'fast' })`: Detox names the finger's direction, e2e the content's |
| `.scroll(200, 'down')` | `screen.swipe({ direction: 'down' })` |
| `waitFor(el).toBeVisible().whileElement(list).scroll(...)` | `screen.scrollUntilVisible(el)` |
| `waitFor(el).toBeVisible().withTimeout(ms)` | `await expect(el).toBeVisible({ timeout: ms })` |
| `expect(el).toBeVisible()`, `.toExist()` | `.toBeVisible()` for visibility; `.toHaveCount(1)` for existence |
| `expect(el).not.toBeVisible()` | `.toBeHidden()` |
| `expect(el).toHaveText(t)` | `.toHaveText(t)` |
| `expect(el).toHaveToggleValue(true)` | `.toBeChecked()` |
| `expect(el).toHaveValue(v)` | `.toHaveValue(v)` |
| `device.launchApp({ newInstance: true })` | `app.restart()` |
| `device.launchApp({ delete: true })` | `app.clearState()` |
| `device.reloadReactNative()` | No equivalent. `app.restart()` is the nearest |
| `device.sendToHome()`, `device.pressBack()` | `device.home()`, `device.back()` |
| `device.setLocation(lat, lon)` | `device.setLocation({ latitude, longitude })` |
| `device.setOrientation('landscape')` | `device.setOrientation('landscape-left')` |
| `device.setBiometricEnrollment(true)` | `device.enrollBiometrics('faceid', true)` |
| `device.matchFace()`, `device.unmatchFace()` | `device.setBiometrics('faceid', 'match' \| 'nonmatch')` |
| `device.setStatusBar`, `device.setURLBlacklist` | No equivalent |
| `device.disableSynchronization()` | No equivalent. Actions wait and assertions retry |
| `device.takeScreenshot()` | `app.screenshot('label')`, secure fields masked |
| `device.takeScreenshot()` compared with an image snapshot library | `await expect(screen).toHaveScreenshot('name.png')`, see [Visual comparison](https://e2e.tester.army/docs/visual-testing) |
| `:ios:` / `:android:` in a test name | `{ platforms: ['ios'] }` on the test |

Two things differ from Detox:

- Actions and single-node reads and assertions need one match. Several fail
  with `LOCATOR_AMBIGUOUS`. Narrow with `.filter()`, `.first()`, or `.nth()`.
  See [screen](https://e2e.tester.army/docs/reference/screen).
- A device has no `test.setup` or `{ session }`. See
  [what differs from the web](https://e2e.tester.army/docs/mobile#what-differs-from-the-web).

## Next

<CardGroup cols={2}>
  <Card title="Mobile" icon="mobile-screen" href="/mobile">
    Configure your app and devices.
  </Card>
  <Card title="Mobile engine reference" icon="book" href="/reference/mobile">
    Every option, the `device` fixture, and the tool pack.
  </Card>
</CardGroup>
