# Migrate an Expo Project from Jest to Vitest

Yes. An agent converts your Expo app's Jest config, mocks, timers and snapshots to Vitest, proves the same tests pass, and opens a PR with both counts.

[Run this on Rush](https://cloud.getrush.ai/run/migrate-jest-to-vitest/expo/), in a Project that has your repository.

## What the agent does

The agent first runs the suite as it is and records the baseline: how many test files, how many tests, how many snapshots, and which pass. In an Expo app like the Ignite boilerplate (`expo` 55.0.5, `react-native` 0.83.2, `react` 19.2.0), the `test` script is plain `jest`, and `jest.config.js` is two lines: `preset: "jest-expo"` and `setupFiles: ["<rootDir>/test/setup.ts"]`. The tests are `app/components/Text.test.tsx` (React Native Testing Library rendering inside `ThemeProvider` and `NavigationContainer`), `app/services/api/apiProblem.test.ts`, `app/utils/storage/storage.test.ts` (MMKV) and `test/i18n.test.ts`, which shells out with `child_process` to grep the source for translation keys. Without that baseline, "the same tests pass" means nothing.

Then it inventories everything Jest-specific. `test/setup.ts` uses `jest.doMock("react-native", ...)` to extend `Image` with `resolveAssetSource` and `getSize` stubs, and `jest.mock` for `i18next`, `expo-localization` (with `jest.requireActual`) and `../app/i18n/index.ts`, plus a global `__TEST__` declaration. Each becomes its Vitest form (`vi.mock`, `vi.fn`, `vi.importActual`, `vi.useFakeTimers` where timers are faked), with `globals: true` or explicit imports from `vitest`, and `@types/jest` replaced by Vitest's types in `test/test-tsconfig.json`. The Ignite repository root also has a separate Jest suite for the CLI (`ts-jest`, `testEnvironment: "node"`, snapshots in `test/vanilla/__snapshots__/`, a 10-minute `jest.setTimeout` in `test/_test-helpers.ts`), and the agent treats it as a second, simpler migration with its own count.

The hard part is what `jest-expo` did for free. The preset supplies React Native's Jest setup, the Babel transform for Flow-typed `react-native` source and platform file resolution (`.ios.tsx`, `.native.tsx`). Vitest has no official Expo preset, so the agent configures `vitest.config.ts` for React Native explicitly: a transform for `react-native` and Expo packages in `node_modules`, `resolve.extensions` with the platform suffixes, the module mocks the preset used to provide, and an environment the testing library can render in. It reruns the suite after each piece, and snapshot files are regenerated only after the test that owns them passes against the old snapshot's content, so an `.snap` file is never rewritten to hide a change.

When the same tests pass with the same count, it removes `jest`, `jest-expo`, `ts-jest`, `babel-jest` and `@types/jest` from `devDependencies`, deletes `jest.config.js`, points `test` and `test:watch` at Vitest, and runs `tsc --noEmit -p .` (the `compile` script) and `eslint .` so the type and lint setup still agree. If a test cannot pass under Vitest without changing what it checks, it stops and reports that test rather than editing its assertions.

## Start it with one API call

`POST https://api.prix.dev/o/{org}/sessions`

```json
{
  "agent": "claude",
  "input": "Migrate this Expo project's tests from Jest to Vitest. First run the existing suite and record the number of test files, tests and snapshots and which pass. Convert jest.config.js and test/setup.ts to vitest.config.ts and a Vitest setup file, and convert every jest.* call, module mock, fake timer and snapshot. Configure Vitest for React Native explicitly, since there is no jest-expo equivalent: transform react-native and Expo packages, resolve platform extensions, and mock what the preset used to. Make the same tests pass with the same count without changing what any test asserts. Then remove Jest, jest-expo, ts-jest, babel-jest and @types/jest, update the test scripts, and run the compile and lint scripts. Open a pull request with the before and after counts. If a test cannot pass under Vitest unchanged, stop and explain why.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-expo-app"
    }
  ]
}
```

1. **Get an API key.** In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
2. **Send the request.** POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
3. **Read what came back.** Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.

## What comes back

What comes back is a pull request that removes Jest and adds Vitest. For an Ignite-shaped app the diff has `vitest.config.ts` replacing `jest.config.js`, the setup file converted from `jest.*` to `vi.*`, each test file's imports and mocks updated, `package.json` and the lockfile with the old test packages out and `vitest` in, the `test` scripts changed, and `test/test-tsconfig.json` using Vitest's types. Any regenerated `.snap` files are in the diff where a reviewer can compare them line by line.

The description carries a small table: test files, tests and snapshots before and after, with the pass count for each, plus a list of every mock that changed shape and why. The session's trajectory keeps both full runs, the Jest baseline and the final Vitest run, so the counts in the table can be checked against real output.

Before merging, a reviewer checks that the counts match and no test was skipped with `.skip` or `todo`, that the React Native mocks in the setup file are no broader than the old ones (a mock that swallows a whole module can make a test pass for the wrong reason), and that Maestro flows under `.maestro/`, which never ran on Jest, are untouched.

## Run it on every app repository still on Jest

Teams with several Expo apps can run this once per repository. List the repositories whose `package.json` still has `jest-expo`, and have a script send one request per repository with it in `resources`. Each gets its own run in its own sandbox and its own pull request, so a migration that stalls in one app does not hold up the others.

Give the API key a webhook URL and Rush POSTs `run.completed` or `run.failed` when each run's turn finishes, with the final answer that carries the counts. A key runs at most `max_concurrent` sessions at once (4 by default) and answers `429` past that, so a long list should be queued.

## Why run it on Rush?

A test-runner migration is many small runs of the suite: baseline, convert, rerun, fix a transform, rerun. On a laptop that occupies `node_modules`, the watcher and the branch for as long as it takes, and a half-migrated tree is hard to put down. On Rush the agent does the whole loop in its own sandbox with its own clone and install, started from the terminal, and it keeps going after you leave your desk.

The result is checked, not asserted. The pull request shows before and after counts, the trajectory holds both full test runs with their output, and the description lists every mock that changed. If the migration cannot be done without weakening a test, the agent says which test and stops, which is a more useful answer than a green run that tests less.

## Frequently asked questions

### Is there a Vitest equivalent of jest-expo?

Not an official one. `jest-expo` bundles React Native's test setup, the transform for Flow-typed `react-native` source and platform file resolution, and the agent configures each of those in `vitest.config.ts` by hand. If one cannot be made to work for your project's dependencies, the agent stops and says which, instead of shipping a partial migration.

### Will my snapshots be regenerated?

Only after the test that owns a snapshot passes against its old content under Vitest. Vitest writes `.snap` files in a slightly different header format, so regenerated files appear in the diff, and a reviewer can confirm that only the header changed.

### What about the Ignite CLI's own tests at the repository root?

They are a separate Jest suite with `ts-jest` in a Node environment and their own snapshots under `test/vanilla/__snapshots__/`. The agent migrates them separately and reports their counts on their own line, or leaves them alone if you limit the request to the app.

### Does it change any test's assertions?

No. The request forbids it: imports, mocks and config change, assertions do not. A test that only passes with a different assertion is reported in the pull request and the run stops there.
