diff --git a/.gitignore b/.gitignore index 98d5c9045315..8ca3bb42743f 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,5 @@ test/cli/fixtures/browser-multiple/basic-* # exclude static html reporter folder test/browser/html/ test/core/html/ -.vitest-attachments \ No newline at end of file +.vitest-attachments +explainFiles.txt \ No newline at end of file diff --git a/docs/advanced/api/vitest.md b/docs/advanced/api/vitest.md index 4db4337dba1f..24c3566ce8de 100644 --- a/docs/advanced/api/vitest.md +++ b/docs/advanced/api/vitest.md @@ -308,7 +308,7 @@ function runTestSpecifications( ): Promise ``` -This method runs every test based on the received [specifications](/advanced/api/test-specification). The second argument, `allTestsRun`, is used by the coverage provider to determine if it needs to instrument coverage on _every_ file in the root (this only matters if coverage is enabled and `coverage.all` is set to `true`). +This method runs every test based on the received [specifications](/advanced/api/test-specification). The second argument, `allTestsRun`, is used by the coverage provider to determine if it needs to include uncovered files in report. ::: warning This method doesn't trigger `onWatcherRerun`, `onWatcherStart` and `onTestsRerun` callbacks. If you are rerunning tests based on the file change, consider using [`rerunTestSpecifications`](#reruntestspecifications) instead. diff --git a/docs/advanced/reporters.md b/docs/advanced/reporters.md index 2a486d622894..6a7e01d87c3c 100644 --- a/docs/advanced/reporters.md +++ b/docs/advanced/reporters.md @@ -93,7 +93,6 @@ class MyReporter implements Reporter { ### Built-in reporters: -1. `BasicReporter` 1. `DefaultReporter` 2. `DotReporter` 3. `JsonReporter` diff --git a/docs/advanced/runner.md b/docs/advanced/runner.md index 5fbd5a5f262b..872a350779be 100644 --- a/docs/advanced/runner.md +++ b/docs/advanced/runner.md @@ -238,7 +238,7 @@ export interface TaskResult { * Errors that occurred during the task execution. It is possible to have several errors * if `expect.soft()` failed multiple times. */ - errors?: ErrorWithDiff[] + errors?: TestError[] /** * How long in milliseconds the task took to run. */ diff --git a/docs/config/index.md b/docs/config/index.md index 433aaaf5bcd3..33d656185c78 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -124,8 +124,8 @@ When using coverage, Vitest automatically adds test files `include` patterns to ### exclude - **Type:** `string[]` -- **Default:** `['**/node_modules/**', '**/dist/**', '**/cypress/**', '**/.{idea,git,cache,output,temp}/**', '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build,eslint,prettier}.config.*']` -- **CLI:** `vitest --exclude "**/excluded-file"` +- **Default:** `['**/node_modules/**', '**/.git/**']` +- **CLI:** `vitest --exclude "**/excluded-file" --exclude "*/other-files/*.js"` A list of glob patterns that should be excluded from your test files. @@ -1310,12 +1310,12 @@ Make sure that your files are not excluded by [`server.watch.ignored`](https://v ### coverage -You can use [`v8`](https://v8.dev/blog/javascript-code-coverage), [`istanbul`](https://istanbul.js.org/) or [a custom coverage solution](/guide/coverage#custom-coverage-provider) for coverage collection. +You can use [`v8`](/guide/coverage.html#v8-provider), [`istanbul`](/guide/coverage.html#istanbul-provider) or [a custom coverage solution](/guide/coverage#custom-coverage-provider) for coverage collection. You can provide coverage options to CLI with dot notation: ```sh -npx vitest --coverage.enabled --coverage.provider=istanbul --coverage.all +npx vitest --coverage.enabled --coverage.provider=istanbul ``` ::: warning @@ -1342,76 +1342,26 @@ Enables coverage collection. Can be overridden using `--coverage` CLI option. #### coverage.include - **Type:** `string[]` -- **Default:** `['**']` +- **Default:** Files that were imported during test run - **Available for providers:** `'v8' | 'istanbul'` -- **CLI:** `--coverage.include=`, `--coverage.include= --coverage.include=` +- **CLI:** `--coverage.include=`, `--coverage.include= --coverage.include=` -List of files included in coverage as glob patterns +List of files included in coverage as glob patterns. By default only files covered by tests are included. -#### coverage.extension +It is recommended to pass file extensions in the pattern. -- **Type:** `string | string[]` -- **Default:** `['.js', '.cjs', '.mjs', '.ts', '.mts', '.tsx', '.jsx', '.vue', '.svelte', '.marko', '.astro']` -- **Available for providers:** `'v8' | 'istanbul'` -- **CLI:** `--coverage.extension=`, `--coverage.extension= --coverage.extension=` +See [Including and excluding files from coverage report](/guide/coverage.html#including-and-excluding-files-from-coverage-report) for examples. #### coverage.exclude - **Type:** `string[]` -- **Default:** -```js -[ - 'coverage/**', - 'dist/**', - '**/node_modules/**', - '**/[.]**', - 'packages/*/test?(s)/**', - '**/*.d.ts', - '**/virtual:*', - '**/__x00__*', - '**/\x00*', - 'cypress/**', - 'test?(s)/**', - 'test?(-*).?(c|m)[jt]s?(x)', - '**/*{.,-}{test,spec,bench,benchmark}?(-d).?(c|m)[jt]s?(x)', - '**/__tests__/**', - '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build,eslint,prettier}.config.*', - '**/vitest.{workspace,projects}.[jt]s?(on)', - '**/.{eslint,mocha,prettier}rc.{?(c|m)js,yml}', -] -``` +- **Default:** : `[]` - **Available for providers:** `'v8' | 'istanbul'` - **CLI:** `--coverage.exclude=`, `--coverage.exclude= --coverage.exclude=` List of files excluded from coverage as glob patterns. -This option overrides all default options. Extend the default options when adding new patterns to ignore: - -```ts -import { coverageConfigDefaults, defineConfig } from 'vitest/config' - -export default defineConfig({ - test: { - coverage: { - exclude: ['**/custom-pattern/**', ...coverageConfigDefaults.exclude] - }, - }, -}) -``` - -::: tip NOTE -Vitest automatically adds test files `include` patterns to the `coverage.exclude`. -It's not possible to show coverage of test files. -::: - -#### coverage.all - -- **Type:** `boolean` -- **Default:** `true` -- **Available for providers:** `'v8' | 'istanbul'` -- **CLI:** `--coverage.all`, `--coverage.all=false` - -Whether to include all files, including the untested ones into report. +See [Including and excluding files from coverage report](/guide/coverage.html#including-and-excluding-files-from-coverage-report) for examples. #### coverage.clean @@ -1678,51 +1628,11 @@ Sets thresholds to 100 for files matching the glob pattern. } ``` -#### coverage.ignoreEmptyLines - -- **Type:** `boolean` -- **Default:** `true` (`false` in v1) -- **Available for providers:** `'v8'` -- **CLI:** `--coverage.ignoreEmptyLines=` - -Ignore empty lines, comments and other non-runtime code, e.g. Typescript types. Requires `experimentalAstAwareRemapping: false`. - -This option works only if the used compiler removes comments and other non-runtime code from the transpiled code. -By default Vite uses ESBuild which removes comments and Typescript types from `.ts`, `.tsx` and `.jsx` files. - -If you want to apply ESBuild to other files as well, define them in [`esbuild` options](https://vitejs.dev/config/shared-options.html#esbuild): - -```ts -import { defineConfig } from 'vitest/config' - -export default defineConfig({ - esbuild: { - // Transpile all files with ESBuild to remove comments from code coverage. - // Required for `test.coverage.ignoreEmptyLines` to work: - include: ['**/*.js', '**/*.jsx', '**/*.mjs', '**/*.ts', '**/*.tsx'], - }, - test: { - coverage: { - provider: 'v8', - ignoreEmptyLines: true, - }, - }, -}) -``` -#### coverage.experimentalAstAwareRemapping - -- **Type:** `boolean` -- **Default:** `false` -- **Available for providers:** `'v8'` -- **CLI:** `--coverage.experimentalAstAwareRemapping=` - -Remap coverage with experimental AST based analysis. Provides more accurate results compared to default mode. - #### coverage.ignoreClassMethods - **Type:** `string[]` - **Default:** `[]` -- **Available for providers:** `'istanbul'` +- **Available for providers:** `'v8' | 'istanbul'` - **CLI:** `--coverage.ignoreClassMethods=` Set to array of class method names to ignore for coverage. diff --git a/docs/guide/browser/index.md b/docs/guide/browser/index.md index 4152d7c4be28..6effa68b8a07 100644 --- a/docs/guide/browser/index.md +++ b/docs/guide/browser/index.md @@ -584,3 +584,45 @@ test('renders a message', async () => { When using Vitest Browser, it's important to note that thread blocking dialogs like `alert` or `confirm` cannot be used natively. This is because they block the web page, which means Vitest cannot continue communicating with the page, causing the execution to hang. In such situations, Vitest provides default mocks with default returned values for these APIs. This ensures that if the user accidentally uses synchronous popup web APIs, the execution would not hang. However, it's still recommended for the user to mock these web APIs for better experience. Read more in [Mocking](/guide/mocking). + +### Spying on Module Exports + +Browser Mode uses the browser's native ESM support to serve modules. The module namespace object is sealed and can't be reconfigured, unlike in Node.js tests where Vitest can patch the Module Runner. This means you can't call `vi.spyOn` on an imported object: + +```ts +import { vi } from 'vitest' +import * as module from './module.js' + +vi.spyOn(module, 'method') // ❌ throws an error +``` + +To bypass this limitation, Vitest supports `{ spy: true }` option in `vi.mock('./module.js')`. This will automatically spy on every export in the module without replacing them with fake ones. + +```ts +import { vi } from 'vitest' +import * as module from './module.js' + +vi.mock('./module.js', { spy: true }) + +vi.mocked(module.method).mockImplementation(() => { + // ... +}) +``` + +However, the only way to mock exported _variables_ is to export a method that will change the internal value: + +::: code-group +```js [module.js] +export let MODE = 'test' +export function changeMode(newMode) { + MODE = newMode +} +``` +```js [module.test.ts] +import { expect } from 'vitest' +import { changeMode, MODE } from './module.js' + +changeMode('production') +expect(MODE).toBe('production') +``` +::: diff --git a/docs/guide/cli-generated.md b/docs/guide/cli-generated.md index a7ab176d6cfd..7d0d57790f4e 100644 --- a/docs/guide/cli-generated.md +++ b/docs/guide/cli-generated.md @@ -89,7 +89,7 @@ Hide logs for skipped tests - **CLI:** `--reporter ` - **Config:** [reporters](/config/#reporters) -Specify reporters (default, basic, blob, verbose, dot, json, tap, tap-flat, junit, hanging-process, github-actions) +Specify reporters (default, blob, verbose, dot, json, tap, tap-flat, junit, hanging-process, github-actions) ### outputFile @@ -98,13 +98,6 @@ Specify reporters (default, basic, blob, verbose, dot, json, tap, tap-flat, juni Write test results to a file when supporter reporter is also specified, use cac's dot notation for individual outputs of multiple reporters (example: `--outputFile.tap=./tap.txt`) -### coverage.all - -- **CLI:** `--coverage.all` -- **Config:** [coverage.all](/config/#coverage-all) - -Whether to include all files, including the untested ones into report - ### coverage.provider - **CLI:** `--coverage.provider ` @@ -133,13 +126,6 @@ Files included in coverage as glob patterns. May be specified more than once whe Files to be excluded in coverage. May be specified more than once when using multiple extensions (default: Visit [`coverage.exclude`](https://vitest.dev/config/#coverage-exclude)) -### coverage.extension - -- **CLI:** `--coverage.extension ` -- **Config:** [coverage.extension](/config/#coverage-extension) - -Extension to be included in coverage. May be specified more than once when using multiple extensions (default: `[".js", ".cjs", ".mjs", ".ts", ".mts", ".tsx", ".jsx", ".vue", ".svelte"]`) - ### coverage.clean - **CLI:** `--coverage.clean` diff --git a/docs/guide/coverage.md b/docs/guide/coverage.md index ef2724d68a61..d1f398c6c042 100644 --- a/docs/guide/coverage.md +++ b/docs/guide/coverage.md @@ -18,7 +18,7 @@ import { defineConfig } from 'vitest/config' export default defineConfig({ test: { coverage: { - provider: 'istanbul' // or 'v8' + provider: 'v8' // or 'istanbul' }, }, }) @@ -132,14 +132,13 @@ globalThis.__VITEST_COVERAGE__[filename] = coverage // [!code ++] ## Coverage Setup -:::tip -It's recommended to always define [`coverage.include`](https://vitest.dev/config/#coverage-include) in your configuration file. -This helps Vitest to reduce the amount of files picked by [`coverage.all`](https://vitest.dev/config/#coverage-all). +::: tip +All coverage options are listed in [Coverage Config Reference](/config/#coverage). ::: -To test with coverage enabled, you can pass the `--coverage` flag in CLI. -By default, reporter `['text', 'html', 'clover', 'json']` will be used. +To test with coverage enabled, you can pass the `--coverage` flag in CLI or set `coverage.enabled` in `vitest.config.ts`: +::: code-group ```json [package.json] { "scripts": { @@ -148,20 +147,92 @@ By default, reporter `['text', 'html', 'clover', 'json']` will be used. } } ``` +```ts [vitest.config.ts] +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + coverage: { + enabled: true + }, + }, +}) +``` +::: -To configure it, set `test.coverage` options in your config file: +## Including and excluding files from coverage report -```ts [vitest.config.ts] +You can define what files are shown in coverage report by configuring [`coverage.include`](/config/#coverage-include) and [`coverage.exclude`](/config/#coverage-exclude). + +By default Vitest will show only files that were imported during test run. +To include uncovered files in the report, you'll need to configure [`coverage.include`](/config/#coverage-include) with a pattern that will pick your source files: + +::: code-group +```ts [vitest.config.ts] {6} +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + coverage: { + include: ['src/**.{ts,tsx}'] + }, + }, +}) +``` +```sh [Covered Files] +├── src +│ ├── components +│ │ └── counter.tsx # [!code ++] +│ ├── mock-data +│ │ ├── products.json # [!code error] +│ │ └── users.json # [!code error] +│ └── utils +│ ├── formatters.ts # [!code ++] +│ ├── time.ts # [!code ++] +│ └── users.ts # [!code ++] +├── test +│ └── utils.test.ts # [!code error] +│ +├── package.json # [!code error] +├── tsup.config.ts # [!code error] +└── vitest.config.ts # [!code error] +``` +::: + +To exclude files that are matching `coverage.include`, you can define an additional [`coverage.exclude`](/config/#coverage-exclude): + +::: code-group +```ts [vitest.config.ts] {7} import { defineConfig } from 'vitest/config' export default defineConfig({ test: { coverage: { - reporter: ['text', 'json', 'html'], + include: ['src/**.{ts,tsx}'], + exclude: ['**/utils/users.ts'] }, }, }) ``` +```sh [Covered Files] +├── src +│ ├── components +│ │ └── counter.tsx # [!code ++] +│ ├── mock-data +│ │ ├── products.json # [!code error] +│ │ └── users.json # [!code error] +│ └── utils +│ ├── formatters.ts # [!code ++] +│ ├── time.ts # [!code ++] +│ └── users.ts # [!code error] +├── test +│ └── utils.test.ts # [!code error] +│ +├── package.json # [!code error] +├── tsup.config.ts # [!code error] +└── vitest.config.ts # [!code error] +``` +::: ## Custom Coverage Reporter @@ -261,29 +332,12 @@ export default CustomCoverageProviderModule Please refer to the type definition for more details. -## Changing the Default Coverage Folder Location - -When running a coverage report, a `coverage` folder is created in the root directory of your project. If you want to move it to a different directory, use the `test.coverage.reportsDirectory` property in the `vitest.config.js` file. - -```js [vitest.config.js] -import { defineConfig } from 'vite' - -export default defineConfig({ - test: { - coverage: { - reportsDirectory: './tests/unit/coverage' - } - } -}) -``` - ## Ignoring Code Both coverage providers have their own ways how to ignore code from coverage reports: -- [`v8`](https://github.com/istanbuljs/v8-to-istanbul#ignoring-uncovered-lines) +- [`v8`](https://github.com/AriPerkkio/ast-v8-to-istanbul?tab=readme-ov-file#ignoring-code) - [`istanbul`](https://github.com/istanbuljs/nyc#parsing-hints-ignoring-lines) -- `v8` with [`experimentalAstAwareRemapping: true`](https://vitest.dev/config/#coverage-experimentalAstAwareRemapping) see [ast-v8-to-istanbul | Ignoring code](https://github.com/AriPerkkio/ast-v8-to-istanbul?tab=readme-ov-file#ignoring-code) When using TypeScript the source codes are transpiled using `esbuild`, which strips all comments from the source codes ([esbuild#516](https://github.com/evanw/esbuild/issues/516)). Comments which are considered as [legal comments](https://esbuild.github.io/api/#legal-comments) are preserved. @@ -301,10 +355,6 @@ if (condition) { if (condition) { ``` -## Other Options - -To see all configurable options for coverage, see the [coverage Config Reference](https://vitest.dev/config/#coverage). - ## Coverage Performance If code coverage generation is slow on your project, see [Profiling Test Performance | Code coverage](/guide/profiling-test-performance.html#code-coverage). diff --git a/docs/guide/improving-performance.md b/docs/guide/improving-performance.md index e9ed9a8bf81e..d34fc2889ac7 100644 --- a/docs/guide/improving-performance.md +++ b/docs/guide/improving-performance.md @@ -52,6 +52,10 @@ export default defineConfig({ ``` ::: +## Limiting directory search + +You can limit the working directory when Vitest searches for files using [`test.dir`](/config/#test-dir) option. This should make the search faster if you have unrelated folders and files in the root directory. + ## Pool By default Vitest runs tests in `pool: 'forks'`. While `'forks'` pool is better for compatibility issues ([hanging process](/guide/common-errors.html#failed-to-terminate-worker) and [segfaults](/guide/common-errors.html#segfaults-and-native-code-errors)), it may be slightly slower than `pool: 'threads'` in larger projects. diff --git a/docs/guide/migration.md b/docs/guide/migration.md index 21b695a9f10d..dfbb103e0715 100644 --- a/docs/guide/migration.md +++ b/docs/guide/migration.md @@ -5,6 +5,86 @@ outline: deep # Migration Guide +## Migrating to Vitest 4.0 {#vitest-4} + +### Removed `reporters: 'basic'` + +Basic reporter is removed as it is equal to: + +```ts +export default defineConfig({ + test: { + reporters: [ + ['default', { summary: false }] + ] + } +}) +``` + +### V8 Code Coverage Major Changes + +Vitest's V8 code coverage provider is now using more accurate coverage result remapping logic. +It is expected for users to see changes in their coverage reports when updating from Vitest v3. + +In the past Vitest used [`v8-to-istanbul`](https://github.com/istanbuljs/v8-to-istanbul) for remapping V8 coverage results into your source files. +This method wasn't very accurate and provided plenty of false positives in the coverage reports. +We've now developed a new package that utilizes AST based analysis for the V8 coverage. +This allows V8 reports to be as accurate as `@vitest/coverage-istanbul` reports. + +- Coverage ignore hints have updated. See [Coverage | Ignoring Code](/guide/coverage.html#ignoring-code). +- `coverage.ignoreEmptyLines` is removed. Lines without runtime code are no longer included in reports. +- `coverage.experimentalAstAwareRemapping` is removed. This option is now enabled by default, and is the only supported remapping method. +- `coverage.ignoreClassMethods` is now supported by V8 provider too. + +### Removed options `coverage.all` and `coverage.extensions` + +In previous versions Vitest included all uncovered files in coverage report by default. +This was due to `coverage.all` defaulting to `true`, and `coverage.include` defaulting to `**`. +These default values were chosen for a good reason - it is impossible for testing tools to guess where users are storing their source files. + +This ended up having Vitest's coverage providers processing unexpected files, like minified Javascript, leading to slow/stuck coverage report generations. +In Vitest v4 we have removed `coverage.all` completely and **defaulted to include only covered files in the report**. + +When upgrading to v4 it is recommended to define `coverage.include` in your configuration, and then start applying simple `coverage.exclusion` patterns if needed. + +```ts [vitest.config.ts] +export default defineConfig({ + test: { + coverage: { + // Include covered and uncovered files matching this pattern: + include: ['packages/**/src/**.{js,jsx,ts,tsx}'], // [!code ++] + + // Exclusion is applied for the files that match include pattern above + // No need to define root level *.config.ts files or node_modules, as we didn't add those in include + exclude: ['**/some-pattern/**'], // [!code ++] + + // These options are removed now + all: true, // [!code --] + extensions: ['js', 'ts'], // [!code --] + } + } +}) +``` + +If `coverage.include` is not defined, coverage report will include only files that were loaded during test run: +```ts [vitest.config.ts] +export default defineConfig({ + test: { + coverage: { + // Include not set, include only files that are loaded during test run + include: undefined, // [!code ++] + + // Loaded files that match this pattern will be excluded: + exclude: ['**/some-pattern/**'], // [!code ++] + } + } +}) +``` + +See also new guides: +- [Including and excluding files from coverage report](/guide/coverage.html#including-and-excluding-files-from-coverage-report) for examples +- [Profiling Test Performance | Code coverage](/guide/profiling-test-performance.html#code-coverage) for tips about debugging coverage generation + ## Migrating to Vitest 3.0 {#vitest-3} ### Test Options as a Third Argument diff --git a/docs/guide/reporters.md b/docs/guide/reporters.md index 748079f7acb9..9adc9df7e3cf 100644 --- a/docs/guide/reporters.md +++ b/docs/guide/reporters.md @@ -141,35 +141,6 @@ Final output after tests have finished: Duration 1.26s (transform 35ms, setup 1ms, collect 90ms, tests 1.47s, environment 0ms, prepare 267ms) ``` -### Basic Reporter - -The `basic` reporter is equivalent to `default` reporter without `summary`. - -:::code-group -```bash [CLI] -npx vitest --reporter=basic -``` - -```ts [vitest.config.ts] -export default defineConfig({ - test: { - reporters: ['basic'] - }, -}) -``` -::: - -Example output using basic reporter: -```bash -✓ __tests__/file1.test.ts (2) 725ms -✓ __tests__/file2.test.ts (2) 746ms - - Test Files 2 passed (2) - Tests 4 passed (4) - Start at 12:34:32 - Duration 1.26s (transform 35ms, setup 1ms, collect 90ms, tests 1.47s, environment 0ms, prepare 267ms) -``` - ### Verbose Reporter Verbose reporter is same as `default` reporter, but it also displays each individual test after the suite has finished. It also displays currently running tests that are taking longer than [`slowTestThreshold`](/config/#slowtestthreshold). Similar to `default` reporter, you can disable the summary by configuring the reporter. @@ -228,7 +199,7 @@ Example of final terminal output for a passing test suite: ### Dot Reporter -Prints a single dot for each completed test to provide minimal output while still showing all tests that have run. Details are only provided for failed tests, along with the `basic` reporter summary for the suite. +Prints a single dot for each completed test to provide minimal output while still showing all tests that have run. Details are only provided for failed tests, along with the summary for the suite. :::code-group ```bash [CLI] diff --git a/eslint.config.js b/eslint.config.js index ab0243ed4a6e..459c82188fd0 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -113,6 +113,7 @@ export default antfu( 'unused-imports/no-unused-imports': 'off', 'ts/method-signature-style': 'off', 'no-self-compare': 'off', + 'import/no-mutable-exports': 'off', }, }, { diff --git a/package.json b/package.json index b9ff289ec731..e32de768a896 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@vitest/monorepo", "type": "module", - "version": "3.2.3", + "version": "3.2.4", "private": true, "packageManager": "pnpm@10.12.1", "description": "Next generation testing framework powered by Vite", @@ -27,6 +27,7 @@ "test:ci": "CI=true pnpm -r --reporter-hide-prefix --stream --sequential --filter '@vitest/test-*' --filter !test-browser run test", "test:examples": "CI=true pnpm -r --reporter-hide-prefix --stream --filter '@vitest/example-*' run test", "test:ecosystem-ci": "ECOSYSTEM_CI=true pnpm test:ci", + "typebuild": "tsx ./scripts/explain-types.ts", "typecheck": "tsc -p tsconfig.check.json --noEmit", "typecheck:why": "tsc -p tsconfig.check.json --noEmit --explainFiles > explainTypes.txt", "ui:build": "vite build packages/ui", @@ -86,7 +87,6 @@ "@sinonjs/fake-timers@14.0.0": "patches/@sinonjs__fake-timers@14.0.0.patch", "cac@6.7.14": "patches/cac@6.7.14.patch", "@types/sinonjs__fake-timers@8.1.5": "patches/@types__sinonjs__fake-timers@8.1.5.patch", - "v8-to-istanbul@9.3.0": "patches/v8-to-istanbul@9.3.0.patch", "acorn@8.11.3": "patches/acorn@8.11.3.patch" }, "onlyBuiltDependencies": [ diff --git a/packages/browser/package.json b/packages/browser/package.json index 7fc568a90d15..7d717d625eb0 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -1,7 +1,7 @@ { "name": "@vitest/browser", "type": "module", - "version": "3.2.3", + "version": "3.2.4", "description": "Browser running for Vitest", "license": "MIT", "funding": "https://opencollective.com/vitest", @@ -64,6 +64,8 @@ "providers" ], "scripts": { + "typecheck": "tsc -p ./src/client/tsconfig.json --noEmit", + "typecheck:why": "tsc -p ./src/client/tsconfig.json --noEmit --explainFiles > explainTypes.txt", "build": "rimraf dist && pnpm build:node && pnpm build:client", "build:client": "vite build src/client", "build:node": "rollup -c", diff --git a/packages/browser/src/client/client.ts b/packages/browser/src/client/client.ts index fa9a72d39849..e425c18c6955 100644 --- a/packages/browser/src/client/client.ts +++ b/packages/browser/src/client/client.ts @@ -1,7 +1,7 @@ import type { ModuleMocker } from '@vitest/mocker/browser' import type { CancelReason } from '@vitest/runner' import type { BirpcReturn } from 'birpc' -import type { WebSocketBrowserEvents, WebSocketBrowserHandlers } from '../node/types' +import type { WebSocketBrowserEvents, WebSocketBrowserHandlers } from '../types' import type { IframeOrchestrator } from './orchestrator' import { createBirpc } from 'birpc' import { parse, stringify } from 'flatted' diff --git a/packages/browser/src/client/tester/context.ts b/packages/browser/src/client/tester/context.ts index 5385b322d09d..2c3d1c761db6 100644 --- a/packages/browser/src/client/tester/context.ts +++ b/packages/browser/src/client/tester/context.ts @@ -1,11 +1,14 @@ -import type { Options as TestingLibraryOptions, UserEvent as TestingLibraryUserEvent } from '@testing-library/user-event' -import type { RunnerTask } from 'vitest' +import type { + Options as TestingLibraryOptions, + UserEvent as TestingLibraryUserEvent, +} from '@testing-library/user-event' import type { BrowserLocators, BrowserPage, Locator, UserEvent, -} from '../../../context' +} from '@vitest/browser/context' +import type { RunnerTask } from 'vitest' import type { IframeViewportEvent } from '../client' import type { BrowserRunnerState } from '../utils' import type { Locator as LocatorAPI } from './locators/index' @@ -289,12 +292,19 @@ export const page: BrowserPage = { const name = options.path || `${taskName.replace(/[^a-z0-9]/gi, '-')}-${number}.png` - return ensureAwaited(error => triggerCommand('__vitest_screenshot', [name, processTimeoutOptions({ - ...options, - element: options.element - ? convertToSelector(options.element) - : undefined, - })], error)) + return ensureAwaited(error => triggerCommand( + '__vitest_screenshot', + [ + name, + processTimeoutOptions({ + ...options, + element: options.element + ? convertToSelector(options.element) + : undefined, + } as any /** TODO */), + ], + error, + )) }, getByRole() { throw new Error(`Method "getByRole" is not implemented in the "${provider}" provider.`) diff --git a/packages/browser/src/client/tester/locators/playwright.ts b/packages/browser/src/client/tester/locators/playwright.ts index 4c0f659fe126..ab6538f07386 100644 --- a/packages/browser/src/client/tester/locators/playwright.ts +++ b/packages/browser/src/client/tester/locators/playwright.ts @@ -1,4 +1,12 @@ -import type { UserEventClearOptions, UserEventClickOptions, UserEventDragAndDropOptions, UserEventFillOptions, UserEventHoverOptions, UserEventSelectOptions, UserEventUploadOptions } from '@vitest/browser/context' +import type { + UserEventClearOptions, + UserEventClickOptions, + UserEventDragAndDropOptions, + UserEventFillOptions, + UserEventHoverOptions, + UserEventSelectOptions, + UserEventUploadOptions, +} from '@vitest/browser/context' import { page, server } from '@vitest/browser/context' import { getByAltTextSelector, diff --git a/packages/browser/src/client/tester/locators/webdriverio.ts b/packages/browser/src/client/tester/locators/webdriverio.ts index 03765a71ac39..630862d84936 100644 --- a/packages/browser/src/client/tester/locators/webdriverio.ts +++ b/packages/browser/src/client/tester/locators/webdriverio.ts @@ -1,4 +1,9 @@ -import type { UserEventClickOptions, UserEventDragAndDropOptions, UserEventHoverOptions, UserEventSelectOptions } from '@vitest/browser/context' +import type { + UserEventClickOptions, + UserEventDragAndDropOptions, + UserEventHoverOptions, + UserEventSelectOptions, +} from '@vitest/browser/context' import { page, server } from '@vitest/browser/context' import { getByAltTextSelector, diff --git a/packages/browser/src/client/tester/rpc.ts b/packages/browser/src/client/tester/rpc.ts index cbf6ec1196d5..d683ef4da6c5 100644 --- a/packages/browser/src/client/tester/rpc.ts +++ b/packages/browser/src/client/tester/rpc.ts @@ -4,19 +4,14 @@ import { getSafeTimers } from 'vitest/internal/browser' const { get } = Reflect function withSafeTimers(getTimers: typeof getSafeTimers, fn: () => void) { - const { setTimeout, clearTimeout, setImmediate, clearImmediate } - = getTimers() + const { setTimeout, clearTimeout } = getTimers() const currentSetTimeout = globalThis.setTimeout const currentClearTimeout = globalThis.clearTimeout - const currentSetImmediate = globalThis.setImmediate - const currentClearImmediate = globalThis.clearImmediate try { globalThis.setTimeout = setTimeout globalThis.clearTimeout = clearTimeout - globalThis.setImmediate = setImmediate - globalThis.clearImmediate = clearImmediate const result = fn() return result @@ -24,8 +19,6 @@ function withSafeTimers(getTimers: typeof getSafeTimers, fn: () => void) { finally { globalThis.setTimeout = currentSetTimeout globalThis.clearTimeout = currentClearTimeout - globalThis.setImmediate = currentSetImmediate - globalThis.clearImmediate = currentClearImmediate } } diff --git a/packages/browser/src/client/tester/runner.ts b/packages/browser/src/client/tester/runner.ts index a0cabe1c6a79..ffe5e960125c 100644 --- a/packages/browser/src/client/tester/runner.ts +++ b/packages/browser/src/client/tester/runner.ts @@ -1,6 +1,5 @@ import type { CancelReason, File, Suite, Task, TaskEventPack, TaskResultPack, Test, TestAnnotation, VitestRunner } from '@vitest/runner' import type { SerializedConfig, TestExecutionMethod, WorkerGlobalState } from 'vitest' -import type { VitestExecutor } from 'vitest/execute' import type { VitestBrowserClientMocker } from './mocker' import { globalChannel, onCancel } from '@vitest/browser/client' import { page, userEvent } from '@vitest/browser/context' @@ -77,7 +76,7 @@ export function createBrowserRunner( if (this.config.browser.screenshotFailures && document.body.clientHeight > 0 && task.result?.state === 'fail') { const screenshot = await page.screenshot({ timeout: this.config.browser.providerOptions?.actionTimeout ?? 5_000, - }).catch((err) => { + } as any /** TODO */).catch((err) => { console.error('[vitest] Failed to take a screenshot', err) }) if (screenshot) { @@ -239,8 +238,8 @@ export async function initiateRunner( }) const [diffOptions] = await Promise.all([ - loadDiffConfig(config, executor as unknown as VitestExecutor), - loadSnapshotSerializers(config, executor as unknown as VitestExecutor), + loadDiffConfig(config, executor as any), + loadSnapshotSerializers(config, executor as any), ]) runner.config.diffOptions = diffOptions getWorkerState().onFilterStackTrace = (stack: string) => { diff --git a/packages/browser/src/client/tester/state.ts b/packages/browser/src/client/tester/state.ts index 5cdbd00e55d8..8bd0e6c41917 100644 --- a/packages/browser/src/client/tester/state.ts +++ b/packages/browser/src/client/tester/state.ts @@ -32,6 +32,7 @@ const state: WorkerGlobalState = { }, onCleanup: fn => getBrowserState().cleanups.push(fn), moduleCache: getBrowserState().moduleCache, + moduleExecutionInfo: new Map(), rpc: null as any, durations: { environment: 0, diff --git a/packages/browser/src/client/tsconfig.json b/packages/browser/src/client/tsconfig.json new file mode 100644 index 000000000000..958f3e6850b5 --- /dev/null +++ b/packages/browser/src/client/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "lib": [ + "dom", + "esnext", + "DOM.Iterable" + ], + "types": ["vite/client"], + "noEmit": true + }, + "include": [ + "./**/*.ts", + "../types.ts", + "../../matchers.d.ts", + "../../../vitest/src/integrations/chai/chai-subset.d.ts" + ], + "exclude": [ + "./vite.config.ts" + ] +} diff --git a/packages/browser/src/client/utils.ts b/packages/browser/src/client/utils.ts index b18aa80a18b7..a6ecd955627d 100644 --- a/packages/browser/src/client/utils.ts +++ b/packages/browser/src/client/utils.ts @@ -65,7 +65,7 @@ export function ensureAwaited(promise: (error?: Error) => Promise): Promis export interface BrowserRunnerState { files: string[] runningFiles: string[] - moduleCache: WorkerGlobalState['moduleCache'] + moduleCache: Map config: SerializedConfig provider: string runner: VitestRunner diff --git a/packages/browser/src/node/cdp.ts b/packages/browser/src/node/cdp.ts index 389a9f964da2..40c23797fadb 100644 --- a/packages/browser/src/node/cdp.ts +++ b/packages/browser/src/node/cdp.ts @@ -1,5 +1,5 @@ import type { CDPSession } from 'vitest/node' -import type { WebSocketBrowserRPC } from './types' +import type { WebSocketBrowserRPC } from '../types' export class BrowserServerCDPHandler { private listenerIds: Record = {} diff --git a/packages/browser/src/node/project.ts b/packages/browser/src/node/project.ts index 8e3f4deeda12..a9e73b6629e1 100644 --- a/packages/browser/src/node/project.ts +++ b/packages/browser/src/node/project.ts @@ -1,6 +1,6 @@ import type { StackTraceParserOptions } from '@vitest/utils/source-map' import type { ViteDevServer } from 'vite' -import type { ErrorWithDiff, ParsedStack, SerializedConfig } from 'vitest' +import type { ParsedStack, SerializedConfig, TestError } from 'vitest' import type { BrowserProvider, ProjectBrowser as IProjectBrowser, @@ -93,7 +93,7 @@ export class ProjectBrowser implements IProjectBrowser { } public parseErrorStacktrace( - e: ErrorWithDiff, + e: TestError, options: StackTraceParserOptions = {}, ): ParsedStack[] { return this.parent.parseErrorStacktrace(e, options) diff --git a/packages/browser/src/node/projectParent.ts b/packages/browser/src/node/projectParent.ts index 32c3e1863c00..bcce0b2ae3e9 100644 --- a/packages/browser/src/node/projectParent.ts +++ b/packages/browser/src/node/projectParent.ts @@ -1,6 +1,6 @@ import type { StackTraceParserOptions } from '@vitest/utils/source-map' import type { HtmlTagDescriptor } from 'vite' -import type { ErrorWithDiff, ParsedStack } from 'vitest' +import type { ParsedStack, TestError } from 'vitest' import type { BrowserCommand, BrowserScript, @@ -159,7 +159,7 @@ export class ParentBrowserProject { } public parseErrorStacktrace( - e: ErrorWithDiff, + e: TestError, options: StackTraceParserOptions = {}, ): ParsedStack[] { return parseErrorStacktrace(e, { diff --git a/packages/browser/src/node/rpc.ts b/packages/browser/src/node/rpc.ts index 7619c5f0fc4b..c7b19290cd18 100644 --- a/packages/browser/src/node/rpc.ts +++ b/packages/browser/src/node/rpc.ts @@ -1,12 +1,12 @@ import type { MockerRegistry } from '@vitest/mocker' import type { Duplex } from 'node:stream' -import type { ErrorWithDiff } from 'vitest' +import type { TestError } from 'vitest' import type { BrowserCommandContext, ResolveSnapshotPathHandlerContext, TestProject } from 'vitest/node' import type { WebSocket } from 'ws' +import type { WebSocketBrowserEvents, WebSocketBrowserHandlers } from '../types' import type { ParentBrowserProject } from './projectParent' import type { WebdriverBrowserProvider } from './providers/webdriver' import type { BrowserServerState } from './state' -import type { WebSocketBrowserEvents, WebSocketBrowserHandlers } from './types' import { existsSync, promises as fs } from 'node:fs' import { AutomockedModule, AutospiedModule, ManualMockedModule, RedirectedModule } from '@vitest/mocker' import { ServerMockResolver } from '@vitest/mocker/node' @@ -128,7 +128,7 @@ export function setupBrowserRpc(globalServer: ParentBrowserProject, defaultMocke { async onUnhandledError(error, type) { if (error && typeof error === 'object') { - const _error = error as ErrorWithDiff + const _error = error as TestError _error.stacks = globalServer.parseErrorStacktrace(_error) } vitest.state.catchError(error, type) diff --git a/packages/browser/src/node/state.ts b/packages/browser/src/node/state.ts index 2f04dc5cb332..9561c234d66e 100644 --- a/packages/browser/src/node/state.ts +++ b/packages/browser/src/node/state.ts @@ -1,5 +1,5 @@ import type { BrowserServerState as IBrowserServerState } from 'vitest/node' -import type { WebSocketBrowserRPC } from './types' +import type { WebSocketBrowserRPC } from '../types' export class BrowserServerState implements IBrowserServerState { public readonly orchestrators: Map = new Map() diff --git a/packages/browser/src/node/types.ts b/packages/browser/src/types.ts similarity index 82% rename from packages/browser/src/node/types.ts rename to packages/browser/src/types.ts index 742b3544c6f1..94fcd72e6381 100644 --- a/packages/browser/src/node/types.ts +++ b/packages/browser/src/types.ts @@ -1,13 +1,12 @@ -import type { MockedModuleSerialized } from '@vitest/mocker' -import type { ServerIdResolution, ServerMockResolution } from '@vitest/mocker/node' +import type { MockedModuleSerialized, ServerIdResolution, ServerMockResolution } from '@vitest/mocker' import type { TaskEventPack, TaskResultPack, TestAnnotation } from '@vitest/runner' import type { BirpcReturn } from 'birpc' import type { AfterSuiteRunMeta, BrowserTesterOptions, CancelReason, - Reporter, RunnerTestFile, + SerializedTestSpecification, SnapshotResult, TestExecutionMethod, UserConsoleLog, @@ -60,16 +59,19 @@ export interface WebSocketBrowserHandlers { trackCdpEvent: (sessionId: string, type: 'on' | 'once' | 'off', event: string, listenerId: string) => void } -export interface WebSocketEvents - extends Pick< - Reporter, - | 'onCollected' - | 'onFinished' - | 'onTaskUpdate' - | 'onUserConsoleLog' - | 'onPathsCollected' - | 'onSpecsCollected' - > { +export type Awaitable = T | PromiseLike + +export interface WebSocketEvents { + onCollected?: (files: RunnerTestFile[]) => Awaitable + onFinished?: ( + files: File[], + errors: unknown[], + coverage?: unknown + ) => Awaitable + onTaskUpdate?: (packs: TaskResultPack[]) => Awaitable + onUserConsoleLog?: (log: UserConsoleLog) => Awaitable + onPathsCollected?: (paths?: string[]) => Awaitable + onSpecsCollected?: (specs?: SerializedTestSpecification[]) => Awaitable onFinishedReportCoverage: () => void } diff --git a/packages/browser/tsconfig.json b/packages/browser/tsconfig.json index 84f0e3d1d81e..89c57bbd409f 100644 --- a/packages/browser/tsconfig.json +++ b/packages/browser/tsconfig.json @@ -4,5 +4,10 @@ "types": ["node", "vite/client"], "isolatedDeclarations": true }, - "exclude": ["dist", "node_modules", "**/vite.config.ts"] + "exclude": [ + "dist", + "node_modules", + "**/vite.config.ts", + "src/client/**/*.ts" + ] } diff --git a/packages/coverage-istanbul/package.json b/packages/coverage-istanbul/package.json index 90c2acefc0bc..b35439ec51ef 100644 --- a/packages/coverage-istanbul/package.json +++ b/packages/coverage-istanbul/package.json @@ -1,7 +1,7 @@ { "name": "@vitest/coverage-istanbul", "type": "module", - "version": "3.2.3", + "version": "3.2.4", "description": "Istanbul coverage provider for Vitest", "author": "Anthony Fu ", "license": "MIT", @@ -52,7 +52,6 @@ "istanbul-lib-source-maps": "catalog:", "istanbul-reports": "catalog:", "magicast": "catalog:", - "test-exclude": "catalog:", "tinyrainbow": "catalog:" }, "devDependencies": { @@ -62,7 +61,6 @@ "@types/istanbul-lib-report": "catalog:", "@types/istanbul-lib-source-maps": "catalog:", "@types/istanbul-reports": "catalog:", - "@types/test-exclude": "catalog:", "pathe": "catalog:", "vitest": "workspace:*" } diff --git a/packages/coverage-istanbul/src/provider.ts b/packages/coverage-istanbul/src/provider.ts index 7a169eb400e3..2ceddd4f8d53 100644 --- a/packages/coverage-istanbul/src/provider.ts +++ b/packages/coverage-istanbul/src/provider.ts @@ -12,8 +12,6 @@ import libReport from 'istanbul-lib-report' import libSourceMaps from 'istanbul-lib-source-maps' import reports from 'istanbul-reports' import { parseModule } from 'magicast' -import { resolve } from 'pathe' -import TestExclude from 'test-exclude' import c from 'tinyrainbow' import { BaseCoverageProvider } from 'vitest/coverage' import { isCSSRequest } from 'vitest/node' @@ -27,20 +25,10 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider initialize(ctx: Vitest): void { this._initialize(ctx) - this.testExclude = new TestExclude({ - cwd: ctx.config.root, - include: this.options.include, - exclude: this.options.exclude, - excludeNodeModules: true, - extension: this.options.extension, - relativePath: !this.options.allowExternal, - }) - this.instrumenter = createInstrumenter({ produceSourceMap: true, autoWrap: false, @@ -69,7 +57,7 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider this.testExclude.shouldInstrument(filename)) + coverageMap.filter(filename => this.isIncluded(filename)) } if (debug.enabled) { @@ -174,20 +162,7 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider - resolve(this.ctx.config.root, file), - ) - - if (this.ctx.config.changed) { - includedFiles = (this.ctx.config.related || []).filter(file => - includedFiles.includes(file), - ) - } - - const uncoveredFiles = includedFiles - .filter(file => !coveredFiles.includes(file)) - .sort() + const uncoveredFiles = await this.getUntestedFiles(coveredFiles) const cacheKey = new Date().getTime() const coverageMap = this.createCoverageMap() diff --git a/packages/coverage-v8/package.json b/packages/coverage-v8/package.json index fe8e3f828ce3..7fa8bec408a1 100644 --- a/packages/coverage-v8/package.json +++ b/packages/coverage-v8/package.json @@ -1,7 +1,7 @@ { "name": "@vitest/coverage-v8", "type": "module", - "version": "3.2.3", + "version": "3.2.4", "description": "V8 coverage provider for Vitest", "author": "Anthony Fu ", "license": "MIT", @@ -65,7 +65,6 @@ "magic-string": "catalog:", "magicast": "catalog:", "std-env": "catalog:", - "test-exclude": "catalog:", "tinyrainbow": "catalog:" }, "devDependencies": { @@ -74,10 +73,8 @@ "@types/istanbul-lib-report": "catalog:", "@types/istanbul-lib-source-maps": "catalog:", "@types/istanbul-reports": "catalog:", - "@types/test-exclude": "catalog:", "@vitest/browser": "workspace:*", "pathe": "catalog:", - "v8-to-istanbul": "^9.3.0", "vite-node": "workspace:*", "vitest": "workspace:*" } diff --git a/packages/coverage-v8/src/provider.ts b/packages/coverage-v8/src/provider.ts index 76acffcbc4ff..8ad81a2ff6cc 100644 --- a/packages/coverage-v8/src/provider.ts +++ b/packages/coverage-v8/src/provider.ts @@ -6,7 +6,6 @@ import type { AfterSuiteRunMeta } from 'vitest' import type { CoverageProvider, ReportContext, ResolvedCoverageOptions, TestProject, Vitest } from 'vitest/node' import { promises as fs } from 'node:fs' import { fileURLToPath, pathToFileURL } from 'node:url' -import remapping from '@ampproject/remapping' // @ts-expect-error -- untyped import { mergeProcessCovs } from '@bcoe/v8-coverage' import astV8ToIstanbul from 'ast-v8-to-istanbul' @@ -15,13 +14,10 @@ import libCoverage from 'istanbul-lib-coverage' import libReport from 'istanbul-lib-report' import libSourceMaps from 'istanbul-lib-source-maps' import reports from 'istanbul-reports' -import MagicString from 'magic-string' import { parseModule } from 'magicast' -import { normalize, resolve } from 'pathe' +import { normalize } from 'pathe' import { provider } from 'std-env' -import TestExclude from 'test-exclude' import c from 'tinyrainbow' -import v8ToIstanbul from 'v8-to-istanbul' import { cleanUrl } from 'vite-node/utils' import { BaseCoverageProvider } from 'vitest/coverage' @@ -35,11 +31,6 @@ export interface ScriptCoverageWithOffset extends Profiler.ScriptCoverage { type TransformResults = Map interface RawCoverage { result: ScriptCoverageWithOffset[] } -// Note that this needs to match the line ending as well -const VITE_EXPORTS_LINE_PATTERN - = /Object\.defineProperty\(__vite_ssr_exports__.*\n/g -const DECORATOR_METADATA_PATTERN - = /_ts_metadata\("design:paramtypes", \[[^\]]*\]\),*/g const FILE_PROTOCOL = 'file://' const debug = createDebug('vitest:coverage') @@ -47,19 +38,9 @@ const debug = createDebug('vitest:coverage') export class V8CoverageProvider extends BaseCoverageProvider> implements CoverageProvider { name = 'v8' as const version: string = version - testExclude!: InstanceType initialize(ctx: Vitest): void { this._initialize(ctx) - - this.testExclude = new TestExclude({ - cwd: ctx.config.root, - include: this.options.include, - exclude: this.options.exclude, - excludeNodeModules: true, - extension: this.options.extension, - relativePath: !this.options.allowExternal, - }) } createCoverageMap(): CoverageMap { @@ -103,15 +84,15 @@ export class V8CoverageProvider extends BaseCoverageProvider this.testExclude.shouldInstrument(filename)) + coverageMap.filter(filename => this.isIncluded(filename)) } if (debug.enabled) { @@ -165,26 +146,13 @@ export class V8CoverageProvider extends BaseCoverageProvider { + private async getCoverageMapForUncoveredFiles(testedFiles: string[]): Promise { const transformResults = normalizeTransformResults( this.ctx.vitenode.fetchCache, ) const transform = this.createUncoveredFileTransformer(this.ctx) - const allFiles = await this.testExclude.glob(this.ctx.config.root) - let includedFiles = allFiles.map(file => - resolve(this.ctx.config.root, file), - ) - - if (this.ctx.config.changed) { - includedFiles = (this.ctx.config.related || []).filter(file => - includedFiles.includes(file), - ) - } - - const uncoveredFiles = includedFiles - .map(file => pathToFileURL(file)) - .filter(file => !testedFiles.includes(file.pathname)) + const uncoveredFiles = await this.getUntestedFiles(testedFiles) let index = 0 @@ -202,31 +170,21 @@ export class V8CoverageProvider extends BaseCoverageProvider debug(c.bgRed(`File "${filename.pathname}" is taking longer than 3s`)), 3_000) + timeout = setTimeout(() => debug(c.bgRed(`File "${filename}" is taking longer than 3s`)), 3_000) } + const url = pathToFileURL(filename) const sources = await this.getSources( - filename.href, + url.href, transformResults, transform, ) - coverageMap.merge(await this.v8ToIstanbul( - filename.href, + coverageMap.merge(await this.remapCoverage( + url.href, 0, sources, - [{ - ranges: [ - { - startOffset: 0, - endOffset: sources.originalSource.length, - count: 0, - }, - ], - isBlockCoverage: true, - // This is magical value that indicates an empty report: https://github.com/istanbuljs/v8-to-istanbul/blob/fca5e6a9e6ef38a9cdc3a178d5a6cf9ef82e6cab/lib/v8-to-istanbul.js#LL131C40-L131C40 - functionName: '(empty-report)', - }], + [], )) if (debug.enabled) { @@ -234,7 +192,7 @@ export class V8CoverageProvider extends BaseCoverageProvider 500 ? c.bgRed : c.bgGreen - debug(`${color(` ${diff.toFixed()} ms `)} ${filename.pathname}`) + debug(`${color(` ${diff.toFixed()} ms `)} ${filename}`) } })) } @@ -242,118 +200,109 @@ export class V8CoverageProvider extends BaseCoverageProvider>, functions: Profiler.FunctionCoverage[]) { - if (this.options.experimentalAstAwareRemapping) { - let ast - try { - ast = await parseAstAsync(sources.source) - } - catch (error) { - this.ctx.logger.error(`Failed to parse ${filename}. Excluding it from coverage.\n`, error) - return {} - } + private async remapCoverage(filename: string, wrapperLength: number, result: Awaited>, functions: Profiler.FunctionCoverage[]) { + let ast - return await astV8ToIstanbul({ - code: sources.source, - sourceMap: sources.sourceMap?.sourcemap, - ast, - coverage: { functions, url: filename }, - ignoreClassMethods: this.options.ignoreClassMethods, - wrapperLength, - ignoreNode: (node, type) => { - // SSR transformed imports - if ( - type === 'statement' - && node.type === 'VariableDeclarator' - && node.id.type === 'Identifier' - && node.id.name.startsWith('__vite_ssr_import_') - ) { - return true - } + try { + ast = await parseAstAsync(result.code) + } + catch (error) { + this.ctx.logger.error(`Failed to parse ${filename}. Excluding it from coverage.\n`, error) + return {} + } - // SSR transformed exports vite@>6.3.5 - if ( - type === 'statement' - && node.type === 'ExpressionStatement' - && node.expression.type === 'AssignmentExpression' - && node.expression.left.type === 'MemberExpression' - && node.expression.left.object.type === 'Identifier' - && node.expression.left.object.name === '__vite_ssr_exports__' - ) { - return true - } + return await astV8ToIstanbul({ + code: result.code, + sourceMap: result.map, + ast, + coverage: { functions, url: filename }, + ignoreClassMethods: this.options.ignoreClassMethods, + wrapperLength, + ignoreNode: (node, type) => { + // SSR transformed imports + if ( + type === 'statement' + && node.type === 'VariableDeclarator' + && node.id.type === 'Identifier' + && node.id.name.startsWith('__vite_ssr_import_') + ) { + return true + } + + // SSR transformed exports vite@>6.3.5 + if ( + type === 'statement' + && node.type === 'ExpressionStatement' + && node.expression.type === 'AssignmentExpression' + && node.expression.left.type === 'MemberExpression' + && node.expression.left.object.type === 'Identifier' + && node.expression.left.object.name === '__vite_ssr_exports__' + ) { + return true + } - // SSR transformed exports vite@^6.3.5 + // SSR transformed exports vite@^6.3.5 + if ( + type === 'statement' + && node.type === 'VariableDeclarator' + && node.id.type === 'Identifier' + && node.id.name === '__vite_ssr_export_default__' + ) { + return true + } + + // in-source test with "if (import.meta.vitest)" + if ( + (type === 'branch' || type === 'statement') + && node.type === 'IfStatement' + && node.test.type === 'MemberExpression' + && node.test.property.type === 'Identifier' + && node.test.property.name === 'vitest' + ) { + // SSR if ( - type === 'statement' - && node.type === 'VariableDeclarator' - && node.id.type === 'Identifier' - && node.id.name === '__vite_ssr_export_default__' + node.test.object.type === 'Identifier' + && node.test.object.name === '__vite_ssr_import_meta__' ) { - return true + return 'ignore-this-and-nested-nodes' } - // in-source test with "if (import.meta.vitest)" + // Web if ( - (type === 'branch' || type === 'statement') - && node.type === 'IfStatement' - && node.test.type === 'MemberExpression' - && node.test.property.type === 'Identifier' - && node.test.property.name === 'vitest' + node.test.object.type === 'MetaProperty' + && node.test.object.meta.name === 'import' + && node.test.object.property.name === 'meta' ) { - // SSR - if ( - node.test.object.type === 'Identifier' - && node.test.object.name === '__vite_ssr_import_meta__' - ) { - return 'ignore-this-and-nested-nodes' - } - - // Web - if ( - node.test.object.type === 'MetaProperty' - && node.test.object.meta.name === 'import' - && node.test.object.property.name === 'meta' - ) { - return 'ignore-this-and-nested-nodes' - } + return 'ignore-this-and-nested-nodes' } + } - // Browser mode's "import.meta.env =" - if ( - type === 'statement' - && node.type === 'ExpressionStatement' - && node.expression.type === 'AssignmentExpression' - && node.expression.left.type === 'MemberExpression' - && node.expression.left.object.type === 'MetaProperty' - && node.expression.left.object.meta.name === 'import' - && node.expression.left.object.property.name === 'meta' - && node.expression.left.property.type === 'Identifier' - && node.expression.left.property.name === 'env') { - return true - } - }, - }, - ) - } + // Browser mode's "import.meta.env =" + if ( + type === 'statement' + && node.type === 'ExpressionStatement' + && node.expression.type === 'AssignmentExpression' + && node.expression.left.type === 'MemberExpression' + && node.expression.left.object.type === 'MetaProperty' + && node.expression.left.object.meta.name === 'import' + && node.expression.left.object.property.name === 'meta' + && node.expression.left.property.type === 'Identifier' + && node.expression.left.property.name === 'env') { + return true + } - const converter = v8ToIstanbul( - filename, - wrapperLength, - sources, - undefined, - this.options.ignoreEmptyLines, + // SWC's decorators + if ( + type === 'statement' + && node.type === 'ExpressionStatement' + && node.expression.type === 'CallExpression' + && node.expression.callee.type === 'Identifier' + && node.expression.callee.name === '_ts_decorate') { + return 'ignore-this-and-nested-nodes' + } + }, + }, ) - await converter.load() - - try { - converter.applyCoverage(functions) - } - catch (error) { - this.ctx.logger.error(`Failed to convert coverage for ${filename}.\n`, error) - } - - return converter.toIstanbul() } private async getSources>)>( @@ -362,9 +311,8 @@ export class V8CoverageProvider extends BaseCoverageProvider Promise, functions: Profiler.FunctionCoverage[] = [], ): Promise<{ - source: string - originalSource: string - sourceMap?: { sourcemap: EncodedSourceMap } + code: string + map?: EncodedSourceMap }> { const filePath = normalize(fileURLToPath(url)) @@ -376,45 +324,32 @@ export class V8CoverageProvider extends BaseCoverageProvider { + if (!code) { + const original = await fs.readFile(filePath, 'utf-8').catch(() => { // If file does not exist construct a dummy source for it. // These can be files that were generated dynamically during the test run and were removed after it. const length = findLongestFunctionLength(functions) return '/'.repeat(length) }) - } - // These can be uncovered files included by "all: true" or files that are loaded outside vite-node - if (!map) { - return { - source: code || sourcesContent[0], - originalSource: sourcesContent[0], - } + return { code: original } } - const sources = (map.sources || []) - .filter(source => source != null) - .map(source => new URL(source, url).href) + // Vue needs special handling for "map.sources" + if (map) { + map.sources ||= [] - if (sources.length === 0) { - sources.push(url) - } + map.sources = map.sources + .filter(source => source != null) + .map(source => new URL(source, url).href) - return { - originalSource: sourcesContent[0], - source: code || sourcesContent[0], - sourceMap: { - sourcemap: excludeGeneratedCode(code, { - ...map, - version: 3, - sources, - sourcesContent, - }), - }, + if (map.sources.length === 0) { + map.sources.push(url) + } } + + return { code, map } } private async convertCoverage( @@ -456,7 +391,7 @@ export class V8CoverageProvider extends BaseCoverageProvider - '\n'.repeat(match.split('\n').length - 1)) - - const trimmedMap = trimmed.generateMap({ hires: 'boundary' }) - - // A merged source map where the first one excludes generated parts - const combinedMap = remapping( - [{ ...trimmedMap, version: 3 }, map], - () => null, - ) - - return combinedMap as EncodedSourceMap -} - /** * Find the function with highest `endOffset` to determine the length of the file */ diff --git a/packages/expect/package.json b/packages/expect/package.json index fbda22d2221a..ae9cf1b5036f 100644 --- a/packages/expect/package.json +++ b/packages/expect/package.json @@ -1,7 +1,7 @@ { "name": "@vitest/expect", "type": "module", - "version": "3.2.3", + "version": "3.2.4", "description": "Jest's expect matchers as a Chai plugin", "license": "MIT", "funding": "https://opencollective.com/vitest", diff --git a/packages/mocker/package.json b/packages/mocker/package.json index 66cd2a05eb66..832ff4c5ab5f 100644 --- a/packages/mocker/package.json +++ b/packages/mocker/package.json @@ -1,7 +1,7 @@ { "name": "@vitest/mocker", "type": "module", - "version": "3.2.3", + "version": "3.2.4", "description": "Vitest module mocker implementation", "license": "MIT", "funding": "https://opencollective.com/vitest", diff --git a/packages/mocker/src/index.ts b/packages/mocker/src/index.ts index 062e194e332e..3909b5a55e25 100644 --- a/packages/mocker/src/index.ts +++ b/packages/mocker/src/index.ts @@ -22,4 +22,6 @@ export type { ModuleMockFactory, ModuleMockFactoryWithHelper, ModuleMockOptions, + ServerIdResolution, + ServerMockResolution, } from './types' diff --git a/packages/mocker/src/node/index.ts b/packages/mocker/src/node/index.ts index 42f7cf80ff02..88376fac0d43 100644 --- a/packages/mocker/src/node/index.ts +++ b/packages/mocker/src/node/index.ts @@ -10,8 +10,4 @@ export type { InterceptorPluginOptions } from './interceptorPlugin' export { mockerPlugin } from './mockerPlugin' export { findMockRedirect } from './redirect' export { ServerMockResolver } from './resolver' -export type { - ServerIdResolution, - ServerMockResolution, - ServerResolverOptions, -} from './resolver' +export type { ServerResolverOptions } from './resolver' diff --git a/packages/mocker/src/node/resolver.ts b/packages/mocker/src/node/resolver.ts index 834472b22ea0..d0aadbf8ed91 100644 --- a/packages/mocker/src/node/resolver.ts +++ b/packages/mocker/src/node/resolver.ts @@ -1,4 +1,5 @@ import type { Rollup, ResolvedConfig as ViteConfig, ViteDevServer } from 'vite' +import type { ServerIdResolution, ServerMockResolution } from '../types' import { existsSync, readFileSync } from 'node:fs' import { isAbsolute, join, resolve } from 'pathe' import { cleanUrl } from '../utils' @@ -181,17 +182,3 @@ function withTrailingSlash(path: string): string { return path } - -export interface ServerMockResolution { - mockType: 'manual' | 'redirect' | 'automock' | 'autospy' - resolvedId: string - resolvedUrl: string - needsInterop?: boolean - redirectUrl?: string | null -} - -export interface ServerIdResolution { - id: string - url: string - optimized: boolean -} diff --git a/packages/mocker/src/types.ts b/packages/mocker/src/types.ts index caf9964d47e1..facd7c01ee51 100644 --- a/packages/mocker/src/types.ts +++ b/packages/mocker/src/types.ts @@ -7,3 +7,17 @@ export type ModuleMockFactory = () => any export interface ModuleMockOptions { spy?: boolean } + +export interface ServerMockResolution { + mockType: 'manual' | 'redirect' | 'automock' | 'autospy' + resolvedId: string + resolvedUrl: string + needsInterop?: boolean + redirectUrl?: string | null +} + +export interface ServerIdResolution { + id: string + url: string + optimized: boolean +} diff --git a/packages/pretty-format/package.json b/packages/pretty-format/package.json index c8775051c10a..86e233cadb2d 100644 --- a/packages/pretty-format/package.json +++ b/packages/pretty-format/package.json @@ -1,7 +1,7 @@ { "name": "@vitest/pretty-format", "type": "module", - "version": "3.2.3", + "version": "3.2.4", "description": "Fork of pretty-format with support for ESM", "license": "MIT", "funding": "https://opencollective.com/vitest", diff --git a/packages/runner/package.json b/packages/runner/package.json index 28cac6e51df1..1a38cd5ceb0e 100644 --- a/packages/runner/package.json +++ b/packages/runner/package.json @@ -1,7 +1,7 @@ { "name": "@vitest/runner", "type": "module", - "version": "3.2.3", + "version": "3.2.4", "description": "Vitest test runner", "license": "MIT", "funding": "https://opencollective.com/vitest", diff --git a/packages/runner/src/suite.ts b/packages/runner/src/suite.ts index 1a8f6a0d4b04..cf473eec3a98 100644 --- a/packages/runner/src/suite.ts +++ b/packages/runner/src/suite.ts @@ -770,7 +770,8 @@ export function createTaskCollector( runner, ) - return createTest(function fn( + const originalWrapper = fn + return createTest(function ( name: string | Function, optionsOrFn?: TestOptions | TestFunction, optionsOrTest?: number | TestOptions | TestFunction, @@ -784,12 +785,9 @@ export function createTaskCollector( scopedFixtures, ) } - collector.test.fn.call( - context, - formatName(name), - optionsOrFn as TestOptions, - optionsOrTest as TestFunction, - ) + const { handler, options } = parseArguments(optionsOrFn, optionsOrTest) + const timeout = options.timeout ?? runner?.config.testTimeout + originalWrapper.call(context, formatName(name), handler, timeout) }, _context) } diff --git a/packages/runner/src/types/tasks.ts b/packages/runner/src/types/tasks.ts index 4554a45016b3..d06966102124 100644 --- a/packages/runner/src/types/tasks.ts +++ b/packages/runner/src/types/tasks.ts @@ -1,4 +1,4 @@ -import type { Awaitable, ErrorWithDiff } from '@vitest/utils' +import type { Awaitable, TestError } from '@vitest/utils' import type { FixtureItem } from '../fixture' import type { ChainableFunction } from '../utils/chain' @@ -118,7 +118,7 @@ export interface TaskResult { * Errors that occurred during the task execution. It is possible to have several errors * if `expect.soft()` failed multiple times or `retry` was triggered. */ - errors?: ErrorWithDiff[] + errors?: TestError[] /** * How long in milliseconds the task took to run. */ diff --git a/packages/snapshot/package.json b/packages/snapshot/package.json index 783df5253722..c7505787470a 100644 --- a/packages/snapshot/package.json +++ b/packages/snapshot/package.json @@ -1,7 +1,7 @@ { "name": "@vitest/snapshot", "type": "module", - "version": "3.2.3", + "version": "3.2.4", "description": "Vitest snapshot manager", "license": "MIT", "funding": "https://opencollective.com/vitest", diff --git a/packages/snapshot/src/index.ts b/packages/snapshot/src/index.ts index aaccc0bff63a..4fa23f225b9f 100644 --- a/packages/snapshot/src/index.ts +++ b/packages/snapshot/src/index.ts @@ -6,6 +6,7 @@ export { default as SnapshotState } from './port/state' export type { SnapshotData, + SnapshotEnvironment, SnapshotMatchOptions, SnapshotResult, SnapshotSerializer, diff --git a/packages/spy/package.json b/packages/spy/package.json index 0f9e2550917f..6b3b5024a9c0 100644 --- a/packages/spy/package.json +++ b/packages/spy/package.json @@ -1,7 +1,7 @@ { "name": "@vitest/spy", "type": "module", - "version": "3.2.3", + "version": "3.2.4", "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 6c6aa717bfc0..ffb251f99b6b 100644 --- a/packages/spy/src/index.ts +++ b/packages/spy/src/index.ts @@ -467,14 +467,34 @@ export function spyOn( state = fn.mock._state() } - const stub = tinyspy.internalSpyOn(obj, objMethod as any) - const spy = enhanceSpy(stub) as MockInstance + try { + const stub = tinyspy.internalSpyOn(obj, objMethod as any) - if (state) { - spy.mock._state(state) + const spy = enhanceSpy(stub) as MockInstance + + if (state) { + spy.mock._state(state) + } + + return spy } + catch (error) { + if ( + error instanceof TypeError + && Symbol.toStringTag + && (obj as any)[Symbol.toStringTag] === 'Module' + && (error.message.includes('Cannot redefine property') + || error.message.includes('Cannot replace module namespace') + || error.message.includes('can\'t redefine non-configurable property')) + ) { + throw new TypeError( + `Cannot spy on export "${String(objMethod)}". Module namespace is not configurable in ESM. See: https://vitest.dev/guide/browser/#limitations`, + { cause: error }, + ) + } - return spy + throw error + } } let callOrder = 0 diff --git a/packages/ui/client/components/dashboard/ErrorEntry.vue b/packages/ui/client/components/dashboard/ErrorEntry.vue index 88801f970ea3..e814ca6e304e 100644 --- a/packages/ui/client/components/dashboard/ErrorEntry.vue +++ b/packages/ui/client/components/dashboard/ErrorEntry.vue @@ -1,8 +1,8 @@ diff --git a/packages/ui/client/components/views/ViewEditor.vue b/packages/ui/client/components/views/ViewEditor.vue index d591f89194bd..e19bb635b39f 100644 --- a/packages/ui/client/components/views/ViewEditor.vue +++ b/packages/ui/client/components/views/ViewEditor.vue @@ -1,7 +1,7 @@