Skip to content

vitest-native 0.14 ​

0.14 runs Expo SDK 57 and 58 projects on real React Native with nothing extra installed, makes React Native Testing Library work without jest-compat or globals, runs workspace monorepos as they are, and makes the native engine answer more as a device and Jest do. The package is about 30% smaller. Per-file isolation stays the default; the hot runtime, which reuses workers across test files, is opt-in.

This page covers what changes for a project on 0.13. Every entry, with the detail behind it, is in the changelog.

Before you upgrade ​

  • Node 22.13 or later on Node 22, or Node 24.3 or later. engines.node is >=22.13.0 <23 || >=24.3.0 (it was >=20.19.0). Node 20 reached end of life in April 2026, and on Node 24.0–24.2 jest.mock, jest.isolateModules and jest.resetModules do not reach modules Node loads. vitest-native doctor checks the range.
  • Snapshots that contain an asset change. require('./logo.png') now evaluates to the number React Native's asset registry assigned, as with Metro, instead of the file name. Re-record them with vitest -u.
  • Native modules answer as on a device. Under the native engine, NativeModules[name] and TurboModuleRegistry.get(name) are undefined/null for a module no app registers, and a React Native module's stub has only the methods its codegen spec declares. Register a module a test needs with mockNativeModule(name, impl) from vitest-native/helpers; nativeModules: 'permissive' restores the earlier behaviour.
  • Expo modules run as their installed packages. Under the native engine, the expo preset no longer replaces expo-constants, expo-status-bar, expo-font, expo-asset, expo-splash-screen and expo-linking. Their values are jest-expo's: Constants.expoConfig is {}, Linking.createURL() needs a scheme mocked into expo-constants, and Font.isLoaded() is false until a font loads. presets: { expo: true } keeps the preset while a suite moves off it.
  • Presets of your own are listed by path. A preset object passed to presets is refused at startup. List the module that exports it instead: presets: { './test/presets/my-lib.ts': true }.
  • hotRuntime: true is stricter. Combined with a configured pool, test.isolate or a conflicting CLI flag, it fails at startup with HOT_RUNTIME_OVERRIDDEN instead of warning and overriding.
  • Expo SDK 57 and 58 are the supported SDKs. On another SDK the plugin prints one warning per run naming them, and vitest-native doctor reports it; tests that load Expo modules can fail there. Upgrade to Expo SDK 57 or 58.
  • Expo's globals need expo declared. The native engine installs Expo's runtime (URL, structuredClone and the other globals jest-expo installs) only when the project, or a manifest above it, declares expo or a package that depends on it, or transform names it. A project where expo merely resolves, for example beside an Expo app in a hoisted workspace, no longer gets them; declare expo or add it to transform if it relies on them.
  • Your Babel config runs on its own thread. Under the native engine, Babel runs apart from the tests, as under Jest. A Babel config or plugin sees the Babel worker's process.argv, isMainThread is false, and the tests' require.cache and globals are not visible. process.env is shared. A transform slower than 60 s plus 15 s per MB of source fails with TRANSFORM_FAILED.

The hot runtime is opt-in ​

hotRuntime defaults to false, as in 0.13: under the native engine each test file runs in a fresh worker. The hot runtime reuses workers across test files instead of starting a fresh one per file. Whether that is faster depends on the suite, so measure it on yours:

ts
reactNative({ hotRuntime: 'auto' })
  • When it is active, the startup banner says so: (platform ios, hot runtime). With 'auto', it keeps per-file isolation whenever the run cannot be bounded, or when you set a pool or test.isolate yourself, and prints the reason.
  • Its known differences from per-file isolation are listed in Troubleshooting.
  • It no longer drops test files from a run. A file whose worker broke is reported as failed by name.
  • Hot workers leave Node's compile cache off when they re-evaluate ES modules, so the cache no longer grows with every test file. That costs about 15–20 ms per worker boot; hotRuntime: { esmGeneration: false } keeps the cache on.
  • The memory plan is sized for the workers that actually run, and a worker count you set is limited only by memory. The cap of four applies only when the count is automatic; set maxWorkers: 4 to keep it.
  • Hot workers no longer keep a copy of each CommonJS package for every test file, so memory stays close to flat across a long run (on Node 22.22.3+, 24.12+ and 26).
  • hotRuntime: { mode: 'auto', memoryLimit } keeps 'auto''s fallback to per-file isolation and sets the recycling threshold.
  • A test that times out inside act() retires its worker, so later files start clean instead of failing one after another.
  • The startup log names the worker count and what chose it, for example workers: 4 (automatic cap of 4; 11 available, set maxWorkers to run more; …).
  • vitest-native init and migrate end with a note on how to turn it on.

Expo ​

  • Expo SDK 57 and 58 projects run real React Native with nothing extra installed. When @react-native/babel-preset does not resolve (babel-preset-expo no longer depends on it), the native engine compiles with Expo's preset, as Metro does. reactNative() used the mock engine there before.
  • React Native 0.88 and Expo SDK 58 join the supported matrix, with an SDK 57 consumer alongside the SDK 58 one.
  • Expo's native modules answer as under jest-expo. They come from a package's own mocks/, or else from the table of Expo's native modules that jest-expo ships, carried for SDK 57 and 58. On an Expo SDK 58 app, every probe of the six packages listed above matches jest-expo.
  • Expo's globals are installed before your setup files, and fetch stays Node's. A fetch mocked in a setup file, an MSW server, nock and undici's MockAgent keep working whether or not a test imports expo.
  • Path aliases resolve. On Vite 8, imports through tsconfig paths (the template's @/…) resolve as Expo's Metro resolves them; a vite-tsconfig-paths plugin or a hand-written alias for them can go.
  • expo-image can be imported, and a test that imports expo or expo-router from SDK 58 no longer hangs.
  • expo-video, expo-audio and expo-image unmount cleanly. globalThis.expo.SharedObject has the release() their cleanup calls.
  • nativeModules: 'permissive' works with Expo. NativeModules.EXDevLauncher is absent, as in Expo Go, so importing expo or expo-constants no longer throws a JSON error.
  • migrate translates jest-expo presets; see the Expo guide.

Workspace monorepos ​

  • A workspace library reached through a regular-expression resolve.alias has its React Native dependencies compiled, as one reached through a string alias does.
  • A workspace library's setup file runs. Listing a React Native workspace library's own file in test.setupFiles no longer stops the run with MODULE_OWNER_CONFLICT.
  • A library's installed dependencies are compiled, however it is reached. A package that depends on a workspace library, or imports one through a resolve.alias entry or tsconfig paths, compiles the installed React Native packages that library depends on. A Cannot find module '…/expo-asset/build/Asset.fx' from a transitive Expo package is gone, and transform entries added only to work around it can be removed.
  • A project that does not use expo runs beside one that does. A package next to an Expo app in a hoisted workspace no longer fails every file in setup on SDK 58.
  • A root reached under another spelling of the same directory (a symlink, or a Windows short path) no longer fails with "the project context is invalid".

React Native Testing Library without jest-compat ​

  • findBy*, waitFor and userEvent advance vi.useFakeTimers(). If you called vi.advanceTimersByTime before a query as a workaround, you can drop it.
  • A setup file that calls vi.useFakeTimers() no longer hangs RNTL.
  • RNTL's automatic cleanup runs without test.globals: true. A suite that relied on trees staying mounted between tests imports @testing-library/react-native/pure.
  • With RNTL 12 or 13 under the native engine, getByText finds a nested <Text> by its own text.

Closer to Jest and to the device ​

  • import.meta.url, filename and dirname work in files Node compiles. Other import.meta properties there fail with IMPORT_META_IN_NODE_OWNED, naming the file and the fix.
  • A deep import of a preset package loads that real file, as jest.mock(pkg) leaves subpaths alone under Jest. Import from the package root if a test relied on the mock.
  • A project file imported and required in one test is one module. Under the native engine, with or without jest-compat, vi.spyOn, export assignments and instanceof reach both sides, as under Jest.
  • jest.mock reaches require(). With jest-compat, a jest.mock factory applies to require(), to jest.requireActual's dependencies and to React Native's own requires of a mocked internal. jest.isolateModules and jest.isolateModulesAsync work.
  • jest-compat follows Jest more closely. A name a jest.mock factory leaves out is undefined instead of an error, new on a jest.fn with an arrow implementation works, and process.env.JEST_WORKER_ID is set.
  • tslib and other __esModule CommonJS import as under Node. A default import of tslib from a compiled ES module is module.exports instead of undefined.
  • React Native's own modules keep their strict-mode semantics, as under Node and Jest, and stack frames inside React Native name the real file instead of the precompiled registry.
  • Every extension Metro treats as an asset by default is an asset in tests too, so expo-router's Android toolbar loads.
  • Assets are registered ids, so Image.resolveAssetSource(require('./splash.png')) resolves as on a device instead of returning null.

New and changed presets ​

  • Unistyles and Skia are mocked automatically. The new unistyles and skia presets are auto-detected. Remove Unistyles' react-native-unistyles/mocks setup file (vitest-native migrate removes it) and any hand-written Skia mock. Skia's preset loads CanvasKit before each test file, about 20–30 ms; presets: { skia: false } turns it off.
  • react-native-mmkv 3 and later run as themselves, with their own in-memory backend. The mmkv preset now applies to mmkv 2 only.
  • The startup banner names the presets a run applies, for example mocked: navigation, safeAreaContext, screens.

Faster runs ​

  • Vitest 5 keeps transformed modules on disk. The plugin turns on Vitest's fsModuleCache unless your config sets it, keyed so that upgrading vitest-native or any dependency invalidates it. Warm runs of a 104-file production Expo suite were about 11% faster. Set test.fsModuleCache: false to opt out.
  • A library a worker loads again in each test file is no longer re-resolved and re-read every time, including on Node 26.
  • Under the hot runtime, externalized imports are no longer re-resolved for every test file.
  • Extensionless imports into large directories resolve faster, under both engines: each directory is read once.
  • A smaller install. The package is about 30% smaller and no longer ships CHANGELOG.md.

Tooling and setup ​

  • Coverage works under the native engine. With coverage.provider: 'v8', files Node compiles are reported against their own source lines, and the "Failed to parse … Excluding it from coverage" messages are gone. See Coverage.
  • Vite+ projects start. The Vite and Vitest versions are read from the running instances, so Vite+ no longer fails with UNSUPPORTED_PEER. Two physical copies of Vitest stop the run at startup with VITEST_TWIN_INSTANCE, naming both.
  • doctor checks the Node range, reports the Vite actually in use, warns about Babel plugins a project needs to add, and reports Expo and expo-router setups as covered.
  • migrate reproduces Jest's effective test set and settings, and sorts the project's Babel plugins into required, mapped, not needed and unknown.
  • A copied checkout no longer fails with "more than one copy of React": the copy builds its own precompiled React Native registry on its first run.
  • An experimental metroConfig: true option reads the project's Metro resolution profile (source and asset extensions and their order).

Thank you ​

Thank you to @AntSplash360, @arvidtorbjoernsen, @believer, @doutatsu, @eshvatskyi, @Joozty, @OpenDeernet, @sincraianul and @swey for the issues and pull requests in this release.

Released under the MIT License. · llms.txt