Vitest 5.0.0-rc.1, the first release candidate of the next major version of Vue's usual test runner, was published to npm on 11 August. It follows seven betas, the first on 23 April. A release candidate is a build for testing before the stable release, not the stable release itself: Vitest 4.1 still is, and that is what you should run in CI today.
If your Vue project tests with Vitest, two changes will touch the most test files: mocks are cleared before every test, and an async assertion you forget to await now fails the test. You can fix both on 4.1 before you switch. The facts here come from the rc.1 release notes, the beta release notes before it, and the migration guide as it stands at the rc.1 tag.
Check Node 22.12 and Vite 6.4 first
The migration guide opens with prerequisites: Vitest 5 needs Node.js 22.12 or newer and Vite 6.4 or newer. The package's engines field reads ^22.12.0 || ^24.0.0 || >=26.0.0, and its Vite peer range is ^6.4.0 || ^7.0.0 || ^8.0.0. Node 20 is out. A create-vue project on Vite 8 meets the Vite requirement already; check node --version locally and in CI before anything else.
Mocks are cleared before every test
clearMocks is now on by default. Before every test, Vitest calls vi.clearAllMocks(), which empties each mock's recorded calls, instances and results. The guide is explicit that implementations stay intact, so a mockResolvedValue you set up survives. Only the history goes. A test that counted calls made in an earlier test now sees a different number. In this file, UserProfile calls fetchUser once when it mounts and once more when Refresh is clicked:
import { flushPromises, mount } from '@vue/test-utils'
import { expect, it, vi } from 'vitest'
import { fetchUser } from '../../api'
import UserProfile from '../UserProfile.vue'
vi.mock('../../api', () => ({ fetchUser: vi.fn().mockResolvedValue({ name: 'Ada' }) }))
it('loads the user on mount', async () => {
mount(UserProfile)
await flushPromises()
expect(fetchUser).toHaveBeenCalledTimes(1)
})
it('loads again when Refresh is clicked', async () => {
const wrapper = mount(UserProfile)
await flushPromises()
await wrapper.find('button').trigger('click')
expect(fetchUser).toHaveBeenCalledTimes(3)
})
On Vitest 4 the second test passes, because the call from the first test is still on the mock: one from before, plus two of its own. On Vitest 5 the history is cleared before the second test starts, the mock reports two calls, and the assertion fails. The fix is to count only what this test did:
it('loads again when Refresh is clicked', async () => {
const wrapper = mount(UserProfile)
await flushPromises()
await wrapper.find('button').trigger('click')
expect(fetchUser).toHaveBeenCalledTimes(2)
})
This version relies on cleared mocks, so on Vitest 4.1, which keeps the history by default, it sees three calls and fails. To make it pass on both majors, turn the new default on early: set clearMocks: true in the test options of your 4.1 vitest.config.ts, or clear in an afterEach(() => vi.clearAllMocks()) hook. On Vitest 5 that hook becomes redundant and can go.
The guide warns that calls recorded outside the test body are hit hardest: in a setup file, at the top of a module, or in a beforeAll hook. That history is cleared before the test that asserts on it runs. If your suite mounts once in beforeAll and asserts on mocks in several tests, move the mount into each test or into beforeEach.
To keep the old behaviour while you migrate, set clearMocks: false in vitest.config.ts.
Unawaited async assertions fail the test
Assertions that return a promise, such as .resolves, .rejects and toMatchFileSnapshot, used to be awaited for you at the end of the test, with a warning. In rc.1 they fail, and the error points at the assertion you forgot to await. Store actions and composables that return promises are where this shows up in a Vue project:
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, expect, it } from 'vitest'
import { useTodoStore } from '../todos'
beforeEach(() => {
setActivePinia(createPinia())
})
it('rejects an empty title', async () => {
const todos = useTodoStore()
expect(todos.add('')).rejects.toThrow('Title is required')
})
it('rejects an empty title', async () => {
const todos = useTodoStore()
await expect(todos.add('')).rejects.toThrow('Title is required')
})
You can fix every one of these on Vitest 4.1 today. Run your suite and search the output for "was not awaited": 4.1 prints that warning, with a stack trace, for each assertion it had to await for you. Each one will fail on 5, so add the await.
Other breaking changes to search for
The guide lists many more. These are the ones most likely to appear in a Vue project:
vi.mockmust be at the top level. Avi.mock,vi.unmockorvi.hoistedcall inside adescribeor a function used to log a warning; now it throws, listing each offending call and its line.vi.doMockis not hoisted and may still go anywhere.-tmatches the full name joined with>.vitest -t 'TodoList adds'becomesvitest -t 'TodoList > adds', the same string the reporter prints.test.sequentialanddescribe.sequentialare removed. Use{ concurrent: false }in the options instead.expect.pollfails when it times out, even if a late attempt would have passed.toThrow('')matches any message. UsetoThrow(/^$/)to assert an empty one.- Reports and artifacts move into one
.vitestfolder, and thejsonandjunitreporters write files there by default instead of printing to stdout. Scripts that pipe--reporter=jsoninto another tool need thestdoutoption. - Config files are no longer looked up in parent directories. Running
vitestfrom a subfolder needs--config. - Inline projects inherit the root config by default, Vite plugins included, and share its Vite server unless they change Vite options. A
@vitejs/plugin-vuein the root config now reaches every inline project, which is usually what you wanted all along.
Browser Mode changes
If your component tests run in a real browser, three changes in the guide affect them:
- Locators now match text exactly by default: the full string, case-sensitive.
browser.locators.exact: falserestores the old matching. - The
toHaveTextContentassertion now means strict equality. The old partial and regular-expression matching moved to a newtoMatchTextContent. - The WebdriverIO provider has moved to a community-maintained package.
The guide also says that render from vitest-browser-vue will return a promise that you must await. The current vitest-browser-vue release, 2.1, still declares Vitest 4 as its peer, so that change arrives with a new major of the companion package. Browser Mode suites built on it should wait for that release before trying the RC.
Browser Mode gains an experimental Trace View as well. Set browser.traceView: true and Vitest records every action and assertion as a DOM snapshot that you can replay step by step in the browser UI, Vitest UI or the HTML report. With the HTML reporter in CI, a failure then comes with the page as it looked at each step.
New helpers for mocks, time and the CLI
Three smaller additions are useful in Vue projects:
vi.when(spy).calledWith(...).thenResolve(...)gives a mock different behaviour per argument. It replaces a hand-writtenmockImplementationwith aswitchinside.- Fake timers now also fake
Temporalwhen it is available (natively from Node 26, or through a global polyfill), so code that reads the time throughTemporal.Nowfollowsvi.setSystemTime. - The CLI gains
-pas a shorthand for--project, and a--repeatsoption for flushing out flaky tests.
What stays the same
Vue Test Utils 2.4 does not depend on the runner, so mount, trigger, emitted and flushPromises behave as before. The happy-dom and jsdom environments stay, with one refinement from the guide: assigning to a global such as window.innerWidth now updates the underlying DOM implementation, so matchMedia in happy-dom sees the new width.
How to try it
Do it on a branch, after your suite is green on 4.1. Install exact versions: rc.1 declares its companion packages, such as @vitest/coverage-v8, as peers at exactly 5.0.0-rc.1.
git switch -c try-vitest-5
npm install -D vitest@5.0.0-rc.1 @vitest/coverage-v8@5.0.0-rc.1
npx vitest run
Add @vitest/ui@5.0.0-rc.1 if you use the UI. Expect the first run to fail in two groups: call counts (clearMocks) and unawaited assertions. Fix both kinds on the branch, then bring the fixes back to your main branch with clearMocks: true in its config, so the new counts pass on 4.1 too. When 5.0 is stable, those two groups of failures are already fixed. If you find a bug, report it now: that is what a release candidate is for.