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.nodeis>=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.2jest.mock,jest.isolateModulesandjest.resetModulesdo not reach modules Node loads.vitest-native doctorchecks 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 withvitest -u. - Native modules answer as on a device. Under the native engine,
NativeModules[name]andTurboModuleRegistry.get(name)areundefined/nullfor 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 withmockNativeModule(name, impl)fromvitest-native/helpers;nativeModules: 'permissive'restores the earlier behaviour. - Expo modules run as their installed packages. Under the native engine, the
expopreset no longer replacesexpo-constants,expo-status-bar,expo-font,expo-asset,expo-splash-screenandexpo-linking. Their values are jest-expo's:Constants.expoConfigis{},Linking.createURL()needs a scheme mocked intoexpo-constants, andFont.isLoaded()isfalseuntil 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
presetsis refused at startup. List the module that exports it instead:presets: { './test/presets/my-lib.ts': true }. hotRuntime: trueis stricter. Combined with a configured pool,test.isolateor a conflicting CLI flag, it fails at startup withHOT_RUNTIME_OVERRIDDENinstead 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 doctorreports it; tests that load Expo modules can fail there. Upgrade to Expo SDK 57 or 58. - Expo's globals need
expodeclared. The native engine installs Expo's runtime (URL,structuredCloneand the other globals jest-expo installs) only when the project, or a manifest above it, declaresexpoor a package that depends on it, ortransformnames it. A project whereexpomerely resolves, for example beside an Expo app in a hoisted workspace, no longer gets them; declareexpoor add it totransformif 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,isMainThreadisfalse, and the tests'require.cacheand globals are not visible.process.envis shared. A transform slower than 60 s plus 15 s per MB of source fails withTRANSFORM_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:
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 ortest.isolateyourself, 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: 4to 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 initandmigrateend 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-presetdoes not resolve (babel-preset-expono 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
fetchstays Node's. Afetchmocked in a setup file, an MSW server, nock and undici'sMockAgentkeep working whether or not a test importsexpo. - Path aliases resolve. On Vite 8, imports through tsconfig
paths(the template's@/…) resolve as Expo's Metro resolves them; avite-tsconfig-pathsplugin or a hand-written alias for them can go. - expo-image can be imported, and a test that imports
expoorexpo-routerfrom SDK 58 no longer hangs. - expo-video, expo-audio and expo-image unmount cleanly.
globalThis.expo.SharedObjecthas therelease()their cleanup calls. nativeModules: 'permissive'works with Expo.NativeModules.EXDevLauncheris absent, as in Expo Go, so importingexpoorexpo-constantsno longer throws a JSON error.migratetranslates jest-expo presets; see the Expo guide.
Workspace monorepos
- A workspace library reached through a regular-expression
resolve.aliashas 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.setupFilesno longer stops the run withMODULE_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.aliasentry or tsconfigpaths, compiles the installed React Native packages that library depends on. ACannot find module '…/expo-asset/build/Asset.fx'from a transitive Expo package is gone, andtransformentries added only to work around it can be removed. - A project that does not use
exporuns 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*,waitForanduserEventadvancevi.useFakeTimers(). If you calledvi.advanceTimersByTimebefore 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,
getByTextfinds a nested<Text>by its own text.
Closer to Jest and to the device
import.meta.url,filenameanddirnamework in files Node compiles. Otherimport.metaproperties there fail withIMPORT_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 andinstanceofreach both sides, as under Jest. jest.mockreachesrequire(). With jest-compat, ajest.mockfactory applies torequire(), tojest.requireActual's dependencies and to React Native's own requires of a mocked internal.jest.isolateModulesandjest.isolateModulesAsyncwork.- jest-compat follows Jest more closely. A name a
jest.mockfactory leaves out isundefinedinstead of an error,newon ajest.fnwith an arrow implementation works, andprocess.env.JEST_WORKER_IDis set. - tslib and other
__esModuleCommonJS import as under Node. A default import of tslib from a compiled ES module ismodule.exportsinstead ofundefined. - 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 returningnull.
New and changed presets
- Unistyles and Skia are mocked automatically. The new
unistylesandskiapresets are auto-detected. Remove Unistyles'react-native-unistyles/mockssetup file (vitest-native migrateremoves 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
mmkvpreset 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
fsModuleCacheunless 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. Settest.fsModuleCache: falseto 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 withVITEST_TWIN_INSTANCE, naming both. doctorchecks 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.migratereproduces 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: trueoption 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.