# Migrate a React Project from Jest to Vitest

Yes. An agent moves your React test suite from Jest to Vitest, keeps every test passing with the same count, removes Jest, and opens a PR with the before and after.

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

## What the agent does

The agent starts with the Jest configuration, because that is where a migration is won or lost. In a React project like `react-hook-form/react-hook-form`, `scripts/jest/jest.config.js` defines two projects: `Web`, which matches `**/__tests__/**/*.(spec|test).ts?(x)`, transforms TypeScript with `@swc/jest`, runs in the `jest-fixed-jsdom` environment with `customExportConditions` set to an empty string, and loads `scripts/jest/setup.ts`; and `Server`, which matches `*.server.(spec|test).ts?(x)` and runs in Node. Both set `clearMocks`, `resetMocks` and `restoreMocks` to true, and the root config lists `collectCoverageFrom`. `package.json` runs tests with `jest --config ./scripts/jest/jest.config.js` on pnpm, with `TEST_ENV=web` selecting one project, and `tsconfig.json` puts `jest` and `@testing-library/jest-dom` in `types`. The CI job in `.github/workflows/build-test.yml` runs `pnpm run test --ci` and then `pnpm test:type`.

Then it measures what the tests use before touching them. A grep across `src` counts the Jest calls that need a replacement: `jest.fn`, `jest.spyOn` and `jest.mock` map to `vi`, `jest.useFakeTimers` and `jest.advanceTimersByTime` to Vitest's timer API, `jest.requireActual` to the asynchronous `vi.importActual`, and the `jest.Mock` and `jest.MockedFunction` types to Vitest's `Mock` type. It also counts the snapshot files under `__snapshots__`, since Vitest formats snapshots differently by default and a mass rewrite of snapshots would hide a real change. The agent runs the Jest suite once first and keeps the total as the number the migration must match.

The new `vitest.config.ts` keeps the shape of the old one: two entries under `projects`, one with `environment: 'jsdom'` and the setup file, one with `environment: 'node'`, the same include globs, `clearMocks`, `mockReset` and `restoreMocks` turned on to match, and `snapshotFormat` set so the existing snapshots still match. The setup file switches to `@testing-library/jest-dom/vitest`. Transform needs no plugin: Vitest compiles TypeScript itself, so `@swc/jest` goes. The `jest-fixed-jsdom` environment exists to repair globals that Jest's jsdom breaks for `fetch` and MSW; the agent checks whether any test depends on that repair under Vitest's jsdom before dropping it, and says what it found.

Then the mechanical part, file by file, with the suite run between batches: `vi` for `jest`, `await vi.importActual` where `jest.requireActual` was synchronous, `vi.useFakeTimers()` and `vi.advanceTimersByTime()`, and `vitest/globals` replacing `jest` in `tsconfig.json`'s `types`. `package.json` loses `jest`, `@swc/jest`, `@types/jest`, `jest-environment-jsdom` and `jest-fixed-jsdom` and gains `vitest` and `jsdom`; `pnpm-workspace.yaml` lists `esbuild` under `allowBuilds` as false, so the agent confirms Vitest still starts with that setting before it finishes. The last run has to report the same number of tests as the first, and `pnpm test:type` and `pnpm lint` have to pass as CI runs them.

## Start it with one API call

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

```json
{
  "agent": "claude",
  "input": "Migrate this project's tests from Jest to Vitest. Read package.json, pnpm-workspace.yaml, tsconfig.json, scripts/jest/jest.config.js, scripts/jest/setup.ts and .github/workflows/build-test.yml first, run the Jest suite once and record the total test count. Write a vitest.config.ts that keeps the two projects, their environments, include patterns, setup file, mock reset settings and coverage includes, and a snapshotFormat that keeps the existing snapshots valid. Replace jest.fn, jest.spyOn, jest.mock, jest.requireActual, the fake timer calls and the Jest mock types with their Vitest equivalents, switch the setup file to @testing-library/jest-dom/vitest, and replace jest with vitest/globals in tsconfig types. Remove Jest, @swc/jest, @types/jest and the Jest environments from package.json and the lockfile, update the test scripts and the CI workflow, and make the suite pass with the same count. Run pnpm lint and pnpm test:type. Open a pull request with the before and after counts and anything that needed a different approach under Vitest.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-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 on its own branch: `vitest.config.ts`, the edited setup file, the test files with `vi` in place of `jest`, `tsconfig.json`, `package.json` and `pnpm-lock.yaml` with Jest gone, and the `Unit Test / Lint` workflow pointing at the new script. `scripts/jest/` is removed. The description gives the Jest total from the first run and the Vitest total from the last, lists every file where a call needed more than a rename, and says whether the snapshots were kept byte for byte.

The transcript keeps the first Jest run, each Vitest run between batches with its failures, and the final run, so a reviewer can see when the count matched and what changed in between. The repository's own workflow runs the new script on the pull request, which is the check that matters for merging.

Before merging, a reviewer looks at three things: that no test was skipped or deleted to reach the count, that `restoreMocks` and the other reset options match the old config so tests do not start leaking state into each other, and that the snapshot files are unchanged or changed for a reason the description gives. One follow-up message to the same session asks for a different coverage provider or a watch script, and the sandbox runs the suite again.

## Run it on every migration issue opened

When a maintainer opens an issue titled for the migration, an automation with a GitHub trigger on the issue event starts this run with the issue text after the prompt. Only work by an owner, member or collaborator of the repository fires it, so an outside account cannot start a run by opening an issue.

The same request can come from your own server with the repository in `resources`. Set a `webhook_url` on the API key and Rush POSTs `run.completed` when the turn finishes, with the `session_url` that holds the pull request and the final test count.

## Why run it on Rush?

A migration like this is a long tail of small edits with a test run after each batch, and the runs are where the time goes. On a laptop the suite competes with the editor and the browser for the afternoon. In a Rush sandbox the agent installs with pnpm once, runs the suite as many times as it takes, and the sandbox parks with its disk and memory when the turn ends, so you can send a follow-up tomorrow without reinstalling.

The pull request comes with proof that the count matched: the first Jest run and the last Vitest run are both in the transcript, with every failing run between them. A reviewer does not have to trust that the migration kept the suite intact, and the project's own workflow runs the new command on the pull request before anyone merges.

## Frequently asked questions

### Do the two Jest projects become two Vitest configs?

No. Vitest's `projects` option takes both in one `vitest.config.ts`, each with its own `environment`, include pattern and setup file, so `TEST_ENV=web` style selection becomes a `--project` flag. The coverage include list moves to the root of the same file.

### What happens to the snapshots?

The agent sets `snapshotFormat` so Vitest prints snapshots the way Jest did, then runs the suite without updating them. A snapshot that still fails is reported in the pull request with the diff rather than overwritten, because an updated snapshot can hide a real rendering change.

### Why does jest.requireActual need more than a rename?

Because `vi.importActual` returns a promise. Each call becomes an `await` inside an async factory passed to `vi.mock`, which is a structural change the agent makes by hand in the files where the grep found it, and names in the description.

### Does it work with pnpm and the workspace's allowBuilds setting?

Yes. The agent installs with the package manager the lockfile belongs to and keeps the lockfile in the same commit. If `pnpm-workspace.yaml` blocks a dependency's install script, the agent runs Vitest once to confirm it starts under that setting and says so in the pull request.

### Will it migrate the Playwright end-to-end tests too?

No. The `e2e` script runs Playwright and is unrelated to Jest; it stays as it is. The migration covers the unit and server tests the Jest config matched.
