All jobs

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 RushIn 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 with this body:

{
  "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.

The use case this job follows, with its automation and trigger: Hand off from your terminal.

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.