Skip to content

Upgrade Vitest to v4#2519

Draft
Copilot wants to merge 5 commits intomainfrom
copilot/upgrade-vitest-to-v4
Draft

Upgrade Vitest to v4#2519
Copilot wants to merge 5 commits intomainfrom
copilot/upgrade-vitest-to-v4

Conversation

Copy link
Contributor

Copilot AI commented Feb 20, 2026

Upgrades all Vitest-related packages to v4 and fixes breaking changes introduced by the new version.

Package upgrades

  • vitest, @vitest/coverage-v8, @vitest/ui, @vitest/utils: ^3.x^4.0.18 across all workspace packages
  • vitest-when: ^0.6.0^0.10.0
  • @vitest/eslint-plugin: ^1.2.1^1.6.9
  • vite-config peer dependencies updated to ^4.0.18
  • react-intersection-observer: ^9.16.0^10.0.3

Breaking change fixes

Vitest 4 now enforces that vi.fn() implementations must use function/class syntax (not arrow functions) when the mock is called as a constructor via new.

UploadToS3.test.ts

Changed arrow function implementations to regular functions in vi.fn().mockImplementation():

// Before
Upload: vi.fn().mockImplementation(() => ({ on: vi.fn(), done: vi.fn() }))

// After
Upload: vi.fn().mockImplementation(function () { return { on: vi.fn(), done: vi.fn() } })

react-intersection-observer upgrade

react-intersection-observer/test-utils v9 passed an arrow function to vi.fn() for the IntersectionObserver mock, which Vitest 4 would reject when used as a constructor. Upgrading to v10 resolves this — v10's test-utils uses a named regular function (function IntersectionObserverMock(...)) that is compatible with Vitest 4's constructor requirements.

Original prompt

This section details on the original issue you should resolve

<issue_title>Upgrade Vitest to v4</issue_title>
<issue_description>- Vitest and vitest-related packages should be upgraded to the latest version

  • Review the migration guide for upgrading from Vitest 3 to Vitest 4
  • Fix any new test errors</issue_description>

Comments on the Issue (you are @copilot in this section)


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Co-authored-by: nickgros <17580037+nickgros@users.noreply.github.com>
Copilot AI changed the title [WIP] Upgrade Vitest and related packages to v4 Upgrade Vitest to v4 Feb 20, 2026
Copilot AI requested a review from nickgros February 20, 2026 16:51
@nickgros
Copy link
Collaborator

@copilot

  • You should be able to upgrade react-intersection-observer to the latest version and remove the _origViFn workaround.
  • Ensure you run the linter (eslint --fix) and code formatter on your changes
  • Below is the migration guide copied verbatim:
Migrating to Vitest 4.0

Migrating to Vitest 4.0 {#vitest-4}

V8 Code Coverage Major Changes

Vitest's V8 code coverage provider is now using more accurate coverage result remapping logic.
It is expected for users to see changes in their coverage reports when updating from Vitest v3.

In the past Vitest used v8-to-istanbul for remapping V8 coverage results into your source files.
This method wasn't very accurate and provided plenty of false positives in the coverage reports.
We've now developed a new package that utilizes AST based analysis for the V8 coverage.
This allows V8 reports to be as accurate as @vitest/coverage-istanbul reports.

  • Coverage ignore hints have updated. See Coverage | Ignoring Code.
  • coverage.ignoreEmptyLines is removed. Lines without runtime code are no longer included in reports.
  • coverage.experimentalAstAwareRemapping is removed. This option is now enabled by default, and is the only supported remapping method.
  • coverage.ignoreClassMethods is now supported by V8 provider too.

Removed Options coverage.all and coverage.extensions

In previous versions Vitest included all uncovered files in coverage report by default.
This was due to coverage.all defaulting to true, and coverage.include defaulting to **.
These default values were chosen for a good reason - it is impossible for testing tools to guess where users are storing their source files.

This ended up having Vitest's coverage providers processing unexpected files, like minified Javascript, leading to slow/stuck coverage report generations.
In Vitest v4 we have removed coverage.all completely and defaulted to include only covered files in the report.

When upgrading to v4 it is recommended to define coverage.include in your configuration, and then start applying simple coverage.exclude patterns if needed.

export default defineConfig({
  test: {
    coverage: {
      // Include covered and uncovered files matching this pattern:
      include: ['packages/**/src/**.{js,jsx,ts,tsx}'], // [!code ++]

      // Exclusion is applied for the files that match include pattern above
      // No need to define root level *.config.ts files or node_modules, as we didn't add those in include
      exclude: ['**/some-pattern/**'], // [!code ++]

      // These options are removed now
      all: true, // [!code --]
      extensions: ['js', 'ts'], // [!code --]
    }
  }
})

If coverage.include is not defined, coverage report will include only files that were loaded during test run:

export default defineConfig({
  test: {
    coverage: {
      // Include not set, include only files that are loaded during test run
      include: undefined, // [!code ++]

      // Loaded files that match this pattern will be excluded:
      exclude: ['**/some-pattern/**'], // [!code ++]
    }
  }
})

See also new guides:

Simplified exclude

By default, Vitest now only excludes tests from node_modules and .git folders. This means that Vitest no longer excludes:

  • dist and cypress folders
  • .idea, .cache, .output, .temp folders
  • config files like rollup.config.js, prettier.config.js, ava.config.js and so on

If you need to limit the directory where your tests files are located, use the test.dir option instead because it is more performant than excluding files:

import { configDefaults, defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    dir: './frontend/tests', // [!code ++]
  },
})

To restore the previous behaviour, specify old excludes manually:

import { configDefaults, defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    exclude: [
      ...configDefaults.exclude,
      '**/dist/**', // [!code ++]
      '**/cypress/**', // [!code ++]
      '**/.{idea,git,cache,output,temp}/**', // [!code ++]
      '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build,eslint,prettier}.config.*' // [!code ++]
    ],
  },
})

spyOn and fn Support Constructors

Previously, if you tried to spy on a constructor with vi.spyOn, you would get an error like Constructor <name> requires 'new'. Since Vitest 4, all mocks called with a new keyword construct the instance instead of calling mock.apply. This means that the mock implementation has to use either the function or the class keyword in these cases:

const cart = {
  Apples: class Apples {
    getApples() {
      return 42
    }
  }
}

const Spy = vi.spyOn(cart, 'Apples')
  .mockImplementation(() => ({ getApples: () => 0 })) // [!code --]
  // with a function keyword
  .mockImplementation(function () {
    this.getApples = () => 0
  })
  // with a custom class
  .mockImplementation(class MockApples {
    getApples() {
      return 0
    }
  })

const mock = new Spy()

Note that now if you provide an arrow function, you will get <anonymous> is not a constructor error when the mock is called.

Changes to Mocking

Alongside new features like supporting constructors, Vitest 4 creates mocks differently to address several module mocking issues that we received over the years. This release attempts to make module spies less confusing, especially when working with classes.

  • vi.fn().getMockName() now returns vi.fn() by default instead of spy. This can affect snapshots with mocks - the name will be changed from [MockFunction spy] to [MockFunction]. Spies created with vi.spyOn will keep using the original name by default for better debugging experience
  • vi.restoreAllMocks no longer resets the state of spies and only restores spies created manually with vi.spyOn, automocks are no longer affected by this function (this also affects the config option restoreMocks). Note that .mockRestore will still reset the mock implementation and clear the state
  • Calling vi.spyOn on a mock now returns the same mock
  • mock.settledResults are now populated immediately on function invocation with an 'incomplete' result. When the promise is finished, the type is changed according to the result.
  • Automocked instance methods are now properly isolated, but share a state with the prototype. Overriding the prototype implementation will always affect instance methods unless the methods have a custom mock implementation of their own. Calling .mockReset on the mock also no longer breaks that inheritance.
import { AutoMockedClass } from './example.js'
const instance1 = new AutoMockedClass()
const instance2 = new AutoMockedClass()

instance1.method.mockReturnValue(42)

expect(instance1.method()).toBe(42)
expect(instance2.method()).toBe(undefined)

expect(AutoMockedClass.prototype.method).toHaveBeenCalledTimes(2)

instance1.method.mockReset()
AutoMockedClass.prototype.method.mockReturnValue(100)

expect(instance1.method()).toBe(100)
expect(instance2.method()).toBe(100)

expect(AutoMockedClass.prototype.method).toHaveBeenCalledTimes(4)
  • Automocked methods can no longer be restored, even with a manual .mockRestore. Automocked modules with spy: true will keep working as before
  • Automocked getters no longer call the original getter. By default, automocked getters now return undefined. You can keep using vi.spyOn(object, name, 'get') to spy on a getter and change its implementation
  • The mock vi.fn(implementation).mockReset() now correctly returns the mock implementation in .getMockImplementation()
  • vi.fn().mock.invocationCallOrder now starts with 1, like Jest does, instead of 0

Standalone Mode with Filename Filter

To improve user experience, Vitest will now start running the matched files when --standalone is used with filename filter.

# In Vitest v3 and below this command would ignore "math.test.ts" filename filter.
# In Vitest v4 the math.test.ts will run automatically.
$ vitest --standalone math.test.ts

This allows users to create re-usable package.json scripts for standalone mode.

::: code-group

{
  "scripts": {
    "test:dev": "vitest --standalone"
  }
}
# Start Vitest in standalone mode, without running any files on start
$ pnpm run test:dev

# Run math.test.ts immediately
$ pnpm run test:dev math.test.ts

:::

Replacing vite-node with Module Runner

Module Runner is a successor to vite-node implemented directly in Vite. Vitest now uses it directly instead of having a wrapper around Vite SSR handler. This means that certain features are no longer available:

  • VITE_NODE_DEPS_MODULE_DIRECTORIES environment variable was replaced with VITEST_MODULE_DIRECTORIES
  • Vitest no longer injects __vitest_executor into every test runner. Instead, it injects moduleRunner which is an instance of ModuleRunner
  • vitest/execute entry point was removed. It was always meant to be internal
  • Custom environments no longer need to provide a transformMode property. Instead, provide viteEnvironment. If it is not provided, Vitest will use the environment name to transform files on the server (see server.environments)
  • vite-node is no longer a dependency of Vitest
  • deps.optimizer.web was renamed to deps.optimizer.client. You can also use any custom names to apply optimizer configs when using other server environments

Vite has its own externalization mechanism, but we decided to keep using the old one to reduce the amount of breaking changes. You can keep using server.deps to inline or externalize packages.

This update should not be noticeable unless you rely on advanced features mentioned above.

workspace is Replaced with projects

The workspace configuration option was renamed to projects in Vitest 3.2. They are functionally the same, except you cannot specify another file as the source of your workspace (previously you could specify a file that would export an array of projects). Migrating to projects is easy, just move the code from vitest.workspace.js to vitest.config.ts:

::: code-group

import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    workspace: './vitest.workspace.js', // [!code --]
    projects: [ // [!code ++]
      './packages/*', // [!code ++]
      { // [!code ++]
        test: { // [!code ++]
          name: 'unit', // [!code ++]
        }, // [!code ++]
      }, // [!code ++]
    ] // [!code ++]
  }
})
import { defineWorkspace } from 'vitest/config' // [!code --]

export default defineWorkspace([ // [!code --]
  './packages/*', // [!code --]
  { // [!code --]
    test: { // [!code --]
      name: 'unit', // [!code --]
    }, // [!code --]
  } // [!code --]
]) // [!code --]

:::

Browser Provider Rework

In Vitest 4.0, the browser provider now accepts an object instead of a string ('playwright', 'webdriverio'). The preview is no longer a default. This makes it simpler to work with custom options and doesn't require adding /// <reference comments anymore.

import { playwright } from '@vitest/browser-playwright' // [!code ++]

export default defineConfig({
  test: {
    browser: {
      provider: 'playwright', // [!code --]
      provider: playwright({ // [!code ++]
        launchOptions: { // [!code ++]
          slowMo: 100, // [!code ++]
        }, // [!code ++]
      }), // [!code ++]
      instances: [
        {
          browser: 'chromium',
          launch: { // [!code --]
            slowMo: 100, // [!code --]
          }, // [!code --]
        },
      ],
    },
  },
})

The naming of properties in playwright factory now also aligns with Playwright documentation making it easier to find.

With this change, the @vitest/browser package is no longer needed, and you can remove it from your dependencies. To support the context import, you should update the @vitest/browser/context to vitest/browser:

import { page } from '@vitest/browser/context' // [!code --]
import { page } from 'vitest/browser' // [!code ++]

test('example', async () => {
  await page.getByRole('button').click()
})

The modules are identical, so doing a simple "Find and Replace" should be sufficient.

If you were using the @vitest/browser/utils module, you can now import those utilities from vitest/browser as well:

import { getElementError } from '@vitest/browser/utils' // [!code --]
import { utils } from 'vitest/browser' // [!code ++]
const { getElementError } = utils // [!code ++]

::: warning
Both @vitest/browser/context and @vitest/browser/utils work at runtime during the transition period, but they will be removed in a future release.
:::

Pool Rework

Vitest has used tinypool for orchestrating how test files are run in the test runner workers. Tinypool has controlled how complex tasks like parallelism, isolation and IPC communication works internally. However we've found that Tinypool has some flaws that are slowing down development of Vitest. In Vitest v4 we've completely removed Tinypool and rewritten how pools work without new dependencies. Read more about reasoning from feat!: rewrite pools without tinypool #8705
.

New pool architecture allows Vitest to simplify many previously complex configuration options:

  • maxThreads and maxForks are now maxWorkers.
  • Environment variables VITEST_MAX_THREADS and VITEST_MAX_FORKS are now VITEST_MAX_WORKERS.
  • singleThread and singleFork are now maxWorkers: 1, isolate: false. If your tests were relying on module reset between tests, you'll need to add setupFile that calls vi.resetModules() in beforeAll test hook.
  • poolOptions is removed. All previous poolOptions are now top-level options. The memoryLimit of VM pools is renamed to vmMemoryLimit.
  • threads.useAtomics is removed. If you have a use case for this, feel free to open a new feature request.
  • Custom pool interface has been rewritten, see Custom Pool
export default defineConfig({
  test: {
    poolOptions: { // [!code --]
      forks: { // [!code --]
        execArgv: ['--expose-gc'], // [!code --]
        isolate: false, // [!code --]
        singleFork: true, // [!code --]
      }, // [!code --]
      vmThreads: { // [!code --]
        memoryLimit: '300Mb' // [!code --]
      }, // [!code --]
    }, // [!code --]
    execArgv: ['--expose-gc'], // [!code ++]
    isolate: false, // [!code ++]
    maxWorkers: 1, // [!code ++]
    vmMemoryLimit: '300Mb', // [!code ++]
  }
})

Previously it was not possible to specify some pool related options per project when using Vitest Projects. With the new architecture this is no longer a blocker.

::: code-group

import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    projects: [
      {
        // Non-isolated unit tests
        name: 'Unit tests',
        isolate: false,
        exclude: ['**.integration.test.ts'],
      },
      {
        // Isolated integration tests
        name: 'Integration tests',
        include: ['**.integration.test.ts'],
      },
    ],
  },
})
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    projects: [
      {
        name: 'Parallel',
        exclude: ['**.sequential.test.ts'],
      },
      {
        name: 'Sequential',
        include: ['**.sequential.test.ts'],
        fileParallelism: false,
      },
    ],
  },
})
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    projects: [
      {
        name: 'Production env',
        execArgv: ['--env-file=.env.prod']
      },
      {
        name: 'Staging env',
        execArgv: ['--env-file=.env.staging']
      },
    ],
  },
})

:::

See Recipes for more examples.

Reporter Updates

Reporter APIs onCollected, onSpecsCollected, onPathsCollected, onTaskUpdate and onFinished were removed. See Reporters API for new alternatives. The new APIs were introduced in Vitest v3.0.0.

The basic reporter was removed as it is equal to:

export default defineConfig({
  test: {
    reporters: [
      ['default', { summary: false }]
    ]
  }
})

The verbose reporter now prints test cases as a flat list. To revert to the previous behaviour, use --reporter=tree:

export default defineConfig({
  test: {
    reporters: ['verbose'], // [!code --]
    reporters: ['tree'], // [!code ++]
  }
})

Snapshots using Custom Elements Print the Shadow Root

In Vitest 4.0 snapshots that include custom elements will print the shadow root contents. To restore the previous behavior, set the printShadowRoot option to false.

// before Vitest 4.0
exports[`custom element with shadow root 1`] = `
"<body>
  <div>
    <custom-element />
  </div>
</body>"
`

// after Vitest 4.0
exports[`custom element with shadow root 1`] = `
"<body>
  <div>
    <custom-element>
      #shadow-root
        <span
          class="some-name"
          data-test-id="33"
          id="5"
        >
          hello
        </span>
    </custom-element>
  </div>
</body>"
`

Deprecated APIs are Removed

Vitest 4.0 removes some deprecated APIs, including:

  • poolMatchGlobs config option. Use projects instead.
  • environmentMatchGlobs config option. Use projects instead.
  • deps.external, deps.inline, deps.fallbackCJS config options. Use server.deps.external, server.deps.inline, or server.deps.fallbackCJS instead.
  • browser.testerScripts config option. Use browser.testerHtmlPath instead.
  • minWorkers config option. Only maxWorkers has any effect on how tests are running, so we are removing this public option.
  • Vitest no longer supports providing test options object as a third argument to test and describe. Use the second argument instead:
test('example', () => { /* ... */ }, { retry: 2 }) // [!code --]
test('example', { retry: 2 }, () => { /* ... */ }) // [!code ++]

Note that providing a timeout number as the last argument is still supported:

test('example', () => { /* ... */ }, 1000) // ✅

This release also removes all deprecated types. This finally fixes an issue where Vitest accidentally pulled in @types/node (see #5481 and #6141).

Co-authored-by: nickgros <17580037+nickgros@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Feb 21, 2026

@copilot

  • You should be able to upgrade react-intersection-observer to the latest version and remove the _origViFn workaround.
    ...

Done in cd96f5e. Upgraded react-intersection-observer to ^10.0.3 — v10's test-utils now uses a named regular function (function IntersectionObserverMock(...)) instead of an arrow function, making it compatible with Vitest 4's constructor requirements. This removes the need for the _origViFn workaround entirely.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Upgrade Vitest to v4

2 participants