diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 9b00c6959038..d4129a7c32d4 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -28,6 +28,9 @@ // manually bumping "node", "vite", + // bumping breaks coverage + // https://github.com/vitest-dev/vitest/actions/runs/12121857184/job/33793728382?pr=6920 + "vue", // we patch these packages "@types/chai", "@sinonjs/fake-timers", diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 913d501d38b2..bae9ce02fa3e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,6 +44,13 @@ jobs: - name: Typecheck run: pnpm run typecheck + - name: Diff LICENSE.md + run: | + if ! git diff --exit-code packages/vitest/LICENSE.md; then + echo "::error::LICENSE.md has changed. Please commit the updated LICENSE.md file after the build." + exit 1 + fi + # From https://github.com/rhysd/actionlint/blob/main/docs/usage.md#use-actionlint-on-github-actions - name: Check workflow files run: | @@ -61,7 +68,7 @@ jobs: - name: Get changed files id: changed-files - uses: tj-actions/changed-files@4edd678ac3f81e2dc578756871e4d00c19191daf # v45.0.4 + uses: tj-actions/changed-files@bab30c2299617f6615ec02a68b9a40d10bd21366 # v45.0.5 with: files: | docs/** @@ -101,7 +108,7 @@ jobs: run: pnpm i - name: Install Playwright Dependencies - run: pnpm exec playwright install chromium --with-deps + run: pnpm exec playwright install chromium --with-deps --only-shell - name: Build run: pnpm run build @@ -150,8 +157,7 @@ jobs: run: pnpm i - name: Install Playwright Dependencies - if: needs.changed.outputs.should_skip != 'true' - run: pnpm exec playwright install ${{ matrix.browser[0] }} --with-deps + run: pnpm exec playwright install ${{ matrix.browser[0] }} --with-deps --only-shell - name: Build run: pnpm run build diff --git a/.gitignore b/.gitignore index a40e76edc2af..d0949feffdb5 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ test/**/__screenshots__/**/* test/browser/fixtures/update-snapshot/basic.test.ts test/cli/fixtures/browser-multiple/basic-* .vitest-reports +*.tsbuildinfo diff --git a/README.md b/README.md index 1779c5f589a5..e6431ffc6701 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@

+ +

diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 532ceb4c467b..388181400ce4 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -1,7 +1,12 @@ import { transformerTwoslash } from '@shikijs/vitepress-twoslash' import { withPwa } from '@vite-pwa/vitepress' +import type { DefaultTheme } from 'vitepress' import { defineConfig } from 'vitepress' import { tabsMarkdownPlugin } from 'vitepress-plugin-tabs' +import { + groupIconMdPlugin, + groupIconVitePlugin, +} from 'vitepress-plugin-group-icons' import { version } from '../../package.json' import { teamMembers } from './contributors' import { @@ -56,9 +61,26 @@ export default ({ mode }: { mode: string }) => { ['link', { rel: 'apple-touch-icon', href: '/apple-touch-icon.png', sizes: '180x180' }], ], lastUpdated: true, + vite: { + plugins: [ + groupIconVitePlugin({ + customIcon: { + 'CLI': 'vscode-icons:file-type-shell', + 'vitest.workspace': 'vscode-icons:file-type-vitest', + 'vitest.config': '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', + }, + }), + ], + }, markdown: { config(md) { md.use(tabsMarkdownPlugin) + md.use(groupIconMdPlugin) }, theme: { light: 'github-light', @@ -116,15 +138,14 @@ export default ({ mode }: { mode: string }) => { }, nav: [ - { text: 'Guide', link: '/guide/', activeMatch: '^/guide/(?!browser)' }, - { text: 'API', link: '/api/', activeMatch: '^/api/' }, + { text: 'Guide & API', link: '/guide/', activeMatch: '^/(guide|api)/(?!browser)' }, { text: 'Config', link: '/config/', activeMatch: '^/config/' }, { text: 'Browser Mode', link: '/guide/browser', activeMatch: '^/guide/browser/' }, { text: 'Resources', items: [ { - text: 'Advanced', + text: 'Advanced API', link: '/advanced/api', activeMatch: '^/advanced/', }, @@ -176,225 +197,123 @@ export default ({ mode }: { mode: string }) => { sidebar: { '/guide/browser': [ { - text: 'Why Browser Mode?', - link: '/guide/browser/why', - docFooterText: 'Why Browser Mode? | Browser Mode', - }, - { - text: 'Getting Started', - link: '/guide/browser/', - docFooterText: 'Getting Started | Browser Mode', - }, - { - text: 'Context API', - link: '/guide/browser/context', - docFooterText: 'Context API | Browser Mode', - }, - { - text: 'Interactivity API', - link: '/guide/browser/interactivity-api', - docFooterText: 'Interactivity API | Browser Mode', - }, - { - text: 'Locators', - link: '/guide/browser/locators', - docFooterText: 'Locators | Browser Mode', - }, - { - text: 'Assertion API', - link: '/guide/browser/assertion-api', - docFooterText: 'Assertion API | Browser Mode', - }, - { - text: 'Commands API', - link: '/guide/browser/commands', - docFooterText: 'Commands | Browser Mode', - }, - ], - // TODO: bring sidebar of apis and config back - '/advanced': [ - { + text: 'Introduction', + collapsed: false, items: [ { - text: 'API', - items: [ - - { - text: 'Vitest Node API', - link: '/advanced/api', - }, - { - text: 'Runner API', - link: '/advanced/runner', - }, - { - text: 'Task Metadata', - link: '/advanced/metadata', - }, - ], + text: 'Why Browser Mode', + link: '/guide/browser/why', + docFooterText: 'Why Browser Mode | Browser Mode', }, { - text: 'Guides', - items: [ - { - text: 'Running Tests', - link: '/advanced/guide/tests', - }, - { - text: 'Extending Reporters', - link: '/advanced/reporters', - }, - { - text: 'Custom Pool', - link: '/advanced/pool', - }, - ], + text: 'Getting Started', + link: '/guide/browser/', + docFooterText: 'Getting Started | Browser Mode', }, ], }, - ], - '/guide/': [ { + text: 'API', + collapsed: false, items: [ { - text: 'Why Vitest', - link: '/guide/why', - }, - { - text: 'Getting Started', - link: '/guide/', - }, - { - text: 'Features', - link: '/guide/features', - }, - { - text: 'Workspace', - link: '/guide/workspace', - }, - { - text: 'CLI', - link: '/guide/cli', - }, - { - text: 'Test Filtering', - link: '/guide/filtering', - }, - { - text: 'Reporters', - link: '/guide/reporters', - }, - { - text: 'Coverage', - link: '/guide/coverage', - }, - { - text: 'Snapshot', - link: '/guide/snapshot', - }, - { - text: 'Mocking', - link: '/guide/mocking', - }, - { - text: 'Testing Types', - link: '/guide/testing-types', - }, - { - text: 'Vitest UI', - link: '/guide/ui', - }, - { - text: 'In-Source Testing', - link: '/guide/in-source', - }, - { - text: 'Test Context', - link: '/guide/test-context', - }, - { - text: 'Environment', - link: '/guide/environment', - }, - { - text: 'Extending Matchers', - link: '/guide/extending-matchers', - }, - { - text: 'IDE Integration', - link: '/guide/ide', - }, - { - text: 'Debugging', - link: '/guide/debugging', - }, - { - text: 'Comparisons', - link: '/guide/comparisons', + text: 'Context API', + link: '/guide/browser/context', + docFooterText: 'Context API | Browser Mode', }, { - text: 'Migration Guide', - link: '/guide/migration', + text: 'Interactivity API', + link: '/guide/browser/interactivity-api', + docFooterText: 'Interactivity API | Browser Mode', }, { - text: 'Common Errors', - link: '/guide/common-errors', + text: 'Locators', + link: '/guide/browser/locators', + docFooterText: 'Locators | Browser Mode', }, { - text: 'Profiling Test Performance', - link: '/guide/profiling-test-performance', + text: 'Assertion API', + link: '/guide/browser/assertion-api', + docFooterText: 'Assertion API | Browser Mode', }, { - text: 'Improving Performance', - link: '/guide/improving-performance', + text: 'Commands API', + link: '/guide/browser/commands', + docFooterText: 'Commands | Browser Mode', }, ], }, + footer(), ], - '/api/': [ + '/advanced': [ { + text: 'API', + collapsed: false, items: [ { - text: 'Test API Reference', - link: '/api/', - }, - { - text: 'Mock Functions', - link: '/api/mock', + text: 'Vitest Node API', + link: '/advanced/api', }, { - text: 'Vi Utility', - link: '/api/vi', + text: 'Runner API', + link: '/advanced/runner', }, { - text: 'Expect', - link: '/api/expect', + text: 'Task Metadata', + link: '/advanced/metadata', }, + ], + }, + { + text: 'Guides', + collapsed: false, + items: [ { - text: 'ExpectTypeOf', - link: '/api/expect-typeof', + text: 'Running Tests', + link: '/advanced/guide/tests', }, { - text: 'Assert', - link: '/api/assert', + text: 'Extending Reporters', + link: '/advanced/reporters', }, { - text: 'AssertType', - link: '/api/assert-type', + text: 'Custom Pool', + link: '/advanced/pool', }, ], }, + footer(), ], - '/config/': [ + '/team': [], + '/': [ + { + text: 'Introduction', + collapsed: false, + items: introduction(), + }, + { + text: 'API', + collapsed: false, + items: api(), + }, + { + text: 'Guides', + collapsed: false, + items: guide(), + }, { items: [ { - text: 'Config File', - link: '/config/file', + text: 'Browser Mode', + link: '/guide/browser', }, { - text: 'Config Reference', - link: '/config/', + text: 'Advanced API', + link: '/advanced/api', + }, + { + text: 'Comparisons', + link: '/guide/comparisons', }, ], }, @@ -405,3 +324,159 @@ export default ({ mode }: { mode: string }) => { transformHead, })) } + +function footer(): DefaultTheme.SidebarItem { + return { + items: [ + { + text: 'Config Reference', + link: '/config/', + }, + { + text: 'Test API Reference', + link: '/api/', + }, + ], + } +} + +function introduction(): DefaultTheme.SidebarItem[] { + return [ + { + text: 'Why Vitest', + link: '/guide/why', + }, + { + text: 'Getting Started', + link: '/guide/', + }, + { + text: 'Features', + link: '/guide/features', + }, + { + text: 'Config Reference', + link: '/config/', + }, + ] +} + +function guide(): DefaultTheme.SidebarItem[] { + return [ + { + text: 'CLI', + link: '/guide/cli', + }, + { + text: 'Test Filtering', + link: '/guide/filtering', + }, + { + text: 'Workspace', + link: '/guide/workspace', + }, + { + text: 'Reporters', + link: '/guide/reporters', + }, + { + text: 'Coverage', + link: '/guide/coverage', + }, + { + text: 'Snapshot', + link: '/guide/snapshot', + }, + { + text: 'Mocking', + link: '/guide/mocking', + }, + { + text: 'Testing Types', + link: '/guide/testing-types', + }, + { + text: 'Vitest UI', + link: '/guide/ui', + }, + { + text: 'In-Source Testing', + link: '/guide/in-source', + }, + { + text: 'Test Context', + link: '/guide/test-context', + }, + { + text: 'Environment', + link: '/guide/environment', + }, + { + text: 'Extending Matchers', + link: '/guide/extending-matchers', + }, + { + text: 'IDE Integration', + link: '/guide/ide', + }, + { + text: 'Debugging', + link: '/guide/debugging', + }, + { + text: 'Migration Guide', + link: '/guide/migration', + }, + { + text: 'Common Errors', + link: '/guide/common-errors', + }, + { + text: 'Performance', + collapsed: false, + items: [ + { + text: 'Profiling Test Performance', + link: '/guide/profiling-test-performance', + }, + { + text: 'Improving Performance', + link: '/guide/improving-performance', + }, + ], + }, + ] +} + +function api(): DefaultTheme.SidebarItem[] { + return [ + { + text: 'Test API Reference', + link: '/api/', + }, + { + text: 'Mock Functions', + link: '/api/mock', + }, + { + text: 'Vi Utility', + link: '/api/vi', + }, + { + text: 'Expect', + link: '/api/expect', + }, + { + text: 'ExpectTypeOf', + link: '/api/expect-typeof', + }, + { + text: 'Assert', + link: '/api/assert', + }, + { + text: 'AssertType', + link: '/api/assert-type', + }, + ] +} diff --git a/docs/.vitepress/sponsors.ts b/docs/.vitepress/sponsors.ts index e6b488b1d733..b3070e42f150 100644 --- a/docs/.vitepress/sponsors.ts +++ b/docs/.vitepress/sponsors.ts @@ -34,7 +34,13 @@ const vitestSponsors = { img: '/bit.svg', }, ], - // gold: [], + gold: [ + { + name: 'vital', + url: 'https://vital.io/', + img: '/vital.svg', + }, + ], } satisfies Record export const sponsors = [ @@ -48,9 +54,9 @@ export const sponsors = [ size: 'big', items: vitestSponsors.platinum, }, - // { - // tier: 'Gold Sponsors', - // size: 'medium', - // items: vitestSponsors.gold, - // }, + { + tier: 'Gold Sponsors', + size: 'medium', + items: vitestSponsors.gold, + }, ] diff --git a/docs/.vitepress/style/main.css b/docs/.vitepress/style/main.css index 87c08069f97e..735f705c9940 100644 --- a/docs/.vitepress/style/main.css +++ b/docs/.vitepress/style/main.css @@ -6,6 +6,10 @@ html:not(.dark) [img-dark] { display: none; } +details summary { + cursor: pointer; +} + /* Overrides */ .sp .sp-link.link:hover, diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts index d684d9dc1fad..c3e91f7db522 100644 --- a/docs/.vitepress/theme/index.ts +++ b/docs/.vitepress/theme/index.ts @@ -10,6 +10,7 @@ import '../style/main.css' import '../style/vars.css' import 'uno.css' import '@shikijs/vitepress-twoslash/style.css' +import 'virtual:group-icons.css' if (inBrowser) { import('./pwa') diff --git a/docs/advanced/api.md b/docs/advanced/api.md index f7798354971e..fafde8716ff2 100644 --- a/docs/advanced/api.md +++ b/docs/advanced/api.md @@ -140,9 +140,9 @@ export default function setup({ provide }) { ``` ::: -## TestProject 2.2.0 {#testproject} +## TestProject 3.0.0 {#testproject} -- **Alias**: `WorkspaceProject` before 2.2.0 +- **Alias**: `WorkspaceProject` before 3.0.0 ### name diff --git a/docs/advanced/metadata.md b/docs/advanced/metadata.md index 2f493d3f0943..6efd276269f2 100644 --- a/docs/advanced/metadata.md +++ b/docs/advanced/metadata.md @@ -22,8 +22,7 @@ test('custom', ({ task }) => { Once a test is completed, Vitest will send a task including the result and `meta` to the Node.js process using RPC. To intercept and process this task, you can utilize the `onTaskUpdate` method available in your reporter implementation: -```ts -// custom-reporter.js +```ts [custom-reporter.js] export default { // you can intercept packs if needed onTaskUpdate(packs) { diff --git a/docs/advanced/pool.md b/docs/advanced/pool.md index 1deb018fa550..36c5a6c9cdd7 100644 --- a/docs/advanced/pool.md +++ b/docs/advanced/pool.md @@ -14,7 +14,7 @@ Vitest runs tests in pools. By default, there are several pools: You can provide your own pool by specifying a file path: -```ts +```ts [vitest.config.ts] import { defineConfig } from 'vitest/config' export default defineConfig({ @@ -27,14 +27,31 @@ export default defineConfig({ customProperty: true, }, }, - // you can also specify pool for a subset of files - poolMatchGlobs: [ - ['**/*.custom.test.ts', './my-custom-pool.ts'], + }, +}) +``` + +If you need to run tests in different pools, use the [workspace](/guide/workspace) feature: + +```ts [vitest.config.ts] +export default defineConfig({ + test: { + workspace: [ + { + extends: true, + test: { + pool: 'threads', + }, + }, ], }, }) ``` +::: info +The `workspace` field was introduced in Vitest 3. To define a workspace in [Vitest <3](https://v2.vitest.dev/), create a separate `vitest.workspace.ts` file. +::: + ## API The file specified in `pool` option should export a function (can be async) that accepts `Vitest` interface as its first option. This function needs to return an object matching `ProcessPool` interface: diff --git a/docs/advanced/reporters.md b/docs/advanced/reporters.md index 6f3236632cd0..49586526003c 100644 --- a/docs/advanced/reporters.md +++ b/docs/advanced/reporters.md @@ -18,8 +18,7 @@ Of course, you can create your reporter from scratch. Just extend the `BaseRepor And here is an example of a custom reporter: -```ts -// ./custom-reporter.js +```ts [custom-reporter.js] import { BaseReporter } from 'vitest/reporters' export default class CustomReporter extends BaseReporter { @@ -32,8 +31,7 @@ export default class CustomReporter extends BaseReporter { Or implement the `Reporter` interface: -```ts -// ./custom-reporter.js +```ts [custom-reporter.js] import { Reporter } from 'vitest/reporters' export default class CustomReporter implements Reporter { @@ -45,7 +43,7 @@ export default class CustomReporter implements Reporter { Then you can use your custom reporter in the `vitest.config.ts` file: -```ts +```ts [vitest.config.ts] import { defineConfig } from 'vitest/config' import CustomReporter from './custom-reporter.js' diff --git a/docs/advanced/runner.md b/docs/advanced/runner.md index 0e969cde0e0b..ab5fb77e510b 100644 --- a/docs/advanced/runner.md +++ b/docs/advanced/runner.md @@ -246,8 +246,7 @@ Vitest exposes a `Custom` task type that allows users to reuse built-int reporte A task is an object that is part of a suite. It is automatically added to the current suite with a `suite.task` method: -```js -// ./utils/custom.js +```js [custom.js] import { createTaskCollector, getCurrentSuite, setFn } from 'vitest/suite' export { afterAll, beforeAll, describe } from 'vitest' @@ -270,9 +269,8 @@ export const myCustomTask = createTaskCollector( ) ``` -```js -// ./garden/tasks.test.js -import { afterAll, beforeAll, describe, myCustomTask } from '../custom.js' +```js [tasks.test.js] +import { afterAll, beforeAll, describe, myCustomTask } from './custom.js' import { gardener } from './gardener.js' describe('take care of the garden', () => { diff --git a/docs/api/expect.md b/docs/api/expect.md index c4bbbd2e016c..0c1df7d6ebb3 100644 --- a/docs/api/expect.md +++ b/docs/api/expect.md @@ -82,7 +82,7 @@ test('element exists', async () => { ``` ::: warning -`expect.poll` makes every assertion asynchronous, so you need to await it. Since Vitest 2.2, if you forget to await it, the test will fail with a warning to do so. +`expect.poll` makes every assertion asynchronous, so you need to await it. Since Vitest 3, if you forget to await it, the test will fail with a warning to do so. `expect.poll` doesn't work with several matchers: @@ -465,7 +465,7 @@ test('structurally the same, but semantically different', () => { - **Type:** `(received: string) => Awaitable` -`toContain` asserts if the actual value is in an array. `toContain` can also check whether a string is a substring of another string. Since Vitest 1.0, if you are running tests in a browser-like environment, this assertion can also check if class is contained in a `classList`, or an element is inside another one. +`toContain` asserts if the actual value is in an array. `toContain` can also check whether a string is a substring of another string. If you are running tests in a browser-like environment, this assertion can also check if class is contained in a `classList`, or an element is inside another one. ```ts import { expect, test } from 'vitest' @@ -876,7 +876,7 @@ test('spy function', () => { }) ``` -## toHaveBeenCalledBefore 2.2.0 {#tohavebeencalledbefore} +## toHaveBeenCalledBefore 3.0.0 {#tohavebeencalledbefore} - **Type**: `(mock: MockInstance, failIfNoFirstInvocation?: boolean) => Awaitable` @@ -895,7 +895,7 @@ test('calls mock1 before mock2', () => { }) ``` -## toHaveBeenCalledAfter 2.2.0 {#tohavebeencalledafter} +## toHaveBeenCalledAfter 3.0.0 {#tohavebeencalledafter} - **Type**: `(mock: MockInstance, failIfNoFirstInvocation?: boolean) => Awaitable` @@ -914,7 +914,7 @@ test('calls mock1 after mock2', () => { }) ``` -## toHaveBeenCalledExactlyOnceWith 2.2.0 {#tohavebeencalledexactlyoncewith} +## toHaveBeenCalledExactlyOnceWith 3.0.0 {#tohavebeencalledexactlyoncewith} - **Type**: `(...args: any[]) => Awaitable` @@ -1248,7 +1248,7 @@ test('buyApples returns new stock id', async () => { :::warning If the assertion is not awaited, then you will have a false-positive test that will pass every time. To make sure that assertions are actually called, you may use [`expect.assertions(number)`](#expect-assertions). -Since Vitest 2.2, if a method is not awaited, Vitest will show a warning at the end of the test. In Vitest 3, the test will be marked as "failed" if the assertion is not awaited. +Since Vitest 3, if a method is not awaited, Vitest will show a warning at the end of the test. In Vitest 4, the test will be marked as "failed" if the assertion is not awaited. ::: ## rejects @@ -1279,7 +1279,7 @@ test('buyApples throws an error when no id provided', async () => { :::warning If the assertion is not awaited, then you will have a false-positive test that will pass every time. To make sure that assertions were actually called, you can use [`expect.assertions(number)`](#expect-assertions). -Since Vitest 2.2, if a method is not awaited, Vitest will show a warning at the end of the test. In Vitest 3, the test will be marked as "failed" if the assertion is not awaited. +Since Vitest 3, if a method is not awaited, Vitest will show a warning at the end of the test. In Vitest 4, the test will be marked as "failed" if the assertion is not awaited. ::: ## expect.assertions diff --git a/docs/api/index.md b/docs/api/index.md index 818085f43d62..42ff30eb1f96 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -906,10 +906,23 @@ Vitest provides a way to run all tests in random order via CLI flag [`--sequence ```ts import { describe, test } from 'vitest' +// or describe('suite', { shuffle: true }, ...) describe.shuffle('suite', () => { test('random test 1', async () => { /* ... */ }) test('random test 2', async () => { /* ... */ }) test('random test 3', async () => { /* ... */ }) + + // `shuffle` is inherited + describe('still random', () => { + test('random 4.1', async () => { /* ... */ }) + test('random 4.2', async () => { /* ... */ }) + }) + + // disable shuffle inside + describe('not random', { shuffle: false }, () => { + test('in order 5.1', async () => { /* ... */ }) + test('in order 5.2', async () => { /* ... */ }) + }) }) // order depends on sequence.seed option in config (Date.now() by default) ``` @@ -1113,7 +1126,7 @@ These hooks will throw an error if they are called outside of the test body. ### onTestFinished {#ontestfinished} -This hook is always called after the test has finished running. It is called after `afterEach` hooks since they can influence the test result. It receives a `TaskResult` object with the current test result. +This hook is always called after the test has finished running. It is called after `afterEach` hooks since they can influence the test result. It receives an `ExtendedContext` object like `beforeEach` and `afterEach`. ```ts {1,5} import { onTestFinished, test } from 'vitest' @@ -1170,7 +1183,7 @@ This hook is always called in reverse order and is not affected by [`sequence.ho ### onTestFailed -This hook is called only after the test has failed. It is called after `afterEach` hooks since they can influence the test result. It receives a `TaskResult` object with the current test result. This hook is useful for debugging. +This hook is called only after the test has failed. It is called after `afterEach` hooks since they can influence the test result. It receives an `ExtendedContext` object like `beforeEach` and `afterEach`. This hook is useful for debugging. ```ts {1,5-7} import { onTestFailed, test } from 'vitest' diff --git a/docs/api/mock.md b/docs/api/mock.md index 9e051cb89fa6..9579cc03f29b 100644 --- a/docs/api/mock.md +++ b/docs/api/mock.md @@ -197,7 +197,13 @@ await asyncMock() // throws Error<'Async error'> function mockReset(): MockInstance ``` -Performs the same actions as `mockClear` and sets the inner implementation to an empty function (returning `undefined` when invoked). This also resets all "once" implementations. It is useful for completely resetting a mock to its default state. +Does what `mockClear` does and resets inner implementation to the original function. +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`. + +This is useful when you want to reset a mock to its original state. To automatically call this method before each test, enable the [`mockReset`](/config/#mockreset) setting in the configuration. @@ -207,9 +213,10 @@ To automatically call this method before each test, enable the [`mockReset`](/co function mockRestore(): MockInstance ``` -Performs the same actions as `mockReset` and restores the inner implementation to the original function. +Does what `mockReset` does and restores original descriptors of spied-on objects. -Note that restoring a mock created with `vi.fn()` will set the implementation to an empty function that returns `undefined`. Restoring a mock created with `vi.fn(impl)` will restore the implementation to `impl`. +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`. To automatically call this method before each test, enable the [`restoreMocks`](/config/#restoremocks) setting in the configuration. diff --git a/docs/api/vi.md b/docs/api/vi.md index 2aed59bc98ec..70f09352ef1b 100644 --- a/docs/api/vi.md +++ b/docs/api/vi.md @@ -33,7 +33,7 @@ If the `factory` function is defined, all imports will return its result. Vitest Unlike in `jest`, the factory can be asynchronous. You can use [`vi.importActual`](#vi-importactual) or a helper with the factory passed in as the first argument, and get the original module inside. -Since Vitest 2.1, you can also provide an object with a `spy` property instead of a factory function. If `spy` is `true`, then Vitest will automock the module as usual, but it won't override the implementation of exports. This is useful if you just want to assert that the exported method was called correctly by another method. +You can also provide an object with a `spy` property instead of a factory function. If `spy` is `true`, then Vitest will automock the module as usual, but it won't override the implementation of exports. This is useful if you just want to assert that the exported method was called correctly by another method. ```ts import { calculator } from './src/calculator.ts' @@ -136,8 +136,7 @@ For example, you have this file structure: If you call `vi.mock` in a test file without a factory or options provided, it will find a file in the `__mocks__` folder to use as a module: -```ts -// increment.test.js +```ts [increment.test.js] import { vi } from 'vitest' // axios is a default export from `__mocks__/axios.js` @@ -175,14 +174,13 @@ import { increment } from './increment.js' ``` ::: -```ts -// ./increment.js +```ts [increment.js] export function increment(number) { return number + 1 } ``` -```ts +```ts [increment.test.js] import { beforeEach, test } from 'vitest' import { increment } from './increment.js' @@ -216,8 +214,7 @@ Type helper for TypeScript. Just returns the object that was passed. When `partial` is `true` it will expect a `Partial` as a return value. By default, this will only make TypeScript believe that the first level values are mocked. You can pass down `{ deep: true }` as a second argument to tell TypeScript that the whole object is mocked, if it actually is. -```ts -// example.ts +```ts [example.ts] export function add(x: number, y: number): number { return x + y } @@ -227,8 +224,7 @@ export function fetchSomething(): Promise { } ``` -```ts -// example.test.ts +```ts [example.test.ts] import * as example from './example' vi.mock('./example') @@ -277,14 +273,13 @@ Removes module from the mocked registry. All calls to import will return the ori 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. -```ts -// ./increment.js +```ts [increment.js] export function increment(number) { return number + 1 } ``` -```ts +```ts [increment.test.js] import { increment } from './increment.js' // increment is already mocked, because vi.mock is hoisted @@ -403,15 +398,18 @@ Checks that a given parameter is a mock function. If you are using TypeScript, i ### vi.clearAllMocks -Will call [`.mockClear()`](/api/mock#mockclear) on all spies. This will clear mock history, but not reset its implementation to the default one. +Calls [`.mockClear()`](/api/mock#mockclear) on all spies. +This will clear mock history without affecting mock implementations. ### vi.resetAllMocks -Will call [`.mockReset()`](/api/mock#mockreset) on all spies. This will clear mock history and reset its implementation to an empty function (will return `undefined`). +Calls [`.mockReset()`](/api/mock#mockreset) on all spies. +This will clear mock history and reset each mock's implementation to its original. ### vi.restoreAllMocks -Will call [`.mockRestore()`](/api/mock#mockrestore) on all spies. This will clear mock history and reset its implementation to the original one. +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. ### vi.spyOn diff --git a/docs/config/file.md b/docs/config/file.md deleted file mode 100644 index 3b6dfe2b5b52..000000000000 --- a/docs/config/file.md +++ /dev/null @@ -1,92 +0,0 @@ ---- -outline: deep ---- - -# Managing Vitest config file - -If you are using Vite and have a `vite.config` file, Vitest will read it to match with the plugins and setup as your Vite app. If you want to have a different configuration for testing or your main app doesn't rely on Vite specifically, you could either: - -- 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` - -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. - -Using `defineConfig` from `vite` you should follow this: - -```ts -/// -import { defineConfig } from 'vite' - -export default defineConfig({ - test: { - // ... Specify options here. - }, -}) -``` - -The `` will stop working in Vitest 3, but you can start migrating to `vitest/config` in Vitest 2.1: - -```ts -/// -import { defineConfig } from 'vite' - -export default defineConfig({ - test: { - // ... Specify options here. - }, -}) -``` - -Using `defineConfig` from `vitest/config` you should follow this: - -```ts -import { defineConfig } from 'vitest/config' - -export default defineConfig({ - test: { - // ... Specify options here. - }, -}) -``` - -You can retrieve Vitest's default options to expand them if needed: - -```ts -import { configDefaults, defineConfig } from 'vitest/config' - -export default defineConfig({ - test: { - exclude: [...configDefaults.exclude, 'packages/template/*'], - }, -}) -``` - -When using a separate `vitest.config.js`, you can also extend Vite's options from another config file if needed: - -```ts -import { defineConfig, mergeConfig } from 'vitest/config' -import viteConfig from './vite.config' - -export default mergeConfig(viteConfig, defineConfig({ - test: { - exclude: ['packages/template/*'], - }, -})) -``` - -If your Vite config is defined as a function, you can define the config like this: - -```ts -import { defineConfig, mergeConfig } from 'vitest/config' -import viteConfig from './vite.config' - -export default defineConfig(configEnv => mergeConfig( - viteConfig(configEnv), - defineConfig({ - test: { - exclude: ['packages/template/*'], - }, - }) -)) -``` diff --git a/docs/config/index.md b/docs/config/index.md index f8c9624455b8..347a174c274b 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -4,24 +4,109 @@ outline: deep # Configuring Vitest -To create a Vitest configuration file, follow [the guide](/config/file). Make sure you understand how Vitest config resolution works before proceeding. +If you are using Vite and have a `vite.config` file, Vitest will read it to match with the plugins and setup as your Vite app. If you want to have a different configuration for testing or your main app doesn't rely on Vite specifically, you could either: + +- 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` + +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. + +::: details Open Config Examples +Using `defineConfig` from `vite` you should follow this: + +```ts [vite.config.js] +/// +import { defineConfig } from 'vite' + +export default defineConfig({ + test: { + // ... Specify options here. + }, +}) +``` + +The `` will stop working in Vitest 4, but you can already start migrating to `vitest/config`: + +```ts [vite.config.js] +/// +import { defineConfig } from 'vite' + +export default defineConfig({ + test: { + // ... Specify options here. + }, +}) +``` + +Using `defineConfig` from `vitest/config` you should follow this: + +```ts [vitest.config.js] +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + // ... Specify options here. + }, +}) +``` + +You can retrieve Vitest's default options to expand them if needed: + +```ts [vitest.config.js] +import { configDefaults, defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + exclude: [...configDefaults.exclude, 'packages/template/*'], + }, +}) +``` + +When using a separate `vitest.config.js`, you can also extend Vite's options from another config file if needed: + +```ts [vitest.config.js] +import { defineConfig, mergeConfig } from 'vitest/config' +import viteConfig from './vite.config' + +export default mergeConfig(viteConfig, defineConfig({ + test: { + exclude: ['packages/template/*'], + }, +})) +``` + +If your Vite config is defined as a function, you can define the config like this: + +```ts [vitest.config.js] +import { defineConfig, mergeConfig } from 'vitest/config' +import viteConfig from './vite.config' + +export default defineConfig(configEnv => mergeConfig( + viteConfig(configEnv), + defineConfig({ + test: { + exclude: ['packages/template/*'], + }, + }) +)) +``` +::: ::: warning -_All_ listed options here are located on a `test` property inside the config: +_All listed options_ on this page are located within a `test` property inside the configuration: -```ts +```ts [vitest.config.js] export default defineConfig({ test: { exclude: [], }, }) ``` -::: -::: tip -In addition to the following options, you can also use any configuration option from [Vite](https://vitejs.dev/config/). For example, `define` to define global variables, or `resolve.alias` to define aliases. +Since Vitest uses Vite config, you can also use any configuration option from [Vite](https://vitejs.dev/config/). For example, `define` to define global variables, or `resolve.alias` to define aliases - these options should be defined on the top level, _not_ within a `test` property. -All configuration options that are not supported inside a [workspace](/guide/workspace) project config have sign next to them. +Configuration options that are not supported inside a [workspace](/guide/workspace) project config have sign next to them. ::: ### include @@ -356,7 +441,6 @@ Vitest uses Vite SSR primitives to run tests which has [certain pitfalls](https: By default, `vitest` does not provide global APIs for explicitness. If you prefer to use the APIs globally like Jest, you can pass the `--globals` option to CLI or add `globals: true` in the config. ```ts -// vitest.config.ts import { defineConfig } from 'vitest/config' export default defineConfig({ @@ -368,8 +452,7 @@ export default defineConfig({ To get TypeScript working with the global APIs, add `vitest/globals` to the `types` field in your `tsconfig.json` -```json -// tsconfig.json +```json [tsconfig.json] { "compilerOptions": { "types": ["vitest/globals"] @@ -379,8 +462,7 @@ To get TypeScript working with the global APIs, add `vitest/globals` to the `typ If you are already using [`unplugin-auto-import`](https://github.com/antfu/unplugin-auto-import) in your project, you can also use it directly for auto importing those APIs. -```ts -// vitest.config.ts +```ts [vitest.config.js] import { defineConfig } from 'vitest/config' import AutoImport from 'unplugin-auto-import/vite' @@ -454,7 +536,7 @@ If you are running Vitest with [`--isolate=false`](#isolate) flag, your tests wi Starting from 0.23.0, you can also define custom environment. When non-builtin environment is used, Vitest will try to load package `vitest-environment-${name}`. That package should export an object with the shape of `Environment`: -```ts +```ts [environment.js] import type { Environment } from 'vitest' export default { @@ -476,7 +558,7 @@ Vitest also exposes `builtinEnvironments` through `vitest/environments` entry, i ::: tip jsdom environment exposes `jsdom` global variable equal to the current [JSDOM](https://github.com/jsdom/jsdom) instance. If you want TypeScript to recognize it, you can add `vitest/jsdom` to your `tsconfig.json` when you use this environment: -```json +```json [tsconfig.json] { "compilerOptions": { "types": ["vitest/jsdom"] @@ -497,6 +579,28 @@ These options are passed down to `setup` method of current [`environment`](#envi - **Type:** `[string, EnvironmentName][]` - **Default:** `[]` +::: danger DEPRECATED +This API was deprecated in Vitest 3. Use [workspace](/guide/workspace) to define different configurations instead. + +```ts +export default defineConfig({ + test: { + environmentMatchGlobs: [ // [!code --] + ['./*.jsdom.test.ts', 'jsdom'], // [!code --] + ], // [!code --] + workspace: [ // [!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: @@ -522,6 +626,28 @@ export default defineConfig({ - **Type:** `[string, 'threads' | 'forks' | 'vmThreads' | 'vmForks' | 'typescript'][]` - **Default:** `[]` +::: danger DEPRECATED +This API was deprecated in Vitest 3. Use [workspace](/guide/workspace) to define different configurations instead: + +```ts +export default defineConfig({ + test: { + poolMatchGlobs: [ // [!code --] + ['./*.threads.test.ts', 'threads'], // [!code --] + ], // [!code --] + workspace: [ // [!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: @@ -1016,7 +1142,7 @@ export default defineConfig({ }, }) ``` -```ts [my.test.js] +```ts [api.test.js] import { expect, inject, test } from 'vitest' test('api key is defined', () => { @@ -1032,9 +1158,7 @@ Properties have to be strings and values need to be [serializable](https://devel ::: tip If you are using TypeScript, you will need to augment `ProvidedContext` type for type safe access: -```ts -// vitest.shims.d.ts - +```ts [vitest.shims.d.ts] declare module 'vitest' { export interface ProvidedContext { API_KEY: string @@ -1064,12 +1188,25 @@ Global setup runs only if there is at least one running test. This means that gl Beware that the global setup is running in a different global scope, so your tests don't have access to variables defined here. However, you can pass down serializable data to tests via [`provide`](#provide) method: :::code-group -```js [globalSetup.js] -export default function setup({ provide }) { - provide('wsPort', 3000) +```ts [example.test.js] +import { inject } from 'vitest' + +inject('wsPort') === 3000 +``` +```ts [globalSetup.ts 3.0.0] +import type { TestProject } from 'vitest/node' + +export default function setup(project: TestProject) { + project.provide('wsPort', 3000) +} + +declare module 'vitest' { + export interface ProvidedContext { + wsPort: number + } } ``` -```ts [globalSetup.ts] +```ts [globalSetup.ts 2.0.0] import type { GlobalSetupContext } from 'vitest/node' export default function setup({ provide }: GlobalSetupContext) { @@ -1082,20 +1219,15 @@ declare module 'vitest' { } } ``` -```ts [example.test.js] -import { inject } from 'vitest' - -inject('wsPort') === 3000 -``` ::: -Since Vitest 2.2.0, you can define a custom callback function to be called when Vitest reruns tests. If the function is asynchronous, the runner will wait for it to complete before executing the tests. +Since Vitest 3, you can define a custom callback function to be called when Vitest reruns tests. If the function is asynchronous, the runner will wait for it to complete before executing tests. Note that you cannot destruct the `project` like `{ onTestsRerun }` because it relies on the context. -```ts -import type { GlobalSetupContext } from 'vitest/node' +```ts [globalSetup.ts] +import type { TestProject } from 'vitest/node' -export default function setup({ onTestsRerun }: GlobalSetupContext) { - onTestsRerun(async () => { +export default function setup(project: TestProject) { + project.onTestsRerun(async () => { await restartDb() }) } @@ -1623,7 +1755,7 @@ This is an experimental feature. Breaking changes might not follow SemVer, pleas - **Default:** `false` - **CLI:** `--browser`, `--browser.enabled=false` -Run all tests inside a browser by default. Can be overridden with [`poolMatchGlobs`](#poolmatchglobs) option. +Run all tests inside a browser by default. #### browser.name @@ -1652,11 +1784,10 @@ Run the browser in a `headless` mode. If you are running Vitest in CI, it will b Run every test in a separate iframe. -#### browser.testerHtmlPath +#### browser.testerHtmlPath 2.1.4 {#browser-testerhtmlpath} - **Type:** `string` - **Default:** `@vitest/browser/tester.html` -- **Version:** Since Vitest 2.1.4 A path to the HTML entry point. Can be relative to the root of the project. This file will be processed with [`transformIndexHtml`](https://vite.dev/guide/api-plugin#transformindexhtml) hook. @@ -1697,31 +1828,27 @@ This is an advanced API for library authors. If you just need to run tests in a Options that will be passed down to provider when calling `provider.initialize`. ```ts +import { defineConfig } from 'vitest/config' + export default defineConfig({ test: { browser: { providerOptions: { launch: { devtools: true, - } - } - } - } + }, + }, + }, + }, }) ``` ::: tip -To have a better type safety when using built-in providers, you can add one of these types (for provider that you are using) to your tsconfig's `compilerOptions.types` field: +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/): -```json -{ - "compilerOptions": { - "types": [ - "@vitest/browser/providers/webdriverio", - "@vitest/browser/providers/playwright" - ] - } -} +```ts +/// +/// ``` ::: @@ -1826,21 +1953,24 @@ Custom [commands](/guide/browser/commands) that can be imported during browser t - **Type:** `boolean` - **Default:** `false` -Will call [`.mockClear()`](/api/mock#mockclear) on all spies before each test. This will clear mock history, but not reset its implementation to the default one. +Will call [`.mockClear()`](/api/mock#mockclear) on all spies before each test. +This will clear mock history without affecting mock implementations. ### mockReset - **Type:** `boolean` - **Default:** `false` -Will call [`.mockReset()`](/api/mock#mockreset) on all spies before each test. This will clear mock history and reset its implementation to an empty function (will return `undefined`). +Will call [`.mockReset()`](/api/mock#mockreset) on all spies before each test. +This will clear mock history and reset each implementation to its original. ### restoreMocks - **Type:** `boolean` - **Default:** `false` -Will call [`.mockRestore()`](/api/mock#mockrestore) on all spies before each test. This will clear mock history and reset its implementation to the original one. +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.. ### unstubEnvs {#unstubenvs} @@ -2302,8 +2432,7 @@ export default defineConfig({ For example, as a config object: -:::code-group -```ts [vitest.config.js] +```ts import { defineConfig } from 'vitest/config' import c from 'picocolors' @@ -2313,11 +2442,10 @@ export default defineConfig({ aIndicator: c.bold('--'), bIndicator: c.bold('++'), omitAnnotationLines: true, - } - } + }, + }, }) ``` -::: Or as a module: @@ -2327,8 +2455,8 @@ import { defineConfig } from 'vitest/config' export default defineConfig({ test: { - diff: './vitest.diff.ts' - } + diff: './vitest.diff.ts', + }, }) ``` @@ -2379,7 +2507,7 @@ Color of truncate annotation, default is output with no color. #### diff.printBasicPrototype - **Type**: `boolean` -- **Default**: `true` +- **Default**: `false` Print basic prototype `Object` and `Array` in diff output @@ -2399,7 +2527,7 @@ Installs fake timers with the specified Unix epoch. #### fakeTimers.toFake - **Type:** `('setTimeout' | 'clearTimeout' | 'setImmediate' | 'clearImmediate' | 'setInterval' | 'clearInterval' | 'Date' | 'nextTick' | 'hrtime' | 'requestAnimationFrame' | 'cancelAnimationFrame' | 'requestIdleCallback' | 'cancelIdleCallback' | 'performance' | 'queueMicrotask')[]` -- **Default:** `['setTimeout', 'clearTimeout', 'setImmediate', 'clearImmediate', 'setInterval', 'clearInterval', 'Date']` +- **Default:** everything available globally except `nextTick` An array with names of global methods and APIs to fake. @@ -2443,7 +2571,7 @@ Tells fake timers to clear "native" (i.e. not fake) timers by delegating to thei Path to a [workspace](/guide/workspace) config file relative to [root](#root). -Since Vitest 2.2, 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. +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. ### isolate diff --git a/docs/guide/browser/assertion-api.md b/docs/guide/browser/assertion-api.md index e7cb87845a0e..2126fe110e22 100644 --- a/docs/guide/browser/assertion-api.md +++ b/docs/guide/browser/assertion-api.md @@ -32,35 +32,17 @@ Vitest bundles the [`@testing-library/jest-dom`](https://github.com/testing-libr - [`toHaveRole`](https://github.com/testing-library/jest-dom#toHaveRole) - [`toHaveErrorMessage`](https://github.com/testing-library/jest-dom#toHaveErrorMessage) -If you are using TypeScript or want to have correct type hints in `expect`, make sure you have either `@vitest/browser/providers/playwright` or `@vitest/browser/providers/webdriverio` specified in your `tsconfig` depending on the provider you use. If you use the default `preview` provider, you can specify `@vitest/browser/matchers` instead. +If you are using [TypeScript](/guide/browser/#typescript) or want to have correct type hints in `expect`, make sure you have either `@vitest/browser/providers/playwright` or `@vitest/browser/providers/webdriverio` referenced in your [setup file](/config/#setupfile) or a [config file](/config/) depending on the provider you use. If you use the default `preview` provider, you can specify `@vitest/browser/matchers` instead. ::: code-group -```json [preview] -{ - "compilerOptions": { - "types": [ - "@vitest/browser/matchers" - ] - } -} +```ts [preview] +/// ``` -```json [playwright] -{ - "compilerOptions": { - "types": [ - "@vitest/browser/providers/playwright" - ] - } -} +```ts [playwright] +/// ``` -```json [webdriverio] -{ - "compilerOptions": { - "types": [ - "@vitest/browser/providers/webdriverio" - ] - } -} +```ts [webdriverio] +/// ``` ::: diff --git a/docs/guide/browser/commands.md b/docs/guide/browser/commands.md index c54fc4f3199a..419cd0b04e4a 100644 --- a/docs/guide/browser/commands.md +++ b/docs/guide/browser/commands.md @@ -149,16 +149,10 @@ export const myCommand: BrowserCommand<[string, number]> = async ( ``` ::: tip -If you are using TypeScript, don't forget to add `@vitest/browser/providers/playwright` to your `tsconfig` "compilerOptions.types" field to get autocompletion in the config and on `userEvent` and `page` options: - -```json -{ - "compilerOptions": { - "types": [ - "@vitest/browser/providers/playwright" - ] - } -} +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 +/// ``` ::: @@ -171,15 +165,9 @@ Vitest exposes some `webdriverio` specific properties on the context object. 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 add `@vitest/browser/providers/webdriverio` to your `tsconfig` "compilerOptions.types" field to get autocompletion: - -```json -{ - "compilerOptions": { - "types": [ - "@vitest/browser/providers/webdriverio" - ] - } -} +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 +/// ``` ::: diff --git a/docs/guide/browser/index.md b/docs/guide/browser/index.md index 2d1d6b708737..4712eac69ec8 100644 --- a/docs/guide/browser/index.md +++ b/docs/guide/browser/index.md @@ -7,6 +7,10 @@ outline: deep This page provides information about the experimental browser mode feature in the Vitest API, which allows you to run your tests in the browser natively, providing access to browser globals like window and document. This feature is currently under development, and APIs may change in the future. +::: tip +If you are looking for documentation for `expect`, `vi` or any general API like workspaces or type testing, refer to the ["Getting Started" guide](/guide/). +::: + Vitest UI Vitest UI @@ -51,7 +55,7 @@ bun add -D vitest @vitest/browser ::: warning However, to run tests in CI you need to install either [`playwright`](https://npmjs.com/package/playwright) or [`webdriverio`](https://www.npmjs.com/package/webdriverio). We also recommend switching to either one of them for testing locally instead of using the default `preview` provider since it relies on simulating events instead of using Chrome DevTools Protocol. -If you don't already use one of these tools, we recommend starting with Playwright because it supports parallel execution, which makes your tests run faster. Additionally, the Chrome DevTools Protocol that Playwright uses is generally faster than WebDriver. +If you don't already use one of these tools, we recommend starting with Playwright because it supports parallel execution, which makes your tests run faster. Additionally, Playwright uses [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/) which is generally faster than WebDriver. ::: tabs key:provider == Playwright @@ -93,7 +97,8 @@ bun add -D vitest @vitest/browser webdriverio To activate browser mode in your Vitest configuration, you can use the `--browser` flag or set the `browser.enabled` field to `true` in your Vitest configuration file. Here is an example configuration using the browser field: -```ts +```ts [vitest.config.ts] +import { defineConfig } from 'vitest/config' export default defineConfig({ test: { browser: { @@ -114,6 +119,21 @@ Since Vitest 2.1.5, the CLI no longer prints the Vite URL automatically. You can If you have not used Vite before, make sure you have your framework's plugin installed and specified in the config. Some frameworks might require extra configuration to work - check their Vite related documentation to be sure. ::: code-group +```ts [react] +import { defineConfig } from 'vitest/config' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], + test: { + browser: { + enabled: true, + provider: 'playwright', + name: 'chromium', + } + } +}) +``` ```ts [vue] import { defineConfig } from 'vitest/config' import vue from '@vitejs/plugin-vue' @@ -176,16 +196,11 @@ export default defineConfig({ ``` ::: -::: tip -`react` doesn't require a plugin to work, but `preact` requires [extra configuration](https://preactjs.com/guide/v10/getting-started/#create-a-vite-powered-preact-app) to make aliases work. -::: - If you need to run some tests using Node-based runner, you can define a [workspace](/guide/workspace) file with separate configurations for different testing strategies: {#workspace-config} -```ts -// vitest.workspace.ts +```ts [vitest.workspace.ts] import { defineWorkspace } from 'vitest/config' export default defineWorkspace([ @@ -225,7 +240,7 @@ export default defineWorkspace([ == Playwright You can configure how Vitest [launches the browser](https://playwright.dev/docs/api/class-browsertype#browser-type-launch) and creates the [page context](https://playwright.dev/docs/api/class-browsercontext) via [`providerOptions`](/config/#browser-provideroptions) field: -```ts +```ts [vitest.config.ts] export default defineConfig({ test: { browser: { @@ -245,8 +260,6 @@ export default defineConfig({ }, }) ``` - -To have type hints, add `@vitest/browser/providers/playwright` to `compilerOptions.types` in your `tsconfig.json` file. == WebdriverIO You can configure what [options](https://webdriver.io/docs/configuration#webdriverio) Vitest should use when starting a browser via [`providerOptions`](/config/#browser-provideroptions) field: @@ -266,8 +279,6 @@ export default defineConfig({ }, }) ``` - -To have type hints, add `@vitest/browser/providers/webdriverio` to `compilerOptions.types` in your `tsconfig.json` file. ::: ## Browser Option Types @@ -284,6 +295,48 @@ 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/#setupfile) 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). @@ -317,11 +370,12 @@ By default, Vitest will automatically open the browser UI for development. Your Headless mode is another option available in the browser mode. In headless mode, the browser runs in the background without a user interface, which makes it useful for running automated tests. The headless option in Vitest can be set to a boolean value to enable or disable headless mode. -When using headless mode, Vitest won't open the UI automatically. If you want to continue using the UI but have tests run headlessly, you can install the [`@vitest/ui`](/guide/ui) package and pass the --ui flag when running Vitest. +When using headless mode, Vitest won't open the UI automatically. If you want to continue using the UI but have tests run headlessly, you can install the [`@vitest/ui`](/guide/ui) package and pass the `--ui` flag when running Vitest. Here's an example configuration enabling headless mode: -```ts +```ts [vitest.config.ts] +import { defineConfig } from 'vitest/config' export default defineConfig({ test: { browser: { @@ -347,7 +401,31 @@ Headless mode is not available by default. You need to use either [`playwright`] ## Examples -Vitest provides packages to render components for several popular frameworks out of the box: +By default, you don't need any external packages to work with the Browser Mode: + +```js [example.test.js] +import { expect, test } from 'vitest' +import { page } from '@vitest/browser/context' +import { render } from './my-render-function.js' + +test('properly handles form inputs', async () => { + render() // mount DOM elements + + // Asserts initial state. + await expect.element(page.getByText('Hi, my name is Alice')).toBeInTheDocument() + + // Get the input DOM node by querying the associated label. + const usernameInput = page.getByLabelText(/username/i) + + // Type the name into the input. This already validates that the input + // is filled correctly, no need to check the value manually. + await usernameInput.fill('Bob') + + await expect.element(page.getByText('Hi, my name is Bob')).toBeInTheDocument() +}) +``` + +However, Vitest also provides packages to render components for several popular frameworks out of the box: - [`vitest-browser-vue`](https://github.com/vitest-dev/vitest-browser-vue) to render [vue](https://vuejs.org) components - [`vitest-browser-svelte`](https://github.com/vitest-dev/vitest-browser-svelte) to render [svelte](https://svelte.dev) components @@ -437,6 +515,8 @@ For unsupported frameworks, we recommend using `testing-library` packages: - [`@solidjs/testing-library`](https://testing-library.com/docs/solid-testing-library/intro) to render [solid](https://www.solidjs.com) components - [`@marko/testing-library`](https://testing-library.com/docs/marko-testing-library/intro) to render [marko](https://markojs.com) components +You can also see more examples in [`browser-examples`](https://github.com/vitest-tests/browser-examples) repository. + ::: warning `testing-library` provides a package `@testing-library/user-event`. We do not recommend using it directly because it simulates events instead of actually triggering them - instead, use [`userEvent`](/guide/browser/interactivity-api) imported from `@vitest/browser/context` that uses Chrome DevTools Protocol or Webdriver (depending on the provider) under the hood. ::: diff --git a/docs/guide/browser/interactivity-api.md b/docs/guide/browser/interactivity-api.md index b82f4a701edb..c41610c26dbc 100644 --- a/docs/guide/browser/interactivity-api.md +++ b/docs/guide/browser/interactivity-api.md @@ -12,26 +12,14 @@ 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 `tsconfig.json` file: +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 -```json [playwright] -{ - "compilerOptions": { - "types": [ - "@vitest/browser/providers/playwright" - ] - } -} +```ts [playwright] +/// ``` -```json [webdriverio] -{ - "compilerOptions": { - "types": [ - "@vitest/browser/providers/webdriverio" - ] - } -} +```ts [webdriverio] +/// ``` ::: diff --git a/docs/guide/browser/locators.md b/docs/guide/browser/locators.md index eaf3a59be5b5..b5a91ab83996 100644 --- a/docs/guide/browser/locators.md +++ b/docs/guide/browser/locators.md @@ -389,7 +389,7 @@ It is recommended to use this only after the other locators don't work for your ## Methods -All methods are asynchronous and must be awaited. Since Vitest 2.2, tests will fail if a method is not awaited. +All methods are asynchronous and must be awaited. Since Vitest 3, tests will fail if a method is not awaited. ### click diff --git a/docs/guide/browser/why.md b/docs/guide/browser/why.md index b7cbc3e95d9d..73201e7c6bbb 100644 --- a/docs/guide/browser/why.md +++ b/docs/guide/browser/why.md @@ -1,9 +1,9 @@ --- -title: Why Browser Mode? | Browser Mode +title: Why Browser Mode | Browser Mode outline: deep --- -# Why Browser Mode? +# Why Browser Mode ## Motivation diff --git a/docs/guide/cli-generated.md b/docs/guide/cli-generated.md index 27d7d0160e3b..350a178d8bf4 100644 --- a/docs/guide/cli-generated.md +++ b/docs/guide/cli-generated.md @@ -395,7 +395,7 @@ Should browser test files run in parallel. Use `--browser.fileParallelism=false` - **CLI:** `--pool ` - **Config:** [pool](/config/#pool) -Specify pool, if not running in the browser (default: `threads`) +Specify pool, if not running in the browser (default: `forks`) ### poolOptions.threads.isolate @@ -677,12 +677,96 @@ Stop test execution when given number of tests have failed (default: `0`) Retry the test specific number of times if it fails (default: `0`) -### diff +### diff.aAnnotation -- **CLI:** `--diff ` -- **Config:** [diff](/config/#diff) +- **CLI:** `--diff.aAnnotation ` +- **Config:** [diff.aAnnotation](/config/#diff-aannotation) -Path to a diff config that will be used to generate diff interface +Annotation for expected lines (default: `Expected`) + +### diff.aIndicator + +- **CLI:** `--diff.aIndicator ` +- **Config:** [diff.aIndicator](/config/#diff-aindicator) + +Indicator for expected lines (default: `-`) + +### diff.bAnnotation + +- **CLI:** `--diff.bAnnotation ` +- **Config:** [diff.bAnnotation](/config/#diff-bannotation) + +Annotation for received lines (default: `Received`) + +### diff.bIndicator + +- **CLI:** `--diff.bIndicator ` +- **Config:** [diff.bIndicator](/config/#diff-bindicator) + +Indicator for received lines (default: `+`) + +### diff.commonIndicator + +- **CLI:** `--diff.commonIndicator ` +- **Config:** [diff.commonIndicator](/config/#diff-commonindicator) + +Indicator for common lines (default: ` `) + +### diff.contextLines + +- **CLI:** `--diff.contextLines ` +- **Config:** [diff.contextLines](/config/#diff-contextlines) + +Number of lines of context to show around each change (default: `5`) + +### diff.emptyFirstOrLastLinePlaceholder + +- **CLI:** `--diff.emptyFirstOrLastLinePlaceholder ` +- **Config:** [diff.emptyFirstOrLastLinePlaceholder](/config/#diff-emptyfirstorlastlineplaceholder) + +Placeholder for an empty first or last line (default: `""`) + +### diff.expand + +- **CLI:** `--diff.expand` +- **Config:** [diff.expand](/config/#diff-expand) + +Expand all common lines (default: `true`) + +### diff.includeChangeCounts + +- **CLI:** `--diff.includeChangeCounts` +- **Config:** [diff.includeChangeCounts](/config/#diff-includechangecounts) + +Include comparison counts in diff output (default: `false`) + +### diff.omitAnnotationLines + +- **CLI:** `--diff.omitAnnotationLines` +- **Config:** [diff.omitAnnotationLines](/config/#diff-omitannotationlines) + +Omit annotation lines from the output (default: `false`) + +### diff.printBasicPrototype + +- **CLI:** `--diff.printBasicPrototype` +- **Config:** [diff.printBasicPrototype](/config/#diff-printbasicprototype) + +Print basic prototype Object and Array (default: `true`) + +### diff.truncateThreshold + +- **CLI:** `--diff.truncateThreshold ` +- **Config:** [diff.truncateThreshold](/config/#diff-truncatethreshold) + +Number of lines to show before and after each change (default: `0`) + +### diff.truncateAnnotation + +- **CLI:** `--diff.truncateAnnotation ` +- **Config:** [diff.truncateAnnotation](/config/#diff-truncateannotation) + +Annotation for truncated lines (default: `... Diff result is truncated`) ### exclude @@ -759,7 +843,7 @@ The name of the project to run if you are using Vitest workspace feature. This c - **CLI:** `--slowTestThreshold ` - **Config:** [slowTestThreshold](/config/#slowtestthreshold) -Threshold in milliseconds for a test to be considered slow (default: `300`) +Threshold in milliseconds for a test or suite to be considered slow (default: `300`) ### teardownTimeout diff --git a/docs/guide/cli.md b/docs/guide/cli.md index ba46c7b1270f..b4ac323c2187 100644 --- a/docs/guide/cli.md +++ b/docs/guide/cli.md @@ -44,8 +44,7 @@ vitest related /src/index.ts /src/hello-world.js ::: tip Don't forget that Vitest runs with enabled watch mode by default. If you are using tools like `lint-staged`, you should also pass `--run` option, so that command can exit normally. -```js -// .lintstagedrc.js +```js [.lintstagedrc.js] export default { '*.{js,ts}': 'vitest related --run', } diff --git a/docs/guide/common-errors.md b/docs/guide/common-errors.md index a5f5044af6a9..47f40e9fbbea 100644 --- a/docs/guide/common-errors.md +++ b/docs/guide/common-errors.md @@ -49,8 +49,7 @@ This error happens when `vi.mock` method is called on a module that was already Remember that `vi.mock` is always hoisted - it means that the module was loaded before the test file started executing - most likely in a setup file. To fix the error, remove the import or clear the cache at the end of a setup file - beware that setup file and your test file will reference different modules in that case. -```ts -// setupFile.js +```ts [setupFile.js] import { vi } from 'vitest' import { sideEffect } from './mocked-file.js' diff --git a/docs/guide/coverage.md b/docs/guide/coverage.md index d29c77f5d893..6ad6d19e4ec9 100644 --- a/docs/guide/coverage.md +++ b/docs/guide/coverage.md @@ -12,8 +12,7 @@ Both `v8` and `istanbul` support are optional. By default, `v8` will be used. You can select the coverage tool by setting `test.coverage.provider` to `v8` or `istanbul`: -```ts -// vitest.config.ts +```ts [vitest.config.ts] import { defineConfig } from 'vitest/config' export default defineConfig({ @@ -29,13 +28,14 @@ When you start the Vitest process, it will prompt you to install the correspondi Or if you prefer to install them manually: -```bash -# For v8 +::: code-group +```bash [v8] npm i -D @vitest/coverage-v8 - -# For istanbul +``` +```bash [istanbul] npm i -D @vitest/coverage-istanbul ``` +::: ## Coverage Setup @@ -47,7 +47,7 @@ This helps Vitest to reduce the amount of files picked by [`coverage.all`](https To test with coverage enabled, you can pass the `--coverage` flag in CLI. By default, reporter `['text', 'html', 'clover', 'json']` will be used. -```json +```json [package.json] { "scripts": { "test": "vitest", @@ -58,8 +58,7 @@ By default, reporter `['text', 'html', 'clover', 'json']` will be used. To configure it, set `test.coverage` options in your config file: -```ts -// vitest.config.ts +```ts [vitest.config.ts] import { defineConfig } from 'vitest/config' export default defineConfig({ @@ -75,7 +74,7 @@ export default defineConfig({ You can use custom coverage reporters by passing either the name of the package or absolute path in `test.coverage.reporter`: -```ts +```ts [vitest.config.ts] import { defineConfig } from 'vitest/config' export default defineConfig({ @@ -95,8 +94,7 @@ export default defineConfig({ Custom reporters are loaded by Istanbul and must match its reporter interface. See [built-in reporters' implementation](https://github.com/istanbuljs/istanbuljs/tree/master/packages/istanbul-reports/lib) for reference. -```js -// custom-reporter.cjs +```js [custom-reporter.cjs] const { ReportBase } = require('istanbul-lib-report') module.exports = class CustomReporter extends ReportBase { @@ -123,8 +121,7 @@ module.exports = class CustomReporter extends ReportBase { It's also possible to provide your custom coverage provider by passing `'custom'` in `test.coverage.provider`: -```ts -// vitest.config.ts +```ts [vitest.config.ts] import { defineConfig } from 'vitest/config' export default defineConfig({ @@ -139,8 +136,7 @@ export default defineConfig({ The custom providers require a `customProviderModule` option which is a module name or path where to load the `CoverageProviderModule` from. It must export an object that implements `CoverageProviderModule` as default export: -```ts -// my-custom-coverage-provider.ts +```ts [my-custom-coverage-provider.ts] import type { CoverageProvider, CoverageProviderModule, @@ -176,7 +172,7 @@ Please refer to the type definition for more details. 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 `vite.config.js` file. -```js +```js [vitest.config.js] import { defineConfig } from 'vite' export default defineConfig({ diff --git a/docs/guide/environment.md b/docs/guide/environment.md index 8c0ff5a06246..474379034f71 100644 --- a/docs/guide/environment.md +++ b/docs/guide/environment.md @@ -16,7 +16,7 @@ By default, you can use these environments: ::: 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`. -Since Vitest 2.0.4 the `require` of CSS and assets inside the external dependencies are resolved automatically. +The `require` of CSS and assets inside the external dependencies are resolved automatically. ::: ::: warning diff --git a/docs/guide/features.md b/docs/guide/features.md index 4fbe414eeedc..b67e2f2ace58 100644 --- a/docs/guide/features.md +++ b/docs/guide/features.md @@ -124,16 +124,18 @@ expect(fn.mock.results[1].value).toBe('world') Vitest supports both [happy-dom](https://github.com/capricorn86/happy-dom) or [jsdom](https://github.com/jsdom/jsdom) for mocking DOM and browser APIs. They don't come with Vitest, you will need to install them separately: -```bash +::: code-group +```bash [happy-dom] $ npm i -D happy-dom -# or +``` +```bash [jsdom] $ npm i -D jsdom ``` +::: After that, change the `environment` option in your config file: -```ts -// vitest.config.ts +```ts [vitest.config.ts] import { defineConfig } from 'vitest/config' export default defineConfig({ @@ -149,7 +151,7 @@ Learn more at [Mocking](/guide/mocking). Vitest supports Native code coverage via [`v8`](https://v8.dev/blog/javascript-code-coverage) and instrumented code coverage via [`istanbul`](https://istanbul.js.org/). -```json +```json [package.json] { "scripts": { "test": "vitest", @@ -166,9 +168,7 @@ Vitest also provides a way to run tests within your source code along with the i This makes the tests share the same closure as the implementations and able to test against private states without exporting. Meanwhile, it also brings the feedback loop closer for development. -```ts -// src/index.ts - +```ts [src/index.ts] // the implementation export function add(...args: number[]): number { return args.reduce((a, b) => a + b, 0) @@ -191,7 +191,7 @@ Learn more at [In-source testing](/guide/in-source). You can run benchmark tests with [`bench`](/api/#bench) function via [Tinybench](https://github.com/tinylibs/tinybench) to compare performance results. -```ts +```ts [sort.bench.ts] import { bench, describe } from 'vitest' describe('sort', () => { @@ -218,7 +218,7 @@ describe('sort', () => { You can [write tests](/guide/testing-types) to catch type regressions. Vitest comes with [`expect-type`](https://github.com/mmkal/expect-type) package to provide you with a similar and easy to understand API. -```ts +```ts [types.test-d.ts] import { assertType, expectTypeOf, test } from 'vitest' import { mount } from './mount.js' @@ -248,7 +248,7 @@ See [`Improving Performance | Sharding`](/guide/improving-performance#sharding) Vitest exclusively autoloads environment variables prefixed with `VITE_` from `.env` files to maintain compatibility with frontend-related tests, adhering to [Vite's established convention](https://vitejs.dev/guide/env-and-mode.html#env-files). To load every environmental variable from `.env` files anyway, you can use `loadEnv` method imported from `vite`: -```ts +```ts [vitest.config.ts] import { loadEnv } from 'vite' import { defineConfig } from 'vitest/config' diff --git a/docs/guide/filtering.md b/docs/guide/filtering.md index d2931fcc2adb..6d7bcebdddaa 100644 --- a/docs/guide/filtering.md +++ b/docs/guide/filtering.md @@ -24,6 +24,21 @@ basic/foo.test.ts You can also use the `-t, --testNamePattern ` option to filter tests by full name. This can be helpful when you want to filter by the name defined within a file rather than the filename itself. +Since Vitest 3, you can also specify the test by filename and line number: + +```bash +$ vitest basic/foo.test.ts:10 +``` + +::: warning +Note that you have to specify the full filename, and specify the exact line number, i.e. you can't do + +```bash +$ vitest foo:10 +$ vitest basic/foo.test.ts:10-25 +``` +::: + ## Specifying a Timeout You can optionally pass a timeout in milliseconds as a third argument to tests. The default is [5 seconds](/config/#testtimeout). diff --git a/docs/guide/improving-performance.md b/docs/guide/improving-performance.md index 68c218f2bda0..9c25fcd44cdd 100644 --- a/docs/guide/improving-performance.md +++ b/docs/guide/improving-performance.md @@ -8,7 +8,7 @@ By default Vitest runs every test file in an isolated environment based on the [ - `forks` pool runs every test file in a separate [forked child process](https://nodejs.org/api/child_process.html#child_processforkmodulepath-args-options) - `vmThreads` pool runs every test file in a separate [VM context](https://nodejs.org/api/vm.html#vmcreatecontextcontextobject-options), but it uses workers for parallelism -This greatly increases test times, which might not be desirable for projects that don't rely on side effects and properly cleanup their state (which is usually true for projects with `node` environment). In this case disabling isolation will improve the speed of your tests. To do that, you can provide `--no-isolate` flag to the CLI or set [`test.isolate`](/config/#isolate) property in the config to `false`. If you are using several pools at once with `poolMatchGlobs`, you can also disable isolation for a specific pool you are using. +This greatly increases test times, which might not be desirable for projects that don't rely on side effects and properly cleanup their state (which is usually true for projects with `node` environment). In this case disabling isolation will improve the speed of your tests. To do that, you can provide `--no-isolate` flag to the CLI or set [`test.isolate`](/config/#isolate) property in the config to `false`. ::: code-group ```bash [CLI] @@ -91,9 +91,7 @@ Collect the results stored in `.vitest-reports` directory from each machine and vitest --merge-reports ``` -
- Github action example - +::: details Github action example This setup is also used at https://github.com/vitest-tests/test-sharding. ```yaml @@ -162,7 +160,7 @@ jobs: run: npx vitest --merge-reports ``` -
+::: :::tip Test sharding can also become useful on high CPU-count machines. diff --git a/docs/guide/in-source.md b/docs/guide/in-source.md index 29822f177841..3864d7a088d1 100644 --- a/docs/guide/in-source.md +++ b/docs/guide/in-source.md @@ -16,9 +16,7 @@ This guide explains how to write tests inside your source code. If you need to w To get started, put a `if (import.meta.vitest)` block at the end of your source file and write some tests inside it. For example: -```ts -// src/index.ts - +```ts [src/index.ts] // the implementation export function add(...args: number[]) { return args.reduce((a, b) => a + b, 0) @@ -37,8 +35,7 @@ if (import.meta.vitest) { Update the `includeSource` config for Vitest to grab the files under `src/`: -```ts -// vitest.config.ts +```ts [vitest.config.ts] import { defineConfig } from 'vitest/config' export default defineConfig({ @@ -58,8 +55,7 @@ $ npx vitest For the production build, you will need to set the `define` options in your config file, letting the bundler do the dead code elimination. For example, in Vite -```ts -// vitest.config.ts +```ts [vitest.config.ts] import { defineConfig } from 'vitest/config' export default defineConfig({ @@ -74,11 +70,8 @@ export default defineConfig({ ### Other Bundlers -
-unbuild - -```ts -// build.config.ts +::: details unbuild +```ts [build.config.ts] import { defineBuildConfig } from 'unbuild' export default defineBuildConfig({ @@ -90,14 +83,10 @@ export default defineBuildConfig({ ``` Learn more: [unbuild](https://github.com/unjs/unbuild) +::: -
- -
-Rollup - -```ts -// rollup.config.js +::: details Rollup +```ts [rollup.config.js] import replace from '@rollup/plugin-replace' // [!code ++] export default { @@ -111,15 +100,13 @@ export default { ``` Learn more: [Rollup](https://rollupjs.org/) - -
+::: ## TypeScript To get TypeScript support for `import.meta.vitest`, add `vitest/importMeta` to your `tsconfig.json`: -```json -// tsconfig.json +```json [tsconfig.json] { "compilerOptions": { "types": [ diff --git a/docs/guide/index.md b/docs/guide/index.md index 23df99383a9d..3a96a2107ae6 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -41,21 +41,19 @@ Vitest requires Vite >=v5.0.0 and Node >=v18.0.0 It is recommended that you install a copy of `vitest` in your `package.json`, using one of the methods listed above. However, if you would prefer to run `vitest` directly, you can use `npx vitest` (the `npx` tool comes with npm and Node.js). -The `npx` tool will execute the specified command. By default, `npx` will first check if the command exists in the local project's binaries. If it is not found there, `npx` will look in the system's $PATH and execute it if found. If the command is not found in either location, `npx` will install it in a temporary location prior to execution. +The `npx` tool will execute the specified command. By default, `npx` will first check if the command exists in the local project's binaries. If it is not found there, `npx` will look in the system's `$PATH` and execute it if found. If the command is not found in either location, `npx` will install it in a temporary location prior to execution. ## Writing Tests As an example, we will write a simple test that verifies the output of a function that adds two numbers. -``` js -// sum.js +``` js [sum.js] export function sum(a, b) { return a + b } ``` -``` js -// sum.test.js +``` js [sum.test.js] import { expect, test } from 'vitest' import { sum } from './sum.js' @@ -65,12 +63,12 @@ test('adds 1 + 2 to equal 3', () => { ``` ::: tip -By default, tests must contain ".test." or ".spec." in their file name. +By default, tests must contain `.test.` or `.spec.` in their file name. ::: Next, in order to execute the test, add the following section to your `package.json`: -```json +```json [package.json] { "scripts": { "test": "vitest" @@ -108,7 +106,7 @@ Vitest supports the same extensions for your configuration file as Vite does: `. If you are not using Vite as your build tool, you can configure Vitest using the `test` property in your config file: -```ts +```ts [vitest.config.ts] import { defineConfig } from 'vitest/config' export default defineConfig({ @@ -124,7 +122,7 @@ Even if you do not use Vite yourself, Vitest relies heavily on it for its transf If you are already using Vite, add `test` property in your Vite config. You'll also need to add a reference to Vitest types using a [triple slash directive](https://www.typescriptlang.org/docs/handbook/triple-slash-directives.html#-reference-types-) at the top of your config file. -```ts +```ts [vite.config.ts] /// import { defineConfig } from 'vite' @@ -137,7 +135,7 @@ export default defineConfig({ The `` will stop working in Vitest 3, but you can start migrating to `vitest/config` in Vitest 2.1: -```ts +```ts [vite.config.ts] /// import { defineConfig } from 'vite' @@ -181,7 +179,7 @@ However, we recommend using the same file for both Vite and Vitest, instead of c Run different project configurations inside the same project with [Vitest Workspaces](/guide/workspace). You can define a list of files and folders that define your workspace in `vitest.workspace` file. The file supports `js`/`ts`/`json` extensions. This feature works great with monorepo setups. -```ts +```ts [vitest.workspace.ts] import { defineWorkspace } from 'vitest/config' export default defineWorkspace([ @@ -216,7 +214,7 @@ export default defineWorkspace([ In a project where Vitest is installed, you can use the `vitest` binary in your npm scripts, or run it directly with `npx vitest`. Here are the default npm scripts in a scaffolded Vitest project: -```json +```json [package.json] { "scripts": { "test": "vitest", diff --git a/docs/guide/migration.md b/docs/guide/migration.md index b0d4055eec74..ea1398229a4e 100644 --- a/docs/guide/migration.md +++ b/docs/guide/migration.md @@ -5,6 +5,47 @@ outline: deep # Migration Guide +## Migrating to Vitest 3.0 + +### Test Options as a Third Argument + +Vitest 3.0 prints a warning if you pass down an object as a third argument to `test` or `describe` functions: + +```ts +test('validation works', () => { + // ... +}, { retry: 3 }) // [!code --] + +test('validation works', { retry: 3 }, () => { // [!code ++] + // ... +}) +``` + +Vitest 4.0 will throw an error if the third argument is an object. Note that the timeout number is not deprecated: + +```ts +test('validation works', () => { + // ... +}, 1000) // Ok ✅ +``` + +### `Custom` Type is Deprecated experimental API {#custom-type-is-deprecated} + +The `Custom` type is now equal to the `Test` type. Note that Vitest updated the public types in 2.1 and changed exported names to `RunnerCustomCase` and `RunnerTestCase`: + +```ts +import { + RunnerCustomCase, // [!code --] + RunnerTestCase, // [!code ++] +} from 'vitest' +``` + +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. + +### `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`. + ## Migrating to Vitest 2.0 ### Default Pool is `forks` @@ -297,6 +338,14 @@ 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` + +Jest's [`mockReset`](https://jestjs.io/docs/mock-function-api#mockfnmockreset) replaces the mock implementation with an +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`. + ### 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: diff --git a/docs/guide/mocking.md b/docs/guide/mocking.md index 9afefe9706ac..d74cbf74f95d 100644 --- a/docs/guide/mocking.md +++ b/docs/guide/mocking.md @@ -4,13 +4,13 @@ title: Mocking | Guide # Mocking -When writing tests it's only a matter of time before you need to create a "fake" version of an internal — or external — service. This is commonly referred to as **mocking**. Vitest provides utility functions to help you out through its **vi** helper. You can `import { vi } from 'vitest'` or access it **globally** (when [global configuration](/config/#globals) is **enabled**). +When writing tests it's only a matter of time before you need to create a "fake" version of an internal — or external — service. This is commonly referred to as **mocking**. Vitest provides utility functions to help you out through its `vi` helper. You can import it from `vitest` or access it globally if [`global` configuration](/config/#globals) is enabled. ::: warning Always remember to clear or restore mocks before or after each test run to undo mock state changes between runs! See [`mockReset`](/api/mock#mockreset) docs for more info. ::: -If you wanna dive in head first, check out the [API section](/api/vi) otherwise keep reading to take a deeper dive into the world of mocking. +If you are not familliar with `vi.fn`, `vi.mock` or `vi.spyOn` methods, check the [API section](/api/vi) first. ## Dates @@ -175,22 +175,22 @@ Vitest supports mocking Vite [virtual modules](https://vitejs.dev/guide/api-plug 1. Provide an alias -```ts -// vitest.config.js -export default { +```ts [vitest.config.js] +import { defineConfig } from 'vitest/config' +export default defineConfig({ test: { alias: { - '$app/forms': resolve('./mocks/forms.js') - } - } -} + '$app/forms': resolve('./mocks/forms.js'), + }, + }, +}) ``` 2. Provide a plugin that resolves a virtual module -```ts -// vitest.config.js -export default { +```ts [vitest.config.js] +import { defineConfig } from 'vitest/config' +export default defineConfig({ plugins: [ { name: 'virtual-modules', @@ -198,10 +198,10 @@ export default { 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. @@ -210,7 +210,7 @@ The benefit of the second approach is that you can dynamically create different 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 +```ts [foobar.js] export function foo() { return 'foo' } @@ -222,7 +222,7 @@ export function foobar() { 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 +```ts [foobar.test.ts] import { vi } from 'vitest' import * as mod from './foobar.js' @@ -239,8 +239,7 @@ vi.mock('./foobar.js', async (importOriginal) => { You can confirm this behaviour by providing the implementation to the `foobar` method directly: -```ts -// foobar.test.js +```ts [foobar.test.js] import * as mod from './foobar.js' vi.spyOn(mod, 'foo') @@ -249,8 +248,7 @@ vi.spyOn(mod, 'foo') mod.foobar(mod.foo) ``` -```ts -// foobar.js +```ts [foobar.js] export function foo() { return 'foo' } @@ -382,8 +380,7 @@ module.exports = fs.promises ``` ::: -```ts -// read-hello-world.js +```ts [read-hello-world.js] import { readFileSync } from 'node:fs' export function readHelloWorld(path) { @@ -391,8 +388,7 @@ export function readHelloWorld(path) { } ``` -```ts -// hello-world.test.js +```ts [hello-world.test.js] import { beforeEach, expect, it, vi } from 'vitest' import { fs, vol } from 'memfs' import { readHelloWorld } from './read-hello-world.js' @@ -595,13 +591,12 @@ vi.mock(import('./dog.js'), () => { 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 +```ts [src/feed.ts] function feed(dog: Dog) { // ... } - -// ./tests/dog.test.ts +``` +```ts [tests/dog.test.ts] import { expect, test, vi } from 'vitest' import { feed } from '../src/feed.js' @@ -667,13 +662,11 @@ You can also spy on getters and setters using the same method. I want to… ### Mock exported variables -```js -// some-path.js +```js [example.js] export const getter = 'variable' ``` -```ts -// some-path.test.ts -import * as exports from './some-path.js' +```ts [example.test.ts] +import * as exports from './example.js' vi.spyOn(exports, 'getter', 'get').mockReturnValue('mocked') ``` @@ -686,21 +679,20 @@ vi.spyOn(exports, 'getter', 'get').mockReturnValue('mocked') Don't forget that a `vi.mock` call is hoisted to top of the file. It will always be executed before all imports. ::: -```ts -// ./some-path.js +```ts [example.js] export function method() {} ``` ```ts -import { method } from './some-path.js' +import { method } from './example.js' -vi.mock('./some-path.js', () => ({ +vi.mock('./example.js', () => ({ method: vi.fn() })) ``` 2. Example with `vi.spyOn`: ```ts -import * as exports from './some-path.js' +import * as exports from './example.js' vi.spyOn(exports, 'method').mockImplementation(() => {}) ``` @@ -708,14 +700,13 @@ vi.spyOn(exports, 'method').mockImplementation(() => {}) ### Mock an exported class implementation 1. Example with `vi.mock` and `.prototype`: -```ts -// ./some-path.ts +```ts [example.js] export class SomeClass {} ``` ```ts -import { SomeClass } from './some-path.js' +import { SomeClass } from './example.js' -vi.mock(import('./some-path.js'), () => { +vi.mock(import('./example.js'), () => { const SomeClass = vi.fn() SomeClass.prototype.someMethod = vi.fn() return { SomeClass } @@ -726,7 +717,7 @@ vi.mock(import('./some-path.js'), () => { 2. Example with `vi.spyOn`: ```ts -import * as mod from './some-path.js' +import * as mod from './example.js' const SomeClass = vi.fn() SomeClass.prototype.someMethod = vi.fn() @@ -738,26 +729,23 @@ vi.spyOn(mod, 'SomeClass').mockImplementation(SomeClass) 1. Example using cache: -```ts -// some-path.ts +```ts [example.js] export function useObject() { return { method: () => true } } ``` -```ts -// useObject.js -import { useObject } from './some-path.js' +```ts [useObject.js] +import { useObject } from './example.js' const obj = useObject() obj.method() ``` -```ts -// useObject.test.js -import { useObject } from './some-path.js' +```ts [useObject.test.js] +import { useObject } from './example.js' -vi.mock(import('./some-path.js'), () => { +vi.mock(import('./example.js'), () => { let _cache const useObject = () => { if (!_cache) { @@ -863,8 +851,7 @@ it('the value is restored before running an other test', () => { }) ``` -```ts -// vitest.config.ts +```ts [vitest.config.ts] export default defineConfig({ test: { unstubEnvs: true, diff --git a/docs/guide/reporters.md b/docs/guide/reporters.md index b6c1513e4e6f..cced55e9c2d7 100644 --- a/docs/guide/reporters.md +++ b/docs/guide/reporters.md @@ -373,7 +373,7 @@ Example of a JSON report: ``` ::: info -Since Vitest 2.2, the JSON reporter includes coverage information in `coverageMap` if coverage is enabled. +Since Vitest 3, the JSON reporter includes coverage information in `coverageMap` if coverage is enabled. ::: ### HTML Reporter diff --git a/docs/guide/snapshot.md b/docs/guide/snapshot.md index 906cd231d979..a7e33498ee4e 100644 --- a/docs/guide/snapshot.md +++ b/docs/guide/snapshot.md @@ -137,7 +137,7 @@ expect.addSnapshotSerializer({ We also support [snapshotSerializers](/config/#snapshotserializers) option to implicitly add custom serializers. -```ts +```ts [path/to/custom-serializer.ts] import { SnapshotSerializer } from 'vitest' export default { @@ -157,12 +157,12 @@ export default { } satisfies SnapshotSerializer ``` -```ts -import { defineConfig } from 'vite' +```ts [vitest.config.ts] +import { defineConfig } from 'vitest/config' export default defineConfig({ test: { - snapshotSerializers: ['path/to/custom-serializer.ts'] + snapshotSerializers: ['path/to/custom-serializer.ts'], }, }) ``` @@ -242,14 +242,15 @@ test('snapshot', () => { We believe this is a more reasonable default for readability and overall DX. If you still prefer Jest's behavior, you can change your config: -```ts -// vitest.config.js +```ts [vitest.config.ts] +import { defineConfig } from 'vitest/config' + export default defineConfig({ test: { snapshotFormat: { - printBasicPrototype: true - } - } + printBasicPrototype: true, + }, + }, }) ``` diff --git a/docs/guide/test-context.md b/docs/guide/test-context.md index 9d0473107425..8b9a8c3be2a1 100644 --- a/docs/guide/test-context.md +++ b/docs/guide/test-context.md @@ -74,8 +74,7 @@ Like [Playwright](https://playwright.dev/docs/api/class-test#test-extend), you c For example, we first create `myTest` with two fixtures, `todos` and `archive`. -```ts -// my-test.ts +```ts [my-test.ts] import { test } from 'vitest' const todos = [] @@ -98,7 +97,7 @@ export const myTest = test.extend({ Then we can import and use it. -```ts +```ts [my-test.test.ts] import { expect } from 'vitest' import { myTest } from './my-test.js' @@ -181,7 +180,7 @@ test('works correctly') #### Default fixture -Since Vitest 2.2, you can provide different values in different [projects](/guide/workspace). To enable this feature, pass down `{ injected: true }` to the options. If the key is not specified in the [project configuration](/config/#provide), then the default value will be used. +Since Vitest 3, you can provide different values in different [projects](/guide/workspace). To enable this feature, pass down `{ injected: true }` to the options. If the key is not specified in the [project configuration](/config/#provide), then the default value will be used. :::code-group ```ts [fixtures.test.ts] diff --git a/docs/guide/testing-types.md b/docs/guide/testing-types.md index e26abc1e86e1..560bd69b889f 100644 --- a/docs/guide/testing-types.md +++ b/docs/guide/testing-types.md @@ -24,7 +24,7 @@ Since Vitest 2.1, if your `include` and `typecheck.include` overlap, Vitest will Using CLI flags, like `--allowOnly` and `-t` are also supported for type checking. -```ts +```ts [mount.test-d.ts] import { assertType, expectTypeOf } from 'vitest' import { mount } from './mount.js' @@ -117,7 +117,7 @@ This will pass, because it expects an error, but the word “answer” has a typ ```ts // @ts-expect-error answer is not a string -assertType(answr) // +assertType(answr) ``` ::: @@ -125,7 +125,7 @@ assertType(answr) // To enable typechecking, just add [`--typecheck`](/config/#typecheck) flag to your Vitest command in `package.json`: -```json +```json [package.json] { "scripts": { "test": "vitest --typecheck" diff --git a/docs/guide/ui.md b/docs/guide/ui.md index d3a1881b2c94..156588dea6b0 100644 --- a/docs/guide/ui.md +++ b/docs/guide/ui.md @@ -23,14 +23,14 @@ Then you can visit the Vitest UI at 2.2.0] +```ts [vitest.config.ts 3.0.0] import { defineConfig } from 'vitest/config' export default defineConfig({ @@ -41,7 +41,7 @@ export default defineConfig({ ``` ::: -Vitest will treat every folder in `packages` as a separate project even if it doesn't have a config file inside. Since Vitest 2.1, 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. +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. ::: warning Vitest does not treat the root `vitest.config` file as a workspace project unless it is explicitly specified in the workspace configuration. Consequently, the root configuration will only influence global options such as `reporters` and `coverage`. @@ -55,7 +55,7 @@ export default [ 'packages/*/vitest.config.{e2e,unit}.ts' ] ``` -```ts [vitest.config.ts 2.2.0] +```ts [vitest.config.ts 3.0.0] import { defineConfig } from 'vitest/config' export default defineConfig({ @@ -97,7 +97,7 @@ export default defineWorkspace([ } ]) ``` -```ts [vitest.config.ts 2.2.0] +```ts [vitest.config.ts 3.0.0] import { defineConfig } from 'vitest/config' export default defineConfig({ @@ -140,7 +140,7 @@ If you do not use inline configurations, you can create a small JSON file in you "packages/*" ] ``` -```ts [vitest.config.ts 2.2.0] +```ts [vitest.config.ts 3.0.0] import { defineConfig } from 'vitest/config' export default defineConfig({ @@ -153,8 +153,7 @@ export default defineConfig({ Workspace projects do not support all configuration properties. For better type safety, use the `defineProject` method instead of `defineConfig` within project configuration files: -:::code-group -```ts [packages/a/vitest.config.ts] twoslash +```ts twoslash [packages/a/vitest.config.ts] // @errors: 2769 import { defineProject } from 'vitest/config' @@ -167,13 +166,12 @@ export default defineProject({ } }) ``` -::: ## Running tests To run tests inside the workspace, define a script in your root `package.json`: -```json +```json [package.json] { "scripts": { "test": "vitest" @@ -237,7 +235,6 @@ bun test --project e2e --project unit None of the configuration options are inherited from the root-level config file. You can create a shared config file and merge it with the project config yourself: -::: code-group ```ts [packages/a/vitest.config.ts] import { defineProject, mergeConfig } from 'vitest/config' import configShared from '../vitest.shared.js' @@ -251,7 +248,6 @@ export default mergeConfig( }) ) ``` -::: Additionally, at the `defineWorkspace` level, you can use the `extends` option to inherit from your root-level configuration. All options will be merged. @@ -276,7 +272,7 @@ export default defineWorkspace([ }, ]) ``` -```ts [vitest.config.ts 2.2.0] +```ts [vitest.config.ts 3.0.0] import { defineConfig } from 'vitest/config' import react from '@vitejs/plugin-react' diff --git a/docs/package.json b/docs/package.json index cb26cf872527..9949cae14cdd 100644 --- a/docs/package.json +++ b/docs/package.json @@ -14,24 +14,25 @@ "generate-pwa-icons": "pwa-assets-generator" }, "dependencies": { - "@vueuse/core": "^11.2.0", + "@vueuse/core": "^12.0.0", "vue": "^3.5.12" }, "devDependencies": { "@iconify-json/carbon": "^1.2.4", "@iconify-json/logos": "^1.2.3", - "@shikijs/vitepress-twoslash": "^1.22.2", - "@unocss/reset": "^0.64.0", + "@shikijs/vitepress-twoslash": "^1.24.1", + "@unocss/reset": "^0.65.1", "@vite-pwa/assets-generator": "^0.2.6", "@vite-pwa/vitepress": "^0.5.3", - "@vitejs/plugin-vue": "^5.1.5", + "@vitejs/plugin-vue": "^5.2.1", "https-localhost": "^4.7.1", "tinyglobby": "^0.2.10", - "unocss": "^0.64.0", - "unplugin-vue-components": "^0.27.4", + "unocss": "^0.65.1", + "unplugin-vue-components": "^0.27.5", "vite": "^5.2.8", - "vite-plugin-pwa": "^0.20.5", + "vite-plugin-pwa": "^0.21.1", "vitepress": "^1.5.0", + "vitepress-plugin-group-icons": "^1.3.1", "vitepress-plugin-tabs": "^0.5.0", "workbox-window": "^7.3.0" } diff --git a/docs/public/vital.svg b/docs/public/vital.svg new file mode 100644 index 000000000000..a2ebba7984d9 --- /dev/null +++ b/docs/public/vital.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/lit/package.json b/examples/lit/package.json index a4f5ce975983..ef389c0ec047 100644 --- a/examples/lit/package.json +++ b/examples/lit/package.json @@ -18,7 +18,7 @@ "devDependencies": { "@vitest/browser": "latest", "jsdom": "latest", - "playwright": "^1.48.0", + "playwright": "^1.49.0", "vite": "latest", "vitest": "latest" }, diff --git a/netlify.toml b/netlify.toml index 29eeea9bced9..fd7d62583398 100755 --- a/netlify.toml +++ b/netlify.toml @@ -18,6 +18,11 @@ from = "/vscode" to = "https://marketplace.visualstudio.com/items?itemName=vitest.explorer" status = 302 +[[redirects]] +from = "/config/file" +to = "/config/" +status = 301 + [[headers]] for = "/manifest.webmanifest" diff --git a/package.json b/package.json index 640d22381b8a..7e6efcf3016b 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { "name": "@vitest/monorepo", "type": "module", - "version": "2.2.0-beta.2", + "version": "3.0.0-beta.1", "private": true, - "packageManager": "pnpm@9.12.3", + "packageManager": "pnpm@9.15.0", "description": "Next generation testing framework powered by Vite", "engines": { "node": "^18.0.0 || >=20.0.0" @@ -36,35 +36,35 @@ "test:browser:playwright": "pnpm -C test/browser run test:playwright" }, "devDependencies": { - "@antfu/eslint-config": "^3.8.0", - "@antfu/ni": "^0.23.0", - "@playwright/test": "^1.48.2", + "@antfu/eslint-config": "^3.11.2", + "@antfu/ni": "^0.23.1", + "@playwright/test": "^1.49.0", "@rollup/plugin-commonjs": "^28.0.1", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^15.3.0", - "@types/node": "^22.9.0", + "@types/node": "^22.10.1", "@types/ws": "^8.5.13", "@vitest/browser": "workspace:*", "@vitest/coverage-istanbul": "workspace:*", "@vitest/coverage-v8": "workspace:*", "@vitest/ui": "workspace:*", - "bumpp": "^9.8.1", + "bumpp": "^9.9.0", "changelogithub": "^0.13.11", "esbuild": "^0.24.0", - "eslint": "^9.14.0", - "magic-string": "^0.30.12", + "eslint": "^9.16.0", + "magic-string": "^0.30.14", "pathe": "^1.1.2", "rimraf": "^6.0.1", - "rollup": "^4.25.0", + "rollup": "^4.28.1", "rollup-plugin-dts": "^6.1.1", "rollup-plugin-esbuild": "^6.1.1", "rollup-plugin-license": "^3.5.3", "tinyglobby": "^0.2.10", "tsx": "^4.19.2", - "typescript": "^5.6.3", + "typescript": "^5.7.2", "vite": "^5.4.0", "vitest": "workspace:*", - "zx": "^8.2.2" + "zx": "^8.2.4" }, "pnpm": { "overrides": { diff --git a/packages/browser/dummy.js b/packages/browser/dummy.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/browser/matchers.d.ts b/packages/browser/matchers.d.ts index bef6ff300771..bc91a219057b 100644 --- a/packages/browser/matchers.d.ts +++ b/packages/browser/matchers.d.ts @@ -1,9 +1,10 @@ import type { Locator } from '@vitest/browser/context' import type jsdomMatchers from './jest-dom.js' -import type { Assertion } from 'vitest' +import type { Assertion, ExpectPollOptions } from 'vitest' declare module 'vitest' { interface JestAssertion extends jsdomMatchers.default.TestingLibraryMatchers {} + interface AsymmetricMatchersContaining extends jsdomMatchers.default.TestingLibraryMatchers {} type Promisify = { [K in keyof O]: O[K] extends (...args: infer A) => infer R @@ -16,6 +17,10 @@ declare module 'vitest' { type PromisifyDomAssertion = Promisify> interface ExpectStatic { + /** + * `expect.element(locator)` is a shorthand for `expect.poll(() => locator.element())`. + * You can set default timeout via `expect.poll.timeout` config. + */ element: (element: T, options?: ExpectPollOptions) => PromisifyDomAssertion> } } diff --git a/packages/browser/package.json b/packages/browser/package.json index 12f17f5ab677..105924d0e077 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -1,7 +1,7 @@ { "name": "@vitest/browser", "type": "module", - "version": "2.2.0-beta.2", + "version": "3.0.0-beta.1", "description": "Browser running for Vitest", "license": "MIT", "funding": "https://opencollective.com/vitest", @@ -32,13 +32,16 @@ "default": "./dist/client.js" }, "./matchers": { - "types": "./matchers.d.ts" + "types": "./matchers.d.ts", + "default": "./dummy.js" }, "./providers/webdriverio": { - "types": "./providers/webdriverio.d.ts" + "types": "./providers/webdriverio.d.ts", + "default": "./dummy.js" }, "./providers/playwright": { - "types": "./providers/playwright.d.ts" + "types": "./providers/playwright.d.ts", + "default": "./dummy.js" }, "./locator": { "types": "./dist/locators/index.d.ts", @@ -88,8 +91,8 @@ "@testing-library/user-event": "^14.5.2", "@vitest/mocker": "workspace:*", "@vitest/utils": "workspace:*", - "magic-string": "^0.30.12", - "msw": "^2.6.4", + "magic-string": "^0.30.14", + "msw": "^2.6.8", "sirv": "^3.0.0", "tinyrainbow": "^1.2.0", "ws": "^8.18.0" @@ -102,13 +105,13 @@ "@vitest/ws-client": "workspace:*", "@wdio/protocols": "^8.40.3", "birpc": "0.2.19", - "flatted": "^3.3.1", + "flatted": "^3.3.2", "ivya": "^1.1.1", "mime": "^4.0.4", "pathe": "^1.1.2", "periscopic": "^4.0.2", - "playwright": "^1.48.2", - "playwright-core": "^1.48.2", + "playwright": "^1.49.0", + "playwright-core": "^1.49.0", "safaridriver": "^1.0.0", "vitest": "workspace:*", "webdriverio": "^8.40.6" diff --git a/packages/browser/providers/playwright.d.ts b/packages/browser/providers/playwright.d.ts index f98cee599724..53f198cdde86 100644 --- a/packages/browser/providers/playwright.d.ts +++ b/packages/browser/providers/playwright.d.ts @@ -16,7 +16,13 @@ declare module 'vitest/node' { context?: Omit< BrowserContextOptions, 'ignoreHTTPSErrors' | 'serviceWorkers' - > + > & { + /** + * The maximum time in milliseconds to wait for `userEvent` action to complete. + * @default 0 (no timeout) + */ + actionTimeout?: number + } } export interface BrowserCommandContext { @@ -27,13 +33,13 @@ declare module 'vitest/node' { } } -type PWHoverOptions = Parameters[1] -type PWClickOptions = Parameters[1] -type PWDoubleClickOptions = Parameters[1] -type PWFillOptions = Parameters[2] -type PWScreenshotOptions = Parameters[0] -type PWSelectOptions = Parameters[2] -type PWDragAndDropOptions = Parameters[2] +type PWHoverOptions = NonNullable[1]> +type PWClickOptions = NonNullable[1]> +type PWDoubleClickOptions = NonNullable[1]> +type PWFillOptions = NonNullable[2]> +type PWScreenshotOptions = NonNullable[0]> +type PWSelectOptions = NonNullable[2]> +type PWDragAndDropOptions = NonNullable[2]> declare module '@vitest/browser/context' { export interface UserEventHoverOptions extends PWHoverOptions {} diff --git a/packages/browser/src/client/tester/context.ts b/packages/browser/src/client/tester/context.ts index d648f50aa7ea..433b5b1220c1 100644 --- a/packages/browser/src/client/tester/context.ts +++ b/packages/browser/src/client/tester/context.ts @@ -30,21 +30,20 @@ function triggerCommand(command: string, ...args: any[]) { } export function createUserEvent(__tl_user_event_base__?: TestingLibraryUserEvent, options?: TestingLibraryOptions): UserEvent { - let __tl_user_event__ = __tl_user_event_base__?.setup(options ?? {}) + if (__tl_user_event_base__) { + return createPreviewUserEvent(__tl_user_event_base__, options ?? {}) + } + const keyboard = { unreleased: [] as string[], } return { - setup(options?: any) { - return createUserEvent(__tl_user_event_base__, options) + setup() { + return createUserEvent() }, async cleanup() { return ensureAwaited(async () => { - if (typeof __tl_user_event_base__ !== 'undefined') { - __tl_user_event__ = __tl_user_event_base__?.setup(options ?? {}) - return - } await triggerCommand('__vitest_cleanup', keyboard) keyboard.unreleased = [] }) @@ -87,14 +86,6 @@ export function createUserEvent(__tl_user_event_base__?: TestingLibraryUserEvent // testing-library user-event async type(element: Element | Locator, text: string, options: UserEventTypeOptions = {}) { return ensureAwaited(async () => { - if (typeof __tl_user_event__ !== 'undefined') { - return __tl_user_event__.type( - element instanceof Element ? element : element.element(), - text, - options, - ) - } - const selector = convertToSelector(element) const { unreleased } = await triggerCommand<{ unreleased: string[] }>( '__vitest_type', @@ -107,17 +98,11 @@ export function createUserEvent(__tl_user_event_base__?: TestingLibraryUserEvent }, tab(options: UserEventTabOptions = {}) { return ensureAwaited(() => { - if (typeof __tl_user_event__ !== 'undefined') { - return __tl_user_event__.tab(options) - } return triggerCommand('__vitest_tab', options) }) }, async keyboard(text: string) { return ensureAwaited(async () => { - if (typeof __tl_user_event__ !== 'undefined') { - return __tl_user_event__.keyboard(text) - } const { unreleased } = await triggerCommand<{ unreleased: string[] }>( '__vitest_keyboard', text, @@ -129,6 +114,101 @@ export function createUserEvent(__tl_user_event_base__?: TestingLibraryUserEvent } } +function createPreviewUserEvent(userEventBase: TestingLibraryUserEvent, options: TestingLibraryOptions): UserEvent { + let userEvent = userEventBase.setup(options) + + function toElement(element: Element | Locator) { + return element instanceof Element ? element : element.element() + } + + const vitestUserEvent: UserEvent = { + setup(options?: any) { + return createPreviewUserEvent(userEventBase, options) + }, + async cleanup() { + userEvent = userEventBase.setup(options ?? {}) + }, + async click(element) { + await userEvent.click(toElement(element)) + }, + async dblClick(element) { + await userEvent.dblClick(toElement(element)) + }, + async tripleClick(element) { + await userEvent.tripleClick(toElement(element)) + }, + async selectOptions(element, value) { + const options = (Array.isArray(value) ? value : [value]).map((option) => { + if (typeof option !== 'string') { + return toElement(option) + } + return option + }) + await userEvent.selectOptions( + element, + options as string[] | HTMLElement[], + ) + }, + async clear(element) { + await userEvent.clear(toElement(element)) + }, + async hover(element: Element | Locator) { + await userEvent.hover(toElement(element)) + }, + async unhover(element: Element | Locator) { + await userEvent.unhover(toElement(element)) + }, + async upload(element: Element | Locator, files: string | string[] | File | File[]) { + const uploadPromise = (Array.isArray(files) ? files : [files]).map(async (file) => { + if (typeof file !== 'string') { + return file + } + + const { content: base64, basename, mime } = await triggerCommand<{ + content: string + basename: string + mime: string + }>('__vitest_fileInfo', file, 'base64') + + const fileInstance = fetch(`data:${mime};base64,${base64}`) + .then(r => r.blob()) + .then(blob => new File([blob], basename, { type: mime })) + return fileInstance + }) + const uploadFiles = await Promise.all(uploadPromise) + return userEvent.upload(toElement(element) as HTMLElement, uploadFiles) + }, + + async fill(element: Element | Locator, text: string) { + await userEvent.clear(toElement(element)) + return userEvent.type(toElement(element), text) + }, + async dragAndDrop() { + throw new Error(`The "preview" provider doesn't support 'userEvent.dragAndDrop'`) + }, + + async type(element: Element | Locator, text: string, options: UserEventTypeOptions = {}) { + await userEvent.type(toElement(element), text, options) + }, + async tab(options: UserEventTabOptions = {}) { + await userEvent.tab(options) + }, + async keyboard(text: string) { + await userEvent.keyboard(text) + }, + } + + for (const [name, fn] of Object.entries(vitestUserEvent)) { + if (name !== 'setup') { + (vitestUserEvent as any)[name] = function (this: any, ...args: any[]) { + return ensureAwaited(() => fn.apply(this, args)) + } + } + } + + return vitestUserEvent +} + export function cdp() { return getBrowserState().cdp! } diff --git a/packages/browser/src/client/tester/locators/preview.ts b/packages/browser/src/client/tester/locators/preview.ts index 0e966a8557c7..5881100fd315 100644 --- a/packages/browser/src/client/tester/locators/preview.ts +++ b/packages/browser/src/client/tester/locators/preview.ts @@ -1,5 +1,4 @@ -import { userEvent } from '@testing-library/user-event' -import { page, server } from '@vitest/browser/context' +import { page, server, userEvent } from '@vitest/browser/context' import { getByAltTextSelector, getByLabelSelector, @@ -9,7 +8,7 @@ import { getByTextSelector, getByTitleSelector, } from 'ivya' -import { convertElementToCssSelector, ensureAwaited } from '../../utils' +import { convertElementToCssSelector } from '../../utils' import { getElementError } from '../public-utils' import { Locator, selectorEngine } from './index' @@ -58,71 +57,39 @@ class PreviewLocator extends Locator { } click(): Promise { - return ensureAwaited(() => userEvent.click(this.element())) + return userEvent.click(this.element()) } dblClick(): Promise { - return ensureAwaited(() => userEvent.dblClick(this.element())) + return userEvent.dblClick(this.element()) } tripleClick(): Promise { - return ensureAwaited(() => userEvent.tripleClick(this.element())) + return userEvent.tripleClick(this.element()) } hover(): Promise { - return ensureAwaited(() => userEvent.hover(this.element())) + return userEvent.hover(this.element()) } unhover(): Promise { - return ensureAwaited(() => userEvent.unhover(this.element())) + return userEvent.unhover(this.element()) } async fill(text: string): Promise { - await this.clear() - return ensureAwaited(() => userEvent.type(this.element(), text)) + return userEvent.fill(this.element(), text) } async upload(file: string | string[] | File | File[]): Promise { - const uploadPromise = (Array.isArray(file) ? file : [file]).map(async (file) => { - if (typeof file !== 'string') { - return file - } - - const { content: base64, basename, mime } = await this.triggerCommand<{ - content: string - basename: string - mime: string - }>('__vitest_fileInfo', file, 'base64') - - const fileInstance = fetch(`data:${mime};base64,${base64}`) - .then(r => r.blob()) - .then(blob => new File([blob], basename, { type: mime })) - return fileInstance - }) - const uploadFiles = await Promise.all(uploadPromise) - return ensureAwaited(() => userEvent.upload(this.element() as HTMLElement, uploadFiles)) + return userEvent.upload(this.element(), file) } selectOptions(options_: string | string[] | HTMLElement | HTMLElement[] | Locator | Locator[]): Promise { - const options = (Array.isArray(options_) ? options_ : [options_]).map((option) => { - if (typeof option !== 'string' && 'element' in option) { - return option.element() as HTMLElement - } - return option - }) - return ensureAwaited(() => userEvent.selectOptions(this.element(), options as string[] | HTMLElement[])) - } - - async dropTo(): Promise { - throw new Error('The "preview" provider doesn\'t support `dropTo` method.') + return userEvent.selectOptions(this.element(), options_) } clear(): Promise { - return ensureAwaited(() => userEvent.clear(this.element())) - } - - async screenshot(): Promise { - throw new Error('The "preview" provider doesn\'t support `screenshot` method.') + return userEvent.clear(this.element()) } protected locator(selector: string) { diff --git a/packages/browser/src/client/tester/tester.ts b/packages/browser/src/client/tester/tester.ts index aa86e0f31cb9..424e22e274b7 100644 --- a/packages/browser/src/client/tester/tester.ts +++ b/packages/browser/src/client/tester/tester.ts @@ -122,7 +122,7 @@ async function executeTests(method: 'run' | 'collect', files: string[]) { try { await Promise.all([ setupCommonEnv(config), - startCoverageInsideWorker(config.coverage, executor), + startCoverageInsideWorker(config.coverage, executor, { isolate: config.browser.isolate }), (async () => { const VitestIndex = await import('vitest') Object.defineProperty(window, '__vitest_index__', { @@ -160,7 +160,7 @@ async function executeTests(method: 'run' | 'collect', files: string[]) { }, 'Cleanup Error') } state.environmentTeardownRun = true - await stopCoverageInsideWorker(config.coverage, executor).catch((error) => { + await stopCoverageInsideWorker(config.coverage, executor, { isolate: config.browser.isolate }).catch((error) => { client.rpc.onUnhandledError({ name: error.name, message: error.message, diff --git a/packages/browser/src/node/commands/clear.ts b/packages/browser/src/node/commands/clear.ts index a55d7c4eea8c..9de4737eeae7 100644 --- a/packages/browser/src/node/commands/clear.ts +++ b/packages/browser/src/node/commands/clear.ts @@ -10,9 +10,7 @@ export const clear: UserEventCommand = async ( if (context.provider instanceof PlaywrightBrowserProvider) { const { iframe } = context const element = iframe.locator(selector) - await element.clear({ - timeout: 1000, - }) + await element.clear() } else if (context.provider instanceof WebdriverBrowserProvider) { const browser = context.browser diff --git a/packages/browser/src/node/commands/click.ts b/packages/browser/src/node/commands/click.ts index d3d24bd179b0..075f24adc059 100644 --- a/packages/browser/src/node/commands/click.ts +++ b/packages/browser/src/node/commands/click.ts @@ -11,10 +11,7 @@ export const click: UserEventCommand = async ( const provider = context.provider if (provider instanceof PlaywrightBrowserProvider) { const tester = context.iframe - await tester.locator(selector).click({ - timeout: 1000, - ...options, - }) + await tester.locator(selector).click(options) } else if (provider instanceof WebdriverBrowserProvider) { const browser = context.browser @@ -53,7 +50,6 @@ export const tripleClick: UserEventCommand = async ( if (provider instanceof PlaywrightBrowserProvider) { const tester = context.iframe await tester.locator(selector).click({ - timeout: 1000, ...options, clickCount: 3, }) diff --git a/packages/browser/src/node/commands/dragAndDrop.ts b/packages/browser/src/node/commands/dragAndDrop.ts index d961910adcd1..6fc66bbb54f3 100644 --- a/packages/browser/src/node/commands/dragAndDrop.ts +++ b/packages/browser/src/node/commands/dragAndDrop.ts @@ -14,10 +14,7 @@ export const dragAndDrop: UserEventCommand = async ( await frame.dragAndDrop( source, target, - { - timeout: 1000, - ...options_, - }, + options_, ) } else if (context.provider instanceof WebdriverBrowserProvider) { diff --git a/packages/browser/src/node/commands/fill.ts b/packages/browser/src/node/commands/fill.ts index db2b68b4b45d..3010a5ffbb47 100644 --- a/packages/browser/src/node/commands/fill.ts +++ b/packages/browser/src/node/commands/fill.ts @@ -12,7 +12,7 @@ export const fill: UserEventCommand = async ( if (context.provider instanceof PlaywrightBrowserProvider) { const { iframe } = context const element = iframe.locator(selector) - await element.fill(text, { timeout: 1000, ...options }) + await element.fill(text, options) } else if (context.provider instanceof WebdriverBrowserProvider) { const browser = context.browser diff --git a/packages/browser/src/node/commands/hover.ts b/packages/browser/src/node/commands/hover.ts index 392b7e3e6f85..9d90d21df604 100644 --- a/packages/browser/src/node/commands/hover.ts +++ b/packages/browser/src/node/commands/hover.ts @@ -9,14 +9,11 @@ export const hover: UserEventCommand = async ( options = {}, ) => { if (context.provider instanceof PlaywrightBrowserProvider) { - await context.iframe.locator(selector).hover({ - timeout: 1000, - ...options, - }) + await context.iframe.locator(selector).hover(options) } else if (context.provider instanceof WebdriverBrowserProvider) { const browser = context.browser - await browser.$(selector).moveTo(options) + await browser.$(selector).moveTo(options as any) } else { throw new TypeError(`Provider "${context.provider.name}" does not support hover`) diff --git a/packages/browser/src/node/commands/screenshot.ts b/packages/browser/src/node/commands/screenshot.ts index f3685d0ab5bf..2f739e2efc27 100644 --- a/packages/browser/src/node/commands/screenshot.ts +++ b/packages/browser/src/node/commands/screenshot.ts @@ -30,7 +30,6 @@ export const screenshot: BrowserCommand<[string, ScreenshotOptions]> = async ( const { element: selector, ...config } = options const element = context.iframe.locator(`${selector}`) const buffer = await element.screenshot({ - timeout: 1000, ...config, path: savePath, }) diff --git a/packages/browser/src/node/commands/select.ts b/packages/browser/src/node/commands/select.ts index 92121f685872..22da023a1781 100644 --- a/packages/browser/src/node/commands/select.ts +++ b/packages/browser/src/node/commands/select.ts @@ -26,10 +26,7 @@ export const selectOptions: UserEventCommand = async return elementHandler })) as (readonly string[]) | (readonly ElementHandle[]) - await selectElement.selectOption(values, { - timeout: 1000, - ...options, - }) + await selectElement.selectOption(values, options) } else if (context.provider instanceof WebdriverBrowserProvider) { const values = userValues as any as [({ index: number })] diff --git a/packages/browser/src/node/providers/playwright.ts b/packages/browser/src/node/providers/playwright.ts index fa9bf02b678f..f01e3cf37f3e 100644 --- a/packages/browser/src/node/providers/playwright.ts +++ b/packages/browser/src/node/providers/playwright.ts @@ -31,7 +31,7 @@ export class PlaywrightBrowserProvider implements BrowserProvider { private options?: { launch?: LaunchOptions - context?: BrowserContextOptions + context?: BrowserContextOptions & { actionTimeout?: number } } public contexts = new Map() @@ -108,8 +108,9 @@ export class PlaywrightBrowserProvider implements BrowserProvider { } const browser = await this.openBrowser() + const { actionTimeout, ...contextOptions } = this.options?.context ?? {} const options = { - ...this.options?.context, + ...contextOptions, ignoreHTTPSErrors: true, serviceWorkers: 'allow', } satisfies BrowserContextOptions @@ -117,6 +118,9 @@ export class PlaywrightBrowserProvider implements BrowserProvider { options.viewport = null } const context = await browser.newContext(options) + if (actionTimeout) { + context.setDefaultTimeout(actionTimeout) + } this.contexts.set(contextId, context) return context } @@ -187,7 +191,7 @@ export class PlaywrightBrowserProvider implements BrowserProvider { async openPage(contextId: string, url: string, beforeNavigate?: () => Promise) { const browserPage = await this.openBrowserPage(contextId) await beforeNavigate?.() - await browserPage.goto(url) + await browserPage.goto(url, { timeout: 0 }) } async getCDPSession(contextId: string) { diff --git a/packages/coverage-istanbul/package.json b/packages/coverage-istanbul/package.json index 91c991e72474..d1e47f770078 100644 --- a/packages/coverage-istanbul/package.json +++ b/packages/coverage-istanbul/package.json @@ -1,7 +1,7 @@ { "name": "@vitest/coverage-istanbul", "type": "module", - "version": "2.2.0-beta.2", + "version": "3.0.0-beta.1", "description": "Istanbul coverage provider for Vitest", "author": "Anthony Fu ", "license": "MIT", @@ -45,7 +45,7 @@ }, "dependencies": { "@istanbuljs/schema": "^0.1.3", - "debug": "^4.3.7", + "debug": "^4.4.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-instrument": "^6.0.3", "istanbul-lib-report": "^3.0.1", diff --git a/packages/coverage-istanbul/src/index.ts b/packages/coverage-istanbul/src/index.ts index 692885b66ffd..1a317c8c908d 100644 --- a/packages/coverage-istanbul/src/index.ts +++ b/packages/coverage-istanbul/src/index.ts @@ -1,33 +1,49 @@ +import type { CoverageMapData } from 'istanbul-lib-coverage' +import type { CoverageProviderModule } from 'vitest/node' import type { IstanbulCoverageProvider } from './provider' import { COVERAGE_STORE_KEY } from './constants' -export async function getProvider(): Promise { - // to not bundle the provider - const providerPath = './provider.js' - const { IstanbulCoverageProvider } = (await import( - /* @vite-ignore */ - providerPath - )) as typeof import('./provider') - return new IstanbulCoverageProvider() -} - -export function takeCoverage(): any { - // @ts-expect-error -- untyped global - const coverage = globalThis[COVERAGE_STORE_KEY] +export default { + takeCoverage() { + // @ts-expect-error -- untyped global + return globalThis[COVERAGE_STORE_KEY] + }, // Reset coverage map to prevent duplicate results if this is called twice in row - // @ts-expect-error -- untyped global - globalThis[COVERAGE_STORE_KEY] = {} + startCoverage() { + // @ts-expect-error -- untyped global + const coverageMap = globalThis[COVERAGE_STORE_KEY] as CoverageMapData + + // When isolated, there are no previous results + if (!coverageMap) { + return + } + + for (const filename in coverageMap) { + const branches = coverageMap[filename].b + + for (const key in branches) { + branches[key] = branches[key].map(() => 0) + } + + for (const metric of ['f', 's'] as const) { + const entry = coverageMap[filename][metric] - return coverage -} + for (const key in entry) { + entry[key] = 0 + } + } + } + }, -const _default: { - getProvider: () => Promise - takeCoverage: () => any -} = { - getProvider, - takeCoverage, -} + async getProvider(): Promise { + // to not bundle the provider + const providerPath = './provider.js' + const { IstanbulCoverageProvider } = (await import( + /* @vite-ignore */ + providerPath + )) as typeof import('./provider') -export default _default + return new IstanbulCoverageProvider() + }, +} satisfies CoverageProviderModule diff --git a/packages/coverage-istanbul/src/provider.ts b/packages/coverage-istanbul/src/provider.ts index 77d0f41847d9..cc85441919f7 100644 --- a/packages/coverage-istanbul/src/provider.ts +++ b/packages/coverage-istanbul/src/provider.ts @@ -1,5 +1,7 @@ import type { CoverageProvider, ReportContext, ResolvedCoverageOptions, Vitest } from 'vitest/node' import { promises as fs } from 'node:fs' +// @ts-expect-error missing types +import { defaults as istanbulDefaults } from '@istanbuljs/schema' import createDebug from 'debug' import libCoverage, { type CoverageMap } from 'istanbul-lib-coverage' import { createInstrumenter, type Instrumenter } from 'istanbul-lib-instrument' @@ -11,8 +13,6 @@ import { resolve } from 'pathe' import TestExclude from 'test-exclude' import c from 'tinyrainbow' import { BaseCoverageProvider } from 'vitest/coverage' -// @ts-expect-error missing types -import { defaults as istanbulDefaults } from '@istanbuljs/schema' import { version } from '../package.json' with { type: 'json' } import { COVERAGE_STORE_KEY } from './constants' diff --git a/packages/coverage-v8/package.json b/packages/coverage-v8/package.json index 4262f4707d38..f2369d3191cc 100644 --- a/packages/coverage-v8/package.json +++ b/packages/coverage-v8/package.json @@ -1,7 +1,7 @@ { "name": "@vitest/coverage-v8", "type": "module", - "version": "2.2.0-beta.2", + "version": "3.0.0-beta.1", "description": "V8 coverage provider for Vitest", "author": "Anthony Fu ", "license": "MIT", @@ -56,12 +56,12 @@ "dependencies": { "@ampproject/remapping": "^2.3.0", "@bcoe/v8-coverage": "^0.2.3", - "debug": "^4.3.7", + "debug": "^4.4.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-lib-source-maps": "^5.0.6", "istanbul-reports": "^3.1.7", - "magic-string": "^0.30.12", + "magic-string": "^0.30.14", "magicast": "^0.3.5", "std-env": "^3.8.0", "test-exclude": "^7.0.1", diff --git a/packages/coverage-v8/src/browser.ts b/packages/coverage-v8/src/browser.ts index 3f3940892129..1891c7df296f 100644 --- a/packages/coverage-v8/src/browser.ts +++ b/packages/coverage-v8/src/browser.ts @@ -1,13 +1,21 @@ +import type { CoverageProviderModule } from 'vitest/node' import type { V8CoverageProvider } from './provider' import { cdp } from '@vitest/browser/context' import { loadProvider } from './load-provider' const session = cdp() +let enabled = false type ScriptCoverage = Awaited>> export default { async startCoverage() { + if (enabled) { + return + } + + enabled = true + await session.send('Profiler.enable') await session.send('Profiler.startPreciseCoverage', { callCount: true, @@ -32,15 +40,14 @@ export default { return { result } }, - async stopCoverage() { - await session.send('Profiler.stopPreciseCoverage') - await session.send('Profiler.disable') + stopCoverage() { + // Browser mode should not stop coverage as same V8 instance is shared between tests }, async getProvider(): Promise { return loadProvider() }, -} +} satisfies CoverageProviderModule function filterResult(coverage: ScriptCoverage['result'][number]): boolean { if (!coverage.url.startsWith(window.location.origin)) { diff --git a/packages/coverage-v8/src/index.ts b/packages/coverage-v8/src/index.ts index 4001d627c81a..9497648ea65b 100644 --- a/packages/coverage-v8/src/index.ts +++ b/packages/coverage-v8/src/index.ts @@ -1,12 +1,20 @@ +import type { CoverageProviderModule } from 'vitest/node' import type { V8CoverageProvider } from './provider' import inspector, { type Profiler } from 'node:inspector' import { provider } from 'std-env' import { loadProvider } from './load-provider' const session = new inspector.Session() +let enabled = false export default { - startCoverage(): void { + startCoverage({ isolate }) { + if (isolate === false && enabled) { + return + } + + enabled = true + session.connect() session.post('Profiler.enable') session.post('Profiler.startPreciseCoverage', { @@ -34,7 +42,11 @@ export default { }) }, - stopCoverage(): void { + stopCoverage({ isolate }) { + if (isolate === false) { + return + } + session.post('Profiler.stopPreciseCoverage') session.post('Profiler.disable') session.disconnect() @@ -43,7 +55,7 @@ export default { async getProvider(): Promise { return loadProvider() }, -} +} satisfies CoverageProviderModule function filterResult(coverage: Profiler.ScriptCoverage): boolean { if (!coverage.url.startsWith('file://')) { diff --git a/packages/expect/package.json b/packages/expect/package.json index 09f9ed9ee872..64c915906cc8 100644 --- a/packages/expect/package.json +++ b/packages/expect/package.json @@ -1,7 +1,7 @@ { "name": "@vitest/expect", "type": "module", - "version": "2.2.0-beta.2", + "version": "3.0.0-beta.1", "description": "Jest's expect matchers as a Chai plugin", "license": "MIT", "funding": "https://opencollective.com/vitest", diff --git a/packages/expect/src/custom-matchers.ts b/packages/expect/src/custom-matchers.ts new file mode 100644 index 000000000000..06f7e5b5af2b --- /dev/null +++ b/packages/expect/src/custom-matchers.ts @@ -0,0 +1,29 @@ +import type { MatchersObject } from './types' + +// selectively ported from https://github.com/jest-community/jest-extended +export const customMatchers: MatchersObject = { + toSatisfy(actual: unknown, expected: (actual: unknown) => boolean, message?: string) { + const { printReceived, printExpected, matcherHint } = this.utils + const pass = expected(actual) + return { + pass, + message: () => + pass + ? `\ +${matcherHint('.not.toSatisfy', 'received', '')} + +Expected value to not satisfy: +${message || printExpected(expected)} +Received: +${printReceived(actual)}` + : `\ +${matcherHint('.toSatisfy', 'received', '')} + +Expected value to satisfy: +${message || printExpected(expected)} + +Received: +${printReceived(actual)}`, + } + }, +} diff --git a/packages/expect/src/index.ts b/packages/expect/src/index.ts index 448a348eaef5..76b87ad04091 100644 --- a/packages/expect/src/index.ts +++ b/packages/expect/src/index.ts @@ -1,4 +1,5 @@ export * from './constants' +export { customMatchers } from './custom-matchers' export * from './jest-asymmetric-matchers' export { JestChaiExpect } from './jest-expect' export { JestExtend } from './jest-extend' diff --git a/packages/expect/src/jest-expect.ts b/packages/expect/src/jest-expect.ts index c15f277edd9e..223b151312d5 100644 --- a/packages/expect/src/jest-expect.ts +++ b/packages/expect/src/jest-expect.ts @@ -1029,9 +1029,6 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { ) }) }) - def('toSatisfy', function (matcher: Function, message?: string) { - return this.be.satisfy(matcher, message) - }) // @ts-expect-error @internal def('withContext', function (this: any, context: Record) { diff --git a/packages/expect/src/types.ts b/packages/expect/src/types.ts index 5e1a76a897bd..a26a448b242e 100644 --- a/packages/expect/src/types.ts +++ b/packages/expect/src/types.ts @@ -106,7 +106,21 @@ export interface ExpectStatic not: AsymmetricMatchersContaining } -export interface AsymmetricMatchersContaining { +interface CustomMatcher { + /** + * Checks that a value satisfies a custom matcher function. + * + * @param matcher - A function returning a boolean based on the custom condition + * @param message - Optional custom error message on failure + * + * @example + * expect(age).toSatisfy(val => val >= 18, 'Age must be at least 18'); + * expect(age).toEqual(expect.toSatisfy(val => val >= 18, 'Age must be at least 18')); + */ + toSatisfy: (matcher: (value: any) => boolean, message?: string) => any +} + +export interface AsymmetricMatchersContaining extends CustomMatcher { /** * Matches if the received string contains the expected substring. * @@ -153,7 +167,7 @@ export interface AsymmetricMatchersContaining { closeTo: (expected: number, precision?: number) => any } -export interface JestAssertion extends jest.Matchers { +export interface JestAssertion extends jest.Matchers, CustomMatcher { /** * Used when you want to check that two objects have the same value. * This matcher recursively checks the equality of all fields, rather than checking for object identity. @@ -645,17 +659,6 @@ export interface Assertion */ toHaveBeenCalledExactlyOnceWith: (...args: E) => void - /** - * Checks that a value satisfies a custom matcher function. - * - * @param matcher - A function returning a boolean based on the custom condition - * @param message - Optional custom error message on failure - * - * @example - * expect(age).toSatisfy(val => val >= 18, 'Age must be at least 18'); - */ - toSatisfy: (matcher: (value: E) => boolean, message?: string) => void - /** * This assertion checks if a `Mock` was called before another `Mock`. * @param mock - A mock function created by `vi.spyOn` or `vi.fn` diff --git a/packages/expect/src/utils.ts b/packages/expect/src/utils.ts index 9d5c44be9173..7ff4966985ff 100644 --- a/packages/expect/src/utils.ts +++ b/packages/expect/src/utils.ts @@ -56,9 +56,9 @@ export function recordAsyncExpect( }) return { - then(onFullfilled, onRejected) { + then(onFulfilled, onRejected) { resolved = true - return promise.then(onFullfilled, onRejected) + return promise.then(onFulfilled, onRejected) }, catch(onRejected) { return promise.catch(onRejected) diff --git a/packages/mocker/package.json b/packages/mocker/package.json index 594f37904efe..29f65d9fd196 100644 --- a/packages/mocker/package.json +++ b/packages/mocker/package.json @@ -1,7 +1,7 @@ { "name": "@vitest/mocker", "type": "module", - "version": "2.2.0-beta.2", + "version": "3.0.0-beta.1", "description": "Vitest module mocker implementation", "license": "MIT", "funding": "https://opencollective.com/vitest", @@ -68,14 +68,14 @@ "dependencies": { "@vitest/spy": "workspace:*", "estree-walker": "^3.0.3", - "magic-string": "^0.30.12" + "magic-string": "^0.30.14" }, "devDependencies": { "@types/estree": "^1.0.6", "@vitest/spy": "workspace:*", "@vitest/utils": "workspace:*", "acorn-walk": "^8.3.4", - "msw": "^2.6.4", + "msw": "^2.6.8", "pathe": "^1.1.2", "vite": "^5.4.0" } diff --git a/packages/mocker/src/automocker.ts b/packages/mocker/src/automocker.ts index e6b193d96899..256dbb952310 100644 --- a/packages/mocker/src/automocker.ts +++ b/packages/mocker/src/automocker.ts @@ -75,7 +75,7 @@ export function mockObject( const isFunction = type.includes('Function') && typeof value === 'function' if ( - (!isFunction || value.__isMockFunction) + (!isFunction || value._isMockFunction) && type !== 'Object' && type !== 'Module' ) { @@ -120,8 +120,9 @@ export function mockObject( const original = this[key] const mock = spyOn(this, key as string) .mockImplementation(original) - mock.mockRestore = () => { - mock.mockReset() + const origMockReset = mock.mockReset + mock.mockRestore = mock.mockReset = () => { + origMockReset.call(mock) mock.mockImplementation(original) return mock } @@ -132,8 +133,9 @@ export function mockObject( const mock = spyOn(newContainer, property) if (options.type === 'automock') { mock.mockImplementation(mockFunction) - mock.mockRestore = () => { - mock.mockReset() + const origMockReset = mock.mockReset + mock.mockRestore = mock.mockReset = () => { + origMockReset.call(mock) mock.mockImplementation(mockFunction) return mock } diff --git a/packages/mocker/src/node/esmWalker.ts b/packages/mocker/src/node/esmWalker.ts index 42b8c6eb04c3..65b8f0cb9e59 100644 --- a/packages/mocker/src/node/esmWalker.ts +++ b/packages/mocker/src/node/esmWalker.ts @@ -195,7 +195,7 @@ export function esmWalker( if ( (parent?.type === 'TemplateLiteral' && parent?.expressions.includes(child)) - || (parent?.type === 'CallExpression' && parent?.callee === child) + || (parent?.type === 'CallExpression' && parent?.callee === child) ) { return } @@ -253,7 +253,7 @@ export function esmWalker( const classDeclaration = (parent.type === 'PropertyDefinition' && grandparent?.type === 'ClassBody') - || (parent.type === 'ClassDeclaration' && node === parent.superClass) + || (parent.type === 'ClassDeclaration' && node === parent.superClass) const classExpression = parent.type === 'ClassExpression' && node === parent.id @@ -277,7 +277,7 @@ function isRefIdentifier(id: Identifier, parent: _Node, parentStack: _Node[]) { parent.type === 'CatchClause' || ((parent.type === 'VariableDeclarator' || parent.type === 'ClassDeclaration') - && parent.id === id) + && parent.id === id) ) { return false } diff --git a/packages/pretty-format/package.json b/packages/pretty-format/package.json index 40f4dbe13965..95d51b967447 100644 --- a/packages/pretty-format/package.json +++ b/packages/pretty-format/package.json @@ -1,7 +1,7 @@ { "name": "@vitest/pretty-format", "type": "module", - "version": "2.2.0-beta.2", + "version": "3.0.0-beta.1", "description": "Fork of pretty-format with support for ESM", "license": "MIT", "funding": "https://opencollective.com/vitest", @@ -37,8 +37,8 @@ "tinyrainbow": "^1.2.0" }, "devDependencies": { - "@types/react-is": "^18.3.0", - "react-is": "^18.3.1", - "react-is-19": "npm:react-is@19.0.0-rc-b01722d5-20241114" + "@types/react-is": "^19.0.0", + "react-is": "^19.0.0", + "react-is-18": "npm:react-is@18.3.1" } } diff --git a/packages/pretty-format/src/plugins/DOMElement.ts b/packages/pretty-format/src/plugins/DOMElement.ts index 123e4040597e..791ec93255a6 100644 --- a/packages/pretty-format/src/plugins/DOMElement.ts +++ b/packages/pretty-format/src/plugins/DOMElement.ts @@ -41,9 +41,9 @@ function testNode(val: any) { return ( (nodeType === ELEMENT_NODE && (ELEMENT_REGEXP.test(constructorName) || isCustomElement)) - || (nodeType === TEXT_NODE && constructorName === 'Text') - || (nodeType === COMMENT_NODE && constructorName === 'Comment') - || (nodeType === FRAGMENT_NODE && constructorName === 'DocumentFragment') + || (nodeType === TEXT_NODE && constructorName === 'Text') + || (nodeType === COMMENT_NODE && constructorName === 'Comment') + || (nodeType === FRAGMENT_NODE && constructorName === 'DocumentFragment') ) } diff --git a/packages/pretty-format/src/plugins/ReactElement.ts b/packages/pretty-format/src/plugins/ReactElement.ts index 4db089fdb8eb..8573c1ad9df1 100644 --- a/packages/pretty-format/src/plugins/ReactElement.ts +++ b/packages/pretty-format/src/plugins/ReactElement.ts @@ -6,9 +6,9 @@ */ import type { Config, NewPlugin, Printer, Refs } from '../types' -import * as ReactIs18 from 'react-is' +import * as ReactIs19 from 'react-is' // @ts-expect-error no type -import * as ReactIs19 from 'react-is-19' +import * as ReactIs18 from 'react-is-18' import { printChildren, printElement, @@ -35,7 +35,7 @@ const reactIsMethods = [ ] as const const ReactIs: typeof ReactIs18 = Object.fromEntries( - reactIsMethods.map(m => [m, (v: any) => (ReactIs18 as any)[m](v) || ReactIs19[m](v)]), + reactIsMethods.map(m => [m, (v: any) => ReactIs18[m](v) || (ReactIs19 as any)[m](v)]), ) as any // Given element.props.children, or subtree during recursive traversal, diff --git a/packages/runner/package.json b/packages/runner/package.json index 91106509987d..7a9ecbb3fc68 100644 --- a/packages/runner/package.json +++ b/packages/runner/package.json @@ -1,7 +1,7 @@ { "name": "@vitest/runner", "type": "module", - "version": "2.2.0-beta.2", + "version": "3.0.0-beta.1", "description": "Vitest test runner", "license": "MIT", "funding": "https://opencollective.com/vitest", diff --git a/packages/runner/src/collect.ts b/packages/runner/src/collect.ts index 1c9354cfe107..8dd17a95cde9 100644 --- a/packages/runner/src/collect.ts +++ b/packages/runner/src/collect.ts @@ -1,4 +1,4 @@ -import type { VitestRunner } from './types/runner' +import type { FileSpec, VitestRunner } from './types/runner' import type { File, SuiteHooks } from './types/tasks' import { toArray } from '@vitest/utils' import { processError } from '@vitest/utils/error' @@ -20,15 +20,19 @@ import { const now = globalThis.performance ? globalThis.performance.now.bind(globalThis.performance) : Date.now export async function collectTests( - paths: string[], + specs: string[] | FileSpec[], runner: VitestRunner, ): Promise { const files: File[] = [] const config = runner.config - for (const filepath of paths) { + for (const spec of specs) { + const filepath = typeof spec === 'string' ? spec : spec.filepath + const testLocations = typeof spec === 'string' ? undefined : spec.testLocations + const file = createFileTask(filepath, config.root, config.name, runner.pool) + file.shuffle = config.sequence.shuffle runner.onCollectStart?.(file) @@ -56,7 +60,7 @@ export async function collectTests( mergeHooks(fileHooks, getHooks(defaultTasks)) for (const c of [...defaultTasks.tasks, ...collectorContext.tasks]) { - if (c.type === 'test' || c.type === 'custom' || c.type === 'suite') { + if (c.type === 'test' || c.type === 'suite') { file.tasks.push(c) } else if (c.type === 'collector') { @@ -97,6 +101,7 @@ export async function collectTests( interpretTaskModes( file, config.testNamePattern, + testLocations, hasOnlyTasks, false, config.allowOnly, diff --git a/packages/runner/src/context.ts b/packages/runner/src/context.ts index 6d1a88ae0faf..a4fe8436d42f 100644 --- a/packages/runner/src/context.ts +++ b/packages/runner/src/context.ts @@ -73,14 +73,18 @@ export function createTestContext( throw new PendingError('test is skipped; abort execution', test, note) } - context.onTestFailed = (fn) => { + context.onTestFailed = (handler, timeout) => { test.onFailed ||= [] - test.onFailed.push(fn) + test.onFailed.push( + withTimeout(handler, timeout ?? runner.config.hookTimeout, true), + ) } - context.onTestFinished = (fn) => { + context.onTestFinished = (handler, timeout) => { test.onFinished ||= [] - test.onFinished.push(fn) + test.onFinished.push( + withTimeout(handler, timeout ?? runner.config.hookTimeout, true), + ) } return (runner.extendTaskContext?.(context) as ExtendedContext) || context diff --git a/packages/runner/src/run.ts b/packages/runner/src/run.ts index b7b03e34ac1d..44c1baf3b189 100644 --- a/packages/runner/src/run.ts +++ b/packages/runner/src/run.ts @@ -1,8 +1,8 @@ import type { Awaitable } from '@vitest/utils' import type { DiffOptions } from '@vitest/utils/diff' -import type { VitestRunner } from './types/runner' +import type { FileSpec, VitestRunner } from './types/runner' import type { - Custom, + ExtendedContext, File, HookCleanupCallback, HookListener, @@ -63,32 +63,48 @@ function getSuiteHooks( async function callTestHooks( runner: VitestRunner, - task: Task, - hooks: ((result: TaskResult) => Awaitable)[], + test: Test, + hooks: ((context: ExtendedContext) => Awaitable)[], sequence: SequenceHooks, ) { if (sequence === 'stack') { hooks = hooks.slice().reverse() } + if (!hooks.length) { + return + } + + const onTestFailed = test.context.onTestFailed + const onTestFinished = test.context.onTestFinished + test.context.onTestFailed = () => { + throw new Error(`Cannot call "onTestFailed" inside a test hook.`) + } + test.context.onTestFinished = () => { + throw new Error(`Cannot call "onTestFinished" inside a test hook.`) + } + if (sequence === 'parallel') { try { - await Promise.all(hooks.map(fn => fn(task.result!))) + await Promise.all(hooks.map(fn => fn(test.context))) } catch (e) { - failTask(task.result!, e, runner.config.diffOptions) + failTask(test.result!, e, runner.config.diffOptions) } } else { for (const fn of hooks) { try { - await fn(task.result!) + await fn(test.context) } catch (e) { - failTask(task.result!, e, runner.config.diffOptions) + failTask(test.result!, e, runner.config.diffOptions) } } } + + test.context.onTestFailed = onTestFailed + test.context.onTestFinished = onTestFinished } export async function callSuiteHook( @@ -177,7 +193,7 @@ async function callCleanupHooks(cleanups: HookCleanupCallback[]) { ) } -export async function runTest(test: Test | Custom, runner: VitestRunner): Promise { +export async function runTest(test: Test, runner: VitestRunner): Promise { await runner.onBeforeRunTask?.(test) if (test.mode !== 'run') { @@ -412,7 +428,7 @@ export async function runSuite(suite: Suite, runner: VitestRunner): Promise group.type === 'suite', @@ -471,7 +487,7 @@ export async function runSuite(suite: Suite, runner: VitestRunner): Promise async function runSuiteChild(c: Task, runner: VitestRunner) { - if (c.type === 'test' || c.type === 'custom') { + if (c.type === 'test') { return limitMaxConcurrency(() => runTest(c, runner)) } else if (c.type === 'suite') { @@ -498,10 +514,11 @@ export async function runFiles(files: File[], runner: VitestRunner): Promise { +export async function startTests(specs: string[] | FileSpec[], runner: VitestRunner): Promise { + const paths = specs.map(f => typeof f === 'string' ? f : f.filepath) await runner.onBeforeCollect?.(paths) - const files = await collectTests(paths, runner) + const files = await collectTests(specs, runner) await runner.onCollected?.(files) await runner.onBeforeRunFiles?.(files) @@ -515,10 +532,12 @@ export async function startTests(paths: string[], runner: VitestRunner): Promise return files } -async function publicCollect(paths: string[], runner: VitestRunner): Promise { +async function publicCollect(specs: string[] | FileSpec[], runner: VitestRunner): Promise { + const paths = specs.map(f => typeof f === 'string' ? f : f.filepath) + await runner.onBeforeCollect?.(paths) - const files = await collectTests(paths, runner) + const files = await collectTests(specs, runner) await runner.onCollected?.(files) return files diff --git a/packages/runner/src/suite.ts b/packages/runner/src/suite.ts index 1a568eea48f8..398fc3399e4a 100644 --- a/packages/runner/src/suite.ts +++ b/packages/runner/src/suite.ts @@ -1,8 +1,6 @@ import type { FixtureItem } from './fixture' import type { VitestRunner } from './types/runner' import type { - Custom, - CustomAPI, File, Fixtures, RunMode, @@ -207,8 +205,7 @@ export function getRunner(): VitestRunner { function createDefaultSuite(runner: VitestRunner) { const config = runner.config.sequence - const api = config.shuffle ? suite.shuffle : suite - return api('', { concurrent: config.concurrent }, () => {}) + return suite('', { concurrent: config.concurrent }, () => {}) } export function clearCollectorContext( @@ -256,8 +253,9 @@ function parseArguments any>( 'Cannot use two objects as arguments. Please provide options and a function callback in that order.', ) } - // TODO: more info, add a name - // console.warn('The third argument is deprecated. Please use the second argument for options.') + console.warn( + 'Using an object as a third argument is deprecated. Vitest 4 will throw an error if the third argument is not a timeout number. Please use the second argument for options. See more at https://vitest.dev/guide/migration', + ) options = optionsOrTest } // it('', () => {}, 1000) @@ -292,11 +290,10 @@ function createSuiteCollector( name: string, factory: SuiteFactory = () => {}, mode: RunMode, - shuffle?: boolean, each?: boolean, suiteOptions?: TestOptions, ) { - const tasks: (Test | Custom | Suite | SuiteCollector)[] = [] + const tasks: (Test | Suite | SuiteCollector)[] = [] const factoryQueue: (Test | Suite | SuiteCollector)[] = [] let suite: Suite @@ -331,9 +328,7 @@ function createSuiteCollector( ) { task.concurrent = true } - if (shuffle) { - task.shuffle = true - } + task.shuffle = suiteOptions?.shuffle const context = createTestContext(task, runner) // create test context @@ -425,7 +420,7 @@ function createSuiteCollector( mode, each, file: undefined!, - shuffle, + shuffle: suiteOptions?.shuffle, tasks: [], meta: Object.create(null), concurrent: suiteOptions?.concurrent, @@ -503,7 +498,7 @@ function createSuite() { this: Record, name: string | Function, factoryOrOptions?: SuiteFactory | TestOptions, - optionsOrFactory: number | TestOptions | SuiteFactory = {}, + optionsOrFactory?: number | TestOptions | SuiteFactory, ) { const mode: RunMode = this.only ? 'only' @@ -523,8 +518,10 @@ function createSuite() { const isSequentialSpecified = options.sequential || this.sequential || options.concurrent === false // inherit options from current suite - if (currentSuite?.options) { - options = { ...currentSuite.options, ...options } + options = { + ...currentSuite?.options, + ...options, + shuffle: this.shuffle ?? options.shuffle ?? currentSuite?.options?.shuffle ?? runner?.config.sequence.shuffle, } // inherit concurrent / sequential from suite @@ -537,7 +534,6 @@ function createSuite() { formatName(name), factory, mode, - this.shuffle, this.each, options, ) @@ -568,7 +564,7 @@ function createSuite() { const { options, handler } = parseArguments(optionsOrFn, fnOrOptions) - const fnFirst = typeof optionsOrFn === 'function' + const fnFirst = typeof optionsOrFn === 'function' && typeof fnOrOptions === 'object' cases.forEach((i, idx) => { const items = Array.isArray(i) ? i : [i] @@ -612,7 +608,7 @@ function createSuite() { export function createTaskCollector( fn: (...args: any[]) => any, context?: Record, -): CustomAPI { +): TestAPI { const taskFn = fn as any taskFn.each = function ( @@ -640,7 +636,7 @@ export function createTaskCollector( const { options, handler } = parseArguments(optionsOrFn, fnOrOptions) - const fnFirst = typeof optionsOrFn === 'function' + const fnFirst = typeof optionsOrFn === 'function' && typeof fnOrOptions === 'object' cases.forEach((i, idx) => { const items = Array.isArray(i) ? i : [i] @@ -733,7 +729,7 @@ export function createTaskCollector( const _test = createChainable( ['concurrent', 'sequential', 'skip', 'only', 'todo', 'fails'], taskFn, - ) as CustomAPI + ) as TestAPI if (context) { (_test as any).mergeContext(context) diff --git a/packages/runner/src/test-state.ts b/packages/runner/src/test-state.ts index 6e7bd01957ea..af1d5b04157f 100644 --- a/packages/runner/src/test-state.ts +++ b/packages/runner/src/test-state.ts @@ -1,11 +1,11 @@ -import type { Custom, Test } from './types/tasks.ts' +import type { Test } from './types/tasks.ts' -let _test: Test | Custom | undefined +let _test: Test | undefined -export function setCurrentTest(test: T | undefined): void { +export function setCurrentTest(test: T | undefined): void { _test = test } -export function getCurrentTest(): T { +export function getCurrentTest(): T { return _test as T } diff --git a/packages/runner/src/types.ts b/packages/runner/src/types.ts index 48a72003b7b6..900fb8bd0755 100644 --- a/packages/runner/src/types.ts +++ b/packages/runner/src/types.ts @@ -1,5 +1,6 @@ export type { CancelReason, + FileSpec, VitestRunner, VitestRunnerConfig, VitestRunnerConstructor, @@ -11,6 +12,7 @@ export type { BeforeAllListener, BeforeEachListener, Custom, + /** @deprecated use `TestAPI` instead */ CustomAPI, DoneCallback, ExtendedContext, diff --git a/packages/runner/src/types/runner.ts b/packages/runner/src/types/runner.ts index ade94a143d1d..a8571f7415b1 100644 --- a/packages/runner/src/types/runner.ts +++ b/packages/runner/src/types/runner.ts @@ -1,6 +1,5 @@ import type { DiffOptions } from '@vitest/utils/diff' import type { - Custom, ExtendedContext, File, SequenceHooks, @@ -40,6 +39,11 @@ export interface VitestRunnerConfig { diffOptions?: DiffOptions } +export interface FileSpec { + filepath: string + testLocations: number[] | undefined +} + export type VitestRunnerImportSource = 'collect' | 'setup' export interface VitestRunnerConstructor { @@ -86,7 +90,7 @@ export interface VitestRunner { /** * When the task has finished running, but before cleanup hooks are called */ - onTaskFinished?: (test: Test | Custom) => unknown + onTaskFinished?: (test: Test) => unknown /** * Called after result and state are set. */ diff --git a/packages/runner/src/types/tasks.ts b/packages/runner/src/types/tasks.ts index 19645f304668..db53a597d28b 100644 --- a/packages/runner/src/types/tasks.ts +++ b/packages/runner/src/types/tasks.ts @@ -223,18 +223,9 @@ export interface Test extends TaskPopulated { /** * @deprecated Use `Test` instead. `type: 'custom'` is not used since 2.2 */ -export interface Custom extends TaskPopulated { - /** - * @deprecated use `test` instead. `custom` is not used since 2.2 - */ - type: 'custom' - /** - * Task context that will be passed to the test function. - */ - context: TaskContext & ExtraContext & TestContext -} +export type Custom = Test -export type Task = Test | Suite | Custom | File +export type Task = Test | Suite | File /** * @deprecated Vitest doesn't provide `done()` anymore @@ -286,16 +277,16 @@ interface EachFunctionReturn { ( name: string | Function, fn: (...args: T) => Awaitable, - options: TestOptions + options: TestCollectorOptions ): void ( name: string | Function, fn: (...args: T) => Awaitable, - options?: number | TestOptions + options?: number | TestCollectorOptions ): void ( name: string | Function, - options: TestOptions, + options: TestCollectorOptions, fn: (...args: T) => Awaitable ): void } @@ -316,7 +307,7 @@ interface TestForFunctionReturn { ): void ( name: string | Function, - options: TestOptions, + options: TestCollectorOptions, fn: (args: Arg, context: Context) => Awaitable ): void } @@ -347,16 +338,16 @@ interface TestCollectorCallable { ( name: string | Function, fn: TestFunction, - options: TestOptions + options: TestCollectorOptions ): void ( name: string | Function, fn?: TestFunction, - options?: number | TestOptions + options?: number | TestCollectorOptions ): void ( name: string | Function, - options?: TestOptions, + options?: TestCollectorOptions, fn?: TestFunction ): void } @@ -370,6 +361,8 @@ type ChainableTestAPI = ChainableFunction< } > +type TestCollectorOptions = Omit + export interface TestOptions { /** * Test timeout. @@ -399,6 +392,10 @@ export interface TestOptions { * Tests inherit `sequential` from `describe()` and nested `describe()` will inherit from parent's `sequential`. */ sequential?: boolean + /** + * Whether the tasks of the suite run in a random order. + */ + shuffle?: boolean /** * Whether the test should be skipped. */ @@ -435,6 +432,7 @@ export type TestAPI = ChainableTestAPI & }> } +/** @deprecated use `TestAPI` instead */ export type { TestAPI as CustomAPI } export interface FixtureOptions { @@ -572,8 +570,6 @@ export interface SuiteCollector { test: TestAPI tasks: ( | Suite - // TODO: remove in Vitest 3 - | Custom | Test | SuiteCollector )[] @@ -612,12 +608,12 @@ export interface TaskContext { /** * Extract hooks on test failed */ - onTestFailed: (fn: OnTestFailedHandler) => void + onTestFailed: (fn: OnTestFailedHandler, timeout?: number) => void /** * Extract hooks on test failed */ - onTestFinished: (fn: OnTestFinishedHandler) => void + onTestFinished: (fn: OnTestFinishedHandler, timeout?: number) => void /** * Mark tests as skipped. All execution after this call will be skipped. @@ -629,8 +625,8 @@ export interface TaskContext { export type ExtendedContext = TaskContext & TestContext -export type OnTestFailedHandler = (result: TaskResult) => Awaitable -export type OnTestFinishedHandler = (result: TaskResult) => Awaitable +export type OnTestFailedHandler = (context: ExtendedContext) => Awaitable +export type OnTestFinishedHandler = (context: ExtendedContext) => Awaitable export interface TaskHook { (fn: HookListener, timeout?: number): void diff --git a/packages/runner/src/utils/collect.ts b/packages/runner/src/utils/collect.ts index bd72b99e66b8..f492cc5c789b 100644 --- a/packages/runner/src/utils/collect.ts +++ b/packages/runner/src/utils/collect.ts @@ -6,53 +6,100 @@ import { relative } from 'pathe' * If any tasks been marked as `only`, mark all other tasks as `skip`. */ export function interpretTaskModes( - suite: Suite, + file: Suite, namePattern?: string | RegExp, + testLocations?: number[] | undefined, onlyMode?: boolean, parentIsOnly?: boolean, allowOnly?: boolean, ): void { - const suiteIsOnly = parentIsOnly || suite.mode === 'only' + const matchedLocations: number[] = [] - suite.tasks.forEach((t) => { - // Check if either the parent suite or the task itself are marked as included - const includeTask = suiteIsOnly || t.mode === 'only' - if (onlyMode) { - if (t.type === 'suite' && (includeTask || someTasksAreOnly(t))) { - // Don't skip this suite - if (t.mode === 'only') { + const traverseSuite = (suite: Suite, parentIsOnly?: boolean, parentMatchedWithLocation?: boolean) => { + const suiteIsOnly = parentIsOnly || suite.mode === 'only' + + suite.tasks.forEach((t) => { + // Check if either the parent suite or the task itself are marked as included + const includeTask = suiteIsOnly || t.mode === 'only' + if (onlyMode) { + if (t.type === 'suite' && (includeTask || someTasksAreOnly(t))) { + // Don't skip this suite + if (t.mode === 'only') { + checkAllowOnly(t, allowOnly) + t.mode = 'run' + } + } + else if (t.mode === 'run' && !includeTask) { + t.mode = 'skip' + } + else if (t.mode === 'only') { checkAllowOnly(t, allowOnly) t.mode = 'run' } } - else if (t.mode === 'run' && !includeTask) { - t.mode = 'skip' - } - else if (t.mode === 'only') { - checkAllowOnly(t, allowOnly) - t.mode = 'run' + + let hasLocationMatch = parentMatchedWithLocation + // Match test location against provided locations, only run if present + // in `testLocations`. Note: if `includeTaskLocations` is not enabled, + // all test will be skipped. + if (testLocations !== undefined && testLocations.length !== 0) { + if (t.location && testLocations?.includes(t.location.line)) { + t.mode = 'run' + matchedLocations.push(t.location.line) + hasLocationMatch = true + } + else if (parentMatchedWithLocation) { + t.mode = 'run' + } + else if (t.type === 'test') { + t.mode = 'skip' + } } - } - if (t.type === 'test') { - if (namePattern && !getTaskFullName(t).match(namePattern)) { - t.mode = 'skip' + + if (t.type === 'test') { + if (namePattern && !getTaskFullName(t).match(namePattern)) { + t.mode = 'skip' + } } - } - else if (t.type === 'suite') { - if (t.mode === 'skip') { - skipAllTasks(t) + else if (t.type === 'suite') { + if (t.mode === 'skip') { + skipAllTasks(t) + } + else { + traverseSuite(t, includeTask, hasLocationMatch) + } } - else { - interpretTaskModes(t, namePattern, onlyMode, includeTask, allowOnly) + }) + + // if all subtasks are skipped, mark as skip + if (suite.mode === 'run') { + if (suite.tasks.length && suite.tasks.every(i => i.mode !== 'run')) { + suite.mode = 'skip' } } - }) + } + + traverseSuite(file, parentIsOnly, false) + + const nonMatching = testLocations?.filter(loc => !matchedLocations.includes(loc)) + if (nonMatching && nonMatching.length !== 0) { + const message = nonMatching.length === 1 + ? `line ${nonMatching[0]}` + : `lines ${nonMatching.join(', ')}` - // if all subtasks are skipped, mark as skip - if (suite.mode === 'run') { - if (suite.tasks.length && suite.tasks.every(i => i.mode !== 'run')) { - suite.mode = 'skip' + if (file.result === undefined) { + file.result = { + state: 'fail', + errors: [], + } + } + if (file.result.errors === undefined) { + file.result.errors = [] } + + file.result.errors.push( + processError(new Error(`No test found in ${file.name} in ${message}`)), + ) } } diff --git a/packages/runner/src/utils/index.ts b/packages/runner/src/utils/index.ts index 3c5233f4398b..1faffeb65163 100644 --- a/packages/runner/src/utils/index.ts +++ b/packages/runner/src/utils/index.ts @@ -18,4 +18,5 @@ export { hasFailed, hasTests, isAtomTest, + isTestCase, } from './tasks' diff --git a/packages/runner/src/utils/tasks.ts b/packages/runner/src/utils/tasks.ts index b1071ca8bb2e..3cd7515d935c 100644 --- a/packages/runner/src/utils/tasks.ts +++ b/packages/runner/src/utils/tasks.ts @@ -1,19 +1,19 @@ -import type { Custom, Suite, Task, Test } from '../types/tasks' +import type { Suite, Task, Test } from '../types/tasks' import { type Arrayable, toArray } from '@vitest/utils' /** * @deprecated use `isTestCase` instead */ -export function isAtomTest(s: Task): s is Test | Custom { +export function isAtomTest(s: Task): s is Test { return isTestCase(s) } -export function isTestCase(s: Task): s is Test | Custom { - return s.type === 'test' || s.type === 'custom' +export function isTestCase(s: Task): s is Test { + return s.type === 'test' } -export function getTests(suite: Arrayable): (Test | Custom)[] { - const tests: (Test | Custom)[] = [] +export function getTests(suite: Arrayable): Test[] { + const tests: Test[] = [] const arraySuites = toArray(suite) for (const s of arraySuites) { if (isTestCase(s)) { diff --git a/packages/snapshot/package.json b/packages/snapshot/package.json index af9152e54a25..c95883114b61 100644 --- a/packages/snapshot/package.json +++ b/packages/snapshot/package.json @@ -1,7 +1,7 @@ { "name": "@vitest/snapshot", "type": "module", - "version": "2.2.0-beta.2", + "version": "3.0.0-beta.1", "description": "Vitest snapshot manager", "license": "MIT", "funding": "https://opencollective.com/vitest", @@ -43,7 +43,7 @@ }, "dependencies": { "@vitest/pretty-format": "workspace:*", - "magic-string": "^0.30.12", + "magic-string": "^0.30.14", "pathe": "^1.1.2" }, "devDependencies": { diff --git a/packages/snapshot/src/client.ts b/packages/snapshot/src/client.ts index cfa9508eb2d3..39858802feca 100644 --- a/packages/snapshot/src/client.ts +++ b/packages/snapshot/src/client.ts @@ -34,8 +34,13 @@ export interface Context { interface AssertOptions { received: unknown - filepath?: string - name?: string + filepath: string + name: string + /** + * Not required but needed for `SnapshotClient.clearTest` to implement test-retry behavior. + * @default name + */ + testId?: string message?: string isInline?: boolean properties?: object @@ -50,51 +55,55 @@ export interface SnapshotClientOptions { } export class SnapshotClient { - filepath?: string - name?: string - snapshotState: SnapshotState | undefined snapshotStateMap: Map = new Map() constructor(private options: SnapshotClientOptions = {}) {} - async startCurrentRun( + async setup( filepath: string, - name: string, options: SnapshotStateOptions, ): Promise { - this.filepath = filepath - this.name = name - - if (this.snapshotState?.testFilePath !== filepath) { - await this.finishCurrentRun() - - if (!this.getSnapshotState(filepath)) { - this.snapshotStateMap.set( - filepath, - await SnapshotState.create(filepath, options), - ) - } - this.snapshotState = this.getSnapshotState(filepath) + if (this.snapshotStateMap.has(filepath)) { + return } + this.snapshotStateMap.set( + filepath, + await SnapshotState.create(filepath, options), + ) } - getSnapshotState(filepath: string): SnapshotState { - return this.snapshotStateMap.get(filepath)! + async finish(filepath: string): Promise { + const state = this.getSnapshotState(filepath) + const result = await state.pack() + this.snapshotStateMap.delete(filepath) + return result + } + + skipTest(filepath: string, testName: string): void { + const state = this.getSnapshotState(filepath) + state.markSnapshotsAsCheckedForTest(testName) } - clearTest(): void { - this.filepath = undefined - this.name = undefined + clearTest(filepath: string, testId: string): void { + const state = this.getSnapshotState(filepath) + state.clearTest(testId) } - skipTestSnapshots(name: string): void { - this.snapshotState?.markSnapshotsAsCheckedForTest(name) + getSnapshotState(filepath: string): SnapshotState { + const state = this.snapshotStateMap.get(filepath) + if (!state) { + throw new Error( + `The snapshot state for '${filepath}' is not found. Did you call 'SnapshotClient.setup()'?`, + ) + } + return state } assert(options: AssertOptions): void { const { - filepath = this.filepath, - name = this.name, + filepath, + name, + testId = name, message, isInline = false, properties, @@ -109,6 +118,8 @@ export class SnapshotClient { throw new Error('Snapshot cannot be used outside of test') } + const snapshotState = this.getSnapshotState(filepath) + if (typeof properties === 'object') { if (typeof received !== 'object' || !received) { throw new Error( @@ -122,7 +133,7 @@ export class SnapshotClient { if (!pass) { throw createMismatchError( 'Snapshot properties mismatched', - this.snapshotState?.expand, + snapshotState.expand, received, properties, ) @@ -139,9 +150,8 @@ export class SnapshotClient { const testName = [name, ...(message ? [message] : [])].join(' > ') - const snapshotState = this.getSnapshotState(filepath) - const { actual, expected, key, pass } = snapshotState.match({ + testId, testName, received, isInline, @@ -153,7 +163,7 @@ export class SnapshotClient { if (!pass) { throw createMismatchError( `Snapshot \`${key || 'unknown'}\` mismatched`, - this.snapshotState?.expand, + snapshotState.expand, actual?.trim(), expected?.trim(), ) @@ -165,7 +175,7 @@ export class SnapshotClient { throw new Error('Raw snapshot is required') } - const { filepath = this.filepath, rawSnapshot } = options + const { filepath, rawSnapshot } = options if (rawSnapshot.content == null) { if (!filepath) { @@ -189,16 +199,6 @@ export class SnapshotClient { return this.assert(options) } - async finishCurrentRun(): Promise { - if (!this.snapshotState) { - return null - } - const result = await this.snapshotState.pack() - - this.snapshotState = undefined - return result - } - clear(): void { this.snapshotStateMap.clear() } diff --git a/packages/snapshot/src/port/inlineSnapshot.ts b/packages/snapshot/src/port/inlineSnapshot.ts index 2f252017fa88..09f059661655 100644 --- a/packages/snapshot/src/port/inlineSnapshot.ts +++ b/packages/snapshot/src/port/inlineSnapshot.ts @@ -9,6 +9,7 @@ import { export interface InlineSnapshot { snapshot: string + testId: string file: string line: number column: number diff --git a/packages/snapshot/src/port/state.ts b/packages/snapshot/src/port/state.ts index d12f66746068..89e777e4155f 100644 --- a/packages/snapshot/src/port/state.ts +++ b/packages/snapshot/src/port/state.ts @@ -23,6 +23,8 @@ import { saveRawSnapshots } from './rawSnapshot' import { addExtraLineBreaks, + CounterMap, + DefaultMap, getSnapshotData, keyToTestName, normalizeNewlines, @@ -47,24 +49,24 @@ interface SaveStatus { } export default class SnapshotState { - private _counters: Map + private _counters = new CounterMap() private _dirty: boolean private _updateSnapshot: SnapshotUpdateState private _snapshotData: SnapshotData private _initialData: SnapshotData private _inlineSnapshots: Array - private _inlineSnapshotStacks: Array + private _inlineSnapshotStacks: Array + private _testIdToKeys = new DefaultMap(() => []) private _rawSnapshots: Array private _uncheckedKeys: Set private _snapshotFormat: PrettyFormatOptions private _environment: SnapshotEnvironment private _fileExists: boolean - - added: number + private added = new CounterMap() + private matched = new CounterMap() + private unmatched = new CounterMap() + private updated = new CounterMap() expand: boolean - matched: number - unmatched: number - updated: number private constructor( public testFilePath: string, @@ -74,20 +76,15 @@ export default class SnapshotState { ) { const { data, dirty } = getSnapshotData(snapshotContent, options) this._fileExists = snapshotContent != null // TODO: update on watch? - this._initialData = data - this._snapshotData = data + this._initialData = { ...data } + this._snapshotData = { ...data } this._dirty = dirty this._inlineSnapshots = [] this._inlineSnapshotStacks = [] this._rawSnapshots = [] this._uncheckedKeys = new Set(Object.keys(this._snapshotData)) - this._counters = new Map() this.expand = options.expand || false - this.added = 0 - this.matched = 0 - this.unmatched = 0 this._updateSnapshot = options.updateSnapshot - this.updated = 0 this._snapshotFormat = { printBasicPrototype: false, escapeString: false, @@ -118,6 +115,31 @@ export default class SnapshotState { }) } + clearTest(testId: string): void { + // clear inline + this._inlineSnapshots = this._inlineSnapshots.filter(s => s.testId !== testId) + this._inlineSnapshotStacks = this._inlineSnapshotStacks.filter(s => s.testId !== testId) + + // clear file + for (const key of this._testIdToKeys.get(testId)) { + const name = keyToTestName(key) + const count = this._counters.get(name) + if (count > 0) { + if (key in this._snapshotData || key in this._initialData) { + this._snapshotData[key] = this._initialData[key] + } + this._counters.set(name, count - 1) + } + } + this._testIdToKeys.delete(testId) + + // clear stats + this.added.delete(testId) + this.updated.delete(testId) + this.matched.delete(testId) + this.unmatched.delete(testId) + } + protected _inferInlineSnapshotStack(stacks: ParsedStack[]): ParsedStack | null { // if called inside resolves/rejects, stacktrace is different const promiseIndex = stacks.findIndex(i => @@ -138,12 +160,13 @@ export default class SnapshotState { private _addSnapshot( key: string, receivedSerialized: string, - options: { rawSnapshot?: RawSnapshotInfo; stack?: ParsedStack }, + options: { rawSnapshot?: RawSnapshotInfo; stack?: ParsedStack; testId: string }, ): void { this._dirty = true if (options.stack) { this._inlineSnapshots.push({ snapshot: receivedSerialized, + testId: options.testId, ...options.stack, }) } @@ -158,17 +181,6 @@ export default class SnapshotState { } } - clear(): void { - this._snapshotData = this._initialData - // this._inlineSnapshots = [] - this._counters = new Map() - this.added = 0 - this.matched = 0 - this.unmatched = 0 - this.updated = 0 - this._dirty = false - } - async save(): Promise { const hasExternalSnapshots = Object.keys(this._snapshotData).length const hasInlineSnapshots = this._inlineSnapshots.length @@ -228,6 +240,7 @@ export default class SnapshotState { } match({ + testId, testName, received, key, @@ -236,12 +249,14 @@ export default class SnapshotState { error, rawSnapshot, }: SnapshotMatchOptions): SnapshotReturnOptions { - this._counters.set(testName, (this._counters.get(testName) || 0) + 1) - const count = Number(this._counters.get(testName)) + // this also increments counter for inline snapshots. maybe we shouldn't? + this._counters.increment(testName) + const count = this._counters.get(testName) if (!key) { key = testNameToKey(testName, count) } + this._testIdToKeys.get(testId).push(key) // Do not mark the snapshot as "checked" if the snapshot is inline and // there's an external snapshot. This way the external snapshot can be @@ -320,7 +335,7 @@ export default class SnapshotState { this._inlineSnapshots = this._inlineSnapshots.filter(s => !(s.file === stack!.file && s.line === stack!.line && s.column === stack!.column)) throw new Error('toMatchInlineSnapshot cannot be called multiple times at the same location.') } - this._inlineSnapshotStacks.push(stack) + this._inlineSnapshotStacks.push({ ...stack, testId }) } // These are the conditions on when to write snapshots: @@ -338,27 +353,29 @@ export default class SnapshotState { if (this._updateSnapshot === 'all') { if (!pass) { if (hasSnapshot) { - this.updated++ + this.updated.increment(testId) } else { - this.added++ + this.added.increment(testId) } this._addSnapshot(key, receivedSerialized, { stack, + testId, rawSnapshot, }) } else { - this.matched++ + this.matched.increment(testId) } } else { this._addSnapshot(key, receivedSerialized, { stack, + testId, rawSnapshot, }) - this.added++ + this.added.increment(testId) } return { @@ -371,7 +388,7 @@ export default class SnapshotState { } else { if (!pass) { - this.unmatched++ + this.unmatched.increment(testId) return { actual: removeExtraLineBreaks(receivedSerialized), count, @@ -384,7 +401,7 @@ export default class SnapshotState { } } else { - this.matched++ + this.matched.increment(testId) return { actual: '', count, @@ -415,10 +432,10 @@ export default class SnapshotState { const status = await this.save() snapshot.fileDeleted = status.deleted - snapshot.added = this.added - snapshot.matched = this.matched - snapshot.unmatched = this.unmatched - snapshot.updated = this.updated + snapshot.added = this.added.total() + snapshot.matched = this.matched.total() + snapshot.unmatched = this.unmatched.total() + snapshot.updated = this.updated.total() snapshot.unchecked = !status.deleted ? uncheckedCount : 0 snapshot.uncheckedKeys = Array.from(uncheckedKeys) diff --git a/packages/snapshot/src/port/utils.ts b/packages/snapshot/src/port/utils.ts index d3435a493354..64902bdef1da 100644 --- a/packages/snapshot/src/port/utils.ts +++ b/packages/snapshot/src/port/utils.ts @@ -265,3 +265,37 @@ export function deepMergeSnapshot(target: any, source: any): any { } return target } + +export class DefaultMap extends Map { + constructor( + private defaultFn: (key: K) => V, + entries?: Iterable, + ) { + super(entries) + } + + override get(key: K): V { + if (!this.has(key)) { + this.set(key, this.defaultFn(key)) + } + return super.get(key)! + } +} + +export class CounterMap extends DefaultMap { + constructor() { + super(() => 0) + } + + increment(key: K): void { + this.set(key, this.get(key) + 1) + } + + total(): number { + let total = 0 + for (const x of this.values()) { + total += x + } + return total + } +} diff --git a/packages/snapshot/src/types/index.ts b/packages/snapshot/src/types/index.ts index b5378e5a8cb6..f314d3befa15 100644 --- a/packages/snapshot/src/types/index.ts +++ b/packages/snapshot/src/types/index.ts @@ -24,6 +24,7 @@ export interface SnapshotStateOptions { } export interface SnapshotMatchOptions { + testId: string testName: string received: unknown key?: string diff --git a/packages/spy/package.json b/packages/spy/package.json index 7b39c4973a41..0d836274067b 100644 --- a/packages/spy/package.json +++ b/packages/spy/package.json @@ -1,7 +1,7 @@ { "name": "@vitest/spy", "type": "module", - "version": "2.2.0-beta.2", + "version": "3.0.0-beta.1", "description": "Lightweight Jest compatible spy implementation", "license": "MIT", "funding": "https://opencollective.com/vitest", diff --git a/packages/spy/src/index.ts b/packages/spy/src/index.ts index ea8bf27084bc..4f3180224621 100644 --- a/packages/spy/src/index.ts +++ b/packages/spy/src/index.ts @@ -198,14 +198,17 @@ export interface MockInstance { */ mockClear(): this /** - * Performs the same actions as `mockClear` and sets the inner implementation to an empty function (returning `undefined` when invoked). This also resets all "once" implementations. It is useful for completely resetting a mock to its default state. + * Does what `mockClear` does and resets inner implementation to the original function. 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 set implementation to `impl`. It is useful for completely resetting a mock to its default state. * * To automatically call this method before each test, enable the [`mockReset`](https://vitest.dev/config/#mockreset) setting in the configuration. * @see https://vitest.dev/api/mock#mockreset */ mockReset(): this /** - * Does what `mockReset` does and restores inner implementation to the original function. + * Does what `mockReset` does and restores original descriptors of spied-on objects. * * Note that restoring mock from `vi.fn()` will set implementation to an empty function that returns `undefined`. Restoring a `vi.fn(impl)` will restore implementation to `impl`. * @see https://vitest.dev/api/mock#mockrestore @@ -410,6 +413,7 @@ export type Mocked = { : T[P]; } & T +const vitestSpy = Symbol.for('vitest.spy') export const mocks: Set = new Set() export function isMockFunction(fn: any): fn is MockInstance { @@ -418,6 +422,20 @@ export function isMockFunction(fn: any): fn is MockInstance { ) } +function getSpy( + obj: unknown, + method: keyof any, + accessType?: 'get' | 'set', +): MockInstance | undefined { + const desc = Object.getOwnPropertyDescriptor(obj, method) + if (desc) { + const fn = desc[accessType ?? 'value'] + if (typeof fn === 'function' && vitestSpy in fn) { + return fn + } + } +} + export function spyOn>>( obj: T, methodName: S, @@ -447,6 +465,11 @@ export function spyOn( } as const const objMethod = accessType ? { [dictionary[accessType]]: method } : method + const currentStub = getSpy(obj, method, accessType) + if (currentStub) { + return currentStub + } + const stub = tinyspy.internalSpyOn(obj, objMethod as any) return enhanceSpy(stub) as MockInstance @@ -520,6 +543,11 @@ function enhanceSpy( let name: string = (stub as any).name + Object.defineProperty(stub, vitestSpy, { + value: true, + enumerable: false, + }) + stub.getMockName = () => name || 'vi.fn()' stub.mockName = (n) => { name = n @@ -536,7 +564,7 @@ function enhanceSpy( stub.mockReset = () => { stub.mockClear() - implementation = (() => undefined) as T + implementation = undefined onceImplementations = [] return stub } @@ -544,7 +572,6 @@ function enhanceSpy( stub.mockRestore = () => { stub.mockReset() state.restore() - implementation = undefined return stub } diff --git a/packages/ui/client/auto-imports.d.ts b/packages/ui/client/auto-imports.d.ts index 620c0ef2bd39..e5a11a01d5a7 100644 --- a/packages/ui/client/auto-imports.d.ts +++ b/packages/ui/client/auto-imports.d.ts @@ -118,6 +118,7 @@ declare global { const resolveComponent: typeof import('vue')['resolveComponent'] const resolveRef: typeof import('@vueuse/core')['resolveRef'] const resolveUnref: typeof import('@vueuse/core')['resolveUnref'] + const selectedTest: typeof import('./composables/params')['selectedTest'] const setIframeViewport: typeof import('./composables/api')['setIframeViewport'] const shallowReactive: typeof import('vue')['shallowReactive'] const shallowReadonly: typeof import('vue')['shallowReadonly'] @@ -127,6 +128,7 @@ declare global { const showDashboard: typeof import('./composables/navigation')['showDashboard'] const showLine: typeof import('./composables/codemirror')['showLine'] const showNavigationPanel: typeof import('./composables/navigation')['showNavigationPanel'] + const showReport: typeof import('./composables/navigation')['showReport'] const showRightPanel: typeof import('./composables/navigation')['showRightPanel'] const showSource: typeof import('./composables/codemirror')['showSource'] const syncRef: typeof import('@vueuse/core')['syncRef'] diff --git a/packages/ui/client/components/BrowserIframe.vue b/packages/ui/client/components/BrowserIframe.vue index 365ad4b57ec7..a36e35eddbc9 100644 --- a/packages/ui/client/components/BrowserIframe.vue +++ b/packages/ui/client/components/BrowserIframe.vue @@ -1,6 +1,6 @@