diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 9204d25b1488..2d0c33562a8b 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -8,6 +8,7 @@ - [ ] It's really useful if your PR references an issue where it is discussed ahead of time. If the feature is substantial or introduces breaking changes without a discussion, PR might be closed. - [ ] Ideally, include a test that fails without this PR but passes with it. - [ ] Please, don't make changes to `pnpm-lock.yaml` unless you introduce a new test example. +- [ ] Please check [Allow edits by maintainers](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork) to make review process faster. Note that this option is not available for repositories that are owned by Github organizations. ### Tests - [ ] Run the tests with `pnpm test:ci`. diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 000000000000..6ee552b62012 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,11 @@ +# copilot-instructions.md + +This file provides guidance to Copilot Agent when working with code in this repository. + +## Codebase Overview + +Vitest is a next-generation testing framework powered by Vite. This is a monorepo using pnpm workspaces. + +## Essential references + +- Agent-specific guide: See [AGENTS.md](../AGENTS.md) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 41b2494af0f8..3aa5acfcdc21 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,7 +27,7 @@ jobs: runs-on: ubuntu-latest name: 'Lint: node-latest, ubuntu-latest' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: ./.github/actions/setup-and-cache @@ -60,7 +60,7 @@ jobs: should_skip: ${{ steps.changed-files.outputs.only_changed == 'true' }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Get changed files id: changed-files @@ -83,7 +83,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - node_version: [18, 20, 22, 24] + node_version: [20, 22, 24] include: - os: macos-latest node_version: 20 @@ -92,13 +92,13 @@ jobs: fail-fast: false steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: ./.github/actions/setup-and-cache with: node-version: ${{ matrix.node_version }} - - uses: browser-actions/setup-chrome@c785b87e244131f27c9f19c1a33e2ead956ab7ce # v1.7.3 + - uses: browser-actions/setup-chrome@b94431e051d1c52dcbe9a7092a4f10f827795416 # v2.1.0 - name: Install run: pnpm i @@ -118,6 +118,13 @@ jobs: - name: Unit Test UI run: pnpm run -C packages/ui test:ui + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: test/ui/test-results/ + retention-days: 30 + test-browser: needs: changed name: 'Browsers: node-20, ${{ matrix.os }}' @@ -134,14 +141,14 @@ jobs: timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: ./.github/actions/setup-and-cache with: node-version: 20 - - uses: browser-actions/setup-chrome@c785b87e244131f27c9f19c1a33e2ead956ab7ce # v1.7.3 - - uses: browser-actions/setup-firefox@634a60ccd6599686158cf5a570481b4cd30455a2 # v1.5.4 + - uses: browser-actions/setup-chrome@b94431e051d1c52dcbe9a7092a4f10f827795416 # v2.1.0 + - uses: browser-actions/setup-firefox@5914774dda97099441f02628f8d46411fcfbd208 # v1.7.0 - name: Install run: pnpm i @@ -161,20 +168,20 @@ jobs: test-rolldown: needs: changed # macos-latest is the fastes one - name: 'Rolldown&Test: node-20, macos-latest' + name: 'Rolldown&Test: node-22, macos-latest' if: needs.changed.outputs.should_skip != 'true' runs-on: macos-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: ./.github/actions/setup-and-cache with: - node-version: 20 + node-version: 22 - - uses: browser-actions/setup-chrome@c785b87e244131f27c9f19c1a33e2ead956ab7ce # v1.7.3 + - uses: browser-actions/setup-chrome@b94431e051d1c52dcbe9a7092a4f10f827795416 # v2.1.0 - name: Install run: pnpm add vite@npm:rolldown-vite && git add . && git commit -m "ci" && pnpm i --prefer-offline --no-frozen-lockfile @@ -193,3 +200,10 @@ jobs: - name: Test Browser (playwright) run: pnpm run test:browser:playwright + + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report-rolldown + path: test/ui/test-results/ + retention-days: 30 diff --git a/.github/workflows/cr.yml b/.github/workflows/cr.yml index 216e55fee15e..7e46394591fc 100644 --- a/.github/workflows/cr.yml +++ b/.github/workflows/cr.yml @@ -19,7 +19,7 @@ jobs: name: 'Release: pkg.pr.new' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 diff --git a/.github/workflows/issue-close-require.yml b/.github/workflows/issue-close-require.yml index 6187c4a42cf2..f5c07cdbde41 100644 --- a/.github/workflows/issue-close-require.yml +++ b/.github/workflows/issue-close-require.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - name: needs reproduction - uses: actions-cool/issues-helper@a610082f8ac0cf03e357eb8dd0d5e2ba075e017e # v3.6.0 + uses: actions-cool/issues-helper@45d75b6cf72bf4f254be6230cb887ad002702491 # v3.6.3 with: actions: close-issues token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/issue-labeled.yml b/.github/workflows/issue-labeled.yml index b17fa43afb9c..b8e17de42e8d 100644 --- a/.github/workflows/issue-labeled.yml +++ b/.github/workflows/issue-labeled.yml @@ -10,7 +10,7 @@ jobs: steps: - name: needs reproduction if: github.event.label.name == 'needs reproduction' - uses: actions-cool/issues-helper@a610082f8ac0cf03e357eb8dd0d5e2ba075e017e # v3.6.0 + uses: actions-cool/issues-helper@45d75b6cf72bf4f254be6230cb887ad002702491 # v3.6.3 with: actions: create-comment token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 1a811a357c62..d76a9a708b53 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest environment: Release steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 @@ -42,9 +42,7 @@ jobs: run: pnpm build - name: Publish to npm - run: pnpm run publish-ci ${{ github.ref_name }} - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: npm i -g npm@^11.5.2 && pnpm run publish-ci ${{ github.ref_name }} - name: Generate Changelog run: npx changelogithub diff --git a/.gitignore b/.gitignore index 98d5c9045315..8ca3bb42743f 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,5 @@ test/cli/fixtures/browser-multiple/basic-* # exclude static html reporter folder test/browser/html/ test/core/html/ -.vitest-attachments \ No newline at end of file +.vitest-attachments +explainFiles.txt \ No newline at end of file diff --git a/.npmrc b/.npmrc index 80bce8dff8bd..ec3a0020dde2 100644 --- a/.npmrc +++ b/.npmrc @@ -3,4 +3,4 @@ strict-peer-dependencies=false provenance=true shell-emulator=true registry=https://registry.npmjs.org/ -VITE_NODE_DEPS_MODULE_DIRECTORIES=/node_modules/,/packages/ +VITEST_MODULE_DIRECTORIES=/node_modules/,/packages/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000000..790f2c23a470 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,144 @@ +# Vitest AI Agent Guide + +This document provides comprehensive information for AI agents working on the Vitest codebase. + +## Project Overview + +Vitest is a next-generation testing framework powered by Vite. This is a monorepo using pnpm workspaces with the following key characteristics: + +- **Language**: TypeScript/JavaScript (ESM-first) +- **Package Manager**: pnpm (required) +- **Node Version**: ^18.0.0 || >=20.0.0 +- **Build System**: Vite + Rollup +- **Monorepo Structure**: 15+ packages in `packages/` directory + +## Setup and Development + +### Initial Setup +1. Run `pnpm install` to install dependencies +2. Run `pnpm build` to build all packages +3. Install Playwright browsers when working with browser features: `npx playwright install --with-deps` + +### Key Scripts +- `pnpm build` - Build all packages +- `pnpm dev` - Watch mode for development +- `pnpm lint` - Run ESLint +- `pnpm lint:fix` - Fix linting issues automatically +- `pnpm typecheck` - Run TypeScript type checking + +## Testing + +### Running Tests +- **All tests**: `CI=true pnpm test:ci` +- **Examples**: `CI=true pnpm test:examples` +- **Specific test suite**: `CI=true cd test/ && pnpm test ` +- **Core directory test**: `CI=true pnpm test ` (for `test/core`) +- **Browser tests**: `CI=true pnpm test:browser:playwright` or `CI=true pnpm test:browser:webdriverio` + +### Testing Utilities +- **`runInlineTests`** from `test/test-utils/index.ts` - You must use this for complex file system setups (>1 file) +- **`runVitest`** from `test/test-utils/index.ts` - You can use this to run Vitest programmatically +- **No mocking policy** - You must never mock anything in tests + +## Project Structure + +### Core Packages (`packages/`) +- `vitest` - Main testing framework +- `vite-node` - Vite SSR runtime +- `browser` - Browser testing support +- `ui` - Web UI for test results +- `runner` - Test runner core +- `expect` - Assertion library +- `spy` - Mocking and spying utilities +- `snapshot` - Snapshot testing +- `coverage-v8` / `coverage-istanbul` - Code coverage +- `utils` - Shared utilities +- `mocker` - Module mocking + +### Test Organization (`test/`) +- `test/core` - Core functionality tests +- `test/browser` - Browser-specific tests +- Various test suites organized by feature + +### Important Directories +- `docs/` - Documentation (Vite-powered) +- `examples/` - Example projects and integrations +- `scripts/` - Build and development scripts +- `.github/` - GitHub Actions workflows +- `patches/` - Package patches via pnpm + +## Code Style and Conventions + +### Formatting and Linting +- **Always run** `pnpm lint:fix` after making changes +- Fix non-auto-fixable errors manually + +### TypeScript +- Strict TypeScript configuration +- Use `pnpm typecheck` to verify types +- Configuration files: `tsconfig.base.json`, `tsconfig.build.json`, `tsconfig.check.json` + +### Code Quality +- ESM-first approach +- Follow existing patterns in the codebase +- Use utilities from `@vitest/utils/*` when available. Never import from `@vitest/utils` main entry point directly. + +## Common Workflows + +### Adding New Features +1. Identify the appropriate package in `packages/` +2. Follow existing code patterns +3. Add tests using testing utilities +4. Run `pnpm build && pnpm typecheck && pnpm lint:fix` +5. Add tests with relevant test suites + +### Debugging +- Use VS Code: `⇧⌘B` (Shift+Cmd+B) or `Ctrl+Shift+B` for dev tasks +- Check `scripts/` directory for specialized development tools + +### Documentation +- Main docs in `docs/` directory +- Built with `pnpm docs:build` +- Local dev server: `pnpm docs` + +## Dependencies and Tools + +### Key Dependencies +- **Vite** - Build tool and dev server +- **Rollup** - Bundler +- **ESLint** - Linting +- **TypeScript** - Type checking +- **Playwright** - Browser testing +- **Chai/Expect** - Assertions +- **Tinypool** - Worker threading +- **Tinybench** - Benchmarking + +### Development Tools +- **tsx** - TypeScript execution +- **ni/nr** - Package manager abstraction +- **bumpp** - Version bumping +- **changelogithub** - Changelog generation + +## Browser Testing +- Two modes: Playwright and WebDriverIO +- Separate test commands for each +- Component testing supported (Vue, React, Svelte, Lit, Marko) + +## Performance Considerations +- This is a performance-critical testing framework +- Pay attention to import costs and bundle size +- Use lazy loading where appropriate +- Consider worker thread implications + +## Troubleshooting + +### Common Issues +- Ensure pnpm is used (not npm/yarn) +- Build before running tests +- Check Node.js version compatibility +- Playwright browsers must be installed for browser tests + +### Getting Help +- Check existing issues and documentation +- Review CONTRIBUTING.md for detailed guidelines +- Follow patterns in existing code diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000000..ca089232890b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,11 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Codebase Overview + +Vitest is a next-generation testing framework powered by Vite. This is a monorepo using pnpm workspaces. + +## Essential references + +- Agent-specific guide: See [AGENTS.md](AGENTS.md) diff --git a/LICENSE b/LICENSE index 5ae481fdb8ea..0e5771dddf4e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021-Present Vitest Team +Copyright (c) 2021-Present VoidZero Inc. and Vitest contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 711aa7a6f6ef..bc35688a76b8 100644 --- a/README.md +++ b/README.md @@ -114,8 +114,9 @@ Thanks to: - [@patak-dev](https://github.com/patak-dev) for the awesome package name! ## Contribution + See [Contributing Guide](https://github.com/vitest-dev/vitest/blob/main/CONTRIBUTING.md). ## License -[MIT](./LICENSE) License © 2021-Present [Anthony Fu](https://github.com/antfu), [Matias Capeletto](https://github.com/patak-dev) +[MIT](./LICENSE) License © 2021-Present VoidZero Inc. and Vitest contributors diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index c08fbe927cc1..c693870c37ed 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -8,6 +8,7 @@ import { groupIconMdPlugin, groupIconVitePlugin, } from 'vitepress-plugin-group-icons' +import llmstxt from 'vitepress-plugin-llms' import { version } from '../../package.json' import { teamMembers } from './contributors' import { @@ -70,15 +71,18 @@ export default ({ mode }: { mode: string }) => { customIcon: { 'CLI': 'vscode-icons:file-type-shell', 'vitest.shims': 'vscode-icons:file-type-vitest', - 'vitest.workspace': 'vscode-icons:file-type-vitest', 'vitest.config': 'vscode-icons:file-type-vitest', + 'vitest.workspace': 'vscode-icons:file-type-vitest', '.spec.ts': 'vscode-icons:file-type-testts', '.test.ts': 'vscode-icons:file-type-testts', '.spec.js': 'vscode-icons:file-type-testjs', '.test.js': 'vscode-icons:file-type-testjs', 'marko': 'vscode-icons:file-type-marko', + 'qwik': 'logos:qwik-icon', + 'next': '', }, }), + llmstxt(), ], }, markdown: { @@ -140,7 +144,7 @@ export default ({ mode }: { mode: string }) => { footer: { message: 'Released under the MIT License.', - copyright: 'Copyright © 2021-PRESENT Anthony Fu, Matías Capeletto and Vitest contributors', + copyright: 'Copyright © 2021-PRESENT VoidZero Inc. and Vitest contributors', }, nav: [ @@ -291,6 +295,11 @@ export default ({ mode }: { mode: string }) => { link: '/guide/browser/multiple-setups', docFooterText: 'Multiple Setups | Browser Mode', }, + { + text: 'Visual Regression Testing', + link: '/guide/browser/visual-regression-testing', + docFooterText: 'Visual Regression Testing | Browser Mode', + }, ], }, { @@ -495,6 +504,41 @@ function guide(): DefaultTheme.SidebarItem[] { { text: 'Mocking', link: '/guide/mocking', + collapsed: true, + items: [ + { + text: 'Mocking Dates', + link: '/guide/mocking/dates', + }, + { + text: 'Mocking Functions', + link: '/guide/mocking/functions', + }, + { + text: 'Mocking Globals', + link: '/guide/mocking/globals', + }, + { + text: 'Mocking Modules', + link: '/guide/mocking/modules', + }, + { + text: 'Mocking the File System', + link: '/guide/mocking/file-system', + }, + { + text: 'Mocking Requests', + link: '/guide/mocking/requests', + }, + { + text: 'Mocking Timers', + link: '/guide/mocking/timers', + }, + { + text: 'Mocking Classes', + link: '/guide/mocking/classes', + }, + ], }, { text: 'Parallelism', @@ -546,8 +590,8 @@ function guide(): DefaultTheme.SidebarItem[] { collapsed: false, items: [ { - text: 'Migrating to Vitest 3.0', - link: '/guide/migration#vitest-3', + text: 'Migrating to Vitest 4.0', + link: '/guide/migration#vitest-4', }, { text: 'Migrating from Jest', @@ -579,7 +623,7 @@ function api(): DefaultTheme.SidebarItem[] { link: '/api/', }, { - text: 'Mock Functions', + text: 'Mocks', link: '/api/mock', }, { diff --git a/docs/.vitepress/sponsors.ts b/docs/.vitepress/sponsors.ts index c02c3317317e..7bf17112490e 100644 --- a/docs/.vitepress/sponsors.ts +++ b/docs/.vitepress/sponsors.ts @@ -5,12 +5,14 @@ interface Sponsor { } const vitestSponsors = { - special: [ + provided: [ { name: 'VoidZero', url: 'https://voidzero.dev', img: '/voidzero.svg', }, + ], + special: [ { name: 'NuxtLabs', url: 'https://nuxtlabs.com', @@ -27,33 +29,47 @@ const vitestSponsors = { img: '/zammad.svg', }, ], - platinum: [ - { - name: 'Bit', - url: 'https://bit.dev', - img: '/bit.svg', - }, - ], + // platinum: [], gold: [ { name: 'vital', url: 'https://vital.io/', img: '/vital.svg', }, + { + name: 'OOMOL', + url: 'https://oomol.com/', + img: '/oomol.svg', + }, + { + name: 'Mailmeteor', + url: 'https://mailmeteor.com/', + img: '/mailmeteor.svg', + }, + { + name: 'Liminity', + url: 'https://www.liminity.se/', + img: '/liminity.svg', + }, ], } satisfies Record export const sponsors = [ { - tier: 'Special Sponsors', + tier: 'Brought to you by', size: 'big', - items: vitestSponsors.special, + items: vitestSponsors.provided, }, { - tier: 'Platinum Sponsors', + tier: 'Special Sponsors', size: 'big', - items: vitestSponsors.platinum, + items: vitestSponsors.special, }, + // { + // tier: 'Platinum Sponsors', + // size: 'big', + // items: vitestSponsors.platinum, + // }, { tier: 'Gold Sponsors', size: 'medium', diff --git a/docs/advanced/api/reporters.md b/docs/advanced/api/reporters.md index 2925fc255227..552f2fd87ffa 100644 --- a/docs/advanced/api/reporters.md +++ b/docs/advanced/api/reporters.md @@ -25,6 +25,7 @@ Vitest has its own test run lifecycle. These are represented by reporter's metho - [`onHookEnd(afterAll)`](#onhookend) - [`onTestSuiteResult`](#ontestsuiteresult) - [`onTestModuleEnd`](#ontestmoduleend) + - [`onCoverage`](#oncoverage) - [`onTestRunEnd`](#ontestrunend) Tests and suites within a single module will be reported in order unless they were skipped. All skipped tests are reported at the end of suite/module. diff --git a/docs/advanced/api/test-module.md b/docs/advanced/api/test-module.md index b585857c30c4..ba74f46291c1 100644 --- a/docs/advanced/api/test-module.md +++ b/docs/advanced/api/test-module.md @@ -24,6 +24,16 @@ This is usually an absolute unix file path (even on Windows). It can be a virtua 'C:\\Users\\Documents\\project\\example.test.ts' // ❌ ``` +## relativeModuleId + +Module id relative to the project. This is the same as `task.name` in the deprecated API. + +```ts +'project/example.test.ts' // ✅ +'example.test.ts' // ✅ +'project\\example.test.ts' // ❌ +``` + ## state ```ts diff --git a/docs/advanced/api/vitest.md b/docs/advanced/api/vitest.md index 4db4337dba1f..90dc0eae2b38 100644 --- a/docs/advanced/api/vitest.md +++ b/docs/advanced/api/vitest.md @@ -99,6 +99,10 @@ You can get the latest summary of snapshots via the `vitest.snapshot.summary` pr Cache manager that stores information about latest test results and test file stats. In Vitest itself this is only used by the default sequencer to sort tests. +## watcher 4.0.0 {#watcher} + +The instance of a Vitest watcher with useful methods to track file changes and rerun tests. You can use `onFileChange`, `onFileDelete` or `onFileCreate` with your own watcher, if the built-in watcher is disabled. + ## projects An array of [test projects](/advanced/api/test-project) that belong to user's projects. If the user did not specify a them, this array will only contain a [root project](#getrootproject). @@ -256,7 +260,7 @@ function start(filters?: string[]): Promise Initialize reporters, the coverage provider, and run tests. This method accepts string filters to match the test files - these are the same filters that [CLI supports](/guide/filtering#cli). ::: warning -This method should not be called if [`vitest.init()`](#init) is also invoked. Use [`runTestSpecifications`](#runtestspecifications) or [`rerunTestSpecifications`](#reruntestspecifications) instead if you need to run tests after Vitest was inititalised. +This method should not be called if [`vitest.init()`](#init) is also invoked. Use [`runTestSpecifications`](#runtestspecifications) or [`rerunTestSpecifications`](#reruntestspecifications) instead if you need to run tests after Vitest was initialised. ::: This method is called automatically by [`startVitest`](/advanced/guide/tests) if `config.mergeReports` and `config.standalone` are not set. @@ -308,7 +312,7 @@ function runTestSpecifications( ): Promise ``` -This method runs every test based on the received [specifications](/advanced/api/test-specification). The second argument, `allTestsRun`, is used by the coverage provider to determine if it needs to instrument coverage on _every_ file in the root (this only matters if coverage is enabled and `coverage.all` is set to `true`). +This method runs every test based on the received [specifications](/advanced/api/test-specification). The second argument, `allTestsRun`, is used by the coverage provider to determine if it needs to include uncovered files in report. ::: warning This method doesn't trigger `onWatcherRerun`, `onWatcherStart` and `onTestsRerun` callbacks. If you are rerunning tests based on the file change, consider using [`rerunTestSpecifications`](#reruntestspecifications) instead. @@ -371,6 +375,14 @@ This methods overrides the global [test name pattern](/config/#testnamepattern). This method doesn't start running any tests. To run tests with updated pattern, call [`runTestSpecifications`](#runtestspecifications). ::: +## getGlobalTestNamePattern + +```ts +function getGlobalTestNamePattern(): RegExp | undefined +``` + +Returns the regexp used for the global test name pattern. + ## resetGlobalTestNamePattern ```ts @@ -528,3 +540,85 @@ function matchesProjectFilter(name: string): boolean Check if the name matches the current [project filter](/guide/cli#project). If there is no project filter, this will always return `true`. It is not possible to programmatically change the `--project` CLI option. + +## waitForTestRunEnd 4.0.0 {#waitfortestrunend} + +```ts +function waitForTestRunEnd(): Promise +``` + +If there is a test run happening, returns a promise that will resolve when the test run is finished. + +## createCoverageProvider 4.0.0 {#createcoverageprovider} + +```ts +function createCoverageProvider(): Promise +``` + +Creates a coverage provider if `coverage` is enabled in the config. This is done automatically if you are running tests with [`start`](#start) or [`init`](#init) methods. + +::: warning +This method will also clean all previous reports if [`coverage.clean`](/config/#coverage-clean) is not set to `false`. +::: + +## enableCoverage 4.0.0 {#enablecoverage} + +```ts +function enableCoverage(): Promise +``` + +This method enables coverage for tests that run after this call. `enableCoverage` doesn't run any tests; it only sets up Vitest to collect coverage. + +It creates a new coverage provider if one doesn't already exist. + +## disableCoverage 4.0.0 {#disablecoverage} + +```ts +function disableCoverage(): void +``` + +This method disables coverage collection for tests that run afterwards. + +## experimental_parseSpecification 4.0.0 experimental {#parsespecification} + +```ts +function experimental_parseSpecification( + specification: TestSpecification +): Promise +``` + +This function will collect all tests inside the file without running it. It uses rollup's `parseAst` function on top of Vite's `ssrTransform` to statically analyse the file and collect all tests that it can. + +::: warning +If Vitest could not analyse the name of the test, it will inject a hidden `dynamic: true` property to the test or a suite. The `id` will also have a postfix with `-dynamic` to not break tests that were collected properly. + +Vitest always injects this property in tests with `for` or `each` modifier or tests with a dynamic name (like, `hello ${property}` or `'hello' + ${property}`). Vitest will still assign a name to the test, but it cannot be used to filter the tests. + +There is nothing Vitest can do to make it possible to filter dynamic tests, but you can turn a test with `for` or `each` modifier into a name pattern with `escapeTestName` function: + +```ts +import { escapeTestName } from 'vitest/node' + +// turns into /hello, .+?/ +const escapedPattern = new RegExp(escapeTestName('hello, %s', true)) +``` +::: + +::: warning +Vitest will only collect tests defined in the file. It will never follow imports to other files. + +Vitest collects all `it`, `test`, `suite` and `describe` definitions even if they were not imported from the `vitest` entry point. +::: + +## experimental_parseSpecifications 4.0.0 experimental {#parsespecifications} + +```ts +function experimental_parseSpecifications( + specifications: TestSpecification[], + options?: { + concurrency?: number + } +): Promise +``` + +This method will [collect tests](#parsespecification) from an array of specifications. By default, Vitest will run only `os.availableParallelism()` number of specifications at a time to reduce the potential performance degradation. You can specify a different number in a second argument. diff --git a/docs/advanced/pool.md b/docs/advanced/pool.md index 0485164bd592..bf63d9af06a8 100644 --- a/docs/advanced/pool.md +++ b/docs/advanced/pool.md @@ -67,7 +67,7 @@ The function is called only once (unless the server config was updated), and it' Vitest calls `runTest` when new tests are scheduled to run. It will not call it if `files` is empty. The first argument is an array of [TestSpecifications](/advanced/api/test-specification). Files are sorted using [`sequencer`](/config/#sequence-sequencer) before `runTests` is called. It's possible (but unlikely) to have the same file twice, but it will always have a different project - this is implemented via [`projects`](/guide/projects) configuration. -Vitest will wait until `runTests` is executed before finishing a run (i.e., it will emit [`onFinished`](/advanced/reporters) only after `runTests` is resolved). +Vitest will wait until `runTests` is executed before finishing a run (i.e., it will emit [`onTestRunEnd`](/advanced/reporters) only after `runTests` is resolved). If you are using a custom pool, you will have to provide test files and their results yourself - you can reference [`vitest.state`](https://github.com/vitest-dev/vitest/blob/main/packages/vitest/src/node/state.ts) for that (most important are `collectFiles` and `updateTasks`). Vitest uses `startTests` function from `@vitest/runner` package to do that. diff --git a/docs/advanced/reporters.md b/docs/advanced/reporters.md index 2a486d622894..a426861d85cc 100644 --- a/docs/advanced/reporters.md +++ b/docs/advanced/reporters.md @@ -26,7 +26,7 @@ And here is an example of a custom reporter: import { BaseReporter } from 'vitest/reporters' export default class CustomReporter extends BaseReporter { - onCollected() { + onTestModuleCollected() { const files = this.ctx.state.getFiles(this.watchFilters) this.reportTestSummary(files) } @@ -39,7 +39,7 @@ Or implement the `Reporter` interface: import type { Reporter } from 'vitest/node' export default class CustomReporter implements Reporter { - onCollected() { + onTestModuleCollected() { // print something } } @@ -65,22 +65,14 @@ Instead of using the tasks that reporters receive, it is recommended to use the You can get access to this API by calling `vitest.state.getReportedEntity(runnerTask)`: ```ts twoslash -import type { Reporter, RunnerTestFile, TestModule, Vitest } from 'vitest/node' +import type { Reporter, TestModule } from 'vitest/node' class MyReporter implements Reporter { - private vitest!: Vitest - - onInit(vitest: Vitest) { - this.vitest = vitest - } - - onFinished(files: RunnerTestFile[]) { - for (const file of files) { - // note that the old task implementation uses "file" instead of "module" - const testModule = this.vitest.state.getReportedEntity(file) as TestModule + onTestRunEnd(testModules: ReadonlyArray) { + for (const testModule of testModules) { for (const task of testModule.children) { // ^? - console.log('finished', task.type, task.fullName) + console.log('test run end', task.type, task.fullName) } } } @@ -93,7 +85,6 @@ class MyReporter implements Reporter { ### Built-in reporters: -1. `BasicReporter` 1. `DefaultReporter` 2. `DotReporter` 3. `JsonReporter` @@ -102,6 +93,7 @@ class MyReporter implements Reporter { 6. `JUnitReporter` 7. `TapFlatReporter` 8. `HangingProcessReporter` +9. `TreeReporter` ### Base Abstract reporters: diff --git a/docs/advanced/runner.md b/docs/advanced/runner.md index 5fbd5a5f262b..52b1d342d6f5 100644 --- a/docs/advanced/runner.md +++ b/docs/advanced/runner.md @@ -121,14 +121,14 @@ export default CustomRunner ``` ::: warning -Vitest also injects an instance of `ViteNodeRunner` as `__vitest_executor` property. You can use it to process files in `importFile` method (this is default behavior of `TestRunner` and `BenchmarkRunner`). +Vitest also injects an instance of `ModuleRunner` from `vite/module-runner` as `moduleRunner` property. You can use it to process files in `importFile` method (this is default behavior of `TestRunner` and `BenchmarkRunner`). -`ViteNodeRunner` exposes `executeId` method, which is used to import test files in a Vite-friendly environment. Meaning, it will resolve imports and transform file content at runtime so that Node can understand it: +`ModuleRunner` exposes `import` method, which is used to import test files in a Vite-friendly environment. Meaning, it will resolve imports and transform file content at runtime so that Node can understand it: ```ts export default class Runner { async importFile(filepath: string) { - await this.__vitest_executor.executeId(filepath) + await this.moduleRunner.import(filepath) } } ``` @@ -139,7 +139,7 @@ If you don't have a custom runner or didn't define `runTest` method, Vitest will ::: ::: tip -Snapshot support and some other features depend on the runner. If you don't want to lose it, you can extend your runner from `VitestTestRunner` imported from `vitest/runners`. It also exposes `BenchmarkNodeRunner`, if you want to extend benchmark functionality. +Snapshot support and some other features depend on the runner. If you don't want to lose it, you can extend your runner from `VitestTestRunner` imported from `vitest/runners`. It also exposes `NodeBenchmarkRunner`, if you want to extend benchmark functionality. ::: ## Tasks @@ -238,7 +238,7 @@ export interface TaskResult { * Errors that occurred during the task execution. It is possible to have several errors * if `expect.soft()` failed multiple times. */ - errors?: ErrorWithDiff[] + errors?: TestError[] /** * How long in milliseconds the task took to run. */ diff --git a/docs/api/expect-typeof.md b/docs/api/expect-typeof.md index d3fce9ce7d7e..c94785dfadda 100644 --- a/docs/api/expect-typeof.md +++ b/docs/api/expect-typeof.md @@ -31,6 +31,9 @@ expectTypeOf({ a: 1, b: 1 }).not.toEqualTypeOf<{ a: number }>() - **Type:** `(expected: T) => void` +::: warning DEPRECATED +This matcher has been deprecated since expect-type v1.2.0. Use [`toExtend`](#toextend) instead. +::: This matcher checks if expect type extends provided type. It is different from `toEqual` and is more similar to [expect's](/api/expect) `toMatchObject()`. With this matcher, you can check if an object “matches” a type. ```ts @@ -41,6 +44,44 @@ expectTypeOf().toMatchTypeOf() expectTypeOf().not.toMatchTypeOf() ``` +## toExtend + +- **Type:** `(expected: T) => void` + +This matcher checks if expect type extends provided type. It is different from `toEqual` and is more similar to [expect's](/api/expect) `toMatchObject()`. With this matcher, you can check if an object "matches" a type. + +```ts +import { expectTypeOf } from 'vitest' + +expectTypeOf({ a: 1, b: 1 }).toExtend({ a: 1 }) +expectTypeOf().toExtend() +expectTypeOf().not.toExtend() +``` + +## toMatchObjectType + +- **Type:** `() => void` + +This matcher performs a strict check on object types, ensuring that the expected type matches the provided object type. It's stricter than [`toExtend`](#toextend) and is the recommended choice when working with object types as it's more likely to catch issues like readonly properties. + +```ts +import { expectTypeOf } from 'vitest' + +expectTypeOf({ a: 1, b: 2 }).toMatchObjectType<{ a: number }>() // preferred +expectTypeOf({ a: 1, b: 2 }).toExtend<{ a: number }>() // works but less strict + +// Supports nested object checking +const user = { + name: 'John', + address: { city: 'New York', zip: '10001' } +} +expectTypeOf(user).toMatchObjectType<{ name: string; address: { city: string } }>() +``` + +::: warning +This matcher only works with plain object types. It will fail for union types and other complex types. For those cases, use [`toExtend`](#toextend) instead. +::: + ## extract - **Type:** `ExpectTypeOf` diff --git a/docs/api/expect.md b/docs/api/expect.md index f8ecd2ace3b0..4df79eb95f53 100644 --- a/docs/api/expect.md +++ b/docs/api/expect.md @@ -75,7 +75,7 @@ test('expect.soft test', () => { ```ts interface ExpectPoll extends ExpectStatic { - (actual: () => T, options: { interval; timeout; message }): Promise> + (actual: () => T, options?: { interval?: number; timeout?: number; message?: string }): Promise> } ``` @@ -299,6 +299,32 @@ test('we don\'t have apples', () => { }) ``` +## toBeNullable + +- **Type:** `() => Awaitable` + +`toBeNullable` simply asserts if something is nullable (`null` or `undefined`). + +```ts +import { expect, test } from 'vitest' + +function apples() { + return null +} + +function bananas() { + return null +} + +test('we don\'t have apples', () => { + expect(apples()).toBeNullable() +}) + +test('we don\'t have bananas', () => { + expect(bananas()).toBeNullable() +}) +``` + ## toBeNaN - **Type:** `() => Awaitable` @@ -502,7 +528,7 @@ Differences from [`.toEqual`](#toequal): - Keys with `undefined` properties are checked. e.g. `{a: undefined, b: 2}` does not match `{b: 2}` when using `.toStrictEqual`. - Array sparseness is checked. e.g. `[, 1]` does not match `[undefined, 1]` when using `.toStrictEqual`. -- Object types are checked to be equal. e.g. A class instance with fields `a` and` b` will not equal a literal object with fields `a` and `b`. +- Object types are checked to be equal. e.g. A class instance with fields `a` and `b` will not equal a literal object with fields `a` and `b`. ```ts import { expect, test } from 'vitest' @@ -651,7 +677,7 @@ test('top fruits', () => { `toMatchObject` asserts if an object matches a subset of the properties of an object. -You can also pass an array of objects. This is useful if you want to check that two arrays match in their number of elements, as opposed to `arrayContaining`, which allows for extra elements in the received array. +You can also pass an array of objects. This is useful if you want to check that two arrays match in their number and order of elements, as opposed to `arrayContaining`, which allows for extra elements in the received array. ```ts import { expect, test } from 'vitest' @@ -1476,7 +1502,7 @@ test.each(errorDirs)('build fails with "%s"', async (dir) => { - **Type:** `() => any` -This asymmetric matcher, when used with equality check, will always return `true`. Useful, if you just want to be sure that the property exist. +This asymmetric matcher matches anything except `null` or `undefined`. Useful if you just want to be sure that a property exists with any value that's not either `null` or `undefined`. ```ts import { expect, test } from 'vitest' diff --git a/docs/api/mock.md b/docs/api/mock.md index 025641b7eda9..cfea4e42ca1f 100644 --- a/docs/api/mock.md +++ b/docs/api/mock.md @@ -1,6 +1,6 @@ -# Mock Functions +# Mocks -You can create a mock function to track its execution with `vi.fn` method. If you want to track a method on an already created object, you can use `vi.spyOn` method: +You can create a mock function or a class to track its execution with the `vi.fn` method. If you want to track a property on an already created object, you can use the `vi.spyOn` method: ```js import { vi } from 'vitest' @@ -18,7 +18,7 @@ market.getApples() getApplesSpy.mock.calls.length === 1 ``` -You should use mock assertions (e.g., [`toHaveBeenCalled`](/api/expect#tohavebeencalled)) on [`expect`](/api/expect) to assert mock result. This API reference describes available properties and methods to manipulate mock behavior. +You should use mock assertions (e.g., [`toHaveBeenCalled`](/api/expect#tohavebeencalled)) on [`expect`](/api/expect) to assert mock results. This API reference describes available properties and methods to manipulate mock behavior. ::: tip The custom function implementation in the types below is marked with a generic ``. @@ -30,7 +30,7 @@ The custom function implementation in the types below is marked with a generic ` function getMockImplementation(): T | undefined ``` -Returns current mock implementation if there is one. +Returns the current mock implementation if there is one. If the mock was created with [`vi.fn`](/api/vi#vi-fn), it will use the provided method as the mock implementation. @@ -42,12 +42,12 @@ If the mock was created with [`vi.spyOn`](/api/vi#vi-spyon), it will return `und function getMockName(): string ``` -Use it to return the name assigned to the mock with the `.mockName(name)` method. By default, it will return `vi.fn()`. +Use it to return the name assigned to the mock with the `.mockName(name)` method. By default, `vi.fn()` mocks will return `'vi.fn()'`, while spies created with `vi.spyOn` will keep the original name. ## mockClear ```ts -function mockClear(): MockInstance +function mockClear(): Mock ``` Clears all information about every call. After calling it, all properties on `.mock` will return to their initial state. This method does not reset implementations. It is useful for cleaning up mocks between different assertions. @@ -72,7 +72,7 @@ To automatically call this method before each test, enable the [`clearMocks`](/c ## mockName ```ts -function mockName(name: string): MockInstance +function mockName(name: string): Mock ``` Sets the internal mock name. This is useful for identifying the mock when an assertion fails. @@ -80,7 +80,7 @@ Sets the internal mock name. This is useful for identifying the mock when an ass ## mockImplementation ```ts -function mockImplementation(fn: T): MockInstance +function mockImplementation(fn: T): Mock ``` Accepts a function to be used as the mock implementation. TypeScript expects the arguments and return type to match those of the original function. @@ -102,7 +102,7 @@ mockFn.mock.calls[1][0] === 1 // true ## mockImplementationOnce ```ts -function mockImplementationOnce(fn: T): MockInstance +function mockImplementationOnce(fn: T): Mock ``` Accepts a function to be used as the mock implementation. TypeScript expects the arguments and return type to match those of the original function. This method can be chained to produce different results for multiple function calls. @@ -135,11 +135,11 @@ console.log(myMockFn(), myMockFn(), myMockFn(), myMockFn()) function withImplementation( fn: T, cb: () => void -): MockInstance +): Mock function withImplementation( fn: T, cb: () => Promise -): Promise> +): Promise> ``` Overrides the original mock implementation temporarily while the callback is being executed. @@ -177,10 +177,10 @@ Note that this method takes precedence over the [`mockImplementationOnce`](#mock ## mockRejectedValue ```ts -function mockRejectedValue(value: unknown): MockInstance +function mockRejectedValue(value: unknown): Mock ``` -Accepts an error that will be rejected when async function is called. +Accepts an error that will be rejected when an async function is called. ```ts const asyncMock = vi.fn().mockRejectedValue(new Error('Async error')) @@ -191,7 +191,7 @@ await asyncMock() // throws Error<'Async error'> ## mockRejectedValueOnce ```ts -function mockRejectedValueOnce(value: unknown): MockInstance +function mockRejectedValueOnce(value: unknown): Mock ``` Accepts a value that will be rejected during the next function call. If chained, each consecutive call will reject the specified value. @@ -209,14 +209,13 @@ await asyncMock() // throws Error<'Async error'> ## mockReset ```ts -function mockReset(): MockInstance +function mockReset(): Mock ``` -Does what [`mockClear`](#mockClear) does and resets inner implementation to the original function. -This also resets all "once" implementations. +Does what [`mockClear`](#mockClear) does and resets the mock implementation. This also resets all "once" implementations. -Note that resetting a mock from `vi.fn()` will set implementation to an empty function that returns `undefined`. -resetting a mock from `vi.fn(impl)` will restore implementation to `impl`. +Note that resetting a mock from `vi.fn()` will set the implementation to an empty function that returns `undefined`. +Resetting a mock from `vi.fn(impl)` will reset the implementation to `impl`. This is useful when you want to reset a mock to its original state. @@ -241,13 +240,12 @@ To automatically call this method before each test, enable the [`mockReset`](/co ## mockRestore ```ts -function mockRestore(): MockInstance +function mockRestore(): Mock ``` -Does what [`mockReset`](#mockReset) does and restores original descriptors of spied-on objects. +Does what [`mockReset`](#mockreset) does and restores the original descriptors of spied-on objects, if the mock was created with [`vi.spyOn`](/api/vi#vi-spyon). -Note that restoring a mock from `vi.fn()` will set implementation to an empty function that returns `undefined`. -Restoring a mock from `vi.fn(impl)` will restore implementation to `impl`. +`mockRestore` on a `vi.fn()` mock is identical to [`mockReset`](#mockreset). ```ts const person = { @@ -270,7 +268,7 @@ To automatically call this method before each test, enable the [`restoreMocks`]( ## mockResolvedValue ```ts -function mockResolvedValue(value: Awaited>): MockInstance +function mockResolvedValue(value: Awaited>): Mock ``` Accepts a value that will be resolved when the async function is called. TypeScript will only accept values that match the return type of the original function. @@ -284,7 +282,7 @@ await asyncMock() // 42 ## mockResolvedValueOnce ```ts -function mockResolvedValueOnce(value: Awaited>): MockInstance +function mockResolvedValueOnce(value: Awaited>): Mock ``` Accepts a value that will be resolved during the next function call. TypeScript will only accept values that match the return type of the original function. If chained, each consecutive call will resolve the specified value. @@ -305,7 +303,7 @@ await asyncMock() // default ## mockReturnThis ```ts -function mockReturnThis(): MockInstance +function mockReturnThis(): Mock ``` Use this if you need to return the `this` context from the method without invoking the actual implementation. This is a shorthand for: @@ -319,7 +317,7 @@ spy.mockImplementation(function () { ## mockReturnValue ```ts -function mockReturnValue(value: ReturnType): MockInstance +function mockReturnValue(value: ReturnType): Mock ``` Accepts a value that will be returned whenever the mock function is called. TypeScript will only accept values that match the return type of the original function. @@ -335,7 +333,7 @@ mock() // 43 ## mockReturnValueOnce ```ts -function mockReturnValueOnce(value: ReturnType): MockInstance +function mockReturnValueOnce(value: ReturnType): Mock ``` Accepts a value that will be returned whenever the mock function is called. TypeScript will only accept values that match the return type of the original function. @@ -373,13 +371,44 @@ fn.mock.calls === [ ] ``` +:::warning Objects are Stored by Reference +Note that Vitest always stores objects by reference in all properies of the `mock` state. This means that if the properties were changed by your code, then some assertions like [`.toHaveBeenCalledWith`](/api/expect#tohavebeencalledwith) will not pass: + +```ts +const argument = { + value: 0, +} +const fn = vi.fn() +fn(argument) // { value: 0 } + +argument.value = 10 + +expect(fn).toHaveBeenCalledWith({ value: 0 }) // [!code --] + +// The equality check is done against the original argument, +// but its property was changed between the call and assertion +expect(fn).toHaveBeenCalledWith({ value: 10 }) // [!code ++] +``` + +In this case you can clone the argument yourself: + +```ts{6} +const calledArguments = [] +const fn = vi.fn((arg) => { + calledArguments.push(structuredClone(arg)) +}) + +expect(calledArguments[0]).toEqual({ value: 0 }) +``` +::: + ## mock.lastCall ```ts const lastCall: Parameters | undefined ``` -This contains the arguments of the last call. If mock wasn't called, it will return `undefined`. +This contains the arguments of the last call. If the mock wasn't called, it will return `undefined`. ## mock.results @@ -388,7 +417,7 @@ interface MockResultReturn { type: 'return' /** * The value that was returned from the function. - * If function returned a Promise, then this will be a resolved value. + * If the function returned a Promise, then this will be a resolved value. */ value: T } @@ -406,10 +435,10 @@ interface MockResultThrow { value: any } -type MockResult = - | MockResultReturn - | MockResultThrow - | MockResultIncomplete +type MockResult + = | MockResultReturn + | MockResultThrow + | MockResultIncomplete const results: MockResult>[] ``` @@ -418,6 +447,7 @@ This is an array containing all values that were `returned` from the function. O - `'return'` - function returned without throwing. - `'throw'` - function threw a value. +- `'incomplete'` - the function did not finish running yet. The `value` property contains the returned value or thrown error. If the function returned a `Promise`, then `result` will always be `'return'` even if the promise was rejected. @@ -450,6 +480,11 @@ fn.mock.results === [ ## mock.settledResults ```ts +interface MockSettledResultIncomplete { + type: 'incomplete' + value: undefined +} + interface MockSettledResultFulfilled { type: 'fulfilled' value: T @@ -460,23 +495,31 @@ interface MockSettledResultRejected { value: any } -export type MockSettledResult = - | MockSettledResultFulfilled - | MockSettledResultRejected +export type MockSettledResult + = | MockSettledResultFulfilled + | MockSettledResultRejected + | MockSettledResultIncomplete const settledResults: MockSettledResult>>[] ``` -An array containing all values that were `resolved` or `rejected` from the function. +An array containing all values that were resolved or rejected by the function. + +If the function returned non-promise values, the `value` will be kept as is, but the `type` will still says `fulfilled` or `rejected`. -This array will be empty if the function was never resolved or rejected. +Until the value is resolved or rejected, the `settledResult` type will be `incomplete`. ```js const fn = vi.fn().mockResolvedValueOnce('result') const result = fn() -fn.mock.settledResults === [] +fn.mock.settledResults === [ + { + type: 'incomplete', + value: undefined, + }, +] await result @@ -533,10 +576,10 @@ fn.mock.contexts[1] === context const instances: ReturnType[] ``` -This property is an array containing all instances that were created when the mock was called with the `new` keyword. Note that this is an actual context (`this`) of the function, not a return value. +This property is an array containing all instances that were created when the mock was called with the `new` keyword. Note that this is the actual context (`this`) of the function, not a return value. ::: warning -If mock was instantiated with `new MyClass()`, then `mock.instances` will be an array with one value: +If the mock was instantiated with `new MyClass()`, then `mock.instances` will be an array with one value: ```js const MyClass = vi.fn() @@ -545,7 +588,7 @@ const a = new MyClass() MyClass.mock.instances[0] === a ``` -If you return a value from constructor, it will not be in `instances` array, but instead inside `results`: +If you return a value from the constructor, it will not be in the `instances` array, but instead inside `results`: ```js const Spy = vi.fn(() => ({ method: vi.fn() })) diff --git a/docs/api/vi.md b/docs/api/vi.md index 64743a0bcc5b..2ef0c70ce3b8 100644 --- a/docs/api/vi.md +++ b/docs/api/vi.md @@ -12,12 +12,28 @@ import { vi } from 'vitest' ## Mock Modules -This section describes the API that you can use when [mocking a module](/guide/mocking#modules). Beware that Vitest doesn't support mocking modules imported using `require()`. +This section describes the API that you can use when [mocking a module](/guide/mocking/modules). Beware that Vitest doesn't support mocking modules imported using `require()`. ### vi.mock -- **Type**: `(path: string, factory?: MockOptions | ((importOriginal: () => unknown) => unknown)) => void` -- **Type**: `(path: Promise, factory?: MockOptions | ((importOriginal: () => T) => T | Promise)) => void` +```ts +interface MockOptions { + spy?: boolean +} + +interface MockFactory { + (importOriginal: () => T): unknown +} + +function mock( + path: string, + factory?: MockOptions | MockFactory +): void +function mock( + module: Promise, + factory?: MockOptions | MockFactory +): void +``` Substitutes all imported modules from provided `path` with another module. You can use configured Vite aliases inside a path. The call to `vi.mock` is hoisted, so it doesn't matter where you call it. It will always be executed before all imports. If you need to reference some variables outside of its scope, you can define them inside [`vi.hoisted`](#vi-hoisted) and reference them inside `vi.mock`. @@ -155,12 +171,20 @@ axios.get(`/apples/${increment(1)}`) Beware that if you don't call `vi.mock`, modules **are not** mocked automatically. To replicate Jest's automocking behaviour, you can call `vi.mock` for each required module inside [`setupFiles`](/config/#setupfiles). ::: -If there is no `__mocks__` folder or a factory provided, Vitest will import the original module and auto-mock all its exports. For the rules applied, see [algorithm](/guide/mocking#automocking-algorithm). +If there is no `__mocks__` folder or a factory provided, Vitest will import the original module and auto-mock all its exports. For the rules applied, see [algorithm](/guide/mocking/modules#automocking-algorithm). ### vi.doMock -- **Type**: `(path: string, factory?: MockOptions | ((importOriginal: () => unknown) => unknown)) => void` -- **Type**: `(path: Promise, factory?: MockOptions | ((importOriginal: () => T) => T | Promise)) => void` +```ts +function doMock( + path: string, + factory?: MockOptions | MockFactory +): void +function doMock( + module: Promise, + factory?: MockOptions | MockFactory +): void +``` The same as [`vi.mock`](#vi-mock), but it's not hoisted to the top of the file, so you can reference variables in the global file scope. The next [dynamic import](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import) of the module will be mocked. @@ -207,8 +231,16 @@ test('importing the next module imports mocked one', async () => { ### vi.mocked -- **Type**: `(obj: T, deep?: boolean) => MaybeMockedDeep` -- **Type**: `(obj: T, options?: { partial?: boolean; deep?: boolean }) => MaybePartiallyMockedDeep` +```ts +function mocked( + object: T, + deep?: boolean +): MaybeMockedDeep +function mocked( + object: T, + options?: { partial?: boolean; deep?: boolean } +): MaybePartiallyMockedDeep +``` Type helper for TypeScript. Just returns the object that was passed. @@ -243,7 +275,9 @@ test('mock return value with only partially correct typing', async () => { ### vi.importActual -- **Type**: `(path: string) => Promise` +```ts +function importActual(path: string): Promise +``` Imports module, bypassing all checks if it should be mocked. Can be useful if you want to mock module partially. @@ -257,19 +291,25 @@ vi.mock('./example.js', async () => { ### vi.importMock -- **Type**: `(path: string) => Promise>` +```ts +function importMock(path: string): Promise> +``` -Imports a module with all of its properties (including nested properties) mocked. Follows the same rules that [`vi.mock`](#vi-mock) does. For the rules applied, see [algorithm](/guide/mocking#automocking-algorithm). +Imports a module with all of its properties (including nested properties) mocked. Follows the same rules that [`vi.mock`](#vi-mock) does. For the rules applied, see [algorithm](/guide/mocking/modules#automocking-algorithm). ### vi.unmock -- **Type**: `(path: string | Promise) => void` +```ts +function unmock(path: string | Promise): void +``` Removes module from the mocked registry. All calls to import will return the original module even if it was mocked before. This call is hoisted to the top of the file, so it will only unmock modules that were defined in `setupFiles`, for example. ### vi.doUnmock -- **Type**: `(path: string | Promise) => void` +```ts +function doUnmock(path: string | Promise): void +``` The same as [`vi.unmock`](#vi-unmock), but is not hoisted to the top of the file. The next import of the module will import the original module instead of the mock. This will not unmock previously imported modules. @@ -308,7 +348,9 @@ unmockedIncrement(30) === 31 ### vi.resetModules -- **Type**: `() => Vitest` +```ts +function resetModules(): Vitest +``` Resets modules registry by clearing the cache of all modules. This allows modules to be reevaluated when reimported. Top-level imports cannot be re-evaluated. Might be useful to isolate modules where local state conflicts between tests. @@ -339,6 +381,10 @@ Does not reset mocks registry. To clear mocks registry, use [`vi.unmock`](#vi-un ### vi.dynamicImportSettled +```ts +function dynamicImportSettled(): Promise +``` + Wait for all imports to load. Useful, if you have a synchronous call that starts importing a module that you cannot wait otherwise. ```ts @@ -370,10 +416,12 @@ This section describes how to work with [method mocks](/api/mock) and replace en ### vi.fn -- **Type:** `(fn?: Function) => Mock` +```ts +function fn(fn?: Procedure | Constructable): Mock +``` -Creates a spy on a function, though can be initiated without one. Every time a function is invoked, it stores its call arguments, returns, and instances. Also, you can manipulate its behavior with [methods](/api/mock). -If no function is given, mock will return `undefined`, when invoked. +Creates a spy on a function, but can also be initiated without one. Every time a function is invoked, it stores its call arguments, returns, and instances. Additionally, you can manipulate its behavior with [methods](/api/mock). +If no function is given, mock will return `undefined` when invoked. ```ts const getApples = vi.fn(() => 0) @@ -390,9 +438,22 @@ expect(res).toBe(5) expect(getApples).toHaveNthReturnedWith(2, 5) ``` +You can also pass down a class to `vi.fn`: + +```ts +const Cart = vi.fn(class { + get = () => 0 +}) + +const cart = new Cart() +expect(Cart).toHaveBeenCalled() +``` + ### vi.mockObject 3.2.0 -- **Type:** `(value: T) => MaybeMockedDeep` +```ts +function mockObject(value: T): MaybeMockedDeep +``` Deeply mocks properties and methods of a given object in the same way as `vi.mock()` mocks module exports. See [automocking](/guide/mocking.html#automocking-algorithm) for the detail. @@ -417,30 +478,66 @@ expect(mocked.simple()).toBe('mocked') expect(mocked.nested.method()).toBe('mocked nested') ``` +Just like `vi.mock()`, you can pass `{ spy: true }` as a second argument to keep function implementations: + +```ts +const spied = vi.mockObject(original, { spy: true }) +expect(spied.simple()).toBe('value') +expect(spied.simple).toHaveBeenCalled() +expect(spied.simple.mock.results[0]).toEqual({ type: 'return', value: 'value' }) +``` + ### vi.isMockFunction -- **Type:** `(fn: Function) => boolean` +```ts +function isMockFunction(fn: unknown): asserts fn is Mock +``` Checks that a given parameter is a mock function. If you are using TypeScript, it will also narrow down its type. ### vi.clearAllMocks +```ts +function clearAllMocks(): Vitest +``` + Calls [`.mockClear()`](/api/mock#mockclear) on all spies. This will clear mock history without affecting mock implementations. ### vi.resetAllMocks +```ts +function resetAllMocks(): Vitest +``` + Calls [`.mockReset()`](/api/mock#mockreset) on all spies. -This will clear mock history and reset each mock's implementation to its original. +This will clear mock history and reset each mock's implementation. ### vi.restoreAllMocks -Calls [`.mockRestore()`](/api/mock#mockrestore) on all spies. -This will clear mock history, restore all original mock implementations, and restore original descriptors of spied-on objects. +```ts +function restoreAllMocks(): Vitest +``` + +This restores all original implementations on spies created with [`vi.spyOn`](#vi-spyon). + +After the mock was restored, you can spy on it again. + +::: warning +This method also does not affect mocks created during [automocking](/guide/mocking/modules#mocking-a-module). + +Note that unlike [`mock.mockRestore`](/api/mock#mockrestore), `vi.restoreAllMocks` will not clear mock history or reset the mock implementation +::: ### vi.spyOn -- **Type:** `(object: T, method: K, accessType?: 'get' | 'set') => MockInstance` +```ts +function spyOn( + object: T, + key: K, + accessor?: 'get' | 'set' +): Mock +``` Creates a spy on a method or getter/setter of an object similar to [`vi.fn()`](#vi-fn). It returns a [mock function](/api/mock). @@ -459,6 +556,33 @@ expect(spy).toHaveBeenCalled() expect(spy).toHaveReturnedWith(1) ``` +If the spying method is a class definition, the mock implementations have to use the `function` or the `class` keyword: + +```ts {12-14,16-20} +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 + } + }) +``` + +If you provide an arrow function, you will get [` is not a constructor` error](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Not_a_constructor) when the mock is called. + ::: tip In environments that support [Explicit Resource Management](https://github.com/tc39/proposal-explicit-resource-management), you can use `using` instead of `const` to automatically call `mockRestore` on any mocked function when the containing block is exited. This is especially useful for spied methods: @@ -473,7 +597,7 @@ it('calls console.log', () => { ::: ::: tip -You can call [`vi.restoreAllMocks`](#vi-restoreallmocks) inside [`afterEach`](/api/#aftereach) (or enable [`test.restoreMocks`](/config/#restoreMocks)) to restore all methods to their original implementations. This will restore the original [object descriptor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty), so you won't be able to change method's implementation: +You can call [`vi.restoreAllMocks`](#vi-restoreallmocks) inside [`afterEach`](/api/#aftereach) (or enable [`test.restoreMocks`](/config/#restoreMocks)) to restore all methods to their original implementations after every test. This will restore the original [object descriptor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty), so you won't be able to change method's implementation anymore, unless you spy again: ```ts const cart = { @@ -509,7 +633,12 @@ And while it is possible to spy on exports in `jsdom` or other Node.js environme ### vi.stubEnv {#vi-stubenv} -- **Type:** `(name: T, value: T extends "PROD" | "DEV" | "SSR" ? boolean : string | undefined) => Vitest` +```ts +function stubEnv( + name: T, + value: T extends 'PROD' | 'DEV' | 'SSR' ? boolean : string | undefined +): Vitest +``` Changes the value of environmental variable on `process.env` and `import.meta.env`. You can restore its value by calling `vi.unstubAllEnvs`. @@ -543,7 +672,9 @@ import.meta.env.MODE = 'test' ### vi.unstubAllEnvs {#vi-unstuballenvs} -- **Type:** `() => Vitest` +```ts +function unstubAllEnvs(): Vitest +``` Restores all `import.meta.env` and `process.env` values that were changed with `vi.stubEnv`. When it's called for the first time, Vitest remembers the original value and will store it, until `unstubAllEnvs` is called again. @@ -572,7 +703,12 @@ import.meta.env.NODE_ENV === 'development' ### vi.stubGlobal -- **Type:** `(name: string | number | symbol, value: unknown) => Vitest` +```ts +function stubGlobal( + name: string | number | symbol, + value: unknown +): Vitest +``` Changes the value of global variable. You can restore its original value by calling `vi.unstubAllGlobals`. @@ -601,7 +737,9 @@ window.innerWidth = 100 ### vi.unstubAllGlobals {#vi-unstuballglobals} -- **Type:** `() => Vitest` +```ts +function unstubAllGlobals(): Vitest +``` Restores all global values on `globalThis`/`global` (and `window`/`top`/`self`/`parent`, if you are using `jsdom` or `happy-dom` environment) that were changed with `vi.stubGlobal`. When it's called for the first time, Vitest remembers the original value and will store it, until `unstubAllGlobals` is called again. @@ -630,11 +768,13 @@ IntersectionObserver === undefined ## Fake Timers -This sections describes how to work with [fake timers](/guide/mocking#timers). +This sections describes how to work with [fake timers](/guide/mocking/timers). ### vi.advanceTimersByTime -- **Type:** `(ms: number) => Vitest` +```ts +function advanceTimersByTime(ms: number): Vitest +``` This method will invoke every initiated timer until the specified number of milliseconds is passed or the queue is empty - whatever comes first. @@ -651,7 +791,9 @@ vi.advanceTimersByTime(150) ### vi.advanceTimersByTimeAsync -- **Type:** `(ms: number) => Promise` +```ts +function advanceTimersByTimeAsync(ms: number): Promise +``` This method will invoke every initiated timer until the specified number of milliseconds is passed or the queue is empty - whatever comes first. This will include asynchronously set timers. @@ -668,7 +810,9 @@ await vi.advanceTimersByTimeAsync(150) ### vi.advanceTimersToNextTimer -- **Type:** `() => Vitest` +```ts +function advanceTimersToNextTimer(): Vitest +``` Will call next available timer. Useful to make assertions between each timer call. You can chain call it to manage timers by yourself. @@ -683,7 +827,9 @@ vi.advanceTimersToNextTimer() // log: 1 ### vi.advanceTimersToNextTimerAsync -- **Type:** `() => Promise` +```ts +function advanceTimersToNextTimerAsync(): Promise +``` Will call next available timer and wait until it's resolved if it was set asynchronously. Useful to make assertions between each timer call. @@ -698,9 +844,11 @@ await vi.advanceTimersToNextTimerAsync() // log: 2 await vi.advanceTimersToNextTimerAsync() // log: 3 ``` -### vi.advanceTimersToNextFrame 2.1.0 {#vi-advancetimerstonextframe} +### vi.advanceTimersToNextFrame {#vi-advancetimerstonextframe} -- **Type:** `() => Vitest` +```ts +function advanceTimersToNextFrame(): Vitest +``` Similar to [`vi.advanceTimersByTime`](https://vitest.dev/api/vi#vi-advancetimersbytime), but will advance timers by the milliseconds needed to execute callbacks currently scheduled with `requestAnimationFrame`. @@ -718,35 +866,49 @@ expect(frameRendered).toBe(true) ### vi.getTimerCount -- **Type:** `() => number` +```ts +function getTimerCount(): number +``` Get the number of waiting timers. ### vi.clearAllTimers +```ts +function clearAllTimers(): void +``` + Removes all timers that are scheduled to run. These timers will never run in the future. ### vi.getMockedSystemTime -- **Type**: `() => Date | null` +```ts +function getMockedSystemTime(): Date | null +``` Returns mocked current date. If date is not mocked the method will return `null`. ### vi.getRealSystemTime -- **Type**: `() => number` +```ts +function getRealSystemTime(): number +``` When using `vi.useFakeTimers`, `Date.now` calls are mocked. If you need to get real time in milliseconds, you can call this function. ### vi.runAllTicks -- **Type:** `() => Vitest` +```ts +function runAllTicks(): Vitest +``` Calls every microtask that was queued by `process.nextTick`. This will also run all microtasks scheduled by themselves. ### vi.runAllTimers -- **Type:** `() => Vitest` +```ts +function runAllTimers(): Vitest +``` This method will invoke every initiated timer until the timer queue is empty. It means that every timer called during `runAllTimers` will be fired. If you have an infinite interval, it will throw after 10 000 tries (can be configured with [`fakeTimers.loopLimit`](/config/#faketimers-looplimit)). @@ -769,7 +931,9 @@ vi.runAllTimers() ### vi.runAllTimersAsync -- **Type:** `() => Promise` +```ts +function runAllTimersAsync(): Promise +``` This method will asynchronously invoke every initiated timer until the timer queue is empty. It means that every timer called during `runAllTimersAsync` will be fired even asynchronous timers. If you have an infinite interval, it will throw after 10 000 tries (can be configured with [`fakeTimers.loopLimit`](/config/#faketimers-looplimit)). @@ -786,7 +950,9 @@ await vi.runAllTimersAsync() ### vi.runOnlyPendingTimers -- **Type:** `() => Vitest` +```ts +function runOnlyPendingTimers(): Vitest +``` This method will call every timer that was initiated after [`vi.useFakeTimers`](#vi-usefaketimers) call. It will not fire any timer that was initiated during its call. @@ -801,7 +967,9 @@ vi.runOnlyPendingTimers() ### vi.runOnlyPendingTimersAsync -- **Type:** `() => Promise` +```ts +function runOnlyPendingTimersAsync(): Promise +``` This method will asynchronously call every timer that was initiated after [`vi.useFakeTimers`](#vi-usefaketimers) call, even asynchronous ones. It will not fire any timer that was initiated during its call. @@ -828,7 +996,9 @@ await vi.runOnlyPendingTimersAsync() ### vi.setSystemTime -- **Type**: `(date: string | number | Date) => void` +```ts +function setSystemTime(date: string | number | Date): Vitest +``` If fake timers are enabled, this method simulates a user changing the system clock (will affect date related API like `hrtime`, `performance.now` or `new Date()`) - however, it will not fire any timers. If fake timers are not enabled, this method will only mock `Date.*` calls. @@ -849,7 +1019,9 @@ vi.useRealTimers() ### vi.useFakeTimers -- **Type:** `(config?: FakeTimerInstallOpts) => Vitest` +```ts +function useFakeTimers(config?: FakeTimerInstallOpts): Vitest +``` To enable mocking timers, you need to call this method. It will wrap all further calls to timers (such as `setTimeout`, `setInterval`, `clearTimeout`, `clearInterval`, `setImmediate`, `clearImmediate`, and `Date`) until [`vi.useRealTimers()`](#vi-userealtimers) is called. @@ -864,13 +1036,17 @@ But you can enable it by specifying the option in `toFake` argument: `vi.useFake ### vi.isFakeTimers {#vi-isfaketimers} -- **Type:** `() => boolean` +```ts +function isFakeTimers(): boolean +``` Returns `true` if fake timers are enabled. ### vi.useRealTimers -- **Type:** `() => Vitest` +```ts +function useRealTimers(): Vitest +``` When timers have run out, you may call this method to return mocked timers to its original implementations. All timers that were scheduled before will be discarded. @@ -880,7 +1056,12 @@ A set of useful helper functions that Vitest provides. ### vi.waitFor {#vi-waitfor} -- **Type:** `(callback: WaitForCallback, options?: number | WaitForOptions) => Promise` +```ts +function waitFor( + callback: WaitForCallback, + options?: number | WaitForOptions +): Promise +``` Wait for the callback to execute successfully. If the callback throws an error or returns a rejected promise it will continue to wait until it succeeds or times out. @@ -942,7 +1123,12 @@ If `vi.useFakeTimers` is used, `vi.waitFor` automatically calls `vi.advanceTimer ### vi.waitUntil {#vi-waituntil} -- **Type:** `(callback: WaitUntilCallback, options?: number | WaitUntilOptions) => Promise` +```ts +function waitUntil( + callback: WaitUntilCallback, + options?: number | WaitUntilOptions +): Promise +``` This is similar to `vi.waitFor`, but if the callback throws any errors, execution is immediately interrupted and an error message is received. If the callback returns falsy value, the next check will continue until truthy value is returned. This is useful when you need to wait for something to exist before taking the next step. @@ -967,7 +1153,9 @@ test('Element render correctly', async () => { ### vi.hoisted {#vi-hoisted} -- **Type**: `(factory: () => T) => T` +```ts +function hoisted(factory: () => T): T +``` All static `import` statements in ES modules are hoisted to the top of the file, so any code that is defined before the imports will actually be executed after imports are evaluated. @@ -1044,7 +1232,9 @@ const json = await vi.hoisted(async () => { ### vi.setConfig -- **Type**: `RuntimeConfig` +```ts +function setConfig(config: RuntimeOptions): void +``` Updates config for the current test file. This method supports only config options that will affect the current test file: @@ -1069,6 +1259,8 @@ vi.setConfig({ ### vi.resetConfig -- **Type**: `RuntimeConfig` +```ts +function resetConfig(): void +``` If [`vi.setConfig`](#vi-setconfig) was called before, this will reset config to the original state. diff --git a/docs/config/index.md b/docs/config/index.md index 433aaaf5bcd3..0f615b6d8eca 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -8,7 +8,7 @@ If you are using Vite and have a `vite.config` file, Vitest will read it to matc - Create `vitest.config.ts`, which will have the higher priority and will **override** the configuration from `vite.config.ts` (Vitest supports all conventional JS and TS extensions, but doesn't support `json`) - it means all options in your `vite.config` will be **ignored** - Pass `--config` option to CLI, e.g. `vitest --config ./path/to/vitest.config.ts` -- Use `process.env.VITEST` or `mode` property on `defineConfig` (will be set to `test`/`benchmark` if not overridden with `--mode`) to conditionally apply different configuration in `vite.config.ts` +- Use `process.env.VITEST` or `mode` property on `defineConfig` (will be set to `test`/`benchmark` if not overridden with `--mode`) to conditionally apply different configuration in `vite.config.ts`. Note that like any other environment variable, `VITEST` is also exposed on `import.meta.env` in your tests To configure `vitest` itself, add `test` property in your Vite config. You'll also need to add a reference to Vitest types using a [triple slash command](https://www.typescriptlang.org/docs/handbook/triple-slash-directives.html#-reference-types-) at the top of your config file, if you are importing `defineConfig` from `vite` itself. @@ -124,8 +124,8 @@ When using coverage, Vitest automatically adds test files `include` patterns to ### exclude - **Type:** `string[]` -- **Default:** `['**/node_modules/**', '**/dist/**', '**/cypress/**', '**/.{idea,git,cache,output,temp}/**', '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build,eslint,prettier}.config.*']` -- **CLI:** `vitest --exclude "**/excluded-file"` +- **Default:** `['**/node_modules/**', '**/.git/**']` +- **CLI:** `vitest --exclude "**/excluded-file" --exclude "*/other-files/*.js"` A list of glob patterns that should be excluded from your test files. @@ -233,7 +233,7 @@ Handling for dependencies resolution. #### deps.optimizer {#deps-optimizer} -- **Type:** `{ ssr?, web? }` +- **Type:** `{ ssr?, client? }` - **See also:** [Dep Optimization Options](https://vitejs.dev/config/dep-optimization-options.html) Enable dependency optimization. If you have a lot of tests, this might improve their performance. @@ -245,7 +245,7 @@ When Vitest encounters the external library listed in `include`, it will be bund - Your `alias` configuration is now respected inside bundled packages - Code in your tests is running closer to how it's running in the browser -Be aware that only packages in `deps.optimizer?.[mode].include` option are bundled (some plugins populate this automatically, like Svelte). You can read more about available options in [Vite](https://vitejs.dev/config/dep-optimization-options.html) docs (Vitest doesn't support `disable` and `noDiscovery` options). By default, Vitest uses `optimizer.web` for `jsdom` and `happy-dom` environments, and `optimizer.ssr` for `node` and `edge` environments, but it is configurable by [`transformMode`](#testtransformmode). +Be aware that only packages in `deps.optimizer?.[mode].include` option are bundled (some plugins populate this automatically, like Svelte). You can read more about available options in [Vite](https://vitejs.dev/config/dep-optimization-options.html) docs (Vitest doesn't support `disable` and `noDiscovery` options). By default, Vitest uses `optimizer.client` for `jsdom` and `happy-dom` environments, and `optimizer.ssr` for `node` and `edge` environments. This options also inherits your `optimizeDeps` configuration (for web Vitest will extend `optimizeDeps`, for ssr - `ssr.optimizeDeps`). If you redefine `include`/`exclude` option in `deps.optimizer` it will extend your `optimizeDeps` when running tests. Vitest automatically removes the same options from `include`, if they are listed in `exclude`. @@ -260,15 +260,15 @@ You will not be able to edit your `node_modules` code for debugging, since the c Enable dependency optimization. -#### deps.web {#deps-web} +#### deps.client {#deps-client} - **Type:** `{ transformAssets?, ... }` -Options that are applied to external files when transform mode is set to `web`. By default, `jsdom` and `happy-dom` use `web` mode, while `node` and `edge` environments use `ssr` transform mode, so these options will have no affect on files inside those environments. +Options that are applied to external files when the environment is set to `client`. By default, `jsdom` and `happy-dom` use `client` environment, while `node` and `edge` environments use `ssr`, so these options will have no affect on files inside those environments. Usually, files inside `node_modules` are externalized, but these options also affect files in [`server.deps.external`](#server-deps-external). -#### deps.web.transformAssets +#### deps.client.transformAssets - **Type:** `boolean` - **Default:** `true` @@ -281,7 +281,7 @@ This module will have a default export equal to the path to the asset, if no que At the moment, this option only works with [`vmThreads`](#vmthreads) and [`vmForks`](#vmforks) pools. ::: -#### deps.web.transformCss +#### deps.client.transformCss - **Type:** `boolean` - **Default:** `true` @@ -294,7 +294,7 @@ If CSS files are disabled with [`css`](#css) options, this option will just sile At the moment, this option only works with [`vmThreads`](#vmthreads) and [`vmForks`](#vmforks) pools. ::: -#### deps.web.transformGlobPattern +#### deps.client.transformGlobPattern - **Type:** `RegExp | RegExp[]` - **Default:** `[]` @@ -328,7 +328,7 @@ TypeError: createAsyncThunk is not a function TypeError: default is not a function ``` -By default, Vitest assumes you are using a bundler to bypass this and will not fail, but you can disable this behaviour manually, if you code is not processed. +By default, Vitest assumes you are using a bundler to bypass this and will not fail, but you can disable this behaviour manually, if your code is not processed. #### deps.moduleDirectories @@ -560,7 +560,7 @@ import type { Environment } from 'vitest' export default { name: 'custom', - transformMode: 'ssr', + viteEnvironment: 'ssr', setup() { // custom setup return { @@ -593,101 +593,6 @@ jsdom environment exposes `jsdom` global variable equal to the current [JSDOM](h These options are passed down to `setup` method of current [`environment`](#environment). By default, you can configure only JSDOM options, if you are using it as your test environment. -### environmentMatchGlobs - -- **Type:** `[string, EnvironmentName][]` -- **Default:** `[]` - -::: danger DEPRECATED -This API was deprecated in Vitest 3. Use [projects](/guide/projects) to define different configurations instead. - -```ts -export default defineConfig({ - test: { - environmentMatchGlobs: [ // [!code --] - ['./*.jsdom.test.ts', 'jsdom'], // [!code --] - ], // [!code --] - projects: [ // [!code ++] - { // [!code ++] - extends: true, // [!code ++] - test: { // [!code ++] - environment: 'jsdom', // [!code ++] - }, // [!code ++] - }, // [!code ++] - ], // [!code ++] - }, -}) -``` -::: - -Automatically assign environment based on globs. The first match will be used. - -For example: - -```ts -import { defineConfig } from 'vitest/config' - -export default defineConfig({ - test: { - environmentMatchGlobs: [ - // all tests in tests/dom will run in jsdom - ['tests/dom/**', 'jsdom'], - // all tests in tests/ with .edge.test.ts will run in edge-runtime - ['**\/*.edge.test.ts', 'edge-runtime'], - // ... - ] - } -}) -``` - -### poolMatchGlobs {#poolmatchglobs} - -- **Type:** `[string, 'threads' | 'forks' | 'vmThreads' | 'vmForks' | 'typescript'][]` -- **Default:** `[]` - -::: danger DEPRECATED -This API was deprecated in Vitest 3. Use [projects](/guide/projects) to define different configurations instead: - -```ts -export default defineConfig({ - test: { - poolMatchGlobs: [ // [!code --] - ['./*.threads.test.ts', 'threads'], // [!code --] - ], // [!code --] - projects: [ // [!code ++] - { // [!code ++] - test: { // [!code ++] - extends: true, // [!code ++] - pool: 'threads', // [!code ++] - }, // [!code ++] - }, // [!code ++] - ], // [!code ++] - }, -}) -``` -::: - -Automatically assign pool in which tests will run based on globs. The first match will be used. - -For example: - -```ts -import { defineConfig } from 'vitest/config' - -export default defineConfig({ - test: { - poolMatchGlobs: [ - // all tests in "worker-specific" directory will run inside a worker as if you enabled `--pool=threads` for them, - ['**/tests/worker-specific/**', 'threads'], - // run all tests in "browser" directory in an actual browser - ['**/tests/browser/**', 'browser'], - // all other tests will run based on "browser.enabled" and "threads" options, if you didn't specify other globs - // ... - ] - } -}) -``` - ### update - **Type:** `boolean` @@ -714,7 +619,7 @@ In CI, or when run from a non-interactive shell, "watch" mode is not the default Vitest reruns tests based on the module graph which is populated by static and dynamic `import` statements. However, if you are reading from the file system or fetching from a proxy, then Vitest cannot detect those dependencies. -To correctly rerun those tests, you can define a regex pattern and a function that retuns a list of test files to run. +To correctly rerun those tests, you can define a regex pattern and a function that returns a list of test files to run. ```ts import { defineConfig } from 'vitest/config' @@ -845,13 +750,6 @@ export default defineConfig({ Maximum number or percentage of threads. You can also use `VITEST_MAX_THREADS` environment variable. -##### poolOptions.threads.minThreads - -- **Type:** `number | string` -- **Default:** _available CPUs_ - -Minimum number or percentage of threads. You can also use `VITEST_MIN_THREADS` environment variable. - ##### poolOptions.threads.singleThread - **Type:** `boolean` @@ -917,13 +815,6 @@ export default defineConfig({ Maximum number or percentage of forks. You can also use `VITEST_MAX_FORKS` environment variable. -##### poolOptions.forks.minForks - -- **Type:** `number | string` -- **Default:** _available CPUs_ - -Minimum number or percentage of forks. You can also use `VITEST_MIN_FORKS` environment variable. - ##### poolOptions.forks.isolate - **Type:** `boolean` @@ -980,13 +871,6 @@ export default defineConfig({ Maximum number or percentage of threads. You can also use `VITEST_MAX_THREADS` environment variable. -##### poolOptions.vmThreads.minThreads - -- **Type:** `number | string` -- **Default:** _available CPUs_ - -Minimum number or percentage of threads. You can also use `VITEST_MIN_THREADS` environment variable. - ##### poolOptions.vmThreads.memoryLimit - **Type:** `string | number` @@ -1061,13 +945,6 @@ export default defineConfig({ Maximum number or percentage of forks. You can also use `VITEST_MAX_FORKS` environment variable. -##### poolOptions.vmForks.minForks - -- **Type:** `number | string` -- **Default:** _available CPUs_ - -Minimum number or percentage of forks. You can also use `VITEST_MIN_FORKS` environment variable. - ##### poolOptions.vmForks.memoryLimit - **Type:** `string | number` @@ -1092,7 +969,7 @@ Be careful when using, it as some options may crash worker, e.g. --prof, --title - **Default:** `true` - **CLI:** `--no-file-parallelism`, `--fileParallelism=false` -Should all test files run in parallel. Setting this to `false` will override `maxWorkers` and `minWorkers` options to `1`. +Should all test files run in parallel. Setting this to `false` will override `maxWorkers` option to `1`. ::: tip This option doesn't affect tests running in the same file. If you want to run those in parallel, use `concurrent` option on [describe](/api/#describe-concurrent) or via [a config](#sequence-concurrent). @@ -1104,12 +981,6 @@ This option doesn't affect tests running in the same file. If you want to run th Maximum number or percentage of workers to run tests in. `poolOptions.{threads,vmThreads}.maxThreads`/`poolOptions.forks.maxForks` has higher priority. -### minWorkers {#minworkers} - -- **Type:** `number | string` - -Minimum number or percentage of workers to run tests in. `poolOptions.{threads,vmThreads}.minThreads`/`poolOptions.forks.minForks` has higher priority. - ### testTimeout - **Type:** `number` @@ -1310,12 +1181,12 @@ Make sure that your files are not excluded by [`server.watch.ignored`](https://v ### coverage -You can use [`v8`](https://v8.dev/blog/javascript-code-coverage), [`istanbul`](https://istanbul.js.org/) or [a custom coverage solution](/guide/coverage#custom-coverage-provider) for coverage collection. +You can use [`v8`](/guide/coverage.html#v8-provider), [`istanbul`](/guide/coverage.html#istanbul-provider) or [a custom coverage solution](/guide/coverage#custom-coverage-provider) for coverage collection. You can provide coverage options to CLI with dot notation: ```sh -npx vitest --coverage.enabled --coverage.provider=istanbul --coverage.all +npx vitest --coverage.enabled --coverage.provider=istanbul ``` ::: warning @@ -1342,76 +1213,26 @@ Enables coverage collection. Can be overridden using `--coverage` CLI option. #### coverage.include - **Type:** `string[]` -- **Default:** `['**']` +- **Default:** Files that were imported during test run - **Available for providers:** `'v8' | 'istanbul'` -- **CLI:** `--coverage.include=`, `--coverage.include= --coverage.include=` +- **CLI:** `--coverage.include=`, `--coverage.include= --coverage.include=` -List of files included in coverage as glob patterns +List of files included in coverage as glob patterns. By default only files covered by tests are included. -#### coverage.extension +It is recommended to pass file extensions in the pattern. -- **Type:** `string | string[]` -- **Default:** `['.js', '.cjs', '.mjs', '.ts', '.mts', '.tsx', '.jsx', '.vue', '.svelte', '.marko', '.astro']` -- **Available for providers:** `'v8' | 'istanbul'` -- **CLI:** `--coverage.extension=`, `--coverage.extension= --coverage.extension=` +See [Including and excluding files from coverage report](/guide/coverage.html#including-and-excluding-files-from-coverage-report) for examples. #### coverage.exclude - **Type:** `string[]` -- **Default:** -```js -[ - 'coverage/**', - 'dist/**', - '**/node_modules/**', - '**/[.]**', - 'packages/*/test?(s)/**', - '**/*.d.ts', - '**/virtual:*', - '**/__x00__*', - '**/\x00*', - 'cypress/**', - 'test?(s)/**', - 'test?(-*).?(c|m)[jt]s?(x)', - '**/*{.,-}{test,spec,bench,benchmark}?(-d).?(c|m)[jt]s?(x)', - '**/__tests__/**', - '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build,eslint,prettier}.config.*', - '**/vitest.{workspace,projects}.[jt]s?(on)', - '**/.{eslint,mocha,prettier}rc.{?(c|m)js,yml}', -] -``` +- **Default:** : `[]` - **Available for providers:** `'v8' | 'istanbul'` - **CLI:** `--coverage.exclude=`, `--coverage.exclude= --coverage.exclude=` List of files excluded from coverage as glob patterns. -This option overrides all default options. Extend the default options when adding new patterns to ignore: - -```ts -import { coverageConfigDefaults, defineConfig } from 'vitest/config' - -export default defineConfig({ - test: { - coverage: { - exclude: ['**/custom-pattern/**', ...coverageConfigDefaults.exclude] - }, - }, -}) -``` - -::: tip NOTE -Vitest automatically adds test files `include` patterns to the `coverage.exclude`. -It's not possible to show coverage of test files. -::: - -#### coverage.all - -- **Type:** `boolean` -- **Default:** `true` -- **Available for providers:** `'v8' | 'istanbul'` -- **CLI:** `--coverage.all`, `--coverage.all=false` - -Whether to include all files, including the untested ones into report. +See [Including and excluding files from coverage report](/guide/coverage.html#including-and-excluding-files-from-coverage-report) for examples. #### coverage.clean @@ -1595,7 +1416,7 @@ Check thresholds per file. ##### coverage.thresholds.autoUpdate -- **Type:** `boolean` +- **Type:** `boolean | function` - **Default:** `false` - **Available for providers:** `'v8' | 'istanbul'` - **CLI:** `--coverage.thresholds.autoUpdate=` @@ -1603,6 +1424,23 @@ Check thresholds per file. Update all threshold values `lines`, `functions`, `branches` and `statements` to configuration file when current coverage is better than the configured thresholds. This option helps to maintain thresholds when coverage is improved. +You can also pass a function for formatting the updated threshold values: + + +```ts +{ + coverage: { + thresholds: { + // Update thresholds without decimals + autoUpdate: (newThreshold) => Math.floor(newThreshold), + + // 95.85 -> 95 + functions: 95, + } + } +} +``` + ##### coverage.thresholds.100 - **Type:** `boolean` @@ -1678,51 +1516,11 @@ Sets thresholds to 100 for files matching the glob pattern. } ``` -#### coverage.ignoreEmptyLines - -- **Type:** `boolean` -- **Default:** `true` (`false` in v1) -- **Available for providers:** `'v8'` -- **CLI:** `--coverage.ignoreEmptyLines=` - -Ignore empty lines, comments and other non-runtime code, e.g. Typescript types. Requires `experimentalAstAwareRemapping: false`. - -This option works only if the used compiler removes comments and other non-runtime code from the transpiled code. -By default Vite uses ESBuild which removes comments and Typescript types from `.ts`, `.tsx` and `.jsx` files. - -If you want to apply ESBuild to other files as well, define them in [`esbuild` options](https://vitejs.dev/config/shared-options.html#esbuild): - -```ts -import { defineConfig } from 'vitest/config' - -export default defineConfig({ - esbuild: { - // Transpile all files with ESBuild to remove comments from code coverage. - // Required for `test.coverage.ignoreEmptyLines` to work: - include: ['**/*.js', '**/*.jsx', '**/*.mjs', '**/*.ts', '**/*.tsx'], - }, - test: { - coverage: { - provider: 'v8', - ignoreEmptyLines: true, - }, - }, -}) -``` -#### coverage.experimentalAstAwareRemapping - -- **Type:** `boolean` -- **Default:** `false` -- **Available for providers:** `'v8'` -- **CLI:** `--coverage.experimentalAstAwareRemapping=` - -Remap coverage with experimental AST based analysis. Provides more accurate results compared to default mode. - #### coverage.ignoreClassMethods - **Type:** `string[]` - **Default:** `[]` -- **Available for providers:** `'istanbul'` +- **Available for providers:** `'v8' | 'istanbul'` - **CLI:** `--coverage.ignoreClassMethods=` Set to array of class method names to ignore for coverage. @@ -1828,7 +1626,7 @@ This is an experimental feature. Breaking changes might not follow SemVer, pleas - **Type:** `boolean` - **Default:** `false` -Will call [`.mockClear()`](/api/mock#mockclear) on all spies before each test. +Will call [`vi.clearAllMocks()`](/api/vi#vi-clearallmocks) before each test. This will clear mock history without affecting mock implementations. ### mockReset @@ -1836,16 +1634,17 @@ This will clear mock history without affecting mock implementations. - **Type:** `boolean` - **Default:** `false` -Will call [`.mockReset()`](/api/mock#mockreset) on all spies before each test. -This will clear mock history and reset each implementation to its original. +Will call [`vi.resetAllMocks()`](/api/vi#vi-resetallmocks) before each test. +This will clear mock history and reset each implementation. ### restoreMocks - **Type:** `boolean` - **Default:** `false` -Will call [`.mockRestore()`](/api/mock#mockrestore) on all spies before each test. -This will clear mock history, restore each implementation to its original, and restore original descriptors of spied-on objects.. +Will call [`vi.restoreAllMocks()`](/api/vi#vi-restoreallmocks) before each test. + +This restores all original implementations on spies created with [`vi.spyOn`](#vi-spyon). ### unstubEnvs {#unstubenvs} @@ -1861,33 +1660,11 @@ Will call [`vi.unstubAllEnvs`](/api/vi#vi-unstuballenvs) before each test. Will call [`vi.unstubAllGlobals`](/api/vi#vi-unstuballglobals) before each test. -### testTransformMode {#testtransformmode} - - - **Type:** `{ web?, ssr? }` - - Determine the transform method for all modules imported inside a test that matches the glob pattern. By default, relies on the environment. For example, tests with JSDOM environment will process all files with `ssr: false` flag and tests with Node environment process all modules with `ssr: true`. - - #### testTransformMode.ssr - - - **Type:** `string[]` - - **Default:** `[]` - - Use SSR transform pipeline for all modules inside specified tests.
- Vite plugins will receive `ssr: true` flag when processing those files. - - #### testTransformMode.web - - - **Type:** `string[]` - - **Default:** `[]` - - First do a normal transform pipeline (targeting browser), then do a SSR rewrite to run the code in Node.
- Vite plugins will receive `ssr: false` flag when processing those files. - ### snapshotFormat - **Type:** `PrettyFormatOptions` -Format options for snapshot testing. These options are passed down to [`pretty-format`](https://www.npmjs.com/package/pretty-format). +Format options for snapshot testing. These options are passed down to our fork of [`pretty-format`](https://www.npmjs.com/package/pretty-format). In addition to the `pretty-format` options we support `printShadowRoot: boolean`. ::: tip Beware that `plugins` field on this object will be ignored. @@ -2323,9 +2100,15 @@ Retry the test specific number of times if it fails. ### onConsoleLog -- **Type**: `(log: string, type: 'stdout' | 'stderr') => boolean | void` +```ts +function onConsoleLog( + log: string, + type: 'stdout' | 'stderr', + entity: TestModule | TestSuite | TestCase | undefined, +): boolean | void +``` -Custom handler for `console.log` in tests. If you return `false`, Vitest will not print the log to the console. +Custom handler for `console` methods in tests. If you return `false`, Vitest will not print the log to the console. Note that Vitest ignores all other falsy values. Can be useful for filtering out logs from third-party libraries. @@ -2370,6 +2153,30 @@ export default defineConfig({ }) ``` +### onUnhandledError {#onunhandlederror} + +- **Type:** `(error: (TestError | Error) & { type: string }) => boolean | void` + +A custom handler to filter out unhandled errors that should not be reported. If an error is filtered out, it will no longer affect the test results. + +If you want unhandled errors to be reported without impacting the test outcome, consider using the [`dangerouslyIgnoreUnhandledErrors`](#dangerouslyIgnoreUnhandledErrors) option + +```ts +import type { ParsedStack } from 'vitest' +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + onUnhandledError(error): boolean | void { + // Ignore all errors with the name "MySpecialError". + if (error.name === 'MySpecialError') { + return false + } + }, + }, +}) +``` + ### diff - **Type:** `string` @@ -2517,20 +2324,6 @@ Relevant only when using with `shouldAdvanceTime: true`. increment mocked time b Tells fake timers to clear "native" (i.e. not fake) timers by delegating to their respective handlers. When disabled, it can lead to potentially unexpected behavior if timers existed prior to starting fake timers session. -### workspace {#workspace} - -::: danger DEPRECATED -This options is deprecated and will be removed in the next major. Please, use [`projects`](#projects) instead. -::: - -- **Type:** `string | TestProjectConfiguration[]` -- **CLI:** `--workspace=./file.js` -- **Default:** `vitest.{workspace,projects}.{js,ts,json}` close to the config file or root - -Path to a [workspace](/guide/projects) config file relative to [root](#root). - -Since Vitest 3, you can also define the workspace array in the root config. If the `workspace` is defined in the config manually, Vitest will ignore the `vitest.workspace` file in the root. - ### projects {#projects} - **Type:** `TestProjectConfiguration[]` diff --git a/docs/guide/browser/assertion-api.md b/docs/guide/browser/assertion-api.md index 090d8b31f3d2..b10901a5b1c9 100644 --- a/docs/guide/browser/assertion-api.md +++ b/docs/guide/browser/assertion-api.md @@ -300,6 +300,27 @@ await expect.element( ).toBeVisible() ``` +## toBeInViewport + +```ts +function toBeInViewport(options: { ratio?: number }): Promise +``` + +This allows you to check if an element is currently in viewport with [IntersectionObserver API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API). + +You can pass `ratio` argument as option, which means the minimal ratio of the element should be in viewport. `ratio` should be in 0~1. + +```ts +// A specific element is in viewport. +await expect.element(page.getByText('Welcome')).toBeInViewport() + +// 50% of a specific element should be in viewport +await expect.element(page.getByText('To')).toBeInViewport({ ratio: 0.5 }) + +// Full of a specific element should be in viewport +await expect.element(page.getByText('Vitest')).toBeInViewport({ ratio: 1 }) +``` + ## toContainElement ```ts @@ -997,7 +1018,7 @@ other element that contains text, such as a paragraph, span, div etc. ::: warning The expected selection is a string, it does not allow to check for -selection range indeces. +selection range indices. ::: ```html @@ -1046,3 +1067,216 @@ await expect.element(queryByTestId('parent')).toHaveSelection('ected text') await expect.element(queryByTestId('prev')).not.toHaveSelection() await expect.element(queryByTestId('next')).toHaveSelection('ne') ``` + +## toMatchScreenshot experimental + +```ts +function toMatchScreenshot( + options?: ScreenshotMatcherOptions, +): Promise +function toMatchScreenshot( + name?: string, + options?: ScreenshotMatcherOptions, +): Promise +``` + +::: tip +The `toMatchScreenshot` assertion can be configured globally in your +[Vitest config](/guide/browser/config#browser-expect-tomatchscreenshot). +::: + +This assertion allows you to perform visual regression testing by comparing +screenshots of elements or pages against stored reference images. + +When differences are detected beyond the configured threshold, the test fails. +To help identify the changes, the assertion generates: + +- The actual screenshot captured during the test +- The expected reference screenshot +- A diff image highlighting the differences (when possible) + +::: warning Screenshots Stability +The assertion automatically retries taking screenshots until two consecutive +captures yield the same result. This helps reduce flakiness caused by +animations, loading states, or other dynamic content. You can control this +behavior with the `timeout` option. + +However, browser rendering can vary across: + +- Different browsers and browser versions +- Operating systems (Windows, macOS, Linux) +- Screen resolutions and pixel densities +- GPU drivers and hardware acceleration +- Font rendering and system fonts + +It is recommended to read the +[Visual Regression Testing guide](/guide/browser/visual-regression-testing) to +implement this testing strategy efficiently. +::: + +::: tip +When a screenshot comparison fails due to **intentional changes**, you can +update the reference screenshot by pressing the `u` key in watch mode, or by +running tests with the `-u` or `--update` flags. +::: + +```html + +``` + +```ts +// basic usage, auto-generates screenshot name +await expect.element(getByTestId('button')).toMatchScreenshot() + +// with custom name +await expect.element(getByTestId('button')).toMatchScreenshot('fancy-button') + +// with options +await expect.element(getByTestId('button')).toMatchScreenshot({ + comparatorName: 'pixelmatch', + comparatorOptions: { + allowedMismatchedPixelRatio: 0.01, + }, +}) + +// with both name and options +await expect.element(getByTestId('button')).toMatchScreenshot('fancy-button', { + comparatorName: 'pixelmatch', + comparatorOptions: { + allowedMismatchedPixelRatio: 0.01, + }, +}) +``` + +### Options + +- `comparatorName: "pixelmatch" = "pixelmatch"` + + The name of the algorithm/library used for comparing images. + + Currently, [`"pixelmatch"`](https://github.com/mapbox/pixelmatch) is the only + supported comparator. + +- `comparatorOptions: object` + + These options allow changing the behavior of the comparator. What properties + can be set depends on the chosen comparator algorithm. + + Vitest has set default values out of the box, but they can be overridden. + + - [`"pixelmatch"` options](#pixelmatch-comparator-options) + + ::: warning + **Always explicitly set `comparatorName` to get proper type inference for + `comparatorOptions`**. + + Without it, TypeScript won't know which options are valid: + + ```ts + // ❌ TypeScript can't infer the correct options + await expect.element(button).toMatchScreenshot({ + comparatorOptions: { + // might error when new comparators are added + allowedMismatchedPixelRatio: 0.01, + }, + }) + + // ✅ TypeScript knows these are pixelmatch options + await expect.element(button).toMatchScreenshot({ + comparatorName: 'pixelmatch', + comparatorOptions: { + allowedMismatchedPixelRatio: 0.01, + }, + }) + ``` + ::: + +- `screenshotOptions: object` + + The same options allowed by + [`locator.screenshot()`](/guide/browser/locators.html#screenshot), except for: + + - `'base64'` + - `'path'` + - `'save'` + - `'type'` + +- `timeout: number = 5_000` + + Time to wait until a stable screenshot is found. + + Setting this value to `0` disables the timeout, but if a stable screenshot + can't be determined the process will not end. + +#### `"pixelmatch"` comparator options + +The following options are available when using the `"pixelmatch"` comparator: + +- `allowedMismatchedPixelRatio: number | undefined = undefined` + + The maximum allowed ratio of differing pixels between the captured screenshot + and the reference image. + + Must be a value between `0` and `1`. + + For example, `allowedMismatchedPixelRatio: 0.02` means the test will pass + if up to 2% of pixels differ, but fail if more than 2% differ. + +- `allowedMismatchedPixels: number | undefined = undefined` + + The maximum number of pixels that are allowed to differ between the captured + screenshot and the stored reference image. + + If set to `undefined`, any non-zero difference will cause the test to fail. + + For example, `allowedMismatchedPixels: 10` means the test will pass if 10 or + fewer pixels differ, but fail if 11 or more differ. + +- `threshold: number = 0.1` + + Acceptable perceived color difference between the same pixel in two images. + + Value ranges from `0` (strict) to `1` (very lenient). Lower values mean small + differences will be detected. + + The comparison uses the [YIQ color space](https://en.wikipedia.org/wiki/YIQ). + +- `includeAA: boolean = false` + + If `true`, disables detection and ignoring of anti-aliased pixels. + +- `alpha: number = 0.1` + + Blending level of unchanged pixels in the diff image. + + Ranges from `0` (white) to `1` (original brightness). + +- `aaColor: [r: number, g: number, b: number] = [255, 255, 0]` + + Color used for anti-aliased pixels in the diff image. + +- `diffColor: [r: number, g: number, b: number] = [255, 0, 0]` + + Color used for differing pixels in the diff image. + +- `diffColorAlt: [r: number, g: number, b: number] | undefined = undefined` + + Optional alternative color for dark-on-light differences, to help show what's + added vs. removed. + + If not set, `diffColor` is used for all differences. + +- `diffMask: boolean = false` + + If `true`, shows only the diff as a mask on a transparent background, instead + of overlaying it on the original image. + + Anti-aliased pixels won't be shown (if detected). + +::: warning +When both `allowedMismatchedPixels` and `allowedMismatchedPixelRatio` are set, +the more restrictive value is used. + +For example, if you allow 100 pixels or 2% ratio, and your image has 10,000 +pixels, the effective limit would be 100 pixels instead of 200. +::: diff --git a/docs/guide/browser/commands.md b/docs/guide/browser/commands.md index c28cff3de65c..0b7bc1254905 100644 --- a/docs/guide/browser/commands.md +++ b/docs/guide/browser/commands.md @@ -11,7 +11,7 @@ Command is a function that invokes another function on the server and passes dow ### Files Handling -You can use the `readFile`, `writeFile`, and `removeFile` APIs to handle files in your browser tests. Since Vitest 3.2, all paths are resolved relative to the [project](/guide/projects) root (which is `process.cwd()`, unless overriden manually). Previously, paths were resolved relative to the test file. +You can use the `readFile`, `writeFile`, and `removeFile` APIs to handle files in your browser tests. Since Vitest 3.2, all paths are resolved relative to the [project](/guide/projects) root (which is `process.cwd()`, unless overridden manually). Previously, paths were resolved relative to the test file. By default, Vitest uses `utf-8` encoding but you can override it with options. @@ -148,26 +148,10 @@ export const myCommand: BrowserCommand<[string, number]> = async ( } ``` -::: tip -If you are using TypeScript, don't forget to reference `@vitest/browser/providers/playwright` in your [setup file](/config/#setupfile) or a [config file](/config/) to get autocompletion in the config and in `userEvent` and `page` options: - -```ts -/// -``` -::: - ### Custom `webdriverio` commands Vitest exposes some `webdriverio` specific properties on the context object. - `browser` is the `WebdriverIO.Browser` API. -Vitest automatically switches the `webdriver` context to the test iframe by calling `browser.switchToFrame` before the command is called, so `$` and `$$` methods refer to the elements inside the iframe, not in the orchestrator, but non-webdriver APIs will still refer to the parent frame context. - -::: tip -If you are using TypeScript, don't forget to reference `@vitest/browser/providers/webdriverio` in your [setup file](/config/#setupfile) or a [config file](/config/) to get autocompletion: - -```ts -/// -``` -::: +Vitest automatically switches the `webdriver` context to the test iframe by calling `browser.switchFrame` before the command is called, so `$` and `$$` methods refer to the elements inside the iframe, not in the orchestrator, but non-webdriver APIs will still refer to the parent frame context. diff --git a/docs/guide/browser/config.md b/docs/guide/browser/config.md index 9b73e1797bfc..d5ea370cb87c 100644 --- a/docs/guide/browser/config.md +++ b/docs/guide/browser/config.md @@ -4,12 +4,13 @@ You can change the browser configuration by updating the `test.browser` field in ```ts [vitest.config.ts] import { defineConfig } from 'vitest/config' +import { playwright } from '@vitest/browser/providers/playwright' export default defineConfig({ test: { browser: { enabled: true, - provider: 'playwright', + provider: playwright(), instances: [ { browser: 'chromium', @@ -53,15 +54,6 @@ Defines multiple browser setups. Every config has to have at least a `browser` f - [Configuring Playwright](/guide/browser/playwright) - [Configuring WebdriverIO](/guide/browser/webdriverio) -::: tip -To have a better type safety when using built-in providers, you should reference one of these types (for provider that you are using) in your [config file](/config/): - -```ts -/// -/// -``` -::: - In addition to that, you can also specify most of the [project options](/config/) (not marked with a icon) and some of the `browser` options like `browser.testerHtmlPath`. ::: warning @@ -100,26 +92,12 @@ List of available `browser` options: - [`browser.testerHtmlPath`](#browser-testerhtmlpath) - [`browser.screenshotDirectory`](#browser-screenshotdirectory) - [`browser.screenshotFailures`](#browser-screenshotfailures) +- [`browser.provider`](#browser-provider) By default, Vitest creates an array with a single element which uses the [`browser.name`](#browser-name) field as a `browser`. Note that this behaviour will be removed with Vitest 4. Under the hood, Vitest transforms these instances into separate [test projects](/advanced/api/test-project) sharing a single Vite server for better caching performance. -## browser.name deprecated {#browser-name} - -- **Type:** `string` -- **CLI:** `--browser=safari` - -::: danger DEPRECATED -This API is deprecated an will be removed in Vitest 4. Please, use [`browser.instances`](#browser-instances) option instead. -::: - -Run all tests in a specific browser. Possible options in different providers: - -- `webdriverio`: `firefox`, `chrome`, `edge`, `safari` -- `playwright`: `firefox`, `webkit`, `chromium` -- custom: any string that will be passed to the provider - ## browser.headless - **Type:** `boolean` @@ -150,70 +128,86 @@ A path to the HTML entry point. Can be relative to the root of the project. This Configure options for Vite server that serves code in the browser. Does not affect [`test.api`](#api) option. By default, Vitest assigns port `63315` to avoid conflicts with the development server, allowing you to run both in parallel. -## browser.provider {#browser-provider} +## browser.provider advanced {#browser-provider} -- **Type:** `'webdriverio' | 'playwright' | 'preview' | string` +- **Type:** `BrowserProviderOption` - **Default:** `'preview'` - **CLI:** `--browser.provider=playwright` -Path to a provider that will be used when running browser tests. Vitest provides three providers which are `preview` (default), `webdriverio` and `playwright`. Custom providers should be exported using `default` export and have this shape: - -```ts -export interface BrowserProvider { - name: string - supportsParallelism: boolean - getSupportedBrowsers: () => readonly string[] - beforeCommand?: (command: string, args: unknown[]) => Awaitable - afterCommand?: (command: string, args: unknown[]) => Awaitable - getCommandsContext: (sessionId: string) => Record - openPage: (sessionId: string, url: string, beforeNavigate?: () => Promise) => Promise - getCDPSession?: (sessionId: string) => Promise - close: () => Awaitable - initialize( - ctx: TestProject, - options: BrowserProviderInitializationOptions - ): Awaitable -} -``` - -::: danger ADVANCED API -The custom provider API is highly experimental and can change between patches. If you just need to run tests in a browser, use the [`browser.instances`](#browser-instances) option instead. -::: - -## browser.providerOptions deprecated {#browser-provideroptions} +The return value of the provider factory. You can import the factory from `@vitest/browser/providers/` or make your own provider: -- **Type:** `BrowserProviderOptions` +```ts{8-10} +import { playwright } from '@vitest/browser/providers/playwright' +import { webdriverio } from '@vitest/browser/providers/webdriverio' +import { preview } from '@vitest/browser/providers/preview' -::: danger DEPRECATED -This API is deprecated an will be removed in Vitest 4. Please, use [`browser.instances`](#browser-instances) option instead. -::: +export default defineConfig({ + test: { + browser: { + provider: playwright(), + provider: webdriverio(), + provider: preview(), // default + }, + }, +}) +``` -Options that will be passed down to provider when calling `provider.initialize`. +To configure how provider initializes the browser, you can pass down options to the factory function: -```ts -import { defineConfig } from 'vitest/config' +```ts{7-15,22-27} +import { playwright } from '@vitest/browser/providers/playwright' export default defineConfig({ test: { browser: { - providerOptions: { - launch: { - devtools: true, + // shared provider options between all instances + provider: playwright({ + launchOptions: { + slowMo: 50, + channel: 'chrome-beta', }, - }, + actionTimeout: 5_000, + }), + instances: [ + { browser: 'chromium' }, + { + browser: 'firefox', + // overriding options only for a single instance + // this will NOT merge options with the parent one + provider: playwright({ + launchOptions: { + firefoxUserPrefs: { + 'browser.startup.homepage': 'https://example.com', + }, + }, + }) + } + ], }, }, }) ``` -::: tip -To have a better type safety when using built-in providers, you should reference one of these types (for provider that you are using) in your [config file](/config/): +### Custom Provider + +::: danger ADVANCED API +The custom provider API is highly experimental and can change between patches. If you just need to run tests in a browser, use the [`browser.instances`](#browser-instances) option instead. +::: ```ts -/// -/// +export interface BrowserProvider { + name: string + mocker?: BrowserModuleMocker + /** + * @experimental opt-in into file parallelisation + */ + supportsParallelism: boolean + getCommandsContext: (sessionId: string) => Record + openPage: (sessionId: string, url: string) => Promise + getCDPSession?: (sessionId: string) => Promise + close: () => Awaitable +} ``` -::: ## browser.ui @@ -295,19 +289,6 @@ export interface BrowserScript { } ``` -## browser.testerScripts - -- **Type:** `BrowserScript[]` -- **Default:** `[]` - -::: danger DEPRECATED -This API is deprecated an will be removed in Vitest 4. Please, use [`browser.testerHtmlPath`](#browser-testerhtmlpath) field instead. -::: - -Custom scripts that should be injected into the tester HTML before the tests environment is initiated. This is useful to inject polyfills required for Vitest browser implementation. It is recommended to use [`setupFiles`](#setupfiles) in almost all cases instead of this. - -The script `src` and `content` will be processed by Vite plugins. - ## browser.commands - **Type:** `Record` @@ -325,3 +306,152 @@ The timeout in milliseconds. If connection to the browser takes longer, the test ::: info This is the time it should take for the browser to establish the WebSocket connection with the Vitest server. In normal circumstances, this timeout should never be reached. ::: + +## browser.trackUnhandledErrors + +- **Type:** `boolean` +- **Default:** `true` + +Enables tracking uncaught errors and exceptions so they can be reported by Vitest. + +If you need to hide certain errors, it is recommended to use [`onUnhandledError`](/config/#onunhandlederror) option instead. + +Disabling this will completely remove all Vitest error handlers, which can help debugging with the "Pause on exceptions" checkbox turned on. + +## browser.expect + +- **Type:** `ExpectOptions` + +### browser.expect.toMatchScreenshot + +Default options for the +[`toMatchScreenshot` assertion](/guide/browser/assertion-api.html#tomatchscreenshot). +These options will be applied to all screenshot assertions. + +::: tip +Setting global defaults for screenshot assertions helps maintain consistency +across your test suite and reduces repetition in individual tests. You can still +override these defaults at the assertion level when needed for specific test cases. +::: + +```ts +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + browser: { + enabled: true, + expect: { + toMatchScreenshot: { + comparatorName: 'pixelmatch', + comparatorOptions: { + threshold: 0.2, + allowedMismatchedPixels: 100, + }, + resolveScreenshotPath: ({ arg, browserName, ext, testFileName }) => + `custom-screenshots/${testFileName}/${arg}-${browserName}${ext}`, + }, + }, + }, + }, +}) +``` + +[All options available in the `toMatchScreenshot` assertion](/guide/browser/assertion-api#options) +can be configured here. Additionally, two path resolution functions are +available: `resolveScreenshotPath` and `resolveDiffPath`. + +#### browser.expect.toMatchScreenshot.resolveScreenshotPath + +- **Type:** `(data: PathResolveData) => string` +- **Default output:** `` `${root}/${testFileDirectory}/${screenshotDirectory}/${testFileName}/${arg}-${browserName}-${platform}${ext}` `` + +A function to customize where reference screenshots are stored. The function +receives an object with the following properties: + +- `arg: string` + + Path **without** extension, sanitized and relative to the test file. + + This comes from the arguments passed to `toMatchScreenshot`; if called + without arguments this will be the auto-generated name. + + ```ts + test('calls `onClick`', () => { + expect(locator).toMatchScreenshot() + // arg = "calls-onclick-1" + }) + + expect(locator).toMatchScreenshot('foo/bar/baz.png') + // arg = "foo/bar/baz" + + expect(locator).toMatchScreenshot('../foo/bar/baz.png') + // arg = "foo/bar/baz" + ``` + +- `ext: string` + + Screenshot extension, with leading dot. + + This can be set through the arguments passed to `toMatchScreenshot`, but + the value will fall back to `'.png'` if an unsupported extension is used. + +- `browserName: string` + + The instance's browser name. + +- `platform: NodeJS.Platform` + + The value of + [`process.platform`](https://nodejs.org/docs/v22.16.0/api/process.html#processplatform). + +- `screenshotDirectory: string` + + The value provided to + [`browser.screenshotDirectory`](/guide/browser/config#browser-screenshotdirectory), + if none is provided, its default value. + +- `root: string` + + Absolute path to the project's [`root`](/config/#root). + +- `testFileDirectory: string` + + Path to the test file, relative to the project's [`root`](/config/#root). + +- `testFileName: string` + + The test's filename. + +- `testName: string` + + The [`test`](/api/#test)'s name, including parent + [`describe`](/api/#describe), sanitized. + +- `attachmentsDir: string` + + The value provided to [`attachmentsDir`](/config/#attachmentsdir), if none is + provided, its default value. + +For example, to group screenshots by browser: + +```ts +resolveScreenshotPath: ({ arg, browserName, ext, root, testFileName }) => + `${root}/screenshots/${browserName}/${testFileName}/${arg}${ext}` +``` + +#### browser.expect.toMatchScreenshot.resolveDiffPath + +- **Type:** `(data: PathResolveData) => string` +- **Default output:** `` `${root}/${attachmentsDir}/${testFileDirectory}/${testFileName}/${arg}-${browserName}-${platform}${ext}` `` + +A function to customize where diff images are stored when screenshot comparisons +fail. Receives the same data object as +[`resolveScreenshotPath`](#browser-expect-tomatchscreenshot-resolvescreenshotpath). + +For example, to store diffs in a subdirectory of attachments: + +```ts +resolveDiffPath: ({ arg, attachmentsDir, browserName, ext, root, testFileName }) => + `${root}/${attachmentsDir}/screenshot-diffs/${testFileName}/${arg}-${browserName}${ext}` +``` diff --git a/docs/guide/browser/context.md b/docs/guide/browser/context.md index d74f8f15ee99..d8710a0cdfc1 100644 --- a/docs/guide/browser/context.md +++ b/docs/guide/browser/context.md @@ -87,6 +87,12 @@ export const page: { * Wrap an HTML element in a `Locator`. When querying for elements, the search will always return this element. */ elementLocator(element: Element): Locator + /** + * The iframe locator. This is a document locator that enters the iframe body + * and works similarly to the `page` object. + * **Warning:** At the moment, this is supported only by the `playwright` provider. + */ + frameLocator(iframeElement: Locator): FrameLocator /** * Locator APIs. See its documentation for more details. @@ -110,8 +116,37 @@ Note that `screenshot` will always return a base64 string if `save` is set to `f The `path` is also ignored in that case. ::: +### frameLocator + +```ts +function frameLocator(iframeElement: Locator): FrameLocator +``` + +The `frameLocator` method returns a `FrameLocator` instance that can be used to find elements inside the iframe. + +The frame locator is similar to `page`. It does not refer to the Iframe HTML element, but to the iframe's document. + +```ts +const frame = page.frameLocator( + page.getByTestId('iframe') +) + +await frame.getByText('Hello World').click() // ✅ +await frame.click() // ❌ Not available +``` + +::: danger IMPORTANT +At the moment, the `frameLocator` method is only supported by the `playwright` provider. + +The interactive methods (like `click` or `fill`) are always available on elements within the iframe, but assertions with `expect.element` require the iframe to have the [same-origin policy](https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy). +::: + ## `cdp` +```ts +function cdp(): CDPSession +``` + The `cdp` export returns the current Chrome DevTools Protocol session. It is mostly useful to library authors to build tools on top of it. ::: warning diff --git a/docs/guide/browser/index.md b/docs/guide/browser/index.md index 6effa68b8a07..adb543221465 100644 --- a/docs/guide/browser/index.md +++ b/docs/guide/browser/index.md @@ -99,10 +99,12 @@ To activate browser mode in your Vitest configuration, set the `browser.enabled` ```ts [vitest.config.ts] import { defineConfig } from 'vitest/config' +import { playwright } from '@vitest/browser/providers/playwright' + export default defineConfig({ test: { browser: { - provider: 'playwright', // or 'webdriverio' + provider: playwright(), enabled: true, // at least one instance is required instances: [ @@ -125,13 +127,14 @@ If you have not used Vite before, make sure you have your framework's plugin ins ```ts [react] import { defineConfig } from 'vitest/config' import react from '@vitejs/plugin-react' +import { playwright } from '@vitest/browser/providers/playwright' export default defineConfig({ plugins: [react()], test: { browser: { enabled: true, - provider: 'playwright', + provider: playwright(), instances: [ { browser: 'chromium' }, ], @@ -141,6 +144,7 @@ export default defineConfig({ ``` ```ts [vue] import { defineConfig } from 'vitest/config' +import { playwright } from '@vitest/browser/providers/playwright' import vue from '@vitejs/plugin-vue' export default defineConfig({ @@ -148,7 +152,7 @@ export default defineConfig({ test: { browser: { enabled: true, - provider: 'playwright', + provider: playwright(), instances: [ { browser: 'chromium' }, ], @@ -159,13 +163,14 @@ export default defineConfig({ ```ts [svelte] import { defineConfig } from 'vitest/config' import { svelte } from '@sveltejs/vite-plugin-svelte' +import { playwright } from '@vitest/browser/providers/playwright' export default defineConfig({ plugins: [svelte()], test: { browser: { enabled: true, - provider: 'playwright', + provider: playwright(), instances: [ { browser: 'chromium' }, ], @@ -176,13 +181,14 @@ export default defineConfig({ ```ts [solid] import { defineConfig } from 'vitest/config' import solidPlugin from 'vite-plugin-solid' +import { playwright } from '@vitest/browser/providers/playwright' export default defineConfig({ plugins: [solidPlugin()], test: { browser: { enabled: true, - provider: 'playwright', + provider: playwright(), instances: [ { browser: 'chromium' }, ], @@ -193,13 +199,14 @@ export default defineConfig({ ```ts [marko] import { defineConfig } from 'vitest/config' import marko from '@marko/vite' +import { playwright } from '@vitest/browser/providers/playwright' export default defineConfig({ plugins: [marko()], test: { browser: { enabled: true, - provider: 'playwright', + provider: playwright(), instances: [ { browser: 'chromium' }, ], @@ -207,6 +214,25 @@ export default defineConfig({ } }) ``` +```ts [qwik] +import { defineConfig } from 'vitest/config' +import { qwikVite } from '@builder.io/qwik/optimizer' +import { playwright } from '@vitest/browser/providers/playwright' + +// optional, run the tests in SSR mode +import { testSSR } from 'vitest-browser-qwik/ssr-plugin' + +export default defineConfig({ + plugins: [testSSR(), qwikVite()], + test: { + browser: { + enabled: true, + provider: playwright(), + instances: [{ browser: 'chromium' }] + }, + }, +}) +``` ::: If you need to run some tests using Node-based runner, you can define a [`projects`](/guide/projects) option with separate configurations for different testing strategies: @@ -215,6 +241,7 @@ If you need to run some tests using Node-based runner, you can define a [`projec ```ts [vitest.config.ts] import { defineConfig } from 'vitest/config' +import { playwright } from '@vitest/browser/providers/playwright' export default defineConfig({ test: { @@ -242,6 +269,7 @@ export default defineConfig({ name: 'browser', browser: { enabled: true, + provider: playwright(), instances: [ { browser: 'chromium' }, ], @@ -267,48 +295,6 @@ The browser option in Vitest depends on the provider. Vitest will fail, if you p - `webkit` - `chromium` -## TypeScript - -By default, TypeScript doesn't recognize providers options and extra `expect` properties. If you don't use any providers, make sure the `@vitest/browser/matchers` is referenced somewhere in your tests, [setup file](/config/#setupfiles) or a [config file](/config/) to pick up the extra `expect` definitions. If you are using custom providers, make sure to add `@vitest/browser/providers/playwright` or `@vitest/browser/providers/webdriverio` to the same file so TypeScript can pick up definitions for custom options: - -::: code-group -```ts [default] -/// -``` -```ts [playwright] -/// -``` -```ts [webdriverio] -/// -``` -::: - -Alternatively, you can also add them to `compilerOptions.types` field in your `tsconfig.json` file. Note that specifying anything in this field will disable [auto loading](https://www.typescriptlang.org/tsconfig/#types) of `@types/*` packages. - -::: code-group -```json [default] -{ - "compilerOptions": { - "types": ["@vitest/browser/matchers"] - } -} -``` -```json [playwright] -{ - "compilerOptions": { - "types": ["@vitest/browser/providers/playwright"] - } -} -``` -```json [webdriverio] -{ - "compilerOptions": { - "types": ["@vitest/browser/providers/webdriverio"] - } -} -``` -::: - ## Browser Compatibility Vitest uses [Vite dev server](https://vitejs.dev/guide/#browser-support) to run your tests, so we only support features specified in the [`esbuild.target`](https://vitejs.dev/config/shared-options.html#esbuild) option (`esnext` by default). @@ -352,10 +338,12 @@ Here's an example configuration enabling headless mode: ```ts [vitest.config.ts] import { defineConfig } from 'vitest/config' +import { playwright } from '@vitest/browser/providers/playwright' + export default defineConfig({ test: { browser: { - provider: 'playwright', + provider: playwright(), enabled: true, headless: true, }, @@ -411,6 +399,7 @@ Community packages are available for other frameworks: - [`vitest-browser-lit`](https://github.com/EskiMojo14/vitest-browser-lit) to render [lit](https://lit.dev) components - [`vitest-browser-preact`](https://github.com/JoviDeCroock/vitest-browser-preact) to render [preact](https://preactjs.com) components +- [`vitest-browser-qwik`](https://github.com/kunai-consulting/vitest-browser-qwik) to render [qwik](https://qwik.dev) components If your framework is not represented, feel free to create your own package - it is a simple wrapper around the framework renderer and `page.elementLocator` API. We will add a link to it on this page. Make sure it has a name starting with `vitest-browser-`. @@ -516,6 +505,21 @@ test('greeting appears on click', async () => { await expect.element(greeting).toBeInTheDocument() }) ``` +```tsx [qwik] +import { render } from 'vitest-browser-qwik' +import Greeting from './greeting' + +test('greeting appears on click', async () => { + // renderSSR and renderHook are also available + const screen = render() + + const button = screen.getByRole('button') + await button.click() + const greeting = screen.getByText(/hello world/iu) + + await expect.element(greeting).toBeInTheDocument() +}) +``` ::: Vitest doesn't support all frameworks out of the box, but you can use external tools to run tests with these frameworks. We also encourage the community to create their own `vitest-browser` wrappers - if you have one, feel free to add it to the examples above. diff --git a/docs/guide/browser/interactivity-api.md b/docs/guide/browser/interactivity-api.md index c3778cf9b3b0..b9a6c2de7033 100644 --- a/docs/guide/browser/interactivity-api.md +++ b/docs/guide/browser/interactivity-api.md @@ -12,16 +12,7 @@ import { userEvent } from '@vitest/browser/context' await userEvent.click(document.querySelector('.button')) ``` -Almost every `userEvent` method inherits its provider options. To see all available options in your IDE, add `webdriver` or `playwright` types (depending on your provider) to your [setup file](/config/#setupfile) or a [config file](/config/) (depending on what is in `included` in your `tsconfig.json`): - -::: code-group -```ts [playwright] -/// -``` -```ts [webdriverio] -/// -``` -::: +Almost every `userEvent` method inherits its provider options. ## userEvent.setup diff --git a/docs/guide/browser/locators.md b/docs/guide/browser/locators.md index ae108c7168a3..e442e9dc2259 100644 --- a/docs/guide/browser/locators.md +++ b/docs/guide/browser/locators.md @@ -234,7 +234,7 @@ function getByLabelText( Creates a locator capable of finding an element that has an associated label. -The `page.getByLabelText('Username')` locator will find every input in the example bellow: +The `page.getByLabelText('Username')` locator will find every input in the example below: ```html // for/htmlFor relationship between label and form element id @@ -956,6 +956,25 @@ test('works correctly', async () => { ``` ::: +### length + +This getter returns a number of elements that this locator is matching. It is equivalent to calling `locator.elements().length`. + +Consider the following DOM structure: + +```html + + +``` + +This property will always succeed: + +```ts +page.getByRole('button').length // ✅ 2 +page.getByRole('button', { title: 'Click Me!' }).length // ✅ 1 +page.getByRole('alert').length // ✅ 0 +``` + ## Custom Locators 3.2.0 advanced {#custom-locators} You can extend built-in locators API by defining an object of locator factories. These methods will exist as methods on the `page` object and any created locator. @@ -1004,7 +1023,7 @@ declare module '@vitest/browser/context' { } ``` -If the method is called on the global `page` object, then selector will be applied to the whole page. In the example bellow, `getByArticleTitle` will find all elements with an attribute `data-title` with the value of `title`. However, if the method is called on the locator, then it will be scoped to that locator. +If the method is called on the global `page` object, then selector will be applied to the whole page. In the example below, `getByArticleTitle` will find all elements with an attribute `data-title` with the value of `title`. However, if the method is called on the locator, then it will be scoped to that locator. ```html
diff --git a/docs/guide/browser/multiple-setups.md b/docs/guide/browser/multiple-setups.md index 811227f30a81..6113e71b7a8a 100644 --- a/docs/guide/browser/multiple-setups.md +++ b/docs/guide/browser/multiple-setups.md @@ -10,11 +10,13 @@ You can use the `browser.instances` field to specify options for different brows ```ts [vitest.config.ts] import { defineConfig } from 'vitest/config' +import { playwright } from '@vitest/browser/providers/playwright' + export default defineConfig({ test: { browser: { enabled: true, - provider: 'playwright', + provider: playwright(), headless: true, instances: [ { browser: 'chromium' }, @@ -33,11 +35,13 @@ You can also specify different config options independently from the browser (al ::: code-group ```ts [vitest.config.ts] import { defineConfig } from 'vitest/config' +import { playwright } from '@vitest/browser/providers/playwright' + export default defineConfig({ test: { browser: { enabled: true, - provider: 'playwright', + provider: playwright(), headless: true, instances: [ { diff --git a/docs/guide/browser/playwright.md b/docs/guide/browser/playwright.md index 2601fcc666b2..c261f5fad67d 100644 --- a/docs/guide/browser/playwright.md +++ b/docs/guide/browser/playwright.md @@ -1,62 +1,59 @@ # Configuring Playwright -By default, TypeScript doesn't recognize providers options and extra `expect` properties. Make sure to reference `@vitest/browser/providers/playwright` so TypeScript can pick up definitions for custom options: +To run tests using playwright, you need to specify it in the `test.browser.provider` property in your config: -```ts [vitest.shims.d.ts] -/// -``` - -Alternatively, you can also add it to `compilerOptions.types` field in your `tsconfig.json` file. Note that specifying anything in this field will disable [auto loading](https://www.typescriptlang.org/tsconfig/#types) of `@types/*` packages. - -```json [tsconfig.json] -{ - "compilerOptions": { - "types": ["@vitest/browser/providers/playwright"] - } -} -``` - -Vitest opens a single page to run all tests in the same file. You can configure the `launch`, `connect` and `context` properties in `instances`: - -```ts{9-11} [vitest.config.ts] +```ts [vitest.config.js] +import { playwright } from '@vitest/browser/providers/playwright' import { defineConfig } from 'vitest/config' export default defineConfig({ test: { browser: { - instances: [ - { - browser: 'firefox', - launch: {}, - connect: {}, - context: {}, - }, - ], + provider: playwright(), + instances: [{ browser: 'chromium' }] }, }, }) ``` -::: warning -Before Vitest 3, these options were located on `test.browser.providerOptions` property: +Vitest opens a single page to run all tests in the same file. You can configure the `launch`, `connect` and `context` when calling `playwright` at the top level or inside instances: + +```ts{7-15,22-27} [vitest.config.js] +import { playwright } from '@vitest/browser/providers/playwright' +import { defineConfig } from 'vitest/config' -```ts [vitest.config.ts] export default defineConfig({ test: { browser: { - providerOptions: { - launch: {}, - context: {}, - }, + // shared provider options between all instances + provider: playwright({ + launchOptions: { + slowMo: 50, + channel: 'chrome-beta', + }, + actionTimeout: 5_000, + }), + instances: [ + { browser: 'chromium' }, + { + browser: 'firefox', + // overriding options only for a single instance + // this will NOT merge options with the parent one + provider: playwright({ + launchOptions: { + firefoxUserPrefs: { + 'browser.startup.homepage': 'https://example.com', + }, + }, + }) + } + ], }, }, }) ``` -`providerOptions` is deprecated in favour of `instances`. -::: - -## launch +## launchOptions These options are directly passed down to `playwright[browser].launch` command. You can read more about the command and available arguments in the [Playwright documentation](https://playwright.dev/docs/api/class-browsertype#browser-type-launch). @@ -66,7 +63,7 @@ Vitest will ignore `launch.headless` option. Instead, use [`test.browser.headles Note that Vitest will push debugging flags to `launch.args` if [`--inspect`](/guide/cli#inspect) is enabled. ::: -## connect 3.2.0 {#connect} +## connectOptions These options are directly passed down to `playwright[browser].connect` command. You can read more about the command and available arguments in the [Playwright documentation](https://playwright.dev/docs/api/class-browsertype#browser-type-connect). @@ -74,9 +71,9 @@ These options are directly passed down to `playwright[browser].connect` command. Since this command connects to an existing Playwright server, any `launch` options will be ignored. ::: -## context +## contextOptions -Vitest creates a new context for every test file by calling [`browser.newContext()`](https://playwright.dev/docs/api/class-browsercontext). You can configure this behaviour by specifying [custom arguments](https://playwright.dev/docs/api/class-apirequest#api-request-new-context). +Vitest creates a new context for every test file by calling [`browser.newContext()`](https://playwright.dev/docs/api/class-browsercontext). You can configure this behaviour by specifying [custom arguments](https://playwright.dev/docs/api/class-browser#browser-new-context). ::: tip Note that the context is created for every _test file_, not every _test_ like in playwright test runner. @@ -88,9 +85,9 @@ Vitest always sets `ignoreHTTPSErrors` to `true` in case your server is served v It is also recommended to use [`test.browser.viewport`](/guide/browser/config#browser-headless) instead of specifying it here as it will be lost when tests are running in headless mode. ::: -## `actionTimeout` 3.0.0 +## `actionTimeout` -- **Default:** no timeout, 1 second before 3.0.0 +- **Default:** no timeout This value configures the default timeout it takes for Playwright to wait until all accessibility checks pass and [the action](/guide/browser/interactivity-api) is actually done. diff --git a/docs/guide/browser/visual-regression-testing.md b/docs/guide/browser/visual-regression-testing.md new file mode 100644 index 000000000000..b6e3b7827d33 --- /dev/null +++ b/docs/guide/browser/visual-regression-testing.md @@ -0,0 +1,719 @@ +--- +title: Visual Regression Testing +outline: [2, 3] +--- + +# Visual Regression Testing + +Vitest can run visual regression tests out of the box. It captures screenshots +of your UI components and pages, then compares them against reference images to +detect unintended visual changes. + +Unlike functional tests that verify behavior, visual tests catch styling issues, +layout shifts, and rendering problems that might otherwise go unnoticed without +thorough manual testing. + +## Why Visual Regression Testing? + +Visual bugs don’t throw errors, they just look wrong. That’s where visual +testing comes in. + +- That button still submits the form... but why is it hot pink now? +- The text fits perfectly... until someone views it on mobile +- Everything works great... except those two containers are out of viewport +- That careful CSS refactor works... but broke the layout on a page no one tests + +Visual regression testing acts as a safety net for your UI, automatically +catching these visual changes before they reach production. + +## Getting Started + +::: warning Browser Rendering Differences +Visual regression tests are **inherently unstable across different +environments**. Screenshots will look different on different machines because +of: + +- Font rendering (the big one. Windows, macOS, Linux, they all render text +differently) +- GPU drivers and hardware acceleration +- Whether you're running headless or not +- Browser settings and versions +- ...and honestly, sometimes just the phase of the moon + +That's why Vitest includes the browser and platform in screenshot names (like +`button-chromium-darwin.png`). + +For stable tests, use the same environment everywhere. We **strongly recommend** +cloud services like +[Microsoft Playwright Testing](https://azure.microsoft.com/en-us/products/playwright-testing) +or [Docker containers](https://playwright.dev/docs/docker). +::: + +Visual regression testing in Vitest can be done through the +[`toMatchScreenshot` assertion](/guide/browser/assertion-api.html#tomatchscreenshot): + +```ts +import { expect, test } from 'vitest' +import { page } from '@vitest/browser/context' + +test('hero section looks correct', async () => { + // ...the rest of the test + + // capture and compare screenshot + await expect(page.getByTestId('hero')).toMatchScreenshot('hero-section') +}) +``` + +### Creating References + +When you run a visual test for the first time, Vitest creates a reference (also +called baseline) screenshot and fails the test with the following error message: + +``` +expect(element).toMatchScreenshot() + +No existing reference screenshot found; a new one was created. Review it before running tests again. + +Reference screenshot: + tests/__screenshots__/hero.test.ts/hero-section-chromium-darwin.png +``` + +This is normal. Check that the screenshot looks right, then run the test again. +Vitest will now compare future runs against this baseline. + +::: tip +Reference screenshots live in `__screenshots__` folders next to your tests. +**Don't forget to commit them!** +::: + +### Screenshot Organization + +By default, screenshots are organized as: + +``` +. +├── __screenshots__ +│ └── test-file.test.ts +│ ├── test-name-chromium-darwin.png +│ ├── test-name-firefox-linux.png +│ └── test-name-webkit-win32.png +└── test-file.test.ts +``` + +The naming convention includes: +- **Test name**: either the first argument of the `toMatchScreenshot()` call, +or automatically generated from the test's name. +- **Browser name**: `chrome`, `chromium`, `firefox` or `webkit`. +- **Platform**: `aix`, `darwin`, `freebsd`, `linux`, `openbsd`, `sunos`, or +`win32`. + +This ensures screenshots from different environments don't overwrite each other. + +### Updating References + +When you intentionally change your UI, you'll need to update the reference +screenshots: + +```bash +$ vitest --update +``` + +Review updated screenshots before committing to make sure changes are +intentional. + +## Configuring Visual Tests + +### Global Configuration + +Configure visual regression testing defaults in your +[Vitest config](/guide/browser/config#browser-expect-tomatchscreenshot): + +```ts [vitest.config.ts] +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + browser: { + expect: { + toMatchScreenshot: { + comparatorName: 'pixelmatch', + comparatorOptions: { + // 0-1, how different can colors be? + threshold: 0.2, + // 1% of pixels can differ + allowedMismatchedPixelRatio: 0.01, + }, + }, + }, + }, + }, +}) +``` + +### Per-Test Configuration + +Override global settings for specific tests: + +```ts +await expect(element).toMatchScreenshot('button-hover', { + comparatorName: 'pixelmatch', + comparatorOptions: { + // more lax comparison for text-heavy elements + allowedMismatchedPixelRatio: 0.1, + }, +}) +``` + +## Best Practices + +### Test Specific Elements + +Unless you explicitly want to test the whole page, prefer capturing specific +components to reduce false positives: + +```ts +// ❌ Captures entire page; prone to unrelated changes +await expect(page).toMatchScreenshot() + +// ✅ Captures only the component under test +await expect(page.getByTestId('product-card')).toMatchScreenshot() +``` + +### Handle Dynamic Content + +Dynamic content like timestamps, user data, or random values will cause tests +to fail. You can either mock the sources of dynamic content or mask them when +using the Playwright provider by using the +[`mask` option](https://playwright.dev/docs/api/class-page#page-screenshot-option-mask) +in `screenshotOptions`. + +```ts +await expect(page.getByTestId('profile')).toMatchScreenshot({ + screenshotOptions: { + mask: [page.getByTestId('last-seen')], + }, +}) +``` + +### Disable Animations + +Animations can cause flaky tests. Disable them during testing by injecting +a custom CSS snippet: + +```css +*, *::before, *::after { + animation-duration: 0s !important; + animation-delay: 0s !important; + transition-duration: 0s !important; + transition-delay: 0s !important; +} +``` + +::: tip +When using the Playwright provider, animations are automatically disabled +when using the assertion: the `animations` option's value in `screenshotOptions` +is set to `"disabled"` by default. +::: + +### Set Appropriate Thresholds + +Tuning thresholds is tricky. It depends on the content, test environment, +what's acceptable for your app, and might also change based on the test. + +Vitest does not set a default for the mismatching pixels, that's up for the +user to decide based on their needs. The recommendation is to use +`allowedMismatchedPixelRatio`, so that the threshold is computed on the size +of the screenshot and not a fixed number. + +When setting both `allowedMismatchedPixelRatio` and +`allowedMismatchedPixels`, Vitest uses whichever limit is stricter. + +### Set consistent viewport sizes + +As the browser instance might have a different default size, it's best to +set a specific viewport size, either on the test or the instance +configuration: + +```ts +await page.viewport(1280, 720) +``` + +```ts [vitest.config.ts] +import { playwright } from '@vitest/browser/providers/playwright' +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + browser: { + enabled: true, + provider: playwright(), + instances: [ + { + browser: 'chromium', + viewport: { width: 1280, height: 720 }, + }, + ], + }, + }, +}) +``` + +### Use Git LFS + +Store reference screenshots in +[Git LFS](https://github.com/git-lfs/git-lfs?tab=readme-ov-file) if you plan to +have a large test suite. + +## Debugging Failed Tests + +When a visual test fails, Vitest provides three images to help debug: + +1. **Reference screenshot**: the expected baseline image +1. **Actual screenshot**: what was captured during the test +1. **Diff image**: highlights the differences, but this might not get generated + +You'll see something like: + +``` +expect(element).toMatchScreenshot() + +Screenshot does not match the stored reference. +245 pixels (ratio 0.03) differ. + +Reference screenshot: + tests/__screenshots__/button.test.ts/button-chromium-darwin.png + +Actual screenshot: + tests/.vitest-attachments/button.test.ts/button-chromium-darwin-actual.png + +Diff image: + tests/.vitest-attachments/button.test.ts/button-chromium-darwin-diff.png +``` + +### Understanding the diff image + +- **Red pixels** are areas that differ between reference and actual +- **Yellow pixels** are anti-aliasing differences (when anti-alias is not ignored) +- **Transparent/original** are unchanged areas + +:::tip +If the diff is mostly red, something's really wrong. If it's speckled with a +few red pixels around text, you probably just need to bump your threshold. +::: + +## Common Issues and Solutions + +### False Positives from Font Rendering + +Font availability and rendering varies significantly between systems. Some +possible solutions might be to: + +- Use web fonts and wait for them to load: + + ```ts + // wait for fonts to load + await document.fonts.ready + + // continue with your tests + ``` + +- Increase comparison threshold for text-heavy areas: + + ```ts + await expect(page.getByTestId('article-summary')).toMatchScreenshot({ + comparatorName: 'pixelmatch', + comparatorOptions: { + // 10% of the pixels are allowed to change + allowedMismatchedPixelRatio: 0.1, + }, + }) + ``` + +- Use a cloud service or containerized environment for consistent font rendering. + +### Flaky Tests or Different Screenshot Sizes + +If tests pass and fail randomly, or if screenshots have different dimensions +between runs: + +- Wait for everything to load, including loading indicators +- Set explicit viewport sizes: `await page.viewport(1920, 1080)` +- Check for responsive behavior at viewport boundaries +- Check for unintended animations or transitions +- Increase test timeout for large screenshots +- Use a cloud service or containerized environment + +## Visual Regression Testing for Teams + +Remember when we mentioned visual tests need a stable environment? Well, here's +the thing: your local machine isn't it. + +For teams, you've basically got three options: + +1. **Self-hosted runners**, complex to set up, painful to maintain +1. **GitHub Actions**, free (for open source), works with any provider +1. **Cloud services**, like +[Microsoft Playwright Testing](https://azure.microsoft.com/en-us/products/playwright-testing), +built for this exact problem + +We'll focus on options 2 and 3 since they're the quickest to get running. + +To be upfront, the main trade-offs for each are: + +- **GitHub Actions**: visual tests only run in CI (developers can't run them +locally) +- **Microsoft's service**: works everywhere but costs money and only works +with Playwright + +:::: tabs key:vrt-for-teams +=== GitHub Actions + +The trick here is keeping visual tests separate from your regular tests, +otherwise, you'll waste hours checking failing logs of screenshot mismatches. + +#### Organizing Your Tests + +First, isolate your visual tests. Stick them in a `visual` folder (or whatever +makes sense for your project): + +```json [package.json] +{ + "scripts": { + "test:unit": "vitest --exclude tests/visual/*.test.ts", + "test:visual": "vitest tests/visual/*.test.ts" + } +} +``` + +Now developers can run `npm run test:unit` locally without visual tests getting +in the way. Visual tests stay in CI where the environment is consistent. + +::: tip Alternative +Not a fan of glob patterns? You could also use separate +[Test Projects](/guide/projects) instead and run them using: + +- `vitest --project unit` +- `vitest --project visual` +::: + +#### CI Setup + +Your CI needs browsers installed. How you do this depends on your provider: + +::: tabs key:provider +== Playwright + +[Playwright](https://npmjs.com/package/playwright) makes this easy. Just pin +your version and add this before running tests: + +```yaml [.github/workflows/ci.yml] +# ...the rest of the workflow +- name: Install Playwright Browsers + run: npx --no playwright install --with-deps --only-shell +``` + +== WebdriverIO + +[WebdriverIO](https://www.npmjs.com/package/webdriverio) expects you to bring +your own browsers. The folks at +[@browser-actions](https://github.com/browser-actions) have your back: + +```yaml [.github/workflows/ci.yml] +# ...the rest of the workflow +- uses: browser-actions/setup-chrome@v1 + with: + chrome-version: 120 +``` + +::: + +Then run your visual tests: + +```yaml [.github/workflows/ci.yml] +# ...the rest of the workflow +# ...browser setup +- name: Visual Regression Testing + run: npm run test:visual +``` + +#### The Update Workflow + +Here's where it gets interesting. You don't want to update screenshots on every +PR automatically *(chaos!)*. Instead, create a +manually-triggered workflow that developers can run when they intentionally +change the UI. + +The workflow below: +- Only runs on feature branches (never on main) +- Credits the person who triggered it as co-author +- Prevents concurrent runs on the same branch +- Shows a nice summary: + - **When screenshots changed**, it lists what changed + + Action summary after updates + Action summary after updates + + - **When nothing changed**, well, it tells you that too + + Action summary after no updates + Action summary after no updates + +::: tip +This is just one approach. Some teams prefer PR comments (`/update-screenshots`), +others use labels. Adjust it to fit your workflow! + +The important part is having a controlled way to update baselines. +::: + +```yaml [.github/workflows/update-screenshots.yml] +name: Update Visual Regression Screenshots + +on: + workflow_dispatch: # manual trigger only + +env: + AUTHOR_NAME: 'github-actions[bot]' + AUTHOR_EMAIL: '41898282+github-actions[bot]@users.noreply.github.com' + COMMIT_MESSAGE: | + test: update visual regression screenshots + + Co-authored-by: ${{ github.actor }} <${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com> + +jobs: + update-screenshots: + runs-on: ubuntu-24.04 + + # safety first: don't run on main + if: github.ref_name != github.event.repository.default_branch + + # one at a time per branch + concurrency: + group: visual-regression-screenshots@${{ github.ref_name }} + cancel-in-progress: true + + permissions: + contents: write # needs to push changes + + steps: + - name: Checkout selected branch + uses: actions/checkout@v4 + with: + ref: ${{ github.ref_name }} + # use PAT if triggering other workflows + # token: ${{ secrets.GITHUB_TOKEN }} + + - name: Configure Git + run: | + git config --global user.name "${{ env.AUTHOR_NAME }}" + git config --global user.email "${{ env.AUTHOR_EMAIL }}" + + # your setup steps here (node, pnpm, whatever) + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 24 + + - name: Install dependencies + run: npm ci + + - name: Install Playwright Browsers + run: npx --no playwright install --with-deps --only-shell + + # the magic happens below 🪄 + - name: Update Visual Regression Screenshots + run: npm run test:visual --update + + # check what changed + - name: Check for changes + id: check_changes + run: | + CHANGED_FILES=$(git status --porcelain | awk '{print $2}') + if [ "${CHANGED_FILES:+x}" ]; then + echo "changes=true" >> $GITHUB_OUTPUT + echo "Changes detected" + + # save the list for the summary + echo "changed_files<> $GITHUB_OUTPUT + echo "$CHANGED_FILES" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + echo "changed_count=$(echo "$CHANGED_FILES" | wc -l)" >> $GITHUB_OUTPUT + else + echo "changes=false" >> $GITHUB_OUTPUT + echo "No changes detected" + fi + + # commit if there are changes + - name: Commit changes + if: steps.check_changes.outputs.changes == 'true' + run: | + git add -A + git commit -m "${{ env.COMMIT_MESSAGE }}" + + - name: Push changes + if: steps.check_changes.outputs.changes == 'true' + run: git push origin ${{ github.ref_name }} + + # pretty summary for humans + - name: Summary + run: | + if [[ "${{ steps.check_changes.outputs.changes }}" == "true" ]]; then + echo "### 📸 Visual Regression Screenshots Updated" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Successfully updated **${{ steps.check_changes.outputs.changed_count }}** screenshot(s) on \`${{ github.ref_name }}\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "#### Changed Files:" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "${{ steps.check_changes.outputs.changed_files }}" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "✅ The updated screenshots have been committed and pushed. Your visual regression baseline is now up to date!" >> $GITHUB_STEP_SUMMARY + else + echo "### ℹ️ No Screenshot Updates Required" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "The visual regression test command ran successfully but no screenshots needed updating." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "All screenshots are already up to date! 🎉" >> $GITHUB_STEP_SUMMARY + fi +``` + +=== Microsoft Playwright Testing + +Your tests stay local, only the browsers run in the cloud. It's Playwright's +remote browser feature, but Microsoft handles all the infrastructure. + +#### Organizing Your Tests + +Keep visual tests separate to control costs. Only tests that actually take +screenshots should use the service. + +The cleanest approach is using [Test Projects](/guide/projects): + +```ts [vitest.config.ts] +import { env } from 'node:process' +import { defineConfig } from 'vitest/config' +import { playwright } from '@vitest/browser/providers/playwright' + +export default defineConfig({ + // ...global Vite config + tests: { + // ...global Vitest config + projects: [ + { + extends: true, + test: { + name: 'unit', + include: ['tests/**/*.test.ts'], + // regular config, can use local browsers + }, + }, + { + extends: true, + test: { + name: 'visual', + // or you could use a different suffix, e.g.,: `tests/**/*.visual.ts?(x)` + include: ['visual-regression-tests/**/*.test.ts?(x)'], + browser: { + enabled: true, + provider: playwright(), + headless: true, + instances: [ + { + browser: 'chromium', + viewport: { width: 2560, height: 1440 }, + connect: { + wsEndpoint: `${env.PLAYWRIGHT_SERVICE_URL}?cap=${JSON.stringify({ + os: 'linux', // always use Linux for consistency + // helps identifying runs in the service's dashboard + runId: `Vitest ${env.CI ? 'CI' : 'local'} run @${new Date().toISOString()}`, + })}`, + options: { + exposeNetwork: '', + headers: { + 'x-mpt-access-key': env.PLAYWRIGHT_SERVICE_ACCESS_TOKEN, + }, + timeout: 30_000, + }, + }, + }, + ], + }, + }, + }, + ], + }, +}) +``` + +The service gives you two environment variables: + +- `PLAYWRIGHT_SERVICE_URL` tells Playwright where to connect +- `PLAYWRIGHT_SERVICE_ACCESS_TOKEN` is your auth token + +::: danger Keep that Token Secret! +Never commit `PLAYWRIGHT_SERVICE_ACCESS_TOKEN` to your repository. Anyone with +the token can rack up your bill. Use environment variables locally and secrets +in CI. +::: + +Then split your `test` script like this: + +```json [package.json] +{ + "scripts": { + "test:visual": "vitest --project visual", + "test:unit": "vitest --project unit" + } +} +``` + +#### Running Tests + +```bash +# Local development +npm run test:unit # free, runs locally +npm run test:visual # uses cloud browsers + +# Update screenshots +npm run test:visual -- --update +``` + +The best part of this approach is that it just works: + +- **Consistent screenshots**, everyone uses the same cloud browsers +- **Works locally**, developers can run and update visual tests on their machines +- **Pay for what you use**, only visual tests consume service minutes +- **No Docker or workflow setups needed**, nothing to manage or maintain + +#### CI Setup + +In your CI, add the secrets: + +```yaml +env: + PLAYWRIGHT_SERVICE_URL: ${{ vars.PLAYWRIGHT_SERVICE_URL }} + PLAYWRIGHT_SERVICE_ACCESS_TOKEN: ${{ secrets.PLAYWRIGHT_SERVICE_ACCESS_TOKEN }} +``` + +Then run your tests like normal. The service handles the rest. + +:::: + +### So Which One? + +Both approaches work. The real question is what pain points matter most to your +team. + +If you're already deep in the GitHub ecosystem, GitHub Actions is hard to beat. +Free for open source, works with any browser provider, and you control +everything. + +The downside? That "works on my machine" conversation when someone generates +screenshots locally and they don't match CI expectations anymore. + +The cloud service makes sense if developers need to run visual tests locally. + +Some teams have designers checking their work or developers who prefer catching +issues before pushing. It allows skipping the push-wait-check-fix-push cycle. + +Still on the fence? Start with GitHub Actions. You can always add the cloud +service later if local testing becomes a pain point. diff --git a/docs/guide/browser/webdriverio.md b/docs/guide/browser/webdriverio.md index b0afdf789713..409beb3a9212 100644 --- a/docs/guide/browser/webdriverio.md +++ b/docs/guide/browser/webdriverio.md @@ -4,66 +4,59 @@ If you do not already use WebdriverIO in your project, we recommend starting with [Playwright](/guide/browser/playwright) as it is easier to configure and has more flexible API. ::: -By default, TypeScript doesn't recognize providers options and extra `expect` properties. Make sure to reference `@vitest/browser/providers/webdriverio` so TypeScript can pick up definitions for custom options: +To run tests using WebdriverIO, you need to specify it in the `test.browser.provider` property in your config: -```ts [vitest.shims.d.ts] -/// -``` - -Alternatively, you can also add it to `compilerOptions.types` field in your `tsconfig.json` file. Note that specifying anything in this field will disable [auto loading](https://www.typescriptlang.org/tsconfig/#types) of `@types/*` packages. - -```json [tsconfig.json] -{ - "compilerOptions": { - "types": ["@vitest/browser/providers/webdriverio"] - } -} -``` - -Vitest opens a single page to run all tests in the same file. You can configure any property specified in `RemoteOptions` in `instances`: - -```ts{9-12} [vitest.config.ts] +```ts [vitest.config.js] +import { webdriverio } from '@vitest/browser/providers/webdriverio' import { defineConfig } from 'vitest/config' export default defineConfig({ test: { browser: { - instances: [ - { - browser: 'chrome', - capabilities: { - browserVersion: 86, - platformName: 'Windows 10', - }, - }, - ], + provider: webdriverio(), + instances: [{ browser: 'chrome' }] }, }, }) ``` -::: warning -Before Vitest 3, these options were located on `test.browser.providerOptions` property: +Vitest opens a single page to run all tests in the same file. You can configure all the parameters that [`remote`](https://webdriver.io/docs/api/modules/#remoteoptions-modifier) function accepts: + +```ts{8-12,19-23} [vitest.config.js] +import { webdriverio } from '@vitest/browser/providers/webdriverio' +import { defineConfig } from 'vitest/config' -```ts [vitest.config.ts] export default defineConfig({ test: { browser: { - providerOptions: { - capabilities: {}, - }, + // shared provider options between all instances + provider: webdriverio({ + capabilities: { + browserVersion: '82', + }, + }), + instances: [ + { browser: 'chrome' }, + { + browser: 'firefox', + // overriding options only for a single instance + // this will NOT merge options with the parent one + provider: webdriverio({ + 'moz:firefoxOptions': { + args: ['--disable-gpu'], + }, + }) + } + ], }, }, }) ``` -`providerOptions` is deprecated in favour of `instances`. -::: - You can find most available options in the [WebdriverIO documentation](https://webdriver.io/docs/configuration/). Note that Vitest will ignore all test runner options because we only use `webdriverio`'s browser capabilities. ::: tip Most useful options are located on `capabilities` object. WebdriverIO allows nested capabilities, but Vitest will ignore those options because we rely on a different mechanism to spawn several browsers. -Note that Vitest will ignore `capabilities.browserName`. Use [`test.browser.instances.name`](/guide/browser/config#browser-capabilities-name) instead. +Note that Vitest will ignore `capabilities.browserName`. Use [`test.browser.instances.browser`](/guide/browser/config#browser-capabilities-name) instead. ::: diff --git a/docs/guide/cli-generated.md b/docs/guide/cli-generated.md index a7ab176d6cfd..994418d7a024 100644 --- a/docs/guide/cli-generated.md +++ b/docs/guide/cli-generated.md @@ -89,7 +89,7 @@ Hide logs for skipped tests - **CLI:** `--reporter ` - **Config:** [reporters](/config/#reporters) -Specify reporters (default, basic, blob, verbose, dot, json, tap, tap-flat, junit, hanging-process, github-actions) +Specify reporters (default, blob, verbose, dot, json, tap, tap-flat, junit, tree, hanging-process, github-actions) ### outputFile @@ -98,13 +98,6 @@ Specify reporters (default, basic, blob, verbose, dot, json, tap, tap-flat, juni Write test results to a file when supporter reporter is also specified, use cac's dot notation for individual outputs of multiple reporters (example: `--outputFile.tap=./tap.txt`) -### coverage.all - -- **CLI:** `--coverage.all` -- **Config:** [coverage.all](/config/#coverage-all) - -Whether to include all files, including the untested ones into report - ### coverage.provider - **CLI:** `--coverage.provider ` @@ -124,21 +117,14 @@ Enables coverage collection. Can be overridden using the `--coverage` CLI option - **CLI:** `--coverage.include ` - **Config:** [coverage.include](/config/#coverage-include) -Files included in coverage as glob patterns. May be specified more than once when using multiple patterns (default: `**`) +Files included in coverage as glob patterns. May be specified more than once when using multiple patterns. By default only files covered by tests are included. ### coverage.exclude - **CLI:** `--coverage.exclude ` - **Config:** [coverage.exclude](/config/#coverage-exclude) -Files to be excluded in coverage. May be specified more than once when using multiple extensions (default: Visit [`coverage.exclude`](https://vitest.dev/config/#coverage-exclude)) - -### coverage.extension - -- **CLI:** `--coverage.extension ` -- **Config:** [coverage.extension](/config/#coverage-extension) - -Extension to be included in coverage. May be specified more than once when using multiple extensions (default: `[".js", ".cjs", ".mjs", ".ts", ".mts", ".tsx", ".jsx", ".vue", ".svelte"]`) +Files to be excluded in coverage. May be specified more than once when using multiple extensions. ### coverage.clean @@ -205,7 +191,7 @@ Check thresholds per file. See `--coverage.thresholds.lines`, `--coverage.thresh ### coverage.thresholds.autoUpdate -- **CLI:** `--coverage.thresholds.autoUpdate` +- **CLI:** `--coverage.thresholds.autoUpdate ` - **Config:** [coverage.thresholds.autoUpdate](/config/#coverage-thresholds-autoupdate) Update threshold values: "lines", "functions", "branches" and "statements" to configuration file when current coverage is above the configured thresholds (default: `false`) @@ -286,13 +272,6 @@ High and low watermarks for functions in the format of `,` Override Vite mode (default: `test` or `benchmark`) -### workspace - -- **CLI:** `--workspace ` -- **Config:** [workspace](/config/#workspace) - -[deprecated] Path to a workspace configuration file - ### isolate - **CLI:** `--isolate` @@ -362,13 +341,6 @@ Set to true to exit if port is already in use, instead of automatically trying t Provider used to run browser tests. Some browsers are only available for specific providers. Can be "webdriverio", "playwright", "preview", or the path to a custom provider. Visit [`browser.provider`](https://vitest.dev/guide/browser/config.html#browser-provider) for more information (default: `"preview"`) -### browser.providerOptions - -- **CLI:** `--browser.providerOptions ` -- **Config:** [browser.providerOptions](/guide/browser/config#browser-provideroptions) - -Options that are passed down to a browser provider. Visit [`browser.providerOptions`](https://vitest.dev/config/#browser-provideroptions) for more information - ### browser.isolate - **CLI:** `--browser.isolate` @@ -397,6 +369,13 @@ Should browser test files run in parallel. Use `--browser.fileParallelism=false` If connection to the browser takes longer, the test suite will fail (default: `60_000`) +### browser.trackUnhandledErrors + +- **CLI:** `--browser.trackUnhandledErrors` +- **Config:** [browser.trackUnhandledErrors](/guide/browser/config#browser-trackunhandlederrors) + +Control if Vitest catches uncaught exceptions so they can be reported (default: `true`) + ### pool - **CLI:** `--pool ` @@ -425,13 +404,6 @@ Run tests inside a single thread (default: `false`) Maximum number or percentage of threads to run tests in -### poolOptions.threads.minThreads - -- **CLI:** `--poolOptions.threads.minThreads ` -- **Config:** [poolOptions.threads.minThreads](/config/#pooloptions-threads-minthreads) - -Minimum number or percentage of threads to run tests in - ### poolOptions.threads.useAtomics - **CLI:** `--poolOptions.threads.useAtomics` @@ -460,13 +432,6 @@ Run tests inside a single thread (default: `false`) Maximum number or percentage of threads to run tests in -### poolOptions.vmThreads.minThreads - -- **CLI:** `--poolOptions.vmThreads.minThreads ` -- **Config:** [poolOptions.vmThreads.minThreads](/config/#pooloptions-vmthreads-minthreads) - -Minimum number or percentage of threads to run tests in - ### poolOptions.vmThreads.useAtomics - **CLI:** `--poolOptions.vmThreads.useAtomics` @@ -502,13 +467,6 @@ Run tests inside a single child_process (default: `false`) Maximum number or percentage of processes to run tests in -### poolOptions.forks.minForks - -- **CLI:** `--poolOptions.forks.minForks ` -- **Config:** [poolOptions.forks.minForks](/config/#pooloptions-forks-minforks) - -Minimum number or percentage of processes to run tests in - ### poolOptions.vmForks.isolate - **CLI:** `--poolOptions.vmForks.isolate` @@ -530,13 +488,6 @@ Run tests inside a single child_process (default: `false`) Maximum number or percentage of processes to run tests in -### poolOptions.vmForks.minForks - -- **CLI:** `--poolOptions.vmForks.minForks ` -- **Config:** [poolOptions.vmForks.minForks](/config/#pooloptions-vmforks-minforks) - -Minimum number or percentage of processes to run tests in - ### poolOptions.vmForks.memoryLimit - **CLI:** `--poolOptions.vmForks.memoryLimit ` @@ -558,13 +509,6 @@ Should all test files run in parallel. Use `--no-file-parallelism` to disable (d Maximum number or percentage of workers to run tests in -### minWorkers - -- **CLI:** `--minWorkers ` -- **Config:** [minWorkers](/config/#minworkers) - -Minimum number or percentage of workers to run tests in - ### environment - **CLI:** `--environment ` @@ -950,4 +894,4 @@ Use `bundle` to bundle the config with esbuild or `runner` (experimental) to pro - **CLI:** `--standalone` -Start Vitest without running tests. File filters will be ignored, tests will be running only on change (default: `false`) +Start Vitest without running tests. Tests will be running only on change. This option is ignored when CLI file filters are passed. (default: `false`) diff --git a/docs/guide/coverage.md b/docs/guide/coverage.md index ef2724d68a61..9ab21399ad6b 100644 --- a/docs/guide/coverage.md +++ b/docs/guide/coverage.md @@ -18,7 +18,7 @@ import { defineConfig } from 'vitest/config' export default defineConfig({ test: { coverage: { - provider: 'istanbul' // or 'v8' + provider: 'v8' // or 'istanbul' }, }, }) @@ -132,14 +132,13 @@ globalThis.__VITEST_COVERAGE__[filename] = coverage // [!code ++] ## Coverage Setup -:::tip -It's recommended to always define [`coverage.include`](https://vitest.dev/config/#coverage-include) in your configuration file. -This helps Vitest to reduce the amount of files picked by [`coverage.all`](https://vitest.dev/config/#coverage-all). +::: tip +All coverage options are listed in [Coverage Config Reference](/config/#coverage). ::: -To test with coverage enabled, you can pass the `--coverage` flag in CLI. -By default, reporter `['text', 'html', 'clover', 'json']` will be used. +To test with coverage enabled, you can pass the `--coverage` flag in CLI or set `coverage.enabled` in `vitest.config.ts`: +::: code-group ```json [package.json] { "scripts": { @@ -148,20 +147,92 @@ By default, reporter `['text', 'html', 'clover', 'json']` will be used. } } ``` +```ts [vitest.config.ts] +import { defineConfig } from 'vitest/config' -To configure it, set `test.coverage` options in your config file: +export default defineConfig({ + test: { + coverage: { + enabled: true + }, + }, +}) +``` +::: -```ts [vitest.config.ts] +## Including and excluding files from coverage report + +You can define what files are shown in coverage report by configuring [`coverage.include`](/config/#coverage-include) and [`coverage.exclude`](/config/#coverage-exclude). + +By default Vitest will show only files that were imported during test run. +To include uncovered files in the report, you'll need to configure [`coverage.include`](/config/#coverage-include) with a pattern that will pick your source files: + +::: code-group +```ts [vitest.config.ts] {6} import { defineConfig } from 'vitest/config' export default defineConfig({ test: { coverage: { - reporter: ['text', 'json', 'html'], + include: ['src/**.{ts,tsx}'] }, }, }) ``` +```sh [Covered Files] +├── src +│ ├── components +│ │ └── counter.tsx # [!code ++] +│ ├── mock-data +│ │ ├── products.json # [!code error] +│ │ └── users.json # [!code error] +│ └── utils +│ ├── formatters.ts # [!code ++] +│ ├── time.ts # [!code ++] +│ └── users.ts # [!code ++] +├── test +│ └── utils.test.ts # [!code error] +│ +├── package.json # [!code error] +├── tsup.config.ts # [!code error] +└── vitest.config.ts # [!code error] +``` +::: + +To exclude files that are matching `coverage.include`, you can define an additional [`coverage.exclude`](/config/#coverage-exclude): + +::: code-group +```ts [vitest.config.ts] {7} +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + coverage: { + include: ['src/**.{ts,tsx}'], + exclude: ['**/utils/users.ts'] + }, + }, +}) +``` +```sh [Covered Files] +├── src +│ ├── components +│ │ └── counter.tsx # [!code ++] +│ ├── mock-data +│ │ ├── products.json # [!code error] +│ │ └── users.json # [!code error] +│ └── utils +│ ├── formatters.ts # [!code ++] +│ ├── time.ts # [!code ++] +│ └── users.ts # [!code error] +├── test +│ └── utils.test.ts # [!code error] +│ +├── package.json # [!code error] +├── tsup.config.ts # [!code error] +└── vitest.config.ts # [!code error] +``` +::: ## Custom Coverage Reporter @@ -261,29 +332,12 @@ export default CustomCoverageProviderModule Please refer to the type definition for more details. -## Changing the Default Coverage Folder Location - -When running a coverage report, a `coverage` folder is created in the root directory of your project. If you want to move it to a different directory, use the `test.coverage.reportsDirectory` property in the `vitest.config.js` file. - -```js [vitest.config.js] -import { defineConfig } from 'vite' - -export default defineConfig({ - test: { - coverage: { - reportsDirectory: './tests/unit/coverage' - } - } -}) -``` - ## Ignoring Code Both coverage providers have their own ways how to ignore code from coverage reports: -- [`v8`](https://github.com/istanbuljs/v8-to-istanbul#ignoring-uncovered-lines) +- [`v8`](https://github.com/AriPerkkio/ast-v8-to-istanbul?tab=readme-ov-file#ignoring-code) - [`istanbul`](https://github.com/istanbuljs/nyc#parsing-hints-ignoring-lines) -- `v8` with [`experimentalAstAwareRemapping: true`](https://vitest.dev/config/#coverage-experimentalAstAwareRemapping) see [ast-v8-to-istanbul | Ignoring code](https://github.com/AriPerkkio/ast-v8-to-istanbul?tab=readme-ov-file#ignoring-code) When using TypeScript the source codes are transpiled using `esbuild`, which strips all comments from the source codes ([esbuild#516](https://github.com/evanw/esbuild/issues/516)). Comments which are considered as [legal comments](https://esbuild.github.io/api/#legal-comments) are preserved. @@ -301,9 +355,110 @@ if (condition) { if (condition) { ``` -## Other Options +### Examples + +::: code-group + +```ts [if else] +/* v8 ignore if -- @preserve */ +if (parameter) { // [!code error] + console.log('Ignored') // [!code error] +} // [!code error] +else { + console.log('Included') +} + +/* v8 ignore else -- @preserve */ +if (parameter) { + console.log('Included') +} +else { // [!code error] + console.log('Ignored') // [!code error] +} // [!code error] +``` + +```ts [next node] +/* v8 ignore next -- @preserve */ +console.log('Ignored') // [!code error] +console.log('Included') + +/* v8 ignore next -- @preserve */ +function ignored() { // [!code error] + console.log('all') // [!code error] + // [!code error] + console.log('lines') // [!code error] + // [!code error] + console.log('are') // [!code error] + // [!code error] + console.log('ignored') // [!code error] +} // [!code error] + +/* v8 ignore next -- @preserve */ +class Ignored { // [!code error] + ignored() {} // [!code error] + alsoIgnored() {} // [!code error] +} // [!code error] + +/* v8 ignore next -- @preserve */ +condition // [!code error] + ? console.log('ignored') // [!code error] + : console.log('also ignored') // [!code error] +``` + +```ts [try catch] +/* v8 ignore next -- @preserve */ +try { // [!code error] + console.log('Ignored') // [!code error] +} // [!code error] +catch (error) { // [!code error] + console.log('Ignored') // [!code error] +} // [!code error] + +try { + console.log('Included') +} +catch (error) { + /* v8 ignore next -- @preserve */ + console.log('Ignored') // [!code error] + /* v8 ignore next -- @preserve */ + console.log('Ignored') // [!code error] +} + +// Requires rolldown-vite due to esbuild's lack of support. +// See https://vite.dev/guide/rolldown.html#how-to-try-rolldown +try { + console.log('Included') +} +catch (error) /* v8 ignore next */ { // [!code error] + console.log('Ignored') // [!code error] +} // [!code error] +``` + +```ts [switch case] +switch (type) { + case 1: + return 'Included' + + /* v8 ignore next -- @preserve */ + case 2: // [!code error] + return 'Ignored' // [!code error] + + case 3: + return 'Included' + + /* v8 ignore next -- @preserve */ + default: // [!code error] + return 'Ignored' // [!code error] +} +``` -To see all configurable options for coverage, see the [coverage Config Reference](https://vitest.dev/config/#coverage). +```ts [whole file] +/* v8 ignore file -- @preserve */ +export function ignored() { // [!code error] + return 'Whole file is ignored'// [!code error] +}// [!code error] +``` +::: ## Coverage Performance diff --git a/docs/guide/debugging.md b/docs/guide/debugging.md index 0e0fd934dd05..144aa89f76b9 100644 --- a/docs/guide/debugging.md +++ b/docs/guide/debugging.md @@ -52,13 +52,14 @@ vitest --inspect-brk --browser --no-file-parallelism ``` ```ts [vitest.config.js] import { defineConfig } from 'vitest/config' +import { playwright } from '@vitest/browser/providers/playwright' export default defineConfig({ test: { inspectBrk: true, fileParallelism: false, browser: { - provider: 'playwright', + provider: playwright(), instances: [{ browser: 'chromium' }] }, }, diff --git a/docs/guide/environment.md b/docs/guide/environment.md index 53cf4e3d4e46..a5759f16f56a 100644 --- a/docs/guide/environment.md +++ b/docs/guide/environment.md @@ -14,7 +14,7 @@ By default, you can use these environments: - `edge-runtime` emulates Vercel's [edge-runtime](https://edge-runtime.vercel.app/), uses [`@edge-runtime/vm`](https://www.npmjs.com/package/@edge-runtime/vm) package ::: info -When using `jsdom` or `happy-dom` environments, Vitest follows the same rules that Vite does when importing [CSS](https://vitejs.dev/guide/features.html#css) and [assets](https://vitejs.dev/guide/features.html#static-assets). If importing external dependency fails with `unknown extension .css` error, you need to inline the whole import chain manually by adding all packages to [`server.deps.external`](/config/#server-deps-external). For example, if the error happens in `package-3` in this import chain: `source code -> package-1 -> package-2 -> package-3`, you need to add all three packages to `server.deps.external`. +When using `jsdom` or `happy-dom` environments, Vitest follows the same rules that Vite does when importing [CSS](https://vitejs.dev/guide/features.html#css) and [assets](https://vitejs.dev/guide/features.html#static-assets). If importing external dependency fails with `unknown extension .css` error, you need to inline the whole import chain manually by adding all packages to [`server.deps.inline`](/config/#server-deps-inline). For example, if the error happens in `package-3` in this import chain: `source code -> package-1 -> package-2 -> package-3`, you need to add all three packages to `server.deps.inline`. The `require` of CSS and assets inside the external dependencies are resolved automatically. ::: @@ -39,8 +39,6 @@ test('test', () => { }) ``` -Or you can also set [`environmentMatchGlobs`](https://vitest.dev/config/#environmentmatchglobs) option specifying the environment based on the glob patterns. - ## Custom Environment You can create your own package to extend Vitest environment. To do so, create package with the name `vitest-environment-${name}` or specify a path to a valid JS/TS file. That package should export an object with the shape of `Environment`: @@ -50,7 +48,7 @@ import type { Environment } from 'vitest/environments' export default { name: 'custom', - transformMode: 'ssr', + viteEnvironment: 'ssr', // optional - only if you support "experimental-vm" pool async setupVM() { const vm = await import('node:vm') @@ -76,7 +74,7 @@ export default { ``` ::: warning -Vitest requires `transformMode` option on environment object. It should be equal to `ssr` or `web`. This value determines how plugins will transform source code. If it's set to `ssr`, plugin hooks will receive `ssr: true` when transforming or resolving files. Otherwise, `ssr` is set to `false`. +Vitest requires `viteEnvironment` option on environment object (fallbacks to the Vitest environment name by default). It should be equal to `ssr`, `client` or any custom [Vite environment](https://vite.dev/guide/api-environment) name. This value determines which environment is used to process file. ::: You also have access to default Vitest environments through `vitest/environments` entry: diff --git a/docs/guide/features.md b/docs/guide/features.md index add249ef1bcd..f1aeb8925ab2 100644 --- a/docs/guide/features.md +++ b/docs/guide/features.md @@ -224,7 +224,7 @@ import { mount } from './mount.js' test('my types work properly', () => { expectTypeOf(mount).toBeFunction() - expectTypeOf(mount).parameter(0).toMatchTypeOf<{ name: string }>() + expectTypeOf(mount).parameter(0).toExtend<{ name: string }>() // @ts-expect-error name is a string assertType(mount({ name: 42 })) diff --git a/docs/guide/improving-performance.md b/docs/guide/improving-performance.md index e9ed9a8bf81e..6156290635b6 100644 --- a/docs/guide/improving-performance.md +++ b/docs/guide/improving-performance.md @@ -52,6 +52,10 @@ export default defineConfig({ ``` ::: +## Limiting directory search + +You can limit the working directory when Vitest searches for files using [`test.dir`](/config/#test-dir) option. This should make the search faster if you have unrelated folders and files in the root directory. + ## Pool By default Vitest runs tests in `pool: 'forks'`. While `'forks'` pool is better for compatibility issues ([hanging process](/guide/common-errors.html#failed-to-terminate-worker) and [segfaults](/guide/common-errors.html#segfaults-and-native-code-errors)), it may be slightly slower than `pool: 'threads'` in larger projects. @@ -75,7 +79,7 @@ export default defineConfig({ ## Sharding -Test sharding is a process of splitting your test suite into groups, or shards. This can be useful when you have a large test suite and multiple matchines that could run subsets of that suite simultaneously. +Test sharding is a process of splitting your test suite into groups, or shards. This can be useful when you have a large test suite and multiple machines that could run subsets of that suite simultaneously. To split Vitest tests on multiple different runs, use [`--shard`](/guide/cli#shard) option with [`--reporter=blob`](/guide/reporters#blob-reporter) option: @@ -93,7 +97,7 @@ Collect the results stored in `.vitest-reports` directory from each machine and vitest run --merge-reports ``` -::: details Github action example +::: details GitHub Actions example This setup is also used at https://github.com/vitest-tests/test-sharding. ```yaml diff --git a/docs/guide/index.md b/docs/guide/index.md index 92bac46af68a..31c3c4d1032c 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -100,7 +100,7 @@ One of the main advantages of Vitest is its unified configuration with Vite. If - Create `vitest.config.ts`, which will have the higher priority - Pass `--config` option to CLI, e.g. `vitest --config ./path/to/vitest.config.ts` -- Use `process.env.VITEST` or `mode` property on `defineConfig` (will be set to `test` if not overridden) to conditionally apply different configuration in `vite.config.ts` +- Use `process.env.VITEST` or `mode` property on `defineConfig` (will be set to `test` if not overridden) to conditionally apply different configuration in `vite.config.ts`. Note that like any other environment variable, `VITEST` is also exposed on `import.meta.env` in your tests Vitest supports the same extensions for your configuration file as Vite does: `.js`, `.mjs`, `.cjs`, `.ts`, `.cts`, `.mts`. Vitest does not support `.json` extension. diff --git a/docs/guide/migration.md b/docs/guide/migration.md index 21b695a9f10d..0f9280f8722d 100644 --- a/docs/guide/migration.md +++ b/docs/guide/migration.md @@ -5,466 +5,318 @@ outline: deep # Migration Guide -## Migrating to Vitest 3.0 {#vitest-3} +## Migrating to Vitest 4.0 {#vitest-4} -### Test Options as a Third Argument +### V8 Code Coverage Major Changes -Vitest 3.0 prints a warning if you pass down an object as a third argument to `test` or `describe` functions: +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. -```ts -test('validation works', () => { - // ... -}, { retry: 3 }) // [!code --] +In the past Vitest used [`v8-to-istanbul`](https://github.com/istanbuljs/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. -test('validation works', { retry: 3 }, () => { // [!code ++] - // ... -}) -``` +- Coverage ignore hints have updated. See [Coverage | Ignoring Code](/guide/coverage.html#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. -The next major version will throw an error if the third argument is an object. Note that the timeout number is not deprecated: +### Removed options `coverage.all` and `coverage.extensions` -```ts -test('validation works', () => { - // ... -}, 1000) // Ok ✅ -``` +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. -### `browser.name` and `browser.providerOptions` are Deprecated +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**. -Both [`browser.name`](/guide/browser/config#browser-name) and [`browser.providerOptions`](/guide/browser/config#browser-provideroptions) will be removed in Vitest 4. Instead of them, use the new [`browser.instances`](/guide/browser/config#browser-instances) option: +When upgrading to v4 it is recommended to define `coverage.include` in your configuration, and then start applying simple `coverage.exclusion` patterns if needed. -```ts +```ts [vitest.config.ts] export default defineConfig({ test: { - browser: { - name: 'chromium', // [!code --] - providerOptions: { // [!code --] - launch: { devtools: true }, // [!code --] - }, // [!code --] - instances: [ // [!code ++] - { // [!code ++] - browser: 'chromium', // [!code ++] - launch: { devtools: true }, // [!code ++] - }, // [!code ++] - ], // [!code ++] - }, - }, -}) -``` - -With the new `browser.instances` field you can also specify multiple browser configurations. - -### `spy.mockReset` Now Restores the Original Implementation - -There was no good way to reset the spy to the original implementation without reapplying the spy. Now, `spy.mockReset` will reset the implementation function to the original one instead of a fake noop. - -```ts -const foo = { - bar: () => 'Hello, world!' -} - -vi.spyOn(foo, 'bar').mockImplementation(() => 'Hello, mock!') - -foo.bar() // 'Hello, mock!' + coverage: { + // Include covered and uncovered files matching this pattern: + include: ['packages/**/src/**.{js,jsx,ts,tsx}'], // [!code ++] -foo.bar.mockReset() + // 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 ++] -foo.bar() // undefined [!code --] -foo.bar() // 'Hello, world!' [!code ++] + // These options are removed now + all: true, // [!code --] + extensions: ['js', 'ts'], // [!code --] + } + } +}) ``` -### `vi.spyOn` Reuses Mock if Method is Already Mocked - -Previously, Vitest would always assign a new spy when spying on an object. This caused errors with `mockRestore` because it would restore the spy to the previous spy instead of the original function: +If `coverage.include` is not defined, coverage report will include only files that were loaded during test run: +```ts [vitest.config.ts] +export default defineConfig({ + test: { + coverage: { + // Include not set, include only files that are loaded during test run + include: undefined, // [!code ++] -```ts -vi.spyOn(fooService, 'foo').mockImplementation(() => 'bar') -vi.spyOn(fooService, 'foo').mockImplementation(() => 'bar') -vi.restoreAllMocks() -vi.isMockFunction(fooService.foo) // true [!code --] -vi.isMockFunction(fooService.foo) // false [!code ++] + // Loaded files that match this pattern will be excluded: + exclude: ['**/some-pattern/**'], // [!code ++] + } + } +}) ``` -### Fake Timers Defaults +See also new guides: +- [Including and excluding files from coverage report](/guide/coverage.html#including-and-excluding-files-from-coverage-report) for examples +- [Profiling Test Performance | Code coverage](/guide/profiling-test-performance.html#code-coverage) for tips about debugging coverage generation -Vitest no longer provides default `fakeTimers.toFake` options. Now, Vitest will mock any timer-related API if it is available (except `nextTick`). Namely, `performance.now()` is now mocked when `vi.useFakeTimers` is called. +### `spyOn` and `fn` Support Constructors -```ts -vi.useFakeTimers() +Previously, if you tried to spy on a constructor with `vi.spyOn`, you would get an error like `Constructor requires 'new'`. Since Vitest 4, all mocks called with a `new` keyword construct the instance instead of callying `mock.apply`. This means that the mock implementation has to use either the `function` or the `class` keyword in these cases: -performance.now() // original [!code --] -performance.now() // fake [!code ++] -``` +```ts {12-14,16-20} +const cart = { + Apples: class Apples { + getApples() { + return 42 + } + } +} -You can revert to the previous behaviour by specifying timers when calling `vi.useFakeTimers` or globally in the config: +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 + } + }) -```ts -export default defineConfig({ - test: { - fakeTimers: { - toFake: [ // [!code ++] - 'setTimeout', // [!code ++] - 'clearTimeout', // [!code ++] - 'setInterval', // [!code ++] - 'clearInterval', // [!code ++] - 'setImmediate', // [!code ++] - 'clearImmediate', // [!code ++] - 'Date', // [!code ++] - ] // [!code ++] - }, - }, -}) +const mock = new Spy() ``` -### More Strict Error Equality +Note that now if you provide an arrow function, you will get [` is not a constructor` error](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Not_a_constructor) when the mock is called. -Vitest now checks more properties when comparing errors via `toEqual` or `toThrowError`. Vitest now compares `name`, `message`, `cause` and `AggregateError.errors`. For `Error.cause`, the comparison is done asymmetrically: +### Changes to Mocking -```ts -expect(new Error('hi', { cause: 'x' })).toEqual(new Error('hi')) // ✅ -expect(new Error('hi')).toEqual(new Error('hi', { cause: 'x' })) // ❌ -``` - -In addition to more properties check, Vitest now compares error prototypes. For example, if `TypeError` was thrown, the equality check should reference `TypeError`, not `Error`: +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 attemts 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`](/config/#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 +- 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. ```ts -expect(() => { - throw new TypeError('type error') -}) - .toThrowError(new Error('type error')) // [!code --] - .toThrowError(new TypeError('type error')) // [!code ++] -``` +import { AutoMockedClass } from './example.js' +const instance1 = new AutoMockedClass() +const instance2 = new AutoMockedClass() -See PR for more details: [#5876](https://github.com/vitest-dev/vitest/pull/5876). +instance1.method.mockReturnValue(42) -### `module` condition export is not resolved by default on Vite 6 +expect(instance1.method()).toBe(42) +expect(instance2.method()).toBe(undefined) -Vite 6 allows more flexible [`resolve.conditions`](https://vite.dev/config/shared-options#resolve-conditions) options and Vitest configures it to exclude `module` conditional export by default. -See also [Vite 6 migration guide](https://v6.vite.dev/guide/migration.html#default-value-for-resolve-conditions) for the detail of Vite side changes. +expect(AutoMockedClass.prototype.method).toHaveBeenCalledTimes(2) -### `Custom` Type is Deprecated API {#custom-type-is-deprecated} +instance1.method.mockReset() +AutoMockedClass.prototype.method.mockReturnValue(100) -The `Custom` type is now an alias for the `Test` type. Note that Vitest updated the public types in 2.1 and changed exported names to `RunnerCustomCase` and `RunnerTestCase`: +expect(instance1.method()).toBe(100) +expect(instance2.method()).toBe(100) -```ts -import { - RunnerCustomCase, // [!code --] - RunnerTestCase, // [!code ++] -} from 'vitest' +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` -If you are using `getCurrentSuite().custom()`, the `type` of the returned task is now is equal to `'test'`. The `Custom` type will be removed in Vitest 4. - -### The `WorkspaceSpec` Type is No Longer Used API {#the-workspacespec-type-is-no-longer-used} - -In the public API this type was used in custom [sequencers](/config/#sequence-sequencer) before. Please, migrate to [`TestSpecification`](/advanced/api/test-specification) instead. - -### `onTestFinished` and `onTestFailed` Now Receive a Context - -The [`onTestFinished`](/api/#ontestfinished) and [`onTestFailed`](/api/#ontestfailed) hooks previously received a test result as the first argument. Now, they receive a test context, like `beforeEach` and `afterEach`. - -### Changes to the Snapshot API API {#changes-to-the-snapshot-api} +### Standalone mode with filename filter -The public Snapshot API in `@vitest/snapshot` was changed to support multiple states within a single run. See PR for more details: [#6817](https://github.com/vitest-dev/vitest/pull/6817) +To improve user experience, Vitest will now start running the matched files when [`--standalone`](/guide/cli#standalone) is used with filename filter. -Note that this changes only affect developers using the Snapshot API directly. There were no changes to `.toMatchSnapshot` API. +```sh +# 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 +``` -### Changes to `resolveConfig` Type Signature API {#changes-to-resolveconfig-type-signature} +This allows users to create re-usable `package.json` scripts for standalone mode. -The [`resolveConfig`](/advanced/api/#resolveconfig) is now more useful. Instead of accepting already resolved Vite config, it now accepts a user config and returns resolved config. +::: code-group +```json [package.json] +{ + "scripts": { + "test:dev": "vitest --standalone" + } +} +``` +```bash [CLI] +# Start Vitest in standalone mode, without running any files on start +$ pnpm run test:dev -This function is not used internally and exposed exclusively as a public API. +# Run math.test.ts immediately +$ pnpm run test:dev math.test.ts +``` +::: -### Cleaned up `vitest/reporters` types API {#cleaned-up-vitest-reporters-types} +### Replacing `vite-node` with [Module Runner](https://vite.dev/guide/api-environment-runtimes.html#modulerunner) -The `vitest/reporters` entrypoint now only exports reporters implementations and options types. If you need access to `TestCase`/`TestSuite` and other task related types, import them additionally from `vitest/node`. +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: -### Coverage ignores test files even when `coverage.excludes` is overwritten. +- `VITE_NODE_DEPS_MODULE_DIRECTORIES` environment variable was replaced with `VITEST_MODULE_DIRECTORIES` +- Vitest no longer injects `__vitest_executor` into every [test runner](/advanced/runner). Instead, it injects `moduleRunner` which is an instance of [`ModuleRunner`](https://vite.dev/guide/api-environment-runtimes.html#modulerunner) +- `vitest/execute` entry point was removed. It was always meant to be internal +- [Custom environments](/guide/environment) 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`](https://vite.dev/guide/api-environment-instances.html)) +- `vite-node` is no longer a dependency of Vitest +- `deps.optimizer.web` was renamed to [`deps.optimizer.client`](/config/#deps-optimizer-client). You can also use any custom names to apply optimizer configs when using other server environments -It is no longer possible to include test files in coverage report by overwriting `coverage.excludes`. Test files are now always excluded. +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`](/config/#server-deps) to inline or externalize packages. -## Migrating to Vitest 2.0 {#vitest-2} +This update should not be noticeable unless you rely on advanced features mentioned above. -### Default Pool is `forks` +### `workspace` is Replaced with `projects` -Vitest 2.0 changes the default configuration for `pool` to `'forks'` for better stability. You can read the full motivation in [PR](https://github.com/vitest-dev/vitest/pull/5047). +The `workspace` configuration option was renamed to [`projects`](/guide/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`: -If you've used `poolOptions` without specifying a `pool`, you might need to update the configuration: +::: code-group +```ts [vitest.config.js] +import { defineConfig } from 'vitest/config' -```ts export default defineConfig({ test: { - poolOptions: { - threads: { // [!code --] - singleThread: true, // [!code --] - }, // [!code --] - forks: { // [!code ++] - singleFork: true, // [!code ++] + workspace: './vitest.workspace.js', // [!code --] + projects: [ // [!code ++] + './packages/*', // [!code ++] + { // [!code ++] + test: { // [!code ++] + name: 'unit', // [!code ++] + }, // [!code ++] }, // [!code ++] - } + ] // [!code ++] } }) ``` +```ts [vitest.workspace.js] +import { defineWorkspace } from 'vitest/config' // [!code --] -### Hooks are Running in a Stack +export default defineWorkspace([ // [!code --] + './packages/*', // [!code --] + { // [!code --] + test: { // [!code --] + name: 'unit', // [!code --] + }, // [!code --] + } // [!code --] +]) // [!code --] +``` +::: -Before Vitest 2.0, all hooks ran in parallel. In 2.0, all hooks run serially. Additionally, `afterAll`/`afterEach` hooks run in reverse order. +### Browser Provider Accepts an Object -To revert to the parallel execution of hooks, change [`sequence.hooks`](/config/#sequence-hooks) to `'parallel'`: +In Vitest 4.0, the browser provider now accepts an object instead of a string (`'playwright'`, `'webdriverio'`). This makes it simpler to work with custom options and doesn't require adding `/// `, `Mock`) - -Previously `vi.fn` accepted two generic types separately for arguments and return value. This is changed to directly accept a function type `vi.fn` to simplify the usage. +The [`verbose`](/guide/reporters#verbose-reporter) reporter now prints test cases as a flat list. To revert to the previous behaviour, use `--reporter=tree`: ```ts -import { vi } from 'vitest' -import type { Mock } from 'vitest' - -const add = (x: number, y: number): number => x + y - -// using vi.fn -const mockAdd = vi.fn, ReturnType>() // [!code --] -const mockAdd = vi.fn() // [!code ++] - -// using Mock -const mockAdd: Mock, ReturnType> = vi.fn() // [!code --] -const mockAdd: Mock = vi.fn() // [!code ++] -``` - -### Accessing Resolved `mock.results` - -Previously Vitest resolved `mock.results` values if the function returned a Promise. Now there is a separate [`mock.settledResults`](/api/mock#mock-settledresults) property that populates only when the returned Promise is resolved or rejected. - -```ts -const fn = vi.fn().mockResolvedValueOnce('result') -await fn() - -const result = fn.mock.results[0] // 'result' [!code --] -const result = fn.mock.results[0] // 'Promise' [!code ++] - -const settledResult = fn.mock.settledResults[0] // 'result' -``` - -With this change, we also introduce new [`toHaveResolved*`](/api/expect#tohaveresolved) matchers similar to `toHaveReturned` to make migration easier if you used `toHaveReturned` before: - -```ts -const fn = vi.fn().mockResolvedValueOnce('result') -await fn() - -expect(fn).toHaveReturned('result') // [!code --] -expect(fn).toHaveResolved('result') // [!code ++] -``` - -### Browser Mode - -Vitest Browser Mode had a lot of changes during the beta cycle. You can read about our philosophy on the Browser Mode in the [GitHub discussion page](https://github.com/vitest-dev/vitest/discussions/5828). - -Most of the changes were additive, but there were some small breaking changes: - -- `none` provider was renamed to `preview` [#5842](https://github.com/vitest-dev/vitest/pull/5826) -- `preview` provider is now a default [#5842](https://github.com/vitest-dev/vitest/pull/5826) -- `indexScripts` is renamed to `orchestratorScripts` [#5842](https://github.com/vitest-dev/vitest/pull/5842) - -### Deprecated Options Removed - -Some deprecated options were removed: - -- `vitest typecheck` command - use `vitest --typecheck` instead -- `VITEST_JUNIT_CLASSNAME` and `VITEST_JUNIT_SUITE_NAME` env variables (use reporter options instead) -- check for `c8` coverage (use coverage-v8 instead) -- export of `SnapshotEnvironment` from `vitest` - import it from `vitest/snapshot` instead -- `SpyInstance` is removed in favor of `MockInstance` - -## Migrating to Vitest 1.0 - -### Minimum Requirements - -Vitest 1.0 requires Vite 5.0 and Node.js 18 or higher. - -All `@vitest/*` sub packages require Vitest version 1.0. - -### Snapshots Update [#3961](https://github.com/vitest-dev/vitest/pull/3961) - -Quotes in snapshots are no longer escaped, and all snapshots use backtick quotes (`) even if the string is just a single line. - -1. Quotes are no longer escaped: - -```diff -expect({ foo: 'bar' }).toMatchInlineSnapshot(` - Object { -- \\"foo\\": \\"bar\\", -+ "foo": "bar", - } -`) -``` - -2. One-line snapshots now use "`" quotes instead of ': - -```diff -- expect('some string').toMatchInlineSnapshot('"some string"') -+ expect('some string').toMatchInlineSnapshot(`"some string"`) -``` - -There were also [changes](https://github.com/vitest-dev/vitest/pull/4076) to `@vitest/snapshot` package. If you are not using it directly, you don't need to change anything. - -- You no longer need to extend `SnapshotClient` just to override `equalityCheck` method: just pass it down as `isEqual` when initiating an instance -- `client.setTest` was renamed to `client.startCurrentRun` -- `client.resetCurrent` was renamed to `client.finishCurrentRun` - -### Pools are Standardized [#4172](https://github.com/vitest-dev/vitest/pull/4172) - -We removed a lot of configuration options to make it easier to configure the runner to your needs. Please, have a look at migration examples if you rely on `--threads` or other related flags. - -- `--threads` is now `--pool=threads` -- `--no-threads` is now `--pool=forks` -- `--single-thread` is now `--poolOptions.threads.singleThread` -- `--experimental-vm-threads` is now `--pool=vmThreads` -- `--experimental-vm-worker-memory-limit` is now `--poolOptions.vmThreads.memoryLimit` -- `--isolate` is now `--poolOptions..isolate` and `browser.isolate` -- `test.maxThreads` is now `test.poolOptions..maxThreads` -- `test.minThreads` is now `test.poolOptions..minThreads` -- `test.useAtomics` is now `test.poolOptions..useAtomics` -- `test.poolMatchGlobs.child_process` is now `test.poolMatchGlobs.forks` -- `test.poolMatchGlobs.experimentalVmThreads` is now `test.poolMatchGlobs.vmThreads` - -```diff -{ - scripts: { -- "test": "vitest --no-threads" - // For identical behaviour: -+ "test": "vitest --pool forks --poolOptions.forks.singleFork" - // Or multi parallel forks: -+ "test": "vitest --pool forks" - - } -} -``` - -```diff -{ - scripts: { -- "test": "vitest --experimental-vm-threads" -+ "test": "vitest --pool vmThreads" - } -} -``` - -```diff -{ - scripts: { -- "test": "vitest --isolate false" -+ "test": "vitest --poolOptions.threads.isolate false" - } -} -``` - -```diff -{ - scripts: { -- "test": "vitest --no-threads --isolate false" -+ "test": "vitest --pool forks --poolOptions.forks.isolate false" - } -} -``` - -### Changes to Coverage [#4265](https://github.com/vitest-dev/vitest/pull/4265), [#4442](https://github.com/vitest-dev/vitest/pull/4442) - -Option `coverage.all` is now enabled by default. This means that all project files matching `coverage.include` pattern will be processed even if they are not executed. - -Coverage thresholds API's shape was changed, and it now supports specifying thresholds for specific files using glob patterns: - -```diff export default defineConfig({ test: { - coverage: { -- perFile: true, -- thresholdAutoUpdate: true, -- 100: true, -- lines: 100, -- functions: 100, -- branches: 100, -- statements: 100, -+ thresholds: { -+ perFile: true, -+ autoUpdate: true, -+ 100: true, -+ lines: 100, -+ functions: 100, -+ branches: 100, -+ statements: 100, -+ } - } + reporters: ['verbose'], // [!code --] + reporters: ['tree'], // [!code ++] } }) ``` -### Mock Types [#4400](https://github.com/vitest-dev/vitest/pull/4400) +### Snapshots using custom elements print the shadow root -A few types were removed in favor of Jest-style "Mock" naming. +In Vitest 4.0 snapshots that include custom elements will print the shadow root contents. To restore the previous behavior, set the [`printShadowRoot` option](/config/#snapshotformat) to `false`. -```diff -- import { EnhancedSpy, SpyInstance } from 'vitest' -+ import { MockInstance } from 'vitest' +```js +// before Vite 4.0 +exports[`custom element with shadow root 1`] = ` +" +
+ +
+" +` + +// after Vite 4.0 +exports[`custom element with shadow root 1`] = ` +" +
+ + #shadow-root + + hello + + +
+" +` ``` -::: warning -`SpyInstance` is deprecated in favor of `MockInstance` and will be removed in the next major release. -::: +### Deprecated APIs are Removed -### Timer mocks [#3925](https://github.com/vitest-dev/vitest/pull/3925) +Vitest 4.0 removes some deprecated APIs, including: -`vi.useFakeTimers()` no longer automatically mocks [`process.nextTick`](https://nodejs.org/api/process.html#processnexttickcallback-args). -It is still possible to mock `process.nextTick` by explicitly specifying it by using `vi.useFakeTimers({ toFake: ['nextTick'] })`. +- `poolMatchGlobs` config option. Use [`projects`](/guide/projects) instead. +- `environmentMatchGlobs` config option. Use [`projects`](/guide/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`](/guide/browser/config#browser-testerhtmlpath) instead. +- `minWorkers` config option. Only `maxWorkers` has any effect on how tests are running, so we are removing this public option. -However, mocking `process.nextTick` is not possible when using `--pool=forks`. Use a different `--pool` option if you need `process.nextTick` mocking. +This release also removes all deprecated types. This finally fixes an issue where Vitest accidentally pulled in `@types/node` (see [#5481](https://github.com/vitest-dev/vitest/issues/5481) and [#6141](https://github.com/vitest-dev/vitest/issues/6141)). ## Migrating from Jest {#jest} @@ -476,7 +328,7 @@ Jest has their [globals API](https://jestjs.io/docs/api) enabled by default. Vit If you decide to keep globals disabled, be aware that common libraries like [`testing-library`](https://testing-library.com/) will not run auto DOM [cleanup](https://testing-library.com/docs/svelte-testing-library/api/#cleanup). -### `spy.mockReset` +### `mock.mockReset` Jest's [`mockReset`](https://jestjs.io/docs/mock-function-api#mockfnmockreset) replaces the mock implementation with an empty function that returns `undefined`. @@ -484,6 +336,18 @@ empty function that returns `undefined`. Vitest's [`mockReset`](/api/mock#mockreset) resets the mock implementation to its original. That is, resetting a mock created by `vi.fn(impl)` will reset the mock implementation to `impl`. +### `mock.mock` is Persistent + +Jest will recreate the mock state when `.mockClear` is called, meaning you always need to access it as a getter. Vitest, on the other hand, holds a persistent reference to the state, meaning you can reuse it: + +```ts +const mock = vi.fn() +const state = mock.mock +mock.mockClear() + +expect(state).toBe(mock.mock) // fails in Jest +``` + ### Module Mocks When mocking a module in Jest, the factory argument's return value is the default export. In Vitest, the factory argument has to return an object with each export explicitly defined. For example, the following `jest.mock` would have to be updated as follows: @@ -537,7 +401,7 @@ If you want to modify the object, you will use [replaceProperty API](https://jes ### Done Callback -From Vitest v0.10.0, the callback style of declaring tests is deprecated. You can rewrite them to use `async`/`await` functions, or use Promise to mimic the callback style. +Vitest does not support the callback style of declaring tests. You can rewrite them to use `async`/`await` functions, or use Promise to mimic the callback style. @@ -550,7 +414,7 @@ beforeEach(() => setActivePinia(createTestingPinia())) // [!code --] beforeEach(() => { setActivePinia(createTestingPinia()) }) // [!code ++] ``` -In Jest hooks are called sequentially (one after another). By default, Vitest runs hooks in parallel. To use Jest's behavior, update [`sequence.hooks`](/config/#sequence-hooks) option: +In Jest hooks are called sequentially (one after another). By default, Vitest runs hooks in a stack. To use Jest's behavior, update [`sequence.hooks`](/config/#sequence-hooks) option: ```ts export default defineConfig({ @@ -587,23 +451,16 @@ vi.setConfig({ testTimeout: 5_000 }) // [!code ++] ### Vue Snapshots -This is not a Jest-specific feature, but if you previously were using Jest with vue-cli preset, you will need to install [`jest-serializer-vue`](https://github.com/eddyerburgh/jest-serializer-vue) package, and use it inside [setupFiles](/config/#setupfiles): +This is not a Jest-specific feature, but if you previously were using Jest with vue-cli preset, you will need to install [`jest-serializer-vue`](https://github.com/eddyerburgh/jest-serializer-vue) package, and specify it in [`snapshotSerializers`](/config/#snapshotserializers): -:::code-group -```js [vite.config.js] -import { defineConfig } from 'vite' +```js [vitest.config.js] +import { defineConfig } from 'vitest/config' export default defineConfig({ test: { - setupFiles: ['./tests/unit/setup.js'] + snapshotSerializers: ['jest-serializer-vue'] } }) ``` -```js [tests/unit/setup.js] -import vueSnapshotSerializer from 'jest-serializer-vue' - -expect.addSnapshotSerializer(vueSnapshotSerializer) -``` -::: Otherwise your snapshots will have a lot of escaped `"` characters. diff --git a/docs/guide/mocking.md b/docs/guide/mocking.md index 14bd3bbbc4d4..a22260d0666f 100644 --- a/docs/guide/mocking.md +++ b/docs/guide/mocking.md @@ -1,5 +1,6 @@ --- title: Mocking | Guide +outline: false --- # Mocking @@ -12,746 +13,21 @@ Always remember to clear or restore mocks before or after each test run to undo If you are not familiar with `vi.fn`, `vi.mock` or `vi.spyOn` methods, check the [API section](/api/vi) first. -## Dates +Vitest has a comprehensive list of guides regarding mocking: -Sometimes you need to be in control of the date to ensure consistency when testing. Vitest uses [`@sinonjs/fake-timers`](https://github.com/sinonjs/fake-timers) package for manipulating timers, as well as system date. You can find more about the specific API in detail [here](/api/vi#vi-setsystemtime). +- [Mocking Classes](/guide/mocking/classes.md) +- [Mocking Dates](/guide/mocking/dates.md) +- [Mocking the File System](/guide/mocking/file-system.md) +- [Mocking Functions](/guide/mocking/functions.md) +- [Mocking Globals](/guide/mocking/globals.md) +- [Mocking Modules](/guide/mocking/modules.md) +- [Mocking Requests](/guide/mocking/requests.md) +- [Mocking Timers](/guide/mocking/timers.md) -### Example - -```js -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' - -const businessHours = [9, 17] - -function purchase() { - const currentHour = new Date().getHours() - const [open, close] = businessHours - - if (currentHour > open && currentHour < close) { - return { message: 'Success' } - } - - return { message: 'Error' } -} - -describe('purchasing flow', () => { - beforeEach(() => { - // tell vitest we use mocked time - vi.useFakeTimers() - }) - - afterEach(() => { - // restoring date after each test run - vi.useRealTimers() - }) - - it('allows purchases within business hours', () => { - // set hour within business hours - const date = new Date(2000, 1, 1, 13) - vi.setSystemTime(date) - - // access Date.now() will result in the date set above - expect(purchase()).toEqual({ message: 'Success' }) - }) - - it('disallows purchases outside of business hours', () => { - // set hour outside business hours - const date = new Date(2000, 1, 1, 19) - vi.setSystemTime(date) - - // access Date.now() will result in the date set above - expect(purchase()).toEqual({ message: 'Error' }) - }) -}) -``` - -## Functions - -Mocking functions can be split up into two different categories; *spying & mocking*. - -Sometimes all you need is to validate whether or not a specific function has been called (and possibly which arguments were passed). In these cases a spy would be all we need which you can use directly with `vi.spyOn()` ([read more here](/api/vi#vi-spyon)). - -However spies can only help you **spy** on functions, they are not able to alter the implementation of those functions. In the case where we do need to create a fake (or mocked) version of a function we can use `vi.fn()` ([read more here](/api/vi#vi-fn)). - -We use [Tinyspy](https://github.com/tinylibs/tinyspy) as a base for mocking functions, but we have our own wrapper to make it `jest` compatible. Both `vi.fn()` and `vi.spyOn()` share the same methods, however only the return result of `vi.fn()` is callable. - -### Example - -```js -import { afterEach, describe, expect, it, vi } from 'vitest' - -const messages = { - items: [ - { message: 'Simple test message', from: 'Testman' }, - // ... - ], - getLatest, // can also be a `getter or setter if supported` -} - -function getLatest(index = messages.items.length - 1) { - return messages.items[index] -} - -describe('reading messages', () => { - afterEach(() => { - vi.restoreAllMocks() - }) - - it('should get the latest message with a spy', () => { - const spy = vi.spyOn(messages, 'getLatest') - expect(spy.getMockName()).toEqual('getLatest') - - expect(messages.getLatest()).toEqual( - messages.items[messages.items.length - 1], - ) - - expect(spy).toHaveBeenCalledTimes(1) - - spy.mockImplementationOnce(() => 'access-restricted') - expect(messages.getLatest()).toEqual('access-restricted') - - expect(spy).toHaveBeenCalledTimes(2) - }) - - it('should get with a mock', () => { - const mock = vi.fn().mockImplementation(getLatest) - - expect(mock()).toEqual(messages.items[messages.items.length - 1]) - expect(mock).toHaveBeenCalledTimes(1) - - mock.mockImplementationOnce(() => 'access-restricted') - expect(mock()).toEqual('access-restricted') - - expect(mock).toHaveBeenCalledTimes(2) - - expect(mock()).toEqual(messages.items[messages.items.length - 1]) - expect(mock).toHaveBeenCalledTimes(3) - }) -}) -``` - -### More - -- [Jest's Mock Functions](https://jestjs.io/docs/mock-function-api) - -## Globals - -You can mock global variables that are not present with `jsdom` or `node` by using [`vi.stubGlobal`](/api/vi#vi-stubglobal) helper. It will put the value of the global variable into a `globalThis` object. - -```ts -import { vi } from 'vitest' - -const IntersectionObserverMock = vi.fn(() => ({ - disconnect: vi.fn(), - observe: vi.fn(), - takeRecords: vi.fn(), - unobserve: vi.fn(), -})) - -vi.stubGlobal('IntersectionObserver', IntersectionObserverMock) - -// now you can access it as `IntersectionObserver` or `window.IntersectionObserver` -``` - -## Modules - -Mock modules observe third-party-libraries, that are invoked in some other code, allowing you to test arguments, output or even redeclare its implementation. - -See the [`vi.mock()` API section](/api/vi#vi-mock) for a more in-depth detailed API description. - -### Automocking Algorithm - -If your code is importing a mocked module, without any associated `__mocks__` file or `factory` for this module, Vitest will mock the module itself by invoking it and mocking every export. - -The following principles apply -* All arrays will be emptied -* All primitives and collections will stay the same -* All objects will be deeply cloned -* All instances of classes and their prototypes will be deeply cloned - -### Virtual Modules - -Vitest supports mocking Vite [virtual modules](https://vitejs.dev/guide/api-plugin.html#virtual-modules-convention). It works differently from how virtual modules are treated in Jest. Instead of passing down `virtual: true` to a `vi.mock` function, you need to tell Vite that module exists otherwise it will fail during parsing. You can do that in several ways: - -1. Provide an alias - -```ts [vitest.config.js] -import { defineConfig } from 'vitest/config' -import { resolve } from 'node:path' -export default defineConfig({ - test: { - alias: { - '$app/forms': resolve('./mocks/forms.js'), - }, - }, -}) -``` - -2. Provide a plugin that resolves a virtual module - -```ts [vitest.config.js] -import { defineConfig } from 'vitest/config' -export default defineConfig({ - plugins: [ - { - name: 'virtual-modules', - resolveId(id) { - if (id === '$app/forms') { - return 'virtual:$app/forms' - } - }, - }, - ], -}) -``` - -The benefit of the second approach is that you can dynamically create different virtual entrypoints. If you redirect several virtual modules into a single file, then all of them will be affected by `vi.mock`, so make sure to use unique identifiers. - -### Mocking Pitfalls - -Beware that it is not possible to mock calls to methods that are called inside other methods of the same file. For example, in this code: - -```ts [foobar.js] -export function foo() { - return 'foo' -} - -export function foobar() { - return `${foo()}bar` -} -``` - -It is not possible to mock the `foo` method from the outside because it is referenced directly. So this code will have no effect on the `foo` call inside `foobar` (but it will affect the `foo` call in other modules): - -```ts [foobar.test.ts] -import { vi } from 'vitest' -import * as mod from './foobar.js' - -// this will only affect "foo" outside of the original module -vi.spyOn(mod, 'foo') -vi.mock('./foobar.js', async (importOriginal) => { - return { - ...await importOriginal(), - // this will only affect "foo" outside of the original module - foo: () => 'mocked' - } -}) -``` - -You can confirm this behaviour by providing the implementation to the `foobar` method directly: - -```ts [foobar.test.js] -import * as mod from './foobar.js' - -vi.spyOn(mod, 'foo') - -// exported foo references mocked method -mod.foobar(mod.foo) -``` - -```ts [foobar.js] -export function foo() { - return 'foo' -} - -export function foobar(injectedFoo) { - return injectedFoo === foo // false -} -``` - -This is the intended behaviour. It is usually a sign of bad code when mocking is involved in such a manner. Consider refactoring your code into multiple files or improving your application architecture by using techniques such as [dependency injection](https://en.wikipedia.org/wiki/Dependency_injection). - -### Example - -```js -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { Client } from 'pg' -import { failure, success } from './handlers.js' - -// get todos -export async function getTodos(event, context) { - const client = new Client({ - // ...clientOptions - }) - - await client.connect() - - try { - const result = await client.query('SELECT * FROM todos;') - - client.end() - - return success({ - message: `${result.rowCount} item(s) returned`, - data: result.rows, - status: true, - }) - } - catch (e) { - console.error(e.stack) - - client.end() - - return failure({ message: e, status: false }) - } -} - -vi.mock('pg', () => { - const Client = vi.fn() - Client.prototype.connect = vi.fn() - Client.prototype.query = vi.fn() - Client.prototype.end = vi.fn() - - return { Client } -}) - -vi.mock('./handlers.js', () => { - return { - success: vi.fn(), - failure: vi.fn(), - } -}) - -describe('get a list of todo items', () => { - let client - - beforeEach(() => { - client = new Client() - }) - - afterEach(() => { - vi.clearAllMocks() - }) - - it('should return items successfully', async () => { - client.query.mockResolvedValueOnce({ rows: [], rowCount: 0 }) - - await getTodos() - - expect(client.connect).toBeCalledTimes(1) - expect(client.query).toBeCalledWith('SELECT * FROM todos;') - expect(client.end).toBeCalledTimes(1) - - expect(success).toBeCalledWith({ - message: '0 item(s) returned', - data: [], - status: true, - }) - }) - - it('should throw an error', async () => { - const mError = new Error('Unable to retrieve rows') - client.query.mockRejectedValueOnce(mError) - - await getTodos() - - expect(client.connect).toBeCalledTimes(1) - expect(client.query).toBeCalledWith('SELECT * FROM todos;') - expect(client.end).toBeCalledTimes(1) - expect(failure).toBeCalledWith({ message: mError, status: false }) - }) -}) -``` - -## File System - -Mocking the file system ensures that the tests do not depend on the actual file system, making the tests more reliable and predictable. This isolation helps in avoiding side effects from previous tests. It allows for testing error conditions and edge cases that might be difficult or impossible to replicate with an actual file system, such as permission issues, disk full scenarios, or read/write errors. - -Vitest doesn't provide any file system mocking API out of the box. You can use `vi.mock` to mock the `fs` module manually, but it's hard to maintain. Instead, we recommend using [`memfs`](https://www.npmjs.com/package/memfs) to do that for you. `memfs` creates an in-memory file system, which simulates file system operations without touching the actual disk. This approach is fast and safe, avoiding any potential side effects on the real file system. - -### Example - -To automatically redirect every `fs` call to `memfs`, you can create `__mocks__/fs.cjs` and `__mocks__/fs/promises.cjs` files at the root of your project: - -::: code-group -```ts [__mocks__/fs.cjs] -// we can also use `import`, but then -// every export should be explicitly defined - -const { fs } = require('memfs') -module.exports = fs -``` - -```ts [__mocks__/fs/promises.cjs] -// we can also use `import`, but then -// every export should be explicitly defined - -const { fs } = require('memfs') -module.exports = fs.promises -``` -::: - -```ts [read-hello-world.js] -import { readFileSync } from 'node:fs' - -export function readHelloWorld(path) { - return readFileSync(path, 'utf-8') -} -``` - -```ts [hello-world.test.js] -import { beforeEach, expect, it, vi } from 'vitest' -import { fs, vol } from 'memfs' -import { readHelloWorld } from './read-hello-world.js' - -// tell vitest to use fs mock from __mocks__ folder -// this can be done in a setup file if fs should always be mocked -vi.mock('node:fs') -vi.mock('node:fs/promises') - -beforeEach(() => { - // reset the state of in-memory fs - vol.reset() -}) - -it('should return correct text', () => { - const path = '/hello-world.txt' - fs.writeFileSync(path, 'hello world') - - const text = readHelloWorld(path) - expect(text).toBe('hello world') -}) - -it('can return a value multiple times', () => { - // you can use vol.fromJSON to define several files - vol.fromJSON( - { - './dir1/hw.txt': 'hello dir1', - './dir2/hw.txt': 'hello dir2', - }, - // default cwd - '/tmp', - ) - - expect(readHelloWorld('/tmp/dir1/hw.txt')).toBe('hello dir1') - expect(readHelloWorld('/tmp/dir2/hw.txt')).toBe('hello dir2') -}) -``` - -## Requests - -Because Vitest runs in Node, mocking network requests is tricky; web APIs are not available, so we need something that will mimic network behavior for us. We recommend [Mock Service Worker](https://mswjs.io/) to accomplish this. It allows you to mock `http`, `WebSocket` and `GraphQL` network requests, and is framework agnostic. - -Mock Service Worker (MSW) works by intercepting the requests your tests make, allowing you to use it without changing any of your application code. In-browser, this uses the [Service Worker API](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API). In Node.js, and for Vitest, it uses the [`@mswjs/interceptors`](https://github.com/mswjs/interceptors) library. To learn more about MSW, read their [introduction](https://mswjs.io/docs/) - -### Configuration - -You can use it like below in your [setup file](/config/#setupfiles) - -::: code-group - -```js [HTTP Setup] -import { afterAll, afterEach, beforeAll } from 'vitest' -import { setupServer } from 'msw/node' -import { http, HttpResponse } from 'msw' - -const posts = [ - { - userId: 1, - id: 1, - title: 'first post title', - body: 'first post body', - }, - // ... -] - -export const restHandlers = [ - http.get('https://rest-endpoint.example/path/to/posts', () => { - return HttpResponse.json(posts) - }), -] - -const server = setupServer(...restHandlers) - -// Start server before all tests -beforeAll(() => server.listen({ onUnhandledRequest: 'error' })) - -// Close server after all tests -afterAll(() => server.close()) - -// Reset handlers after each test for test isolation -afterEach(() => server.resetHandlers()) -``` - -```js [GraphQL Setup] -import { afterAll, afterEach, beforeAll } from 'vitest' -import { setupServer } from 'msw/node' -import { graphql, HttpResponse } from 'msw' - -const posts = [ - { - userId: 1, - id: 1, - title: 'first post title', - body: 'first post body', - }, - // ... -] - -const graphqlHandlers = [ - graphql.query('ListPosts', () => { - return HttpResponse.json({ - data: { posts }, - }) - }), -] - -const server = setupServer(...graphqlHandlers) - -// Start server before all tests -beforeAll(() => server.listen({ onUnhandledRequest: 'error' })) - -// Close server after all tests -afterAll(() => server.close()) - -// Reset handlers after each test for test isolation -afterEach(() => server.resetHandlers()) -``` - -```js [WebSocket Setup] -import { afterAll, afterEach, beforeAll } from 'vitest' -import { setupServer } from 'msw/node' -import { ws } from 'msw' - -const chat = ws.link('wss://chat.example.com') - -const wsHandlers = [ - chat.addEventListener('connection', ({ client }) => { - client.addEventListener('message', (event) => { - console.log('Received message from client:', event.data) - // Echo the received message back to the client - client.send(`Server received: ${event.data}`) - }) - }), -] - -const server = setupServer(...wsHandlers) - -// Start server before all tests -beforeAll(() => server.listen({ onUnhandledRequest: 'error' })) - -// Close server after all tests -afterAll(() => server.close()) - -// Reset handlers after each test for test isolation -afterEach(() => server.resetHandlers()) -``` -::: - -> Configuring the server with `onUnhandledRequest: 'error'` ensures that an error is thrown whenever there is a request that does not have a corresponding request handler. - -### More -There is much more to MSW. You can access cookies and query parameters, define mock error responses, and much more! To see all you can do with MSW, read [their documentation](https://mswjs.io/docs). - -## Timers - -When we test code that involves timeouts or intervals, instead of having our tests wait it out or timeout, we can speed up our tests by using "fake" timers that mock calls to `setTimeout` and `setInterval`. - -See the [`vi.useFakeTimers` API section](/api/vi#vi-usefaketimers) for a more in depth detailed API description. - -### Example - -```js -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' - -function executeAfterTwoHours(func) { - setTimeout(func, 1000 * 60 * 60 * 2) // 2 hours -} - -function executeEveryMinute(func) { - setInterval(func, 1000 * 60) // 1 minute -} - -const mock = vi.fn(() => console.log('executed')) - -describe('delayed execution', () => { - beforeEach(() => { - vi.useFakeTimers() - }) - afterEach(() => { - vi.restoreAllMocks() - }) - it('should execute the function', () => { - executeAfterTwoHours(mock) - vi.runAllTimers() - expect(mock).toHaveBeenCalledTimes(1) - }) - it('should not execute the function', () => { - executeAfterTwoHours(mock) - // advancing by 2ms won't trigger the func - vi.advanceTimersByTime(2) - expect(mock).not.toHaveBeenCalled() - }) - it('should execute every minute', () => { - executeEveryMinute(mock) - vi.advanceTimersToNextTimer() - expect(mock).toHaveBeenCalledTimes(1) - vi.advanceTimersToNextTimer() - expect(mock).toHaveBeenCalledTimes(2) - }) -}) -``` - -## Classes - -You can mock an entire class with a single `vi.fn` call - since all classes are also functions, this works out of the box. Beware that currently Vitest doesn't respect the `new` keyword so the `new.target` is always `undefined` in the body of a function. - -```ts -class Dog { - name: string - - constructor(name: string) { - this.name = name - } - - static getType(): string { - return 'animal' - } - - greet = (): string => { - return `Hi! My name is ${this.name}!` - } - - speak(): string { - return 'bark!' - } - - isHungry() {} - feed() {} -} -``` - -We can re-create this class with ES5 functions: - -```ts -const Dog = vi.fn(function (name) { - this.name = name - // mock instance methods in the constructor, each instance will have its own spy - this.greet = vi.fn(() => `Hi! My name is ${this.name}!`) -}) - -// notice that static methods are mocked directly on the function, -// not on the instance of the class -Dog.getType = vi.fn(() => 'mocked animal') - -// mock the "speak" and "feed" methods on every instance of a class -// all `new Dog()` instances will inherit and share these spies -Dog.prototype.speak = vi.fn(() => 'loud bark!') -Dog.prototype.feed = vi.fn() -``` - -::: warning -If a non-primitive is returned from the constructor function, that value will become the result of the new expression. In this case the `[[Prototype]]` may not be correctly bound: - -```ts -const CorrectDogClass = vi.fn(function (name) { - this.name = name -}) - -const IncorrectDogClass = vi.fn(name => ({ - name -})) - -const Marti = new CorrectDogClass('Marti') -const Newt = new IncorrectDogClass('Newt') - -Marti instanceof CorrectDogClass // ✅ true -Newt instanceof IncorrectDogClass // ❌ false! -``` -::: - -::: tip WHEN TO USE? -Generally speaking, you would re-create a class like this inside the module factory if the class is re-exported from another module: - -```ts -import { Dog } from './dog.js' - -vi.mock(import('./dog.js'), () => { - const Dog = vi.fn() - Dog.prototype.feed = vi.fn() - // ... other mocks - return { Dog } -}) -``` - -This method can also be used to pass an instance of a class to a function that accepts the same interface: - -```ts [src/feed.ts] -function feed(dog: Dog) { - // ... -} -``` -```ts [tests/dog.test.ts] -import { expect, test, vi } from 'vitest' -import { feed } from '../src/feed.js' - -const Dog = vi.fn() -Dog.prototype.feed = vi.fn() - -test('can feed dogs', () => { - const dogMax = new Dog('Max') - - feed(dogMax) - - expect(dogMax.feed).toHaveBeenCalled() - expect(dogMax.isHungry()).toBe(false) -}) -``` -::: - -Now, when we create a new instance of the `Dog` class its `speak` method (alongside `feed` and `greet`) is already mocked: - -```ts -const Cooper = new Dog('Cooper') -Cooper.speak() // loud bark! -Cooper.greet() // Hi! My name is Cooper! - -// you can use built-in assertions to check the validity of the call -expect(Cooper.speak).toHaveBeenCalled() -expect(Cooper.greet).toHaveBeenCalled() - -const Max = new Dog('Max') - -// methods assigned to the prototype are shared between instances -expect(Max.speak).toHaveBeenCalled() -expect(Max.greet).not.toHaveBeenCalled() -``` - -We can reassign the return value for a specific instance: - -```ts -const dog = new Dog('Cooper') - -// "vi.mocked" is a type helper, since -// TypeScript doesn't know that Dog is a mocked class, -// it wraps any function in a MockInstance type -// without validating if the function is a mock -vi.mocked(dog.speak).mockReturnValue('woof woof') - -dog.speak() // woof woof -``` - -To mock the property, we can use the `vi.spyOn(dog, 'name', 'get')` method. This makes it possible to use spy assertions on the mocked property: - -```ts -const dog = new Dog('Cooper') - -const nameSpy = vi.spyOn(dog, 'name', 'get').mockReturnValue('Max') - -expect(dog.name).toBe('Max') -expect(nameSpy).toHaveBeenCalledTimes(1) -``` - -::: tip -You can also spy on getters and setters using the same method. -::: +For a simpler and quicker way to get started with mocking, you can check the Cheat Sheet below. ## Cheat Sheet -:::info -`vi` in the examples below is imported directly from `vitest`. You can also use it globally, if you set `globals` to `true` in your [config](/config/). -::: - I want to… ### Mock exported variables @@ -764,6 +40,10 @@ import * as exports from './example.js' vi.spyOn(exports, 'getter', 'get').mockReturnValue('mocked') ``` +::: warning +This will not work in the Browser Mode. For a workaround, see [Limitations](/guide/browser/#spying-on-module-exports). +::: + ### Mock an exported function 1. Example with `vi.mock`: @@ -790,9 +70,13 @@ import * as exports from './example.js' vi.spyOn(exports, 'method').mockImplementation(() => {}) ``` +::: warning +`vi.spyOn` example will not work in the Browser Mode. For a workaround, see [Limitations](/guide/browser/#spying-on-module-exports). +::: + ### Mock an exported class implementation -1. Example with `vi.mock` and `.prototype`: +1. Example with a fake `class`: ```ts [example.js] export class SomeClass {} ``` @@ -800,11 +84,11 @@ export class SomeClass {} import { SomeClass } from './example.js' vi.mock(import('./example.js'), () => { - const SomeClass = vi.fn() - SomeClass.prototype.someMethod = vi.fn() + const SomeClass = vi.fn(class FakeClass { + someMethod = vi.fn() + }) return { SomeClass } }) -// SomeClass.mock.instances will have SomeClass ``` 2. Example with `vi.spyOn`: @@ -812,12 +96,15 @@ vi.mock(import('./example.js'), () => { ```ts import * as mod from './example.js' -const SomeClass = vi.fn() -SomeClass.prototype.someMethod = vi.fn() - -vi.spyOn(mod, 'SomeClass').mockImplementation(SomeClass) +vi.spyOn(mod, 'SomeClass').mockImplementation(class FakeClass { + someMethod = vi.fn() +}) ``` +::: warning +`vi.spyOn` example will not work in the Browser Mode. For a workaround, see [Limitations](/guide/browser/#spying-on-module-exports). +::: + ### Spy on an object returned from a function 1. Example using cache: diff --git a/docs/guide/mocking/classes.md b/docs/guide/mocking/classes.md new file mode 100644 index 000000000000..abfebc77ce6b --- /dev/null +++ b/docs/guide/mocking/classes.md @@ -0,0 +1,158 @@ +# Mocking Classes + +You can mock an entire class with a single [`vi.fn`](/api/vi#fn) call. + +```ts +class Dog { + name: string + + constructor(name: string) { + this.name = name + } + + static getType(): string { + return 'animal' + } + + greet = (): string => { + return `Hi! My name is ${this.name}!` + } + + speak(): string { + return 'bark!' + } + + isHungry() {} + feed() {} +} +``` + +We can re-create this class with `vi.fn` (or `vi.spyOn().mockImplementation()`): + +```ts +const Dog = vi.fn(class { + static getType = vi.fn(() => 'mocked animal') + + constructor(name) { + this.name = name + } + + greet = vi.fn(() => `Hi! My name is ${this.name}!`) + speak = vi.fn(() => 'loud bark!') + feed = vi.fn() +}) +``` + +::: warning +If a non-primitive is returned from the constructor function, that value will become the result of the new expression. In this case the `[[Prototype]]` may not be correctly bound: + +```ts +const CorrectDogClass = vi.fn(function (name) { + this.name = name +}) + +const IncorrectDogClass = vi.fn(name => ({ + name +})) + +const Marti = new CorrectDogClass('Marti') +const Newt = new IncorrectDogClass('Newt') + +Marti instanceof CorrectDogClass // ✅ true +Newt instanceof IncorrectDogClass // ❌ false! +``` + +If you are mocking classes, prefer the class syntax over the function. +::: + +::: tip WHEN TO USE? +Generally speaking, you would re-create a class like this inside the module factory if the class is re-exported from another module: + +```ts +import { Dog } from './dog.js' + +vi.mock(import('./dog.js'), () => { + const Dog = vi.fn(class { + feed = vi.fn() + // ... other mocks + }) + return { Dog } +}) +``` + +This method can also be used to pass an instance of a class to a function that accepts the same interface: + +```ts [src/feed.ts] +function feed(dog: Dog) { + // ... +} +``` +```ts [tests/dog.test.ts] +import { expect, test, vi } from 'vitest' +import { feed } from '../src/feed.js' + +const Dog = vi.fn(class { + feed = vi.fn() +}) + +test('can feed dogs', () => { + const dogMax = new Dog('Max') + + feed(dogMax) + + expect(dogMax.feed).toHaveBeenCalled() + expect(dogMax.isHungry()).toBe(false) +}) +``` +::: + +Now, when we create a new instance of the `Dog` class its `speak` method (alongside `feed` and `greet`) is already mocked: + +```ts +const Cooper = new Dog('Cooper') +Cooper.speak() // loud bark! +Cooper.greet() // Hi! My name is Cooper! + +// you can use built-in assertions to check the validity of the call +expect(Cooper.speak).toHaveBeenCalled() +expect(Cooper.greet).toHaveBeenCalled() + +const Max = new Dog('Max') + +// methods are not shared between instances if you assigned them directly +expect(Max.speak).not.toHaveBeenCalled() +expect(Max.greet).not.toHaveBeenCalled() +``` + +We can reassign the return value for a specific instance: + +```ts +const dog = new Dog('Cooper') + +// "vi.mocked" is a type helper, since +// TypeScript doesn't know that Dog is a mocked class, +// it wraps any function in a Mock type +// without validating if the function is a mock +vi.mocked(dog.speak).mockReturnValue('woof woof') + +dog.speak() // woof woof +``` + +To mock the property, we can use the `vi.spyOn(dog, 'name', 'get')` method. This makes it possible to use spy assertions on the mocked property: + +```ts +const dog = new Dog('Cooper') + +const nameSpy = vi.spyOn(dog, 'name', 'get').mockReturnValue('Max') + +expect(dog.name).toBe('Max') +expect(nameSpy).toHaveBeenCalledTimes(1) +``` + +::: tip +You can also spy on getters and setters using the same method. +::: + +::: danger +Using classes with `vi.fn()` was introduced in Vitest 4. Previously, you had to use `function` and `prototype` inheritence directly. See [v3 guide](https://v3.vitest.dev/guide/mocking.html#classes). +::: diff --git a/docs/guide/mocking/dates.md b/docs/guide/mocking/dates.md new file mode 100644 index 000000000000..f021556ed5de --- /dev/null +++ b/docs/guide/mocking/dates.md @@ -0,0 +1,52 @@ +# Mocking Dates + +Sometimes you need to be in control of the date to ensure consistency when testing. Vitest uses [`@sinonjs/fake-timers`](https://github.com/sinonjs/fake-timers) package for manipulating timers, as well as system date. You can find more about the specific API in detail [here](/api/vi#vi-setsystemtime). + +## Example + +```js +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +const businessHours = [9, 17] + +function purchase() { + const currentHour = new Date().getHours() + const [open, close] = businessHours + + if (currentHour > open && currentHour < close) { + return { message: 'Success' } + } + + return { message: 'Error' } +} + +describe('purchasing flow', () => { + beforeEach(() => { + // tell vitest we use mocked time + vi.useFakeTimers() + }) + + afterEach(() => { + // restoring date after each test run + vi.useRealTimers() + }) + + it('allows purchases within business hours', () => { + // set hour within business hours + const date = new Date(2000, 1, 1, 13) + vi.setSystemTime(date) + + // access Date.now() will result in the date set above + expect(purchase()).toEqual({ message: 'Success' }) + }) + + it('disallows purchases outside of business hours', () => { + // set hour outside business hours + const date = new Date(2000, 1, 1, 19) + vi.setSystemTime(date) + + // access Date.now() will result in the date set above + expect(purchase()).toEqual({ message: 'Error' }) + }) +}) +``` diff --git a/docs/guide/mocking/file-system.md b/docs/guide/mocking/file-system.md new file mode 100644 index 000000000000..a8be3df0835a --- /dev/null +++ b/docs/guide/mocking/file-system.md @@ -0,0 +1,74 @@ +# Mocking the File System + +Mocking the file system ensures that the tests do not depend on the actual file system, making the tests more reliable and predictable. This isolation helps in avoiding side effects from previous tests. It allows for testing error conditions and edge cases that might be difficult or impossible to replicate with an actual file system, such as permission issues, disk full scenarios, or read/write errors. + +Vitest doesn't provide any file system mocking API out of the box. You can use `vi.mock` to mock the `fs` module manually, but it's hard to maintain. Instead, we recommend using [`memfs`](https://www.npmjs.com/package/memfs) to do that for you. `memfs` creates an in-memory file system, which simulates file system operations without touching the actual disk. This approach is fast and safe, avoiding any potential side effects on the real file system. + +## Example + +To automatically redirect every `fs` call to `memfs`, you can create `__mocks__/fs.cjs` and `__mocks__/fs/promises.cjs` files at the root of your project: + +::: code-group +```ts [__mocks__/fs.cjs] +// we can also use `import`, but then +// every export should be explicitly defined + +const { fs } = require('memfs') +module.exports = fs +``` + +```ts [__mocks__/fs/promises.cjs] +// we can also use `import`, but then +// every export should be explicitly defined + +const { fs } = require('memfs') +module.exports = fs.promises +``` +::: + +```ts [read-hello-world.js] +import { readFileSync } from 'node:fs' + +export function readHelloWorld(path) { + return readFileSync(path, 'utf-8') +} +``` + +```ts [hello-world.test.js] +import { beforeEach, expect, it, vi } from 'vitest' +import { fs, vol } from 'memfs' +import { readHelloWorld } from './read-hello-world.js' + +// tell vitest to use fs mock from __mocks__ folder +// this can be done in a setup file if fs should always be mocked +vi.mock('node:fs') +vi.mock('node:fs/promises') + +beforeEach(() => { + // reset the state of in-memory fs + vol.reset() +}) + +it('should return correct text', () => { + const path = '/hello-world.txt' + fs.writeFileSync(path, 'hello world') + + const text = readHelloWorld(path) + expect(text).toBe('hello world') +}) + +it('can return a value multiple times', () => { + // you can use vol.fromJSON to define several files + vol.fromJSON( + { + './dir1/hw.txt': 'hello dir1', + './dir2/hw.txt': 'hello dir2', + }, + // default cwd + '/tmp', + ) + + expect(readHelloWorld('/tmp/dir1/hw.txt')).toBe('hello dir1') + expect(readHelloWorld('/tmp/dir2/hw.txt')).toBe('hello dir2') +}) +``` diff --git a/docs/guide/mocking/functions.md b/docs/guide/mocking/functions.md new file mode 100644 index 000000000000..58c729911ded --- /dev/null +++ b/docs/guide/mocking/functions.md @@ -0,0 +1,61 @@ +# Mocking Functions + +Mocking functions can be split up into two different categories: spying and mocking. + +If you need to observe the behaviour of a method on an object, you can use [`vi.spyOn()`](/api/vi#vi-spyon) to create a spy that tracks calls to that method. + +If you need to pass down a custom function implementation as an argument or create a new mocked entity, you can use [`vi.fn()`](/api/vi#vi-fn) to create a mock function. + +Both `vi.spyOn` and `vi.fn` share the same methods. + +## Example + +```js +import { afterEach, describe, expect, it, vi } from 'vitest' + +const messages = { + items: [ + { message: 'Simple test message', from: 'Testman' }, + // ... + ], + addItem(item) { + messages.items.push(item) + messages.callbacks.forEach(callback => callback(item)) + }, + onItem(callback) { + messages.callbacks.push(callback) + }, + getLatest, // can also be a `getter or setter if supported` +} + +function getLatest(index = messages.items.length - 1) { + return messages.items[index] +} + +it('should get the latest message with a spy', () => { + const spy = vi.spyOn(messages, 'getLatest') + expect(spy.getMockName()).toEqual('getLatest') + + expect(messages.getLatest()).toEqual( + messages.items[messages.items.length - 1], + ) + + expect(spy).toHaveBeenCalledTimes(1) + + spy.mockImplementationOnce(() => 'access-restricted') + expect(messages.getLatest()).toEqual('access-restricted') + + expect(spy).toHaveBeenCalledTimes(2) +}) + +it('passing down the mock', () => { + const callback = vi.fn() + messages.onItem(callback) + + messages.addItem({ message: 'Another test message', from: 'Testman' }) + expect(callback).toHaveBeenCalledWith({ + message: 'Another test message', + from: 'Testman', + }) +}) +``` diff --git a/docs/guide/mocking/globals.md b/docs/guide/mocking/globals.md new file mode 100644 index 000000000000..fe195628cc0c --- /dev/null +++ b/docs/guide/mocking/globals.md @@ -0,0 +1,20 @@ +# Mocking Globals + +You can mock global variables that are not present with `jsdom` or `node` by using [`vi.stubGlobal`](/api/vi#vi-stubglobal) helper. It will put the value of the global variable into a `globalThis` object. + +By default, Vitest does not reset these globals, but you can turn on the [`unstubGlobals`](/config/#unstubglobals) option in your config to restore the original values after each test or call [`vi.unstubAllGlobals()`](/api/vi#vi-unstuballglobals) manually. + +```ts +import { vi } from 'vitest' + +const IntersectionObserverMock = vi.fn(() => ({ + disconnect: vi.fn(), + observe: vi.fn(), + takeRecords: vi.fn(), + unobserve: vi.fn(), +})) + +vi.stubGlobal('IntersectionObserver', IntersectionObserverMock) + +// now you can access it as `IntersectionObserver` or `window.IntersectionObserver` +``` diff --git a/docs/guide/mocking/modules.md b/docs/guide/mocking/modules.md new file mode 100644 index 000000000000..9a0a29ef480e --- /dev/null +++ b/docs/guide/mocking/modules.md @@ -0,0 +1,412 @@ +# Mocking Modules + +## Defining a Module + +Before mocking a "module", we should define what it is. In Vitest context, the "module" is a file that exports something. Using [plugins](https://vite.dev/guide/api-plugin.html), any file can be turned into a JavaScript module. The "module object" is a namespace object that holds dynamic references to exported identifiers. Simply put, it's an object with exported methods and properties. In this example, `example.js` is a module that exports `method` and `variable`: + +```js [example.js] +export function answer() { + // ... + return 42 +} + +export const variable = 'example' +``` + +The `exampleObject` here is a module object: + +```js [example.test.js] +import * as exampleObject from './example.js' +``` + +The `exampleObject` will always exist even if you imported the example using named imports: + +```js [example.test.js] +import { answer, variable } from './example.js' +``` + +You can only reference `exampleObject` outside the example module itself. For example, in a test. + +## Mocking a Module + +For the purpose of this guide, let's introduce some definitions. + +- **Mocked module** is a module that was completely replaced with another one. +- **Spied module** is a mocked module, but its exported methods keep the original implementation. They can also be tracked. +- **Mocked export** is a module export, which invocations can be tracked. +- **Spied export** is a mocked export. + +To mock a module completely, you can use the [`vi.mock` API](/api/vi#vi-mock). You can define a new module dynamically by providing a factory that returns a new module as a second argument: + +```ts +import { vi } from 'vitest' + +// The ./example.js module will be replaced with +// the result of a factory function, and the +// original ./example.js module will never be called +vi.mock(import('./example.js'), () => { + return { + answer() { + // ... + return 42 + }, + variable: 'mock', + } +}) +``` + +::: tip +Remember that you can call `vi.mock` in a [setup file](/config/#setupfiles) to apply the module mock in every test file automatically. +::: + +::: tip +Note the usage of dynamic import: `import('./example.ts')`. Vitest will strip it before the code is executed, but it allows TypeScript to properly validate the string and type the `importOriginal` method in your IDE or CLI. +::: + +If your code is trying to access a method that was not returned from this factory, Vitest will throw an error with a helpful message. Note that `answer` is not mocked, i.e. it cannot be tracked. To make it trackable, use `vi.fn()` instead: + +```ts +import { vi } from 'vitest' + +vi.mock(import('./example.js'), () => { + return { + answer: vi.fn(), + variable: 'mock', + } +}) +``` + +The factory method accepts an `importOriginal` function that will execute the original module and return its module object: + +```ts +import { expect, vi } from 'vitest' +import { answer } from './example.js' + +vi.mock(import('./example.js'), async (importOriginal) => { + const originalModule = await importOriginal() + return { + answer: vi.fn(originalModule.answer), + variable: 'mock', + } +}) + +expect(answer()).toBe(42) + +expect(answer).toHaveBeenCalled() +expect(answer).toHaveReturned(42) +``` + +::: warning +Note that `importOriginal` is asynchronous and needs to be awaited. +::: + +In the above example, we provided the original `answer` to the `vi.fn()` call so it can keep calling it while being tracked at the same time. + +If you require the use of `importOriginal`, consider spying on the export directly via another API: `vi.spyOn`. Instead of replacing the whole module, you can spy only on a single exported method. To do that, you need to import the module as a namespace object: + +```ts +import { expect, vi } from 'vitest' +import * as exampleObject from './example.js' + +const spy = vi.spyOn(exampleObject, 'answer').mockReturnValue(0) + +expect(exampleObject.answer()).toBe(0) +expect(exampleObject.answer).toHaveBeenCalled() +``` + +::: danger Browser Mode Support +This will not work in the [Browser Mode](/guide/browser/) because it uses the browser's native ESM support to serve modules. The module namespace object is sealed and can't be reconfigured. To bypass this limitation, Vitest supports `{ spy: true }` option in `vi.mock('./example.js')`. This will automatically spy on every export in the module without replacing them with fake ones. + +```ts +import { vi } from 'vitest' +import * as exampleObject from './example.js' + +vi.mock('./example.js', { spy: true }) + +vi.mocked(exampleObject.answer).mockReturnValue(0) +``` +::: + +::: warning +You only need to import the module as a namespace object in the file where you are using the `vi.spyOn` utility. If the `answer` is called in another file and is imported there as a named export, Vitest will be able to properly track it as long as the function that called it is called after `vi.spyOn`: + +```ts [source.js] +import { answer } from './example.js' + +export function question() { + if (answer() === 42) { + return 'Ultimate Question of Life, the Universe, and Everything' + } + + return 'Unknown Question' +} +``` +::: + +Note that `vi.spyOn` will only spy on calls that were done after it spied on the method. So, if the function is executed at the top level during an import or it was called before the spying, `vi.spyOn` will not be able to report on it. + +To automatically mock any module before it is imported, you can call `vi.mock` with a path: + +```ts +import { vi } from 'vitest' + +vi.mock(import('./example.js')) +``` + +If the file `./__mocks__/example.js` exists, then Vitest will load it instead. Otherwise, Vitest will load the original module and replace everything recursively: + +{#automocking-algorithm} + +- All arrays will be empty +- All primitives will stay untouched +- All getters will return `undefined` +- All methods will return `undefined` +- All objects will be deeply cloned +- All instances of classes and their prototypes will be cloned + +To disable this behavior, you can pass down `spy: true` as the second argument: + +```ts +import { vi } from 'vitest' + +vi.mock(import('./example.js'), { spy: true }) +``` + +Instead of returning `undefined`, all methods will call the original implementation, but you can still keep track of these calls: + +```ts +import { expect, vi } from 'vitest' +import { answer } from './example.js' + +vi.mock(import('./example.js'), { spy: true }) + +// calls the original implementation +expect(answer()).toBe(42) +// vitest can still track the invocations +expect(answer).toHaveBeenCalled() +``` + +One nice thing that mocked modules support is sharing the state between the instance and its prototype. Consider this module: + +```ts [answer.js] +export class Answer { + constructor(value) { + this._value = value + } + + value() { + return this._value + } +} +``` + +By mocking it, we can keep track of every invocation of `.value()` even without having access to the instance itself: + +```ts [answer.test.js] +import { expect, test, vi } from 'vitest' +import { Answer } from './answer.js' + +vi.mock(import('./answer.js'), { spy: true }) + +test('instance inherits the state', () => { + // these invocations could be private inside another function + // that you don't have access to in your test + const answer1 = new Answer(42) + const answer2 = new Answer(0) + + expect(answer1.value()).toBe(42) + expect(answer1.value).toHaveBeenCalled() + // note that different instances have their own states + expect(answer2.value).not.toHaveBeenCalled() + + expect(answer2.value()).toBe(0) + + // but the prototype state accumulates all calls + expect(Answer.prototype.value).toHaveBeenCalledTimes(2) + expect(Answer.prototype.value).toHaveReturned(42) + expect(Answer.prototype.value).toHaveReturned(0) +}) +``` + +This can be very useful to track calls to instances that are never exposed. + +## Mocking Non-existing Module + +Vitest supports mocking virtual modules. These modules don't exist on the file system, but your code imports them. For example, this can happen when your development environment is different from production. One common example is mocking `vscode` APIs in your unit tests. + +By default, Vitest will fail transforming files if it cannot find the source of the import. To bypass this, you need to specify it in your config. You can either always redirect the import to a file, or just signal Vite to ignore it and use the `vi.mock` factory to define its exports. + +To redirect the import, use [`test.alias`](/config/#alias) config option: + +```ts [vitest.config.ts] +import { defineConfig } from 'vitest/config' +import { resolve } from 'node:path' + +export default defineConfig({ + test: { + alias: { + vscode: resolve(import.meta.dirname, './mock/vscode.js'), + }, + }, +}) +``` + +To mark the module as always resolved, return the same string from `resolveId` hook of a plugin: + +```ts [vitest.config.ts] +import { defineConfig } from 'vitest/config' +import { resolve } from 'node:path' + +export default defineConfig({ + plugins: [ + { + name: 'virtual-vscode', + resolveId(id) { + if (id === 'vscode') { + return 'vscode' + } + } + } + ] +}) +``` + +Now you can use `vi.mock` as usual in your tests: + +```ts +import { vi } from 'vitest' + +vi.mock(import('vscode'), () => { + return { + window: { + createOutputChannel: vi.fn(), + } + } +}) +``` + +## How it Works + +Vitest implements different module mocking mechanisms depending on the environment. The only feature they share is the plugin transformer. When Vitest sees that a file has `vi.mock` inside, it will transform every static import into a dynamic one and move the `vi.mock` call to the top of the file. This allows Vitest to register the mock before the import happens without breaking the ESM rule of hoisted imports. + +::: code-group +```ts [example.js] +import { answer } from './answer.js' + +vi.mock(import('./answer.js')) + +console.log(answer) +``` +```ts [example.transformed.js] +vi.mock('./answer.js') + +const __vitest_module_0__ = await __handle_mock__( + () => import('./answer.js') +) +// to keep the live binding, we have to access +// the export on the module namespace +console.log(__vitest_module_0__.answer()) +``` +::: + +The `__handle_mock__` wrapper just makes sure the mock is resolved before the import is initiated, it doesn't modify the module in any way. + +The module mocking plugins are available in the [`@vitest/mocker` package](https://github.com/vitest-dev/vitest/tree/main/packages/mocker). + +### JSDOM, happy-dom, Node + +When you run your tests in an emulated environment, Vitest creates a [module runner](https://vite.dev/guide/api-environment-runtimes.html#modulerunner) that can consume Vite code. The module runner is designed in such a way that Vitest can hook into the module evaluation and replace it with the mock, if it was registered. This means that Vitest runs your code in an ESM-like environment, but it doesn't use native ESM mechanism directly. This allows the test runner to bend the rules around ES Modules immutability, allowing users to call `vi.spyOn` on a seemingly ES Module. + +### Browser Mode + +Vitest uses native ESM in the Browser Mode. This means that we cannot replace the module so easily. Instead, Vitest intercepts the fetch request (via playwright's `page.route` or a Vite plugin API if using `preview` or `webdriverio`) and serves transformed code, if the module was mocked. + +For example, if the module is automocked, Vitest can parse static exports and create a placeholder module: + +::: code-group +```ts [answer.js] +export function answer() { + return 42 +} +``` +```ts [answer.transformed.js] +function answer() { + return 42 +} + +const __private_module__ = { + [Symbol.toStringTag]: 'Module', + answer: vi.fn(answer), +} + +export const answer = __private_module__.answer +``` +::: + +The example is simplified for brevity, but the concept is unchanged. We can inject a `__private_module__` variable into the module to hold the mocked values. If the user called `vi.mock` with `spy: true`, we pass down the original value; otherwise, we create a simple `vi.fn()` mock. + +If user defined a custom factory, this makes it harder to inject the code, but not impossible. When the mocked file is served, we first resolve the factory in the browser, then pass down the keys back to the server, and use them to create a placeholder module: + +```ts +const resolvedFactoryKeys = await resolveBrowserFactory(url) +const mockedModule = ` +const __private_module__ = getFactoryReturnValue(${url}) +${resolvedFactoryKeys.map(key => `export const ${key} = __private_module__["${key}"]`).join('\n')} +` +``` + +This module can now be served back to the browser. You can inspect the code in the devtools when you run the tests. + +## Mocking Modules Pitfalls + +Beware that it is not possible to mock calls to methods that are called inside other methods of the same file. For example, in this code: + +```ts [foobar.js] +export function foo() { + return 'foo' +} + +export function foobar() { + return `${foo()}bar` +} +``` + +It is not possible to mock the `foo` method from the outside because it is referenced directly. So this code will have no effect on the `foo` call inside `foobar` (but it will affect the `foo` call in other modules): + +```ts [foobar.test.ts] +import { vi } from 'vitest' +import * as mod from './foobar.js' + +// this will only affect "foo" outside of the original module +vi.spyOn(mod, 'foo') +vi.mock(import('./foobar.js'), async (importOriginal) => { + return { + ...await importOriginal(), + // this will only affect "foo" outside of the original module + foo: () => 'mocked' + } +}) +``` + +You can confirm this behavior by providing the implementation to the `foobar` method directly: + +```ts [foobar.test.js] +import * as mod from './foobar.js' + +vi.spyOn(mod, 'foo') + +// exported foo references mocked method +mod.foobar(mod.foo) +``` + +```ts [foobar.js] +export function foo() { + return 'foo' +} + +export function foobar(injectedFoo) { + return injectedFoo === foo // false +} +``` + +This is the intended behavior, and we do not plan to implement a workaround. Consider refactoring your code into multiple files or use techniques such as [dependency injection](https://en.wikipedia.org/wiki/Dependency_injection). We believe that making the application testable is not the responsibility of the test runner, but of the application architecture. diff --git a/docs/guide/mocking/requests.md b/docs/guide/mocking/requests.md new file mode 100644 index 000000000000..f6e532ecb4de --- /dev/null +++ b/docs/guide/mocking/requests.md @@ -0,0 +1,114 @@ +# Mocking Requests + +Because Vitest runs in Node, mocking network requests is tricky; web APIs are not available, so we need something that will mimic network behavior for us. We recommend [Mock Service Worker](https://mswjs.io/) to accomplish this. It allows you to mock `http`, `WebSocket` and `GraphQL` network requests, and is framework agnostic. + +Mock Service Worker (MSW) works by intercepting the requests your tests make, allowing you to use it without changing any of your application code. In-browser, this uses the [Service Worker API](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API). In Node.js, and for Vitest, it uses the [`@mswjs/interceptors`](https://github.com/mswjs/interceptors) library. To learn more about MSW, read their [introduction](https://mswjs.io/docs/) + +## Configuration + +You can use it like below in your [setup file](/config/#setupfiles) + +::: code-group + +```js [HTTP Setup] +import { afterAll, afterEach, beforeAll } from 'vitest' +import { setupServer } from 'msw/node' +import { http, HttpResponse } from 'msw' + +const posts = [ + { + userId: 1, + id: 1, + title: 'first post title', + body: 'first post body', + }, + // ... +] + +export const restHandlers = [ + http.get('https://rest-endpoint.example/path/to/posts', () => { + return HttpResponse.json(posts) + }), +] + +const server = setupServer(...restHandlers) + +// Start server before all tests +beforeAll(() => server.listen({ onUnhandledRequest: 'error' })) + +// Close server after all tests +afterAll(() => server.close()) + +// Reset handlers after each test for test isolation +afterEach(() => server.resetHandlers()) +``` + +```js [GraphQL Setup] +import { afterAll, afterEach, beforeAll } from 'vitest' +import { setupServer } from 'msw/node' +import { graphql, HttpResponse } from 'msw' + +const posts = [ + { + userId: 1, + id: 1, + title: 'first post title', + body: 'first post body', + }, + // ... +] + +const graphqlHandlers = [ + graphql.query('ListPosts', () => { + return HttpResponse.json({ + data: { posts }, + }) + }), +] + +const server = setupServer(...graphqlHandlers) + +// Start server before all tests +beforeAll(() => server.listen({ onUnhandledRequest: 'error' })) + +// Close server after all tests +afterAll(() => server.close()) + +// Reset handlers after each test for test isolation +afterEach(() => server.resetHandlers()) +``` + +```js [WebSocket Setup] +import { afterAll, afterEach, beforeAll } from 'vitest' +import { setupServer } from 'msw/node' +import { ws } from 'msw' + +const chat = ws.link('wss://chat.example.com') + +const wsHandlers = [ + chat.addEventListener('connection', ({ client }) => { + client.addEventListener('message', (event) => { + console.log('Received message from client:', event.data) + // Echo the received message back to the client + client.send(`Server received: ${event.data}`) + }) + }), +] + +const server = setupServer(...wsHandlers) + +// Start server before all tests +beforeAll(() => server.listen({ onUnhandledRequest: 'error' })) + +// Close server after all tests +afterAll(() => server.close()) + +// Reset handlers after each test for test isolation +afterEach(() => server.resetHandlers()) +``` +::: + +> Configuring the server with `onUnhandledRequest: 'error'` ensures that an error is thrown whenever there is a request that does not have a corresponding request handler. + +## More +There is much more to MSW. You can access cookies and query parameters, define mock error responses, and much more! To see all you can do with MSW, read [their documentation](https://mswjs.io/docs). diff --git a/docs/guide/mocking/timers.md b/docs/guide/mocking/timers.md new file mode 100644 index 000000000000..7caa9cc7381f --- /dev/null +++ b/docs/guide/mocking/timers.md @@ -0,0 +1,48 @@ +# Timers + +When we test code that involves timeouts or intervals, instead of having our tests wait it out or timeout, we can speed up our tests by using "fake" timers that mock calls to `setTimeout` and `setInterval`. + +See the [`vi.useFakeTimers` API section](/api/vi#vi-usefaketimers) for a more in depth detailed API description. + +## Example + +```js +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +function executeAfterTwoHours(func) { + setTimeout(func, 1000 * 60 * 60 * 2) // 2 hours +} + +function executeEveryMinute(func) { + setInterval(func, 1000 * 60) // 1 minute +} + +const mock = vi.fn(() => console.log('executed')) + +describe('delayed execution', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + afterEach(() => { + vi.restoreAllMocks() + }) + it('should execute the function', () => { + executeAfterTwoHours(mock) + vi.runAllTimers() + expect(mock).toHaveBeenCalledTimes(1) + }) + it('should not execute the function', () => { + executeAfterTwoHours(mock) + // advancing by 2ms won't trigger the func + vi.advanceTimersByTime(2) + expect(mock).not.toHaveBeenCalled() + }) + it('should execute every minute', () => { + executeEveryMinute(mock) + vi.advanceTimersToNextTimer() + expect(mock).toHaveBeenCalledTimes(1) + vi.advanceTimersToNextTimer() + expect(mock).toHaveBeenCalledTimes(2) + }) +}) +``` diff --git a/docs/guide/parallelism.md b/docs/guide/parallelism.md index d856c9324568..5cda4aae9fd9 100644 --- a/docs/guide/parallelism.md +++ b/docs/guide/parallelism.md @@ -12,7 +12,7 @@ By default, Vitest runs _test files_ in parallel. Depending on the specified `po - `forks` (the default) and `vmForks` run tests in different [child processes](https://nodejs.org/api/child_process.html) - `threads` and `vmThreads` run tests in different [worker threads](https://nodejs.org/api/worker_threads.html) -Both "child processes" and "worker threads" are refered to as "workers". You can configure the number of running workers with [`minWorkers`](/config/#minworkers) and [`maxWorkers`](/config/#maxworkers) options. Or more granually with [`poolOptions`](/config/#pooloptions) configuration. +Both "child processes" and "worker threads" are refered to as "workers". You can configure the number of running workers with [`maxWorkers`](/config/#maxworkers) option. Or more granually with [`poolOptions`](/config/#pooloptions) configuration. If you have a lot of tests, it is usually faster to run them in parallel, but it also depends on the project, the environment and [isolation](/config/#isolate) state. To disable file parallelisation, you can set [`fileParallelism`](/config/#fileparallelism) to `false`. To learn more about possible performance improvements, read the [Performance Guide](/guide/improving-performance). diff --git a/docs/guide/projects.md b/docs/guide/projects.md index f51f286bbafe..20a3eb1b9820 100644 --- a/docs/guide/projects.md +++ b/docs/guide/projects.md @@ -42,7 +42,54 @@ export default defineConfig({ }) ``` -Vitest will treat every folder in `packages` as a separate project even if it doesn't have a config file inside. If this glob pattern matches _any file_, it will be considered a Vitest config even if it doesn't have a `vitest` in its name or has an obscure file extension. +Vitest will treat every folder in `packages` as a separate project even if it doesn't have a config file inside. If the glob pattern matches a file, it will validate that the name starts with `vitest.config`/`vite.config` or matches `(vite|vitest).*.config.*` pattern to ensure it's a Vitest configuration file. For example, these config files are valid: + +- `vitest.config.ts` +- `vite.config.js` +- `vitest.unit.config.ts` +- `vite.e2e.config.js` +- `vitest.config.unit.js` +- `vite.config.e2e.js` + +To exclude folders and files, you can use the negation pattern: + +```ts [vitest.config.ts] +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + // include all folders inside "packages" except "excluded" + projects: [ + 'packages/*', + '!packages/excluded' + ], + }, +}) +``` + +If you have a nested structure where some folders need to be projects, but other folders have their own subfolders, you have to use brackets to avoid matching the parent folder: + +```ts [vitest.config.ts] +import { defineConfig } from 'vitest/config' + +// For example, this will create projects: +// packages/a +// packages/b +// packages/business/c +// packages/business/d +// Notice that "packages/business" is not a project itself + +export default defineConfig({ + test: { + projects: [ + // matches every folder inside "packages" except "business" + 'packages/!(business)', + // matches every folder inside "packages/business" + 'packages/business/*', + ], + }, +}) +``` ::: warning Vitest does not treat the root `vitest.config` file as a project unless it is explicitly specified in the configuration. Consequently, the root configuration will only influence global options such as `reporters` and `coverage`. Note that Vitest will always run certain plugin hooks, like `apply`, `config`, `configResolved` or `configureServer`, specified in the root config file. Vitest also uses the same plugins to execute global setups and custom coverage provider. diff --git a/docs/guide/reporters.md b/docs/guide/reporters.md index 748079f7acb9..b411babb3583 100644 --- a/docs/guide/reporters.md +++ b/docs/guide/reporters.md @@ -141,28 +141,51 @@ Final output after tests have finished: Duration 1.26s (transform 35ms, setup 1ms, collect 90ms, tests 1.47s, environment 0ms, prepare 267ms) ``` -### Basic Reporter +If there is only one test file running, Vitest will output the full test tree of that file, simillar to the [`tree`](#tree-reporter) reporter. The default reporter will also print the test tree if there is at least one failed test in the file. -The `basic` reporter is equivalent to `default` reporter without `summary`. +```bash +✓ __tests__/file1.test.ts (2) 725ms + ✓ first test file (2) 725ms + ✓ 2 + 2 should equal 4 + ✓ 4 - 2 should equal 2 + + Test Files 1 passed (1) + Tests 2 passed (2) + Start at 12:34:32 + Duration 1.26s (transform 35ms, setup 1ms, collect 90ms, tests 1.47s, environment 0ms, prepare 267ms) +``` + +### Verbose Reporter + +The verbose reporter prints every test case once it is finished. It does not report suites or files separately. If `--includeTaskLocation` is enabled, it will also include the location of each test in the output. Similar to `default` reporter, you can disable the summary by configuring the reporter. + +In addition to this, the `verbose` reporter prints test error messages right away. The full test error is reported when the test run is finished. + +This is the only terminal reporter that reports [annotations](/guide/test-annotations) when the test doesn't fail. :::code-group ```bash [CLI] -npx vitest --reporter=basic +npx vitest --reporter=verbose ``` ```ts [vitest.config.ts] export default defineConfig({ test: { - reporters: ['basic'] + reporters: [ + ['verbose', { summary: false }] + ] }, }) ``` ::: -Example output using basic reporter: +Example output: + ```bash -✓ __tests__/file1.test.ts (2) 725ms -✓ __tests__/file2.test.ts (2) 746ms +✓ __tests__/file1.test.ts > first test file > 2 + 2 should equal 4 1ms +✓ __tests__/file1.test.ts > first test file > 4 - 2 should equal 2 1ms +✓ __tests__/file2.test.ts > second test file > 1 + 1 should equal 2 1ms +✓ __tests__/file2.test.ts > second test file > 2 - 1 should equal 1 1ms Test Files 2 passed (2) Tests 4 passed (4) @@ -170,20 +193,34 @@ Example output using basic reporter: Duration 1.26s (transform 35ms, setup 1ms, collect 90ms, tests 1.47s, environment 0ms, prepare 267ms) ``` -### Verbose Reporter +An example with `--includeTaskLocation`: + +```bash +✓ __tests__/file1.test.ts:2:1 > first test file > 2 + 2 should equal 4 1ms +✓ __tests__/file1.test.ts:3:1 > first test file > 4 - 2 should equal 2 1ms +✓ __tests__/file2.test.ts:2:1 > second test file > 1 + 1 should equal 2 1ms +✓ __tests__/file2.test.ts:3:1 > second test file > 2 - 1 should equal 1 1ms + + Test Files 2 passed (2) + Tests 4 passed (4) + Start at 12:34:32 + Duration 1.26s (transform 35ms, setup 1ms, collect 90ms, tests 1.47s, environment 0ms, prepare 267ms) +``` + +### Tree Reporter -Verbose reporter is same as `default` reporter, but it also displays each individual test after the suite has finished. It also displays currently running tests that are taking longer than [`slowTestThreshold`](/config/#slowtestthreshold). Similar to `default` reporter, you can disable the summary by configuring the reporter. +The tree reporter is same as `default` reporter, but it also displays each individual test after the suite has finished. Similar to `default` reporter, you can disable the summary by configuring the reporter. :::code-group ```bash [CLI] -npx vitest --reporter=verbose +npx vitest --reporter=tree ``` ```ts [vitest.config.ts] export default defineConfig({ test: { reporters: [ - ['verbose', { summary: false }] + ['tree', { summary: false }] ] }, }) @@ -228,7 +265,7 @@ Example of final terminal output for a passing test suite: ### Dot Reporter -Prints a single dot for each completed test to provide minimal output while still showing all tests that have run. Details are only provided for failed tests, along with the `basic` reporter summary for the suite. +Prints a single dot for each completed test to provide minimal output while still showing all tests that have run. Details are only provided for failed tests, along with the summary for the suite. :::code-group ```bash [CLI] @@ -492,13 +529,13 @@ export default defineConfig({ ``` ::: -### Github Actions Reporter {#github-actions-reporter} +### GitHub Actions Reporter {#github-actions-reporter} Output [workflow commands](https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-error-message) to provide annotations for test failures. This reporter is automatically enabled with a [`default`](#default-reporter) reporter when `process.env.GITHUB_ACTIONS === 'true'`. -Github Actions -Github Actions +GitHub Actions +GitHub Actions If you configure non-default reporters, you need to explicitly add `github-actions`. diff --git a/docs/guide/test-annotations.md b/docs/guide/test-annotations.md index 68983720991f..88f2362543c1 100644 --- a/docs/guide/test-annotations.md +++ b/docs/guide/test-annotations.md @@ -53,7 +53,7 @@ Error: thrown error ### verbose -In a TTY terminal, the `verbose` reporter works similarly to the `default` reporter. However, in a non-TTY environment, the `verbose` reporter will also print annotations after every test. +The `verbose` reporter is the only terminal reporter that reports annotations when the test doesn't fail. ``` ✓ example.test.js > an example of a test with annotation diff --git a/docs/guide/test-context.md b/docs/guide/test-context.md index fe598dec809e..e0ff4b70e1ab 100644 --- a/docs/guide/test-context.md +++ b/docs/guide/test-context.md @@ -325,7 +325,7 @@ describe('use scoped values', () => { test.scoped({ dependency: 'new' }) test('uses scoped value', ({ dependant }) => { - // `dependant` uses the new overriden value that is scoped + // `dependant` uses the new overridden value that is scoped // to all tests in this suite expect(dependant).toEqual({ dependency: 'new' }) }) diff --git a/docs/guide/testing-types.md b/docs/guide/testing-types.md index d16efb108d34..d6933de4d63b 100644 --- a/docs/guide/testing-types.md +++ b/docs/guide/testing-types.md @@ -30,7 +30,7 @@ import { mount } from './mount.js' test('my types work properly', () => { expectTypeOf(mount).toBeFunction() - expectTypeOf(mount).parameter(0).toMatchTypeOf<{ name: string }>() + expectTypeOf(mount).parameter(0).toExtend<{ name: string }>() // @ts-expect-error name is a string assertType(mount({ name: 42 })) @@ -45,7 +45,7 @@ You can see a list of possible matchers in [API section](/api/expect-typeof). If you are using `expectTypeOf` API, refer to the [expect-type documentation on its error messages](https://github.com/mmkal/expect-type#error-messages). -When types don't match, `.toEqualTypeOf` and `.toMatchTypeOf` use a special helper type to produce error messages that are as actionable as possible. But there's a bit of an nuance to understanding them. Since the assertions are written "fluently", the failure should be on the "expected" type, not the "actual" type (`expect().toEqualTypeOf()`). This means that type errors can be a little confusing - so this library produces a `MismatchInfo` type to try to make explicit what the expectation is. For example: +When types don't match, `.toEqualTypeOf` and `.toExtend` use a special helper type to produce error messages that are as actionable as possible. But there's a bit of an nuance to understanding them. Since the assertions are written "fluently", the failure should be on the "expected" type, not the "actual" type (`expect().toEqualTypeOf()`). This means that type errors can be a little confusing - so this library produces a `MismatchInfo` type to try to make explicit what the expectation is. For example: ```ts expectTypeOf({ a: 1 }).toEqualTypeOf<{ a: string }>() @@ -91,7 +91,7 @@ Will be less helpful than for an assertion like this: expectTypeOf({ a: 1 }).toEqualTypeOf<{ a: string }>() ``` -This is because the TypeScript compiler needs to infer the typearg for the `.toEqualTypeOf({a: ''})` style, and this library can only mark it as a failure by comparing it against a generic `Mismatch` type. So, where possible, use a typearg rather than a concrete type for `.toEqualTypeOf` and `toMatchTypeOf`. If it's much more convenient to compare two concrete types, you can use `typeof`: +This is because the TypeScript compiler needs to infer the typearg for the `.toEqualTypeOf({a: ''})` style, and this library can only mark it as a failure by comparing it against a generic `Mismatch` type. So, where possible, use a typearg rather than a concrete type for `.toEqualTypeOf` and `.toExtend`. If it's much more convenient to compare two concrete types, you can use `typeof`: ```ts const one = valueFromFunctionOne({ some: { complex: inputs } }) diff --git a/docs/package.json b/docs/package.json index b29a970fb12c..c8ff86de01da 100644 --- a/docs/package.json +++ b/docs/package.json @@ -20,20 +20,21 @@ "devDependencies": { "@iconify-json/carbon": "catalog:", "@iconify-json/logos": "catalog:", - "@shikijs/transformers": "^3.6.0", - "@shikijs/vitepress-twoslash": "^3.6.0", + "@shikijs/transformers": "^3.12.2", + "@shikijs/vitepress-twoslash": "^3.12.2", "@unocss/reset": "catalog:", - "@vite-pwa/assets-generator": "^1.0.0", + "@vite-pwa/assets-generator": "^1.0.1", "@vite-pwa/vitepress": "^1.0.0", "@vitejs/plugin-vue": "catalog:", "https-localhost": "^4.7.1", "tinyglobby": "catalog:", "unocss": "catalog:", "unplugin-vue-components": "catalog:", - "vite": "^5.2.8", + "vite": "^6.3.5", "vite-plugin-pwa": "^0.21.2", - "vitepress": "2.0.0-alpha.6", - "vitepress-plugin-group-icons": "^1.6.0", + "vitepress": "2.0.0-alpha.12", + "vitepress-plugin-group-icons": "^1.6.3", + "vitepress-plugin-llms": "^1.7.4", "vitepress-plugin-tabs": "^0.7.1", "workbox-window": "^7.3.0" } diff --git a/docs/public/bit.svg b/docs/public/bit.svg deleted file mode 100644 index 8750e3ef4683..000000000000 --- a/docs/public/bit.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/public/liminity.svg b/docs/public/liminity.svg new file mode 100644 index 000000000000..d68b18e4bfb3 --- /dev/null +++ b/docs/public/liminity.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/public/mailmeteor.svg b/docs/public/mailmeteor.svg new file mode 100644 index 000000000000..99d4aaccc7f5 --- /dev/null +++ b/docs/public/mailmeteor.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/docs/public/oomol.svg b/docs/public/oomol.svg new file mode 100644 index 000000000000..0b3dd5e20034 --- /dev/null +++ b/docs/public/oomol.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/docs/public/vrt-gha-summary-no-update-dark.png b/docs/public/vrt-gha-summary-no-update-dark.png new file mode 100644 index 000000000000..78ad5ecd850e Binary files /dev/null and b/docs/public/vrt-gha-summary-no-update-dark.png differ diff --git a/docs/public/vrt-gha-summary-no-update-light.png b/docs/public/vrt-gha-summary-no-update-light.png new file mode 100644 index 000000000000..77de12a06804 Binary files /dev/null and b/docs/public/vrt-gha-summary-no-update-light.png differ diff --git a/docs/public/vrt-gha-summary-update-dark.png b/docs/public/vrt-gha-summary-update-dark.png new file mode 100644 index 000000000000..6d8d498a677c Binary files /dev/null and b/docs/public/vrt-gha-summary-update-dark.png differ diff --git a/docs/public/vrt-gha-summary-update-light.png b/docs/public/vrt-gha-summary-update-light.png new file mode 100644 index 000000000000..e9381f79d112 Binary files /dev/null and b/docs/public/vrt-gha-summary-update-light.png differ diff --git a/examples/fastify/package.json b/examples/fastify/package.json index 47d9d1d3e817..bdf5e6587aa2 100644 --- a/examples/fastify/package.json +++ b/examples/fastify/package.json @@ -12,9 +12,9 @@ }, "devDependencies": { "@vitest/ui": "latest", - "fastify": "^4.29.1", + "fastify": "^5.6.0", "supertest": "^6.3.4", - "tsx": "^4.20.3", + "tsx": "^4.20.5", "vite": "latest", "vitest": "latest" }, diff --git a/examples/in-source-test/package.json b/examples/in-source-test/package.json index 0f4bdbc29e12..f78731dbffac 100644 --- a/examples/in-source-test/package.json +++ b/examples/in-source-test/package.json @@ -7,7 +7,7 @@ "test:run": "vitest run" }, "devDependencies": { - "typescript": "^5.8.3", + "typescript": "^5.9.2", "vitest": "latest" } } diff --git a/examples/lit/package.json b/examples/lit/package.json index bb09bf860ec1..7d684e6e5bc6 100644 --- a/examples/lit/package.json +++ b/examples/lit/package.json @@ -14,12 +14,12 @@ "test:ui": "vitest --ui" }, "dependencies": { - "lit": "^3.3.0" + "lit": "^3.3.1" }, "devDependencies": { "@vitest/browser": "latest", "jsdom": "latest", - "playwright": "^1.53.0", + "playwright": "^1.55.0", "vite": "latest", "vitest": "latest" }, diff --git a/examples/lit/vite.config.ts b/examples/lit/vite.config.ts index 72ea883e64b2..d4a0022e4b28 100644 --- a/examples/lit/vite.config.ts +++ b/examples/lit/vite.config.ts @@ -1,5 +1,6 @@ /// +import { playwright } from '@vitest/browser/providers/playwright' import { defineConfig } from 'vite' // https://vitejs.dev/config/ @@ -9,7 +10,7 @@ export default defineConfig({ // https://lit.dev/docs/tools/testing/#testing-in-the-browser browser: { enabled: true, - provider: 'playwright', + provider: playwright(), instances: [ { browser: 'chromium' }, ], diff --git a/examples/projects/package.json b/examples/projects/package.json index 99c99628d123..744b658dc13a 100644 --- a/examples/projects/package.json +++ b/examples/projects/package.json @@ -9,17 +9,17 @@ "test:run": "vitest run" }, "devDependencies": { - "@testing-library/jest-dom": "^6.6.3", + "@testing-library/jest-dom": "^6.8.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^14.6.1", - "@types/react": "^19.1.8", - "@vitejs/plugin-react": "^4.5.2", + "@types/react": "^19.1.12", + "@vitejs/plugin-react": "^5.0.2", "@vitest/ui": "latest", - "fastify": "^4.29.1", + "fastify": "^5.6.0", "jsdom": "^26.1.0", - "react": "^19.1.0", + "react": "^19.1.1", "supertest": "^6.3.4", - "tsx": "^4.20.3", + "tsx": "^4.20.5", "vite": "latest", "vitest": "latest" }, diff --git a/examples/typecheck/package.json b/examples/typecheck/package.json index b79ef601e7ab..67cc4d0f0b73 100644 --- a/examples/typecheck/package.json +++ b/examples/typecheck/package.json @@ -10,9 +10,9 @@ "test:run": "vitest run" }, "devDependencies": { - "@types/node": "^20.19.1", + "@types/node": "^20.19.13", "@vitest/ui": "latest", - "typescript": "^5.8.3", + "typescript": "^5.9.2", "vite": "latest", "vitest": "latest" }, diff --git a/package.json b/package.json index c7866cfd1aaf..d20be8c0db02 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { "name": "@vitest/monorepo", "type": "module", - "version": "3.2.4", + "version": "4.0.0-beta.11", "private": true, - "packageManager": "pnpm@10.12.1", + "packageManager": "pnpm@10.15.1", "description": "Next generation testing framework powered by Vite", "engines": { "node": "^18.0.0 || >=20.0.0" @@ -27,6 +27,7 @@ "test:ci": "CI=true pnpm -r --reporter-hide-prefix --stream --sequential --filter '@vitest/test-*' --filter !test-browser run test", "test:examples": "CI=true pnpm -r --reporter-hide-prefix --stream --filter '@vitest/example-*' run test", "test:ecosystem-ci": "ECOSYSTEM_CI=true pnpm test:ci", + "typebuild": "tsx ./scripts/explain-types.ts", "typecheck": "tsc -p tsconfig.check.json --noEmit", "typecheck:why": "tsc -p tsconfig.check.json --noEmit --explainFiles > explainTypes.txt", "ui:build": "vite build packages/ui", @@ -36,43 +37,43 @@ "test:browser:playwright": "pnpm -C test/browser run test:playwright" }, "devDependencies": { - "@antfu/eslint-config": "^4.13.2", + "@antfu/eslint-config": "^5.3.0", "@antfu/ni": "^25.0.0", - "@playwright/test": "^1.53.0", - "@rollup/plugin-commonjs": "^28.0.5", + "@playwright/test": "^1.55.0", + "@rollup/plugin-commonjs": "^28.0.6", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.1", - "@types/node": "^22.15.32", + "@types/node": "^22.18.1", "@types/ws": "catalog:", "@vitest/browser": "workspace:*", "@vitest/coverage-istanbul": "workspace:*", "@vitest/coverage-v8": "workspace:*", "@vitest/ui": "workspace:*", - "bumpp": "^10.1.1", - "changelogithub": "^13.15.0", - "esbuild": "^0.25.5", - "eslint": "^9.28.0", - "magic-string": "^0.30.17", + "bumpp": "^10.2.3", + "changelogithub": "^13.16.0", + "esbuild": "^0.25.9", + "eslint": "^9.35.0", + "magic-string": "^0.30.19", "pathe": "^2.0.3", "rimraf": "^6.0.1", - "rollup": "^4.43.0", - "rollup-plugin-dts": "^6.2.1", + "rollup": "^4.50.1", + "rollup-plugin-dts": "^6.2.3", "rollup-plugin-license": "^3.6.0", "tinyglobby": "catalog:", - "tsx": "^4.20.3", - "typescript": "^5.8.3", - "unplugin-isolated-decl": "^0.14.3", - "unplugin-oxc": "^0.4.5", + "tsx": "^4.20.5", + "typescript": "^5.9.2", + "unplugin-isolated-decl": "^0.15.1", + "unplugin-oxc": "^0.5.1", "vite": "^6.3.5", "vitest": "workspace:*", - "zx": "^8.5.5" + "zx": "^8.8.1" }, "pnpm": { "overrides": { "@vitest/browser": "workspace:*", "@vitest/ui": "workspace:*", "acorn": "8.11.3", - "mlly": "^1.7.4", + "mlly": "^1.8.0", "rollup": "$rollup", "vite": "$vite", "vitest": "workspace:*" @@ -86,7 +87,6 @@ "@sinonjs/fake-timers@14.0.0": "patches/@sinonjs__fake-timers@14.0.0.patch", "cac@6.7.14": "patches/cac@6.7.14.patch", "@types/sinonjs__fake-timers@8.1.5": "patches/@types__sinonjs__fake-timers@8.1.5.patch", - "v8-to-istanbul@9.3.0": "patches/v8-to-istanbul@9.3.0.patch", "acorn@8.11.3": "patches/acorn@8.11.3.patch" }, "onlyBuiltDependencies": [ diff --git a/packages/browser/README.md b/packages/browser/README.md index 1e5eac4cf290..d6810bf7daa5 100644 --- a/packages/browser/README.md +++ b/packages/browser/README.md @@ -1,19 +1,7 @@ # @vitest/browser -[Browser runner](https://vitest.dev/guide/browser/) for Vitest. +[![NPM version](https://img.shields.io/npm/v/@vitest/browser?color=a1b858&label=)](https://www.npmjs.com/package/@vitest/browser) -> ⚠️ This package is **experimental**. While this package will be released along with other packages, it will not follow SemVer for breaking changes until we mark it as ready. +Running Vitest tests in the real browser. -## Development Setup - -At project root: - -```bash -cd test/browser -# runs relevant tests for the browser mode -# useful to confirm everything works fine -pnpm test -# runs tests as the browser mode -# useful during development -pnpm test-fixtures -``` +[GitHub](https://github.com/vitest-dev/vitest) | [Documentation](https://vitest.dev/guide/browser/) diff --git a/packages/browser/context.d.ts b/packages/browser/context.d.ts index 6468f1e88754..1b5bdd83fd40 100644 --- a/packages/browser/context.d.ts +++ b/packages/browser/context.d.ts @@ -44,6 +44,129 @@ export interface ScreenshotOptions { save?: boolean } +export interface ScreenshotComparatorRegistry { + pixelmatch: { + /** + * The maximum number of pixels that are allowed to differ between the captured + * screenshot and the stored reference image. + * + * If set to `undefined`, any non-zero difference will cause the test to fail. + * + * For example, `allowedMismatchedPixels: 10` means the test will pass if 10 + * or fewer pixels differ, but fail if 11 or more differ. + * + * If both this and `allowedMismatchedPixelRatio` are set, the more restrictive + * value (i.e., fewer allowed mismatches) will be used. + * + * @default undefined + */ + allowedMismatchedPixels?: number | undefined + /** + * The maximum allowed ratio of differing pixels between the captured screenshot + * and the reference image. + * + * Must be a value between `0` and `1`. + * + * For example, `allowedMismatchedPixelRatio: 0.02` means the test will pass + * if up to 2% of pixels differ, but fail if more than 2% differ. + * + * If both this and `allowedMismatchedPixels` are set, the more restrictive + * value (i.e., fewer allowed mismatches) will be used. + * + * @default undefined + */ + allowedMismatchedPixelRatio?: number | undefined + /** + * Acceptable perceived color difference between the same pixel in two images. + * + * Value ranges from `0` (strict) to `1` (very lenient). Lower values mean + * small differences will be detected. + * + * The comparison uses the {@link https://en.wikipedia.org/wiki/YIQ | YIQ color space}. + * + * @default 0.1 + */ + threshold?: number | undefined + /** + * If `true`, disables detection and ignoring of anti-aliased pixels. + * + * @default false + */ + includeAA?: boolean | undefined + /** + * Blending level of unchanged pixels in the diff image. + * + * Ranges from `0` (white) to `1` (original brightness). + * + * @default 0.1 + */ + alpha?: number | undefined + /** + * Color used for anti-aliased pixels in the diff image. + * + * Format: `[R, G, B]` + * + * @default [255, 255, 0] + */ + aaColor?: [r: number, g: number, b: number] | undefined + /** + * Color used for differing pixels in the diff image. + * + * Format: `[R, G, B]` + * + * @default [255, 0, 0] + */ + diffColor?: [r: number, g: number, b: number] | undefined + /** + * Optional alternative color for dark-on-light differences, to help show + * what's added vs. removed. + * + * If not set, `diffColor` is used for all differences. + * + * Format: `[R, G, B]` + * + * @default undefined + */ + diffColorAlt?: [r: number, g: number, b: number] | undefined + /** + * If `true`, shows only the diff as a mask on a transparent background, + * instead of overlaying it on the original image. + * + * Anti-aliased pixels won't be shown (if detected). + * + * @default false + */ + diffMask?: boolean | undefined + } +} + +export interface ScreenshotMatcherOptions< + ComparatorName extends keyof ScreenshotComparatorRegistry = keyof ScreenshotComparatorRegistry +> { + /** + * The name of the comparator to use for visual diffing. + * + * Must be one of the keys from {@linkcode ScreenshotComparatorRegistry}. + * + * @defaultValue `'pixelmatch'` + */ + comparatorName?: ComparatorName + comparatorOptions?: ScreenshotComparatorRegistry[ComparatorName] + screenshotOptions?: Omit< + ScreenshotOptions, + 'element' | 'base64' | 'path' | 'save' | 'type' + > + /** + * Time to wait until a stable screenshot is found. + * + * Setting this value to `0` disables the timeout, but if a stable screenshot + * can't be determined the process will not end. + * + * @default 5000 + */ + timeout?: number +} + export interface BrowserCommands { readFile: ( path: string, @@ -332,6 +455,8 @@ interface LocatorSelectors { getByTestId: (text: string | RegExp) => Locator } +export interface FrameLocator extends LocatorSelectors {} + export interface Locator extends LocatorSelectors { /** * Selector string that will be used to locate the element by the browser provider. @@ -350,6 +475,12 @@ export interface Locator extends LocatorSelectors { */ readonly selector: string + /** + * The number of elements that this locator is matching. + * @see {@link https://vitest.dev/guide/browser/locators#length} + */ + readonly length: number + /** * Click on an element. You can use the options to set the cursor position. * @see {@link https://vitest.dev/guide/browser/interactivity-api#userevent-click} @@ -421,7 +552,7 @@ export interface Locator extends LocatorSelectors { * * @see {@link https://vitest.dev/guide/browser/locators#element} */ - element(): Element + element(): HTMLElement | SVGElement /** * Returns an array of elements matching the selector. * @@ -429,7 +560,7 @@ export interface Locator extends LocatorSelectors { * * @see {@link https://vitest.dev/guide/browser/locators#elements} */ - elements(): Element[] + elements(): (HTMLElement | SVGElement)[] /** * Returns an element matching the selector. * @@ -438,7 +569,7 @@ export interface Locator extends LocatorSelectors { * * @see {@link https://vitest.dev/guide/browser/locators#query} */ - query(): Element | null + query(): HTMLElement | SVGElement | null /** * Wraps an array of `.elements()` matching the selector in a new `Locator`. * @@ -578,6 +709,25 @@ export interface BrowserPage extends LocatorSelectors { * @see {@link https://vitest.dev/guide/browser/locators} */ elementLocator(element: Element): Locator + /** + * The iframe locator. This is a document locator that enters the iframe body + * and works similarly to the `page` object. + * + * As the first argument, pass down the locator to the `