[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
12

Configure Feed

Select the types of activity you want to include in your feed.

feat: deprecate `workspace` in favor of `projects` (#7923)

Co-authored-by: Ari Perkkiö <ari.perkkio@gmail.com>

authored by

Vladimir
Ari Perkkiö
and committed by
GitHub
(May 5, 2025, 6:49 PM +0200) 41beb261 5f504958

+1742 -1735
+1 -1
README.md
··· 43 43 - Components testing ([Vue](https://github.com/vitest-tests/browser-examples/tree/main/examples/vue), [React](https://github.com/vitest-tests/browser-examples/tree/main/examples/react), [Svelte](https://github.com/vitest-tests/browser-examples/tree/main/examples/svelte), [Lit](./examples/lit), [Marko](https://github.com/marko-js/examples/tree/master/examples/library-ts)) 44 44 - Workers multi-threading via [Tinypool](https://github.com/tinylibs/tinypool) (a lightweight fork of [Piscina](https://github.com/piscinajs/piscina)) 45 45 - Benchmarking support with [Tinybench](https://github.com/tinylibs/tinybench) 46 - - [Workspace](https://vitest.dev/guide/workspace) support 46 + - [Projects](https://vitest.dev/guide/projects) support 47 47 - [expect-type](https://github.com/mmkal/expect-type) for type-level testing 48 48 - ESM first, top level await 49 49 - Out-of-box TypeScript / JSX support
+10
netlify.toml
··· 25 25 to = "/config/" 26 26 status = 301 27 27 28 + [[redirects]] 29 + from = "/guide/workspace" 30 + to = "/guide/projects" 31 + status = 301 32 + 33 + [[redirects]] 34 + from = "/guide/workspace.html" 35 + to = "/guide/projects" 36 + status = 301 37 + 28 38 [[headers]] 29 39 for = "/manifest.webmanifest" 30 40
+27 -49
pnpm-lock.yaml
··· 382 382 specifier: workspace:* 383 383 version: link:../../packages/vitest 384 384 385 - examples/typecheck: 386 - devDependencies: 387 - '@types/node': 388 - specifier: ^20.17.30 389 - version: 20.17.30 390 - '@vitest/ui': 391 - specifier: workspace:* 392 - version: link:../../packages/ui 393 - typescript: 394 - specifier: ^5.8.3 395 - version: 5.8.3 396 - vite: 397 - specifier: ^6.2.0 398 - version: 6.2.0(@types/node@20.17.30)(jiti@2.4.2)(terser@5.36.0)(tsx@4.19.3)(yaml@2.7.0) 399 - vitest: 400 - specifier: workspace:* 401 - version: link:../../packages/vitest 402 - 403 - examples/workspace: 385 + examples/projects: 404 386 devDependencies: 405 387 '@testing-library/jest-dom': 406 388 specifier: ^6.6.3 ··· 438 420 vite: 439 421 specifier: ^6.2.0 440 422 version: 6.2.0(@types/node@22.14.1)(jiti@2.4.2)(terser@5.36.0)(tsx@4.19.3)(yaml@2.7.0) 423 + vitest: 424 + specifier: workspace:* 425 + version: link:../../packages/vitest 426 + 427 + examples/typecheck: 428 + devDependencies: 429 + '@types/node': 430 + specifier: ^20.17.30 431 + version: 20.17.30 432 + '@vitest/ui': 433 + specifier: workspace:* 434 + version: link:../../packages/ui 435 + typescript: 436 + specifier: ^5.8.3 437 + version: 5.8.3 438 + vite: 439 + specifier: ^6.2.0 440 + version: 6.2.0(@types/node@20.17.30)(jiti@2.4.2)(terser@5.36.0)(tsx@4.19.3)(yaml@2.7.0) 441 441 vitest: 442 442 specifier: workspace:* 443 443 version: link:../../packages/vitest ··· 5476 5476 resolution: {integrity: sha512-Fv96DCsdOgB6mdGl67MT5JaTNKRzrzill5OH5s8bjYJXVlcXyPYGyPsUkWyGV5p1TXI5esYIYMMeDJL0hEIwaA==} 5477 5477 engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 5478 5478 5479 - decimal.js@10.4.3: 5480 - resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} 5481 - 5482 5479 decimal.js@10.5.0: 5483 5480 resolution: {integrity: sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==} 5484 5481 ··· 8768 8765 tr46@1.0.1: 8769 8766 resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} 8770 8767 8771 - tr46@5.0.0: 8772 - resolution: {integrity: sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==} 8773 - engines: {node: '>=18'} 8774 - 8775 8768 tr46@5.1.0: 8776 8769 resolution: {integrity: sha512-IUWnUK7ADYR5Sl1fZlO1INDUhVhatWl7BtJWsIhwJ0UAK7ilzzIa8uIqOO/aYVWHZPJkKbEL+362wrzoeRF7bw==} 8777 8770 engines: {node: '>=18'} ··· 9340 9333 9341 9334 whatwg-mimetype@4.0.0: 9342 9335 resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} 9343 - engines: {node: '>=18'} 9344 - 9345 - whatwg-url@14.1.0: 9346 - resolution: {integrity: sha512-jlf/foYIKywAt3x/XWKZ/3rz8OSJPiWktjmk891alJUEjiVxKX9LEO92qH3hv4aJ0mN3MWPvGMCy8jQi95xK4w==} 9347 9336 engines: {node: '>=18'} 9348 9337 9349 9338 whatwg-url@14.2.0: ··· 11891 11880 11892 11881 '@types/babel__core@7.20.5': 11893 11882 dependencies: 11894 - '@babel/parser': 7.26.2 11895 - '@babel/types': 7.26.0 11883 + '@babel/parser': 7.27.0 11884 + '@babel/types': 7.27.0 11896 11885 '@types/babel__generator': 7.6.6 11897 11886 '@types/babel__template': 7.4.3 11898 11887 '@types/babel__traverse': 7.20.3 11899 11888 11900 11889 '@types/babel__generator@7.6.6': 11901 11890 dependencies: 11902 - '@babel/types': 7.26.0 11891 + '@babel/types': 7.27.0 11903 11892 11904 11893 '@types/babel__template@7.4.3': 11905 11894 dependencies: 11906 - '@babel/parser': 7.26.2 11907 - '@babel/types': 7.26.0 11895 + '@babel/parser': 7.27.0 11896 + '@babel/types': 7.27.0 11908 11897 11909 11898 '@types/babel__traverse@7.20.3': 11910 11899 dependencies: 11911 - '@babel/types': 7.26.0 11900 + '@babel/types': 7.27.0 11912 11901 11913 11902 '@types/braces@3.0.1': {} 11914 11903 ··· 13651 13640 ms: 2.1.3 13652 13641 13653 13642 decamelize@6.0.0: {} 13654 - 13655 - decimal.js@10.4.3: {} 13656 13643 13657 13644 decimal.js@10.5.0: {} 13658 13645 ··· 15435 15422 dependencies: 15436 15423 cssstyle: 4.2.1 15437 15424 data-urls: 5.0.0 15438 - decimal.js: 10.4.3 15425 + decimal.js: 10.5.0 15439 15426 form-data: 4.0.1 15440 15427 html-encoding-sniffer: 4.0.0 15441 15428 http-proxy-agent: 7.0.2 ··· 15451 15438 webidl-conversions: 7.0.0 15452 15439 whatwg-encoding: 3.1.1 15453 15440 whatwg-mimetype: 4.0.0 15454 - whatwg-url: 14.1.0 15441 + whatwg-url: 14.2.0 15455 15442 ws: 8.18.1 15456 15443 xml-name-validator: 5.0.0 15457 15444 transitivePeerDependencies: ··· 17679 17666 dependencies: 17680 17667 punycode: 2.3.1 17681 17668 17682 - tr46@5.0.0: 17683 - dependencies: 17684 - punycode: 2.3.1 17685 - 17686 17669 tr46@5.1.0: 17687 17670 dependencies: 17688 17671 punycode: 2.3.1 ··· 18388 18371 whatwg-mimetype@3.0.0: {} 18389 18372 18390 18373 whatwg-mimetype@4.0.0: {} 18391 - 18392 - whatwg-url@14.1.0: 18393 - dependencies: 18394 - tr46: 5.0.0 18395 - webidl-conversions: 7.0.0 18396 18374 18397 18375 whatwg-url@14.2.0: 18398 18376 dependencies:
+2 -2
docs/.vitepress/config.ts
··· 477 477 link: '/guide/filtering', 478 478 }, 479 479 { 480 - text: 'Workspace', 481 - link: '/guide/workspace', 480 + text: 'Test Projects', 481 + link: '/guide/projects', 482 482 }, 483 483 { 484 484 text: 'Reporters',
+3 -7
docs/advanced/pool.md
··· 31 31 }) 32 32 ``` 33 33 34 - If you need to run tests in different pools, use the [workspace](/guide/workspace) feature: 34 + If you need to run tests in different pools, use the [`projects`](/guide/projects) feature: 35 35 36 36 ```ts [vitest.config.ts] 37 37 export default defineConfig({ 38 38 test: { 39 - workspace: [ 39 + projects: [ 40 40 { 41 41 extends: true, 42 42 test: { ··· 47 47 }, 48 48 }) 49 49 ``` 50 - 51 - ::: info 52 - The `workspace` field was introduced in Vitest 3. To define a workspace in [Vitest 2](https://v2.vitest.dev/), create a separate `vitest.workspace.ts` file. 53 - ::: 54 50 55 51 ## API 56 52 ··· 69 65 70 66 The function is called only once (unless the server config was updated), and it's generally a good idea to initialize everything you need for tests inside that function and reuse it when `runTests` is called. 71 67 72 - Vitest calls `runTest` when new tests are scheduled to run. It will not call it if `files` is empty. The first argument is an array of [TestSpecifications](/advanced/api/test-specification). Files are sorted using [`sequencer`](/config/#sequence-sequencer) before `runTests` is called. It's possible (but unlikely) to have the same file twice, but it will always have a different project - this is implemented via [`vitest.workspace.ts`](/guide/workspace) configuration. 68 + Vitest calls `runTest` when new tests are scheduled to run. It will not call it if `files` is empty. The first argument is an array of [TestSpecifications](/advanced/api/test-specification). Files are sorted using [`sequencer`](/config/#sequence-sequencer) before `runTests` is called. It's possible (but unlikely) to have the same file twice, but it will always have a different project - this is implemented via [`projects`](/guide/projects) configuration. 73 69 74 70 Vitest will wait until `runTests` is executed before finishing a run (i.e., it will emit [`onFinished`](/advanced/reporters) only after `runTests` is resolved). 75 71
+1 -1
docs/advanced/runner.md
··· 164 164 */ 165 165 filepath: string 166 166 /** 167 - * The name of the workspace project the file belongs to. 167 + * The name of the test project the file belongs to. 168 168 */ 169 169 projectName: string | undefined 170 170 /**
+1 -1
docs/blog/vitest-3.md
··· 72 72 73 73 ## Inline Workspace 74 74 75 - Rejoice! No more separate files to define your [workspace](/guide/workspace) - specify an array of projects using the `workspace` field in your `vitest.config` file: 75 + Rejoice! No more separate files to define your [workspace](/guide/projects) - specify an array of projects using the `workspace` field in your `vitest.config` file: 76 76 77 77 ```jsx 78 78 import { defineConfig } from 'vitest/config'
+19 -8
docs/config/index.md
··· 106 106 107 107 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. 108 108 109 - Configuration options that are not supported inside a [workspace](/guide/workspace) project config have <NonProjectOption /> sign next to them. This means they can only be set in the root Vitest config. 109 + Configuration options that are not supported inside a [project](/guide/projects) config have <NonProjectOption /> sign next to them. This means they can only be set in the root Vitest config. 110 110 ::: 111 111 112 112 ### include ··· 588 588 - **Default:** `[]` 589 589 590 590 ::: danger DEPRECATED 591 - This API was deprecated in Vitest 3. Use [workspace](/guide/workspace) to define different configurations instead. 591 + This API was deprecated in Vitest 3. Use [projects](/guide/projects) to define different configurations instead. 592 592 593 593 ```ts 594 594 export default defineConfig({ ··· 596 596 environmentMatchGlobs: [ // [!code --] 597 597 ['./*.jsdom.test.ts', 'jsdom'], // [!code --] 598 598 ], // [!code --] 599 - workspace: [ // [!code ++] 599 + projects: [ // [!code ++] 600 600 { // [!code ++] 601 601 extends: true, // [!code ++] 602 602 test: { // [!code ++] ··· 635 635 - **Default:** `[]` 636 636 637 637 ::: danger DEPRECATED 638 - This API was deprecated in Vitest 3. Use [workspace](/guide/workspace) to define different configurations instead: 638 + This API was deprecated in Vitest 3. Use [projects](/guide/projects) to define different configurations instead: 639 639 640 640 ```ts 641 641 export default defineConfig({ ··· 643 643 poolMatchGlobs: [ // [!code --] 644 644 ['./*.threads.test.ts', 'threads'], // [!code --] 645 645 ], // [!code --] 646 - workspace: [ // [!code ++] 646 + projects: [ // [!code ++] 647 647 { // [!code ++] 648 648 test: { // [!code ++] 649 649 extends: true, // [!code ++] ··· 724 724 ``` 725 725 726 726 ::: warning 727 - Returned files should be either absolute or relative to the root. Note that this is a global option, and it cannot be used inside of [project](/guide/workspace) configs. 727 + Returned files should be either absolute or relative to the root. Note that this is a global option, and it cannot be used inside of [project](/guide/projects) configs. 728 728 ::: 729 729 730 730 ### root ··· 2436 2436 2437 2437 ### workspace<NonProjectOption /> {#workspace} 2438 2438 2439 - - **Type:** `string | TestProjectConfiguration` 2439 + ::: danger DEPRECATED 2440 + This options is deprecated and will be removed in the next major. Please, use [`projects`](#projects) instead. 2441 + ::: 2442 + 2443 + - **Type:** `string | TestProjectConfiguration[]` 2440 2444 - **CLI:** `--workspace=./file.js` 2441 2445 - **Default:** `vitest.{workspace,projects}.{js,ts,json}` close to the config file or root 2442 2446 2443 - Path to a [workspace](/guide/workspace) config file relative to [root](#root). 2447 + Path to a [workspace](/guide/projects) config file relative to [root](#root). 2444 2448 2445 2449 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. 2450 + 2451 + ### projects<NonProjectOption /> {#projects} 2452 + 2453 + - **Type:** `TestProjectConfiguration[]` 2454 + - **Default:** `[]` 2455 + 2456 + An array of [projects](/guide/projects). 2446 2457 2447 2458 ### isolate 2448 2459
+2 -2
docs/guide/cli-generated.md
··· 291 291 - **CLI:** `--workspace <path>` 292 292 - **Config:** [workspace](/config/#workspace) 293 293 294 - Path to a workspace configuration file 294 + [deprecated] Path to a workspace configuration file 295 295 296 296 ### isolate 297 297 ··· 360 360 - **CLI:** `--browser.provider <name>` 361 361 - **Config:** [browser.provider](/guide/browser/config#browser-provider) 362 362 363 - Provider used to run browser tests. Some browsers are only available for specific providers. Can be "webdriverio", "playwright", "preview", or the path to a custom provider. Visit [`browser.provider`](https://vitest.dev/config/#browser-provider) for more information (default: `"preview"`) 363 + Provider used to run browser tests. Some browsers are only available for specific providers. Can be "webdriverio", "playwright", "preview", or the path to a custom provider. Visit [`browser.provider`](https://vitest.dev/guide/browser/config.html#browser-provider) for more information (default: `"preview"`) 364 364 365 365 ### browser.providerOptions 366 366
+1 -1
docs/guide/environment.md
··· 22 22 ::: warning 23 23 "Environments" exist only when running tests in Node.js. 24 24 25 - `browser` is not considered an environment in Vitest. If you wish to run part of your tests using [Browser Mode](/guide/browser/), you can create a [workspace project](/guide/browser/#workspace-config). 25 + `browser` is not considered an environment in Vitest. If you wish to run part of your tests using [Browser Mode](/guide/browser/), you can create a [test project](/guide/browser/#projects-config). 26 26 ::: 27 27 28 28 ## Environments for Specific Files
+5 -5
docs/guide/index.md
··· 175 175 However, we recommend using the same file for both Vite and Vitest, instead of creating two separate files. 176 176 ::: 177 177 178 - ## Workspaces Support 178 + ## Projects Support 179 179 180 - 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.config` file. 180 + Run different project configurations inside the same project with [Test Projects](/guide/projects). You can define a list of files and folders that define your projects in `vitest.config` file. 181 181 182 182 ```ts [vitest.config.ts] 183 183 import { defineConfig } from 'vitest/config' 184 184 185 185 export default defineConfig({ 186 186 test: { 187 - workspace: [ 188 - // you can use a list of glob patterns to define your workspaces 187 + projects: [ 188 + // you can use a list of glob patterns to define your projects 189 189 // Vitest expects a list of config files 190 190 // or directories where there is a config file 191 191 'packages/*', ··· 261 261 | `sveltekit` | [GitHub](https://github.com/vitest-dev/vitest/tree/main/examples/sveltekit) | [Play Online](https://stackblitz.com/fork/github/vitest-dev/vitest/tree/main/examples/sveltekit?initialPath=__vitest__/) | 262 262 | `profiling` | [GitHub](https://github.com/vitest-dev/vitest/tree/main/examples/profiling) | Not Available | 263 263 | `typecheck` | [GitHub](https://github.com/vitest-dev/vitest/tree/main/examples/typecheck) | [Play Online](https://stackblitz.com/fork/github/vitest-dev/vitest/tree/main/examples/typecheck?initialPath=__vitest__/) | 264 - | `workspace` | [GitHub](https://github.com/vitest-dev/vitest/tree/main/examples/workspace) | [Play Online](https://stackblitz.com/fork/github/vitest-dev/vitest/tree/main/examples/workspace?initialPath=__vitest__/) | 264 + | `projects` | [GitHub](https://github.com/vitest-dev/vitest/tree/main/examples/projects) | [Play Online](https://stackblitz.com/fork/github/vitest-dev/vitest/tree/main/examples/projects?initialPath=__vitest__/) | 265 265 266 266 ## Projects using Vitest 267 267
+242
docs/guide/projects.md
··· 1 + --- 2 + title: Test Projects | Guide 3 + --- 4 + 5 + # Test Projects 6 + 7 + ::: tip Sample Project 8 + 9 + [GitHub](https://github.com/vitest-dev/vitest/tree/main/examples/projects) - [Play Online](https://stackblitz.com/fork/github/vitest-dev/vitest/tree/main/examples/projects?initialPath=__vitest__/) 10 + 11 + ::: 12 + 13 + ::: warning 14 + This feature is also known as a `workspace`. The `workspace` is deprecated since 3.2 and replaced with the `projects` configuration. They are functionally the same. 15 + ::: 16 + 17 + Vitest provides a way to define multiple project configurations within a single Vitest process. This feature is particularly useful for monorepo setups but can also be used to run tests with different configurations, such as `resolve.alias`, `plugins`, or `test.browser` and more. 18 + 19 + ## Defining Projects 20 + 21 + You can define projects in your root [config](/config/): 22 + 23 + ```ts [vitest.config.ts] 24 + import { defineConfig } from 'vitest/config' 25 + 26 + export default defineConfig({ 27 + test: { 28 + projects: ['packages/*'], 29 + }, 30 + }) 31 + ``` 32 + 33 + Project configurations are inlined configs, files, or glob patterns referencing your projects. For example, if you have a folder named `packages` that contains your projects, you can define an array in your root Vitest config: 34 + 35 + ```ts [vitest.config.ts] 36 + import { defineConfig } from 'vitest/config' 37 + 38 + export default defineConfig({ 39 + test: { 40 + projects: ['packages/*'], 41 + }, 42 + }) 43 + ``` 44 + 45 + Vitest will treat every folder in `packages` as a separate project even if it doesn't have a config file inside. If this glob pattern matches _any file_, it will be considered a Vitest config even if it doesn't have a `vitest` in its name or has an obscure file extension. 46 + 47 + ::: warning 48 + Vitest does not treat the root `vitest.config` file as a project unless it is explicitly specified in the configuration. Consequently, the root configuration will only influence global options such as `reporters` and `coverage`. Note that Vitest will always run certain plugin hooks, like `apply`, `config`, `configResolved` or `configureServer`, specified in the root config file. Vitest also uses the same plugins to execute global setups and custom coverage provider. 49 + ::: 50 + 51 + You can also reference projects with their config files: 52 + 53 + ```ts [vitest.config.ts] 54 + import { defineConfig } from 'vitest/config' 55 + 56 + export default defineConfig({ 57 + test: { 58 + projects: ['packages/*/vitest.config.{e2e,unit}.ts'], 59 + }, 60 + }) 61 + ``` 62 + 63 + This pattern will only include projects with a `vitest.config` file that contains `e2e` or `unit` before the extension. 64 + 65 + You can also define projects using inline configuration. The configuration supports both syntaxes simultaneously. 66 + 67 + ```ts [vitest.config.ts] 68 + import { defineConfig } from 'vitest/config' 69 + 70 + export default defineConfig({ 71 + test: { 72 + projects: [ 73 + // matches every folder and file inside the `packages` folder 74 + 'packages/*', 75 + { 76 + // add "extends: true" to inherit the options from the root config 77 + extends: true, 78 + test: { 79 + include: ['tests/**/*.{browser}.test.{ts,js}'], 80 + // it is recommended to define a name when using inline configs 81 + name: 'happy-dom', 82 + environment: 'happy-dom', 83 + } 84 + }, 85 + { 86 + test: { 87 + include: ['tests/**/*.{node}.test.{ts,js}'], 88 + // color of the name label can be changed 89 + name: { label: 'node', color: 'green' }, 90 + environment: 'node', 91 + } 92 + } 93 + ] 94 + } 95 + }) 96 + ``` 97 + 98 + ::: warning 99 + All projects must have unique names; otherwise, Vitest will throw an error. If a name is not provided in the inline configuration, Vitest will assign a number. For project configurations defined with glob syntax, Vitest will default to using the "name" property in the nearest `package.json` file or, if none exists, the folder name. 100 + ::: 101 + 102 + Projects do not support all configuration properties. For better type safety, use the `defineProject` method instead of `defineConfig` within project configuration files: 103 + 104 + ```ts twoslash [packages/a/vitest.config.ts] 105 + // @errors: 2769 106 + import { defineProject } from 'vitest/config' 107 + 108 + export default defineProject({ 109 + test: { 110 + environment: 'jsdom', 111 + // "reporters" is not supported in a project config, 112 + // so it will show an error 113 + reporters: ['json'] 114 + } 115 + }) 116 + ``` 117 + 118 + ## Running tests 119 + 120 + To run tests, define a script in your root `package.json`: 121 + 122 + ```json [package.json] 123 + { 124 + "scripts": { 125 + "test": "vitest" 126 + } 127 + } 128 + ``` 129 + 130 + Now tests can be run using your package manager: 131 + 132 + ::: code-group 133 + ```bash [npm] 134 + npm run test 135 + ``` 136 + ```bash [yarn] 137 + yarn test 138 + ``` 139 + ```bash [pnpm] 140 + pnpm run test 141 + ``` 142 + ```bash [bun] 143 + bun run test 144 + ``` 145 + ::: 146 + 147 + If you need to run tests only inside a single project, use the `--project` CLI option: 148 + 149 + ::: code-group 150 + ```bash [npm] 151 + npm run test --project e2e 152 + ``` 153 + ```bash [yarn] 154 + yarn test --project e2e 155 + ``` 156 + ```bash [pnpm] 157 + pnpm run test --project e2e 158 + ``` 159 + ```bash [bun] 160 + bun run test --project e2e 161 + ``` 162 + ::: 163 + 164 + ::: tip 165 + CLI option `--project` can be used multiple times to filter out several projects: 166 + 167 + ::: code-group 168 + ```bash [npm] 169 + npm run test --project e2e --project unit 170 + ``` 171 + ```bash [yarn] 172 + yarn test --project e2e --project unit 173 + ``` 174 + ```bash [pnpm] 175 + pnpm run test --project e2e --project unit 176 + ``` 177 + ```bash [bun] 178 + bun run test --project e2e --project unit 179 + ``` 180 + ::: 181 + 182 + ## Configuration 183 + 184 + 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: 185 + 186 + ```ts [packages/a/vitest.config.ts] 187 + import { defineProject, mergeConfig } from 'vitest/config' 188 + import configShared from '../vitest.shared.js' 189 + 190 + export default mergeConfig( 191 + configShared, 192 + defineProject({ 193 + test: { 194 + environment: 'jsdom', 195 + } 196 + }) 197 + ) 198 + ``` 199 + 200 + Additionally, you can use the `extends` option to inherit from your root-level configuration. All options will be merged. 201 + 202 + ```ts [vitest.config.ts] 203 + import { defineConfig } from 'vitest/config' 204 + import react from '@vitejs/plugin-react' 205 + 206 + export default defineConfig({ 207 + plugins: [react()], 208 + test: { 209 + pool: 'threads', 210 + projects: [ 211 + { 212 + // will inherit options from this config like plugins and pool 213 + extends: true, 214 + test: { 215 + name: 'unit', 216 + include: ['**/*.unit.test.ts'], 217 + }, 218 + }, 219 + { 220 + // won't inherit any options from this config 221 + // this is the default behaviour 222 + extends: false, 223 + test: { 224 + name: 'integration', 225 + include: ['**/*.integration.test.ts'], 226 + }, 227 + }, 228 + ], 229 + }, 230 + }) 231 + ``` 232 + 233 + ::: danger Unsupported Options 234 + Some of the configuration options are not allowed in a project config. Most notably: 235 + 236 + - `coverage`: coverage is done for the whole process 237 + - `reporters`: only root-level reporters can be supported 238 + - `resolveSnapshotPath`: only root-level resolver is respected 239 + - all other options that don't affect test runners 240 + 241 + All configuration options that are not supported inside a project configuration are marked with a <NonProjectOption /> sign in the ["Config"](/config/) guide. They have to be defined once in the root config file. 242 + :::
+27 -23
docs/guide/test-context.md
··· 232 232 233 233 #### Default fixture 234 234 235 - 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. 235 + Since Vitest 3, you can provide different values in different [projects](/guide/projects). 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. 236 236 237 237 :::code-group 238 238 ```ts [fixtures.test.ts] ··· 253 253 // url is "/empty" in "project-empty" 254 254 }) 255 255 ``` 256 - ```ts [vitest.workspace.ts] 257 - import { defineWorkspace } from 'vitest/config' 256 + ```ts [vitest.config.ts] 257 + import { defineConfig } from 'vitest/config' 258 258 259 - export default defineWorkspace([ 260 - { 261 - test: { 262 - name: 'project-new', 263 - }, 264 - }, 265 - { 266 - test: { 267 - name: 'project-full', 268 - provide: { 269 - url: '/full', 259 + export default defineConfig({ 260 + test: { 261 + projects: [ 262 + { 263 + test: { 264 + name: 'project-new', 265 + }, 270 266 }, 271 - }, 272 - }, 273 - { 274 - test: { 275 - name: 'project-empty', 276 - provide: { 277 - url: '/empty', 267 + { 268 + test: { 269 + name: 'project-full', 270 + provide: { 271 + url: '/full', 272 + }, 273 + }, 278 274 }, 279 - }, 275 + { 276 + test: { 277 + name: 'project-empty', 278 + provide: { 279 + url: '/empty', 280 + }, 281 + }, 282 + }, 283 + ], 280 284 }, 281 - ]) 285 + }) 282 286 ``` 283 287 ::: 284 288
-316
docs/guide/workspace.md
··· 1 - --- 2 - title: Workspace | Guide 3 - --- 4 - 5 - # Workspace 6 - 7 - ::: tip Sample Project 8 - 9 - [GitHub](https://github.com/vitest-dev/vitest/tree/main/examples/workspace) - [Play Online](https://stackblitz.com/fork/github/vitest-dev/vitest/tree/main/examples/workspace?initialPath=__vitest__/) 10 - 11 - ::: 12 - 13 - Vitest provides a way to define multiple project configurations within a single Vitest process. This feature is particularly useful for monorepo setups but can also be used to run tests with different configurations, such as `resolve.alias`, `plugins`, or `test.browser` and more. 14 - 15 - ## Defining a Workspace 16 - 17 - Since Vitest 3, you can define a workspace in your root [config](/config/). In this case, Vitest will ignore the `vitest.workspace` file in the root, if one exists. 18 - 19 - ```ts [vitest.config.ts] 20 - import { defineConfig } from 'vitest/config' 21 - 22 - export default defineConfig({ 23 - test: { 24 - workspace: ['packages/*'], 25 - }, 26 - }) 27 - ``` 28 - 29 - If you are using an older version, a workspace must include `vitest.workspace` or `vitest.projects` file in its root directory (located in the same folder as your root configuration file or working directory if it doesn't exist). Note that `projects` is just an alias and does not change the behavior or semantics of this feature. Vitest supports `ts`, `js`, and `json` extensions for this file. 30 - 31 - ::: tip NAMING 32 - Please note that this feature is named `workspace`, not `workspaces` (without an "s" at the end). 33 - ::: 34 - 35 - A workspace is a list of inlined configs, files, or glob patterns referencing your projects. For example, if you have a folder named `packages` that contains your projects, you can either create a workspace file or define an array in the root config: 36 - 37 - :::code-group 38 - ```ts [vitest.config.ts <Version>3.0.0</Version>] 39 - import { defineConfig } from 'vitest/config' 40 - 41 - export default defineConfig({ 42 - test: { 43 - workspace: ['packages/*'], 44 - }, 45 - }) 46 - ``` 47 - ```ts [vitest.workspace.ts] 48 - export default [ 49 - 'packages/*' 50 - ] 51 - ``` 52 - ::: 53 - 54 - 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. 55 - 56 - ::: warning 57 - 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`. Note that Vitest will always run certain plugin hooks, like `apply`, `config`, `configResolved` or `configureServer`, specified in the root config file. Vitest also uses the same plugins to execute global setups, workspace files and custom coverage provider. 58 - ::: 59 - 60 - You can also reference projects with their config files: 61 - 62 - :::code-group 63 - ```ts [vitest.config.ts <Version>3.0.0</Version>] 64 - import { defineConfig } from 'vitest/config' 65 - 66 - export default defineConfig({ 67 - test: { 68 - workspace: ['packages/*/vitest.config.{e2e,unit}.ts'], 69 - }, 70 - }) 71 - ``` 72 - ```ts [vitest.workspace.ts] 73 - export default [ 74 - 'packages/*/vitest.config.{e2e,unit}.ts' 75 - ] 76 - ``` 77 - ::: 78 - 79 - This pattern will only include projects with a `vitest.config` file that contains `e2e` or `unit` before the extension. 80 - 81 - You can also define projects using inline configuration. The workspace configuration supports both syntaxes simultaneously. 82 - 83 - :::code-group 84 - ```ts [vitest.config.ts <Version>3.0.0</Version>] 85 - import { defineConfig } from 'vitest/config' 86 - 87 - export default defineConfig({ 88 - test: { 89 - workspace: [ 90 - // matches every folder and file inside the `packages` folder 91 - 'packages/*', 92 - { 93 - // add "extends: true" to inherit the options from the root config 94 - extends: true, 95 - test: { 96 - include: ['tests/**/*.{browser}.test.{ts,js}'], 97 - // it is recommended to define a name when using inline configs 98 - name: 'happy-dom', 99 - environment: 'happy-dom', 100 - } 101 - }, 102 - { 103 - test: { 104 - include: ['tests/**/*.{node}.test.{ts,js}'], 105 - // color of the name label can be changed 106 - name: { label: 'node', color: 'green' }, 107 - environment: 'node', 108 - } 109 - } 110 - ] 111 - } 112 - }) 113 - ``` 114 - ```ts [vitest.workspace.ts] 115 - import { defineWorkspace } from 'vitest/config' 116 - 117 - // defineWorkspace provides a nice type hinting DX 118 - export default defineWorkspace([ 119 - // matches every folder and file inside the `packages` folder 120 - 'packages/*', 121 - { 122 - // add "extends" to merge two configs together 123 - extends: './vite.config.js', 124 - test: { 125 - include: ['tests/**/*.{browser}.test.{ts,js}'], 126 - // it is recommended to define a name when using inline configs 127 - name: 'happy-dom', 128 - environment: 'happy-dom', 129 - } 130 - }, 131 - { 132 - test: { 133 - include: ['tests/**/*.{node}.test.{ts,js}'], 134 - name: 'node', 135 - environment: 'node', 136 - } 137 - } 138 - ]) 139 - ``` 140 - ::: 141 - 142 - ::: warning 143 - All projects must have unique names; otherwise, Vitest will throw an error. If a name is not provided in the inline configuration, Vitest will assign a number. For project configurations defined with glob syntax, Vitest will default to using the "name" property in the nearest `package.json` file or, if none exists, the folder name. 144 - ::: 145 - 146 - If you do not use inline configurations, you can create a small JSON file in your root directory or just specify it in the root config: 147 - 148 - ```json [vitest.workspace.json] 149 - [ 150 - "packages/*" 151 - ] 152 - ``` 153 - 154 - Workspace projects do not support all configuration properties. For better type safety, use the `defineProject` method instead of `defineConfig` within project configuration files: 155 - 156 - ```ts twoslash [packages/a/vitest.config.ts] 157 - // @errors: 2769 158 - import { defineProject } from 'vitest/config' 159 - 160 - export default defineProject({ 161 - test: { 162 - environment: 'jsdom', 163 - // "reporters" is not supported in a project config, 164 - // so it will show an error 165 - reporters: ['json'] 166 - } 167 - }) 168 - ``` 169 - 170 - ## Running tests 171 - 172 - To run tests inside the workspace, define a script in your root `package.json`: 173 - 174 - ```json [package.json] 175 - { 176 - "scripts": { 177 - "test": "vitest" 178 - } 179 - } 180 - ``` 181 - 182 - Now tests can be run using your package manager: 183 - 184 - ::: code-group 185 - ```bash [npm] 186 - npm run test 187 - ``` 188 - ```bash [yarn] 189 - yarn test 190 - ``` 191 - ```bash [pnpm] 192 - pnpm run test 193 - ``` 194 - ```bash [bun] 195 - bun run test 196 - ``` 197 - ::: 198 - 199 - If you need to run tests only inside a single project, use the `--project` CLI option: 200 - 201 - ::: code-group 202 - ```bash [npm] 203 - npm run test --project e2e 204 - ``` 205 - ```bash [yarn] 206 - yarn test --project e2e 207 - ``` 208 - ```bash [pnpm] 209 - pnpm run test --project e2e 210 - ``` 211 - ```bash [bun] 212 - bun run test --project e2e 213 - ``` 214 - ::: 215 - 216 - ::: tip 217 - CLI option `--project` can be used multiple times to filter out several projects: 218 - 219 - ::: code-group 220 - ```bash [npm] 221 - npm run test --project e2e --project unit 222 - ``` 223 - ```bash [yarn] 224 - yarn test --project e2e --project unit 225 - ``` 226 - ```bash [pnpm] 227 - pnpm run test --project e2e --project unit 228 - ``` 229 - ```bash [bun] 230 - bun run test --project e2e --project unit 231 - ``` 232 - ::: 233 - 234 - ## Configuration 235 - 236 - None of the configuration options are inherited from the root-level config file, even if the workspace is defined inside that config and not in a separate `vitest.workspace` file. You can create a shared config file and merge it with the project config yourself: 237 - 238 - ```ts [packages/a/vitest.config.ts] 239 - import { defineProject, mergeConfig } from 'vitest/config' 240 - import configShared from '../vitest.shared.js' 241 - 242 - export default mergeConfig( 243 - configShared, 244 - defineProject({ 245 - test: { 246 - environment: 'jsdom', 247 - } 248 - }) 249 - ) 250 - ``` 251 - 252 - Additionally, at the `defineWorkspace` level, you can use the `extends` option to inherit from your root-level configuration. All options will be merged. 253 - 254 - ::: code-group 255 - ```ts [vitest.config.ts <Version>3.0.0</Version>] 256 - import { defineConfig } from 'vitest/config' 257 - import react from '@vitejs/plugin-react' 258 - 259 - export default defineConfig({ 260 - plugins: [react()], 261 - test: { 262 - pool: 'threads', 263 - workspace: [ 264 - { 265 - // will inherit options from this config like plugins and pool 266 - extends: true, 267 - test: { 268 - name: 'unit', 269 - include: ['**/*.unit.test.ts'], 270 - }, 271 - }, 272 - { 273 - // won't inherit any options from this config 274 - // this is the default behaviour 275 - extends: false, 276 - test: { 277 - name: 'integration', 278 - include: ['**/*.integration.test.ts'], 279 - }, 280 - }, 281 - ], 282 - }, 283 - }) 284 - ``` 285 - ```ts [vitest.workspace.ts] 286 - import { defineWorkspace } from 'vitest/config' 287 - 288 - export default defineWorkspace([ 289 - { 290 - extends: './vitest.config.ts', 291 - test: { 292 - name: 'unit', 293 - include: ['**/*.unit.test.ts'], 294 - }, 295 - }, 296 - { 297 - extends: './vitest.config.ts', 298 - test: { 299 - name: 'integration', 300 - include: ['**/*.integration.test.ts'], 301 - }, 302 - }, 303 - ]) 304 - ``` 305 - ::: 306 - 307 - ::: danger Unsupported Options 308 - Some of the configuration options are not allowed in a project config. Most notably: 309 - 310 - - `coverage`: coverage is done for the whole workspace 311 - - `reporters`: only root-level reporters can be supported 312 - - `resolveSnapshotPath`: only root-level resolver is respected 313 - - all other options that don't affect test runners 314 - 315 - All configuration options that are not supported inside a project configuration are marked with a <NonProjectOption /> sign in the ["Config"](/config/) guide. They have to be defined once in the root config file. 316 - :::
+32
examples/projects/package.json
··· 1 + { 2 + "name": "@vitest/example-projects", 3 + "type": "module", 4 + "private": true, 5 + "license": "MIT", 6 + "scripts": { 7 + "test": "vitest", 8 + "test:ui": "vitest --ui", 9 + "test:run": "vitest run" 10 + }, 11 + "devDependencies": { 12 + "@testing-library/jest-dom": "^6.6.3", 13 + "@testing-library/react": "^13.4.0", 14 + "@testing-library/user-event": "^14.6.1", 15 + "@types/react": "^19.1.2", 16 + "@vitejs/plugin-react": "^4.4.1", 17 + "@vitest/ui": "latest", 18 + "fastify": "^4.29.0", 19 + "jsdom": "^24.1.3", 20 + "react": "^19.1.0", 21 + "supertest": "^6.3.4", 22 + "tsx": "^4.19.3", 23 + "vite": "latest", 24 + "vitest": "latest" 25 + }, 26 + "workspaces": [ 27 + "packages/*" 28 + ], 29 + "stackblitz": { 30 + "startCommand": "npm run test:ui" 31 + } 32 + }
+13
examples/projects/tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + "target": "es2020", 4 + "module": "node16", 5 + "strict": true, 6 + "declaration": true, 7 + "declarationMap": true, 8 + "sourceMap": true, 9 + "verbatimModuleSyntax": true 10 + }, 11 + "include": ["packages"], 12 + "exclude": ["node_modules"] 13 + }
+7
examples/projects/vitest.config.ts
··· 1 + import { defineConfig } from 'vitest/config' 2 + 3 + export default defineConfig({ 4 + test: { 5 + projects: ['packages/*'], 6 + }, 7 + })
-32
examples/workspace/package.json
··· 1 - { 2 - "name": "@vitest/example-workspace", 3 - "type": "module", 4 - "private": true, 5 - "license": "MIT", 6 - "scripts": { 7 - "test": "vitest", 8 - "test:ui": "vitest --ui", 9 - "test:run": "vitest run" 10 - }, 11 - "devDependencies": { 12 - "@testing-library/jest-dom": "^6.6.3", 13 - "@testing-library/react": "^13.4.0", 14 - "@testing-library/user-event": "^14.6.1", 15 - "@types/react": "^19.1.2", 16 - "@vitejs/plugin-react": "^4.4.1", 17 - "@vitest/ui": "latest", 18 - "fastify": "^4.29.0", 19 - "jsdom": "^24.1.3", 20 - "react": "^19.1.0", 21 - "supertest": "^6.3.4", 22 - "tsx": "^4.19.3", 23 - "vite": "latest", 24 - "vitest": "latest" 25 - }, 26 - "workspaces": [ 27 - "packages/*" 28 - ], 29 - "stackblitz": { 30 - "startCommand": "npm run test:ui" 31 - } 32 - }
-13
examples/workspace/tsconfig.json
··· 1 - { 2 - "compilerOptions": { 3 - "target": "es2020", 4 - "module": "node16", 5 - "strict": true, 6 - "declaration": true, 7 - "declarationMap": true, 8 - "sourceMap": true, 9 - "verbatimModuleSyntax": true 10 - }, 11 - "include": ["packages"], 12 - "exclude": ["node_modules"] 13 - }
-5
examples/workspace/vitest.workspace.ts
··· 1 - import { defineWorkspace } from 'vitest/config' 2 - 3 - export default defineWorkspace([ 4 - 'packages/*', 5 - ])
+17
test/core/vite.config.ts
··· 1 + import type { LabelColor } from 'vitest' 2 + import type { Pool } from 'vitest/node' 1 3 import { basename, dirname, join, resolve } from 'pathe' 2 4 import { defaultExclude, defineConfig } from 'vitest/config' 3 5 ··· 135 137 return false 136 138 } 137 139 }, 140 + projects: [ 141 + project('threads', 'red'), 142 + project('forks', 'green'), 143 + project('vmThreads', 'blue'), 144 + ], 138 145 }, 139 146 }) 147 + 148 + function project(pool: Pool, color: LabelColor) { 149 + return { 150 + extends: './vite.config.ts', 151 + test: { 152 + name: { label: pool, color }, 153 + pool, 154 + }, 155 + } 156 + }
-25
test/core/vitest.workspace.ts
··· 1 - import { defineWorkspace } from 'vitest/config' 2 - 3 - export default defineWorkspace([ 4 - { 5 - extends: './vite.config.ts', 6 - test: { 7 - name: { label: 'threads', color: 'red' }, 8 - pool: 'threads', 9 - }, 10 - }, 11 - { 12 - extends: './vite.config.ts', 13 - test: { 14 - name: { label: 'forks', color: 'green' }, 15 - pool: 'forks', 16 - }, 17 - }, 18 - { 19 - extends: './vite.config.ts', 20 - test: { 21 - name: { label: 'vmThreads', color: 'blue' }, 22 - pool: 'vmThreads', 23 - }, 24 - }, 25 - ])
+23
test/workspaces-browser/vitest.config.ts
··· 11 11 reporters: ['default', 'json'], 12 12 outputFile: './results.json', 13 13 globalSetup: './globalTest.ts', 14 + projects: [ 15 + './space_*/*.config.ts', 16 + { 17 + test: { 18 + name: 'space_browser_inline', 19 + root: './space_browser_inline', 20 + browser: { 21 + enabled: true, 22 + instances: [{ browser: process.env.BROWSER || 'chromium' }], 23 + headless: true, 24 + provider: process.env.PROVIDER || 'playwright', 25 + }, 26 + alias: { 27 + 'test-alias-from-vitest': new URL('./space_browser_inline/test-alias-to.ts', import.meta.url).pathname, 28 + }, 29 + }, 30 + resolve: { 31 + alias: { 32 + 'test-alias-from-vite': new URL('./space_browser_inline/test-alias-to.ts', import.meta.url).pathname, 33 + }, 34 + }, 35 + }, 36 + ], 14 37 }, 15 38 })
-25
test/workspaces-browser/vitest.workspace.ts
··· 1 - import { defineWorkspace } from 'vitest/config' 2 - 3 - export default defineWorkspace([ 4 - './space_*/*.config.ts', 5 - { 6 - test: { 7 - name: 'space_browser_inline', 8 - root: './space_browser_inline', 9 - browser: { 10 - enabled: true, 11 - instances: [{ browser: process.env.BROWSER || 'chromium' }], 12 - headless: true, 13 - provider: process.env.PROVIDER || 'playwright', 14 - }, 15 - alias: { 16 - 'test-alias-from-vitest': new URL('./space_browser_inline/test-alias-to.ts', import.meta.url).pathname, 17 - }, 18 - }, 19 - resolve: { 20 - alias: { 21 - 'test-alias-from-vite': new URL('./space_browser_inline/test-alias-to.ts', import.meta.url).pathname, 22 - }, 23 - }, 24 - }, 25 - ])
+159
test/workspaces/vitest.config.ts
··· 1 + import type { Vite } from 'vitest/node' 2 + import remapping from '@ampproject/remapping' 3 + import MagicString from 'magic-string' 1 4 import { defineConfig } from 'vitest/config' 2 5 import { cwdPlugin } from './cwdPlugin.js' 3 6 ··· 19 22 provide: { 20 23 globalConfigValue: true, 21 24 }, 25 + projects: [ 26 + 27 + 'space_2', 28 + './space_*/vitest.config.ts', 29 + './space_1/*.config.ts', 30 + async () => ({ 31 + test: { 32 + name: 'happy-dom', 33 + root: './space_shared', 34 + environment: 'happy-dom', 35 + setupFiles: ['./setup.jsdom.ts'], 36 + provide: { 37 + providedConfigValue: 'actual config value', 38 + }, 39 + }, 40 + }), 41 + Promise.resolve({ 42 + test: { 43 + name: 'node', 44 + root: './space_shared', 45 + environment: 'node', 46 + setupFiles: ['./setup.node.ts'], 47 + }, 48 + }), 49 + 50 + // Projects testing pool and poolOptions 51 + { 52 + test: { 53 + name: 'Threads pool', 54 + include: [ 55 + './space-pools/threads.test.ts', 56 + './space-pools/multi-worker.test.ts', 57 + './space-pools/isolate.test.ts', 58 + ], 59 + pool: 'threads', 60 + }, 61 + }, 62 + { 63 + test: { 64 + name: 'Single thread pool', 65 + include: [ 66 + './space-pools/threads.test.ts', 67 + './space-pools/single-worker.test.ts', 68 + ], 69 + pool: 'threads', 70 + poolOptions: { threads: { singleThread: true } }, 71 + }, 72 + }, 73 + { 74 + test: { 75 + name: 'Non-isolated thread pool #1', 76 + include: [ 77 + './space-pools/threads.test.ts', 78 + './space-pools/no-isolate.test.ts', 79 + ], 80 + pool: 'threads', 81 + poolOptions: { threads: { isolate: false } }, 82 + }, 83 + }, 84 + { 85 + test: { 86 + name: 'Non-isolated thread pool #2', 87 + include: [ 88 + './space-pools/threads.test.ts', 89 + './space-pools/no-isolate.test.ts', 90 + ], 91 + pool: 'threads', 92 + isolate: false, 93 + }, 94 + }, 95 + { 96 + test: { 97 + name: 'Forks pool', 98 + include: [ 99 + './space-pools/forks.test.ts', 100 + './space-pools/multi-worker.test.ts', 101 + './space-pools/isolate.test.ts', 102 + ], 103 + pool: 'forks', 104 + }, 105 + }, 106 + { 107 + test: { 108 + name: 'Single fork pool', 109 + include: [ 110 + './space-pools/forks.test.ts', 111 + './space-pools/single-worker.test.ts', 112 + ], 113 + pool: 'forks', 114 + poolOptions: { forks: { singleFork: true } }, 115 + }, 116 + }, 117 + { 118 + test: { 119 + name: 'Non-isolated fork pool #1', 120 + include: [ 121 + './space-pools/forks.test.ts', 122 + './space-pools/no-isolate.test.ts', 123 + ], 124 + pool: 'forks', 125 + poolOptions: { forks: { isolate: false } }, 126 + }, 127 + }, 128 + { 129 + test: { 130 + name: 'Non-isolated fork pool #2', 131 + include: [ 132 + './space-pools/forks.test.ts', 133 + './space-pools/no-isolate.test.ts', 134 + ], 135 + pool: 'forks', 136 + isolate: false, 137 + }, 138 + }, 139 + 140 + // These two projects run on same environment but still transform 141 + // a single file differently due to Vite plugins 142 + { 143 + plugins: [customPlugin(0)], 144 + test: { 145 + name: 'Project with custom plugin #1', 146 + environment: 'node', 147 + include: ['./space-multi-transform/test/project-1.test.ts'], 148 + }, 149 + }, 150 + { 151 + plugins: [customPlugin(15)], 152 + test: { 153 + name: 'Project with custom plugin #2', 154 + environment: 'node', 155 + include: ['./space-multi-transform/test/project-2.test.ts'], 156 + }, 157 + }, 158 + ], 22 159 }, 23 160 }) 161 + 162 + function customPlugin(offset: number): Vite.Plugin { 163 + return { 164 + name: 'vitest-custom-multi-transform', 165 + enforce: 'pre', 166 + transform(code, id) { 167 + if (id.includes('space-multi-transform/src/multi-transform.ts')) { 168 + const padding = '\n*****'.repeat(offset) 169 + 170 + const transformed = new MagicString(code) 171 + transformed.replace('\'default-padding\'', `\`${padding}\``) 172 + 173 + const map = remapping( 174 + [transformed.generateMap({ hires: true }), this.getCombinedSourcemap() as any], 175 + () => null, 176 + ) as any 177 + 178 + return { code: transformed.toString(), map } 179 + } 180 + }, 181 + } 182 + }
-160
test/workspaces/vitest.workspace.ts
··· 1 - import type { Plugin } from 'vite' 2 - import remapping from '@ampproject/remapping' 3 - import MagicString from 'magic-string' 4 - import { defineWorkspace } from 'vitest/config' 5 - 6 - export default defineWorkspace([ 7 - 'space_2', 8 - './space_*/vitest.config.ts', 9 - './space_1/*.config.ts', 10 - async () => ({ 11 - test: { 12 - name: 'happy-dom', 13 - root: './space_shared', 14 - environment: 'happy-dom', 15 - setupFiles: ['./setup.jsdom.ts'], 16 - provide: { 17 - providedConfigValue: 'actual config value', 18 - }, 19 - }, 20 - }), 21 - Promise.resolve({ 22 - test: { 23 - name: 'node', 24 - root: './space_shared', 25 - environment: 'node', 26 - setupFiles: ['./setup.node.ts'], 27 - }, 28 - }), 29 - 30 - // Projects testing pool and poolOptions 31 - { 32 - test: { 33 - name: 'Threads pool', 34 - include: [ 35 - './space-pools/threads.test.ts', 36 - './space-pools/multi-worker.test.ts', 37 - './space-pools/isolate.test.ts', 38 - ], 39 - pool: 'threads', 40 - }, 41 - }, 42 - { 43 - test: { 44 - name: 'Single thread pool', 45 - include: [ 46 - './space-pools/threads.test.ts', 47 - './space-pools/single-worker.test.ts', 48 - ], 49 - pool: 'threads', 50 - poolOptions: { threads: { singleThread: true } }, 51 - }, 52 - }, 53 - { 54 - test: { 55 - name: 'Non-isolated thread pool #1', 56 - include: [ 57 - './space-pools/threads.test.ts', 58 - './space-pools/no-isolate.test.ts', 59 - ], 60 - pool: 'threads', 61 - poolOptions: { threads: { isolate: false } }, 62 - }, 63 - }, 64 - { 65 - test: { 66 - name: 'Non-isolated thread pool #2', 67 - include: [ 68 - './space-pools/threads.test.ts', 69 - './space-pools/no-isolate.test.ts', 70 - ], 71 - pool: 'threads', 72 - isolate: false, 73 - }, 74 - }, 75 - { 76 - test: { 77 - name: 'Forks pool', 78 - include: [ 79 - './space-pools/forks.test.ts', 80 - './space-pools/multi-worker.test.ts', 81 - './space-pools/isolate.test.ts', 82 - ], 83 - pool: 'forks', 84 - }, 85 - }, 86 - { 87 - test: { 88 - name: 'Single fork pool', 89 - include: [ 90 - './space-pools/forks.test.ts', 91 - './space-pools/single-worker.test.ts', 92 - ], 93 - pool: 'forks', 94 - poolOptions: { forks: { singleFork: true } }, 95 - }, 96 - }, 97 - { 98 - test: { 99 - name: 'Non-isolated fork pool #1', 100 - include: [ 101 - './space-pools/forks.test.ts', 102 - './space-pools/no-isolate.test.ts', 103 - ], 104 - pool: 'forks', 105 - poolOptions: { forks: { isolate: false } }, 106 - }, 107 - }, 108 - { 109 - test: { 110 - name: 'Non-isolated fork pool #2', 111 - include: [ 112 - './space-pools/forks.test.ts', 113 - './space-pools/no-isolate.test.ts', 114 - ], 115 - pool: 'forks', 116 - isolate: false, 117 - }, 118 - }, 119 - 120 - // These two projects run on same environment but still transform 121 - // a single file differently due to Vite plugins 122 - { 123 - plugins: [customPlugin(0)], 124 - test: { 125 - name: 'Project with custom plugin #1', 126 - environment: 'node', 127 - include: ['./space-multi-transform/test/project-1.test.ts'], 128 - }, 129 - }, 130 - { 131 - plugins: [customPlugin(15)], 132 - test: { 133 - name: 'Project with custom plugin #2', 134 - environment: 'node', 135 - include: ['./space-multi-transform/test/project-2.test.ts'], 136 - }, 137 - }, 138 - ]) 139 - 140 - function customPlugin(offset: number): Plugin { 141 - return { 142 - name: 'vitest-custom-multi-transform', 143 - enforce: 'pre', 144 - transform(code, id) { 145 - if (id.includes('space-multi-transform/src/multi-transform.ts')) { 146 - const padding = '\n*****'.repeat(offset) 147 - 148 - const transformed = new MagicString(code) 149 - transformed.replace('\'default-padding\'', `\`${padding}\``) 150 - 151 - const map = remapping( 152 - [transformed.generateMap({ hires: true }), this.getCombinedSourcemap() as any], 153 - () => null, 154 - ) as any 155 - 156 - return { code: transformed.toString(), map } 157 - } 158 - }, 159 - } 160 - }
+1 -1
docs/.vitepress/components/FeaturesList.vue
··· 13 13 <ListItem>Workers multi-threading via <a target="_blank" href="https://github.com/tinylibs/tinypool" rel="noopener noreferrer">Tinypool</a></ListItem> 14 14 <ListItem>Benchmarking support with <a target="_blank" href="https://github.com/tinylibs/tinybench" rel="noopener noreferrer">Tinybench</a></ListItem> 15 15 <ListItem>Filtering, timeouts, concurrent for suite and tests</ListItem> 16 - <ListItem><a href="/guide/workspace">Workspace</a> support</ListItem> 16 + <ListItem><a href="/guide/projects">Projects</a> support</ListItem> 17 17 <ListItem> 18 18 <a href="/guide/snapshot"> 19 19 Jest-compatible Snapshot
+1 -1
docs/advanced/api/index.md
··· 119 119 ::: 120 120 121 121 ::: warning 122 - The `resolveConfig` doesn't resolve the `workspace`. To resolve workspace configs, Vitest needs an established Vite server. 122 + The `resolveConfig` doesn't resolve `projects`. To resolve projects configs, Vitest needs an established Vite server. 123 123 124 124 Also note that `viteConfig.test` will not be fully resolved. If you need Vitest config, use `vitestConfig` instead. 125 125 :::
+2 -2
docs/advanced/api/plugin.md
··· 53 53 ``` 54 54 ::: 55 55 56 - Unlike [`reporter.onInit`](/advanced/api/reporters#oninit), this hooks runs early in Vitest lifecycle allowing you to make changes to configuration like `coverage` and `reporters`. A more notable change is that you can manipulate the global config from a [workspace project](/guide/workspace) if your plugin is defined in the project and not in the global config. 56 + Unlike [`reporter.onInit`](/advanced/api/reporters#oninit), this hooks runs early in Vitest lifecycle allowing you to make changes to configuration like `coverage` and `reporters`. A more notable change is that you can manipulate the global config from a [test project](/guide/projects) if your plugin is defined in the project and not in the global config. 57 57 58 58 ## Context 59 59 ··· 107 107 ``` 108 108 109 109 ::: warning Projects are Filtered 110 - Vitest filters projects during the config resolution, so if the user defined a filter, injected project might not be resolved unless it [matches the filter](./vitest#matchesprojectfilter). You can update the filter via the `vitest.config.project` option to always include your workspace project: 110 + Vitest filters projects during the config resolution, so if the user defined a filter, injected project might not be resolved unless it [matches the filter](./vitest#matchesprojectfilter). You can update the filter via the `vitest.config.project` option to always include your test project: 111 111 112 112 ```ts 113 113 vitest.config.project.push('my-project-name')
+25 -21
docs/advanced/api/test-project.md
··· 4 4 5 5 # TestProject <Version>3.0.0</Version> {#testproject} 6 6 7 - - **Alias**: `WorkspaceProject` before 3.0.0 8 - 9 7 ::: warning 10 - This guide describes the advanced Node.js API. If you just want to create a workspace, follow the ["Workspace"](/guide/workspace) guide. 8 + This guide describes the advanced Node.js API. If you just want to define projects, follow the ["Test Projects"](/guide/projects) guide. 11 9 ::: 12 10 13 11 ## name ··· 26 24 'custom' 27 25 ] 28 26 ``` 29 - ```ts [vitest.workspace.js] 30 - export default [ 31 - './packages/server', // has package.json with "@pkg/server" 32 - './utils', // doesn't have a package.json file 33 - { 34 - // doesn't customize the name 35 - test: { 36 - pool: 'threads', 37 - }, 27 + ```ts [vitest.config.js] 28 + import { defineConfig } from 'vitest/config' 29 + 30 + export default defineConfig({ 31 + test: { 32 + projects: [ 33 + './packages/server', // has package.json with "@pkg/server" 34 + './utils', // doesn't have a package.json file 35 + { 36 + // doesn't customize the name 37 + test: { 38 + pool: 'threads', 39 + }, 40 + }, 41 + { 42 + // customized the name 43 + test: { 44 + name: 'custom', 45 + }, 46 + }, 47 + ], 38 48 }, 39 - { 40 - // customized the name 41 - test: { 42 - name: 'custom', 43 - }, 44 - }, 45 - ] 49 + }) 46 50 ``` 47 51 ::: 48 52 49 53 ::: info 50 - If the [root project](/advanced/api/vitest#getroottestproject) is not part of a user workspace, its `name` will not be resolved. 54 + If the [root project](/advanced/api/vitest#getroottestproject) is not part of user projects, its `name` will not be resolved. 51 55 ::: 52 56 53 57 ## vitest ··· 279 283 ::: 280 284 281 285 ::: info 282 - Internally, Vitest uses this method to import global setups, custom coverage providers, workspace file, and custom reporters, meaning all of them share the same module graph as long as they belong to the same Vite server. 286 + Internally, Vitest uses this method to import global setups, custom coverage providers and custom reporters, meaning all of them share the same module graph as long as they belong to the same Vite server. 283 287 ::: 284 288 285 289 ## onTestsRerun
+5 -5
docs/advanced/api/vitest.md
··· 64 64 65 65 ## config 66 66 67 - The root (or global) config. If workspace feature is enabled, projects will reference this as `globalConfig`. 67 + The root (or global) config. If projects are defined, they will reference this as `globalConfig`. 68 68 69 69 ::: warning 70 70 This is Vitest config, it doesn't extend _Vite_ config. It only has resolved values from the `test` property. ··· 101 101 102 102 ## projects 103 103 104 - An array of [test projects](/advanced/api/test-project) that belong to the user's workspace. If the user did not specify a custom workspace, the workspace will only have a [root project](#getrootproject). 104 + An array of [test projects](/advanced/api/test-project) that belong to user's projects. If the user did not specify a them, this array will only contain a [root project](#getrootproject). 105 105 106 - Vitest will ensure that there is always at least one project in the workspace. If the user specifies a non-existent `--project` name, Vitest will throw an error. 106 + Vitest will ensure that there is always at least one project in this array. If the user specifies a non-existent `--project` name, Vitest will throw an error before this array is defined. 107 107 108 108 ## getRootProject 109 109 ··· 111 111 function getRootProject(): TestProject 112 112 ``` 113 113 114 - This returns the root test project. The root project generally doesn't run any tests and is not included in `vitest.projects` unless the user explicitly includes the root config in their workspace, or the workspace is not defined at all. 114 + This returns the root test project. The root project generally doesn't run any tests and is not included in `vitest.projects` unless the user explicitly includes the root config in their configuration, or projects are not defined at all. 115 115 116 116 The primary goal of the root project is to setup the global config. In fact, `rootProject.config` references `rootProject.globalConfig` and `vitest.config` directly: 117 117 ··· 433 433 ::: 434 434 435 435 ::: info 436 - Internally, Vitest uses this method to import global setups, custom coverage providers, workspace file, and custom reporters, meaning all of them share the same module graph as long as they belong to the same Vite server. 436 + Internally, Vitest uses this method to import global setups, custom coverage providers, and custom reporters, meaning all of them share the same module graph as long as they belong to the same Vite server. 437 437 ::: 438 438 439 439 ## close
+1 -1
docs/guide/browser/commands.md
··· 11 11 12 12 ### Files Handling 13 13 14 - You can use the `readFile`, `writeFile`, and `removeFile` APIs to handle files in your browser tests. Since Vitest 3.2, all paths are resolved relative to the [project](/guide/workspace) root (which is `process.cwd()`, unless overriden manually). Previously, paths were resolved relative to the test file. 14 + You can use the `readFile`, `writeFile`, and `removeFile` APIs to handle files in your browser tests. Since Vitest 3.2, all paths are resolved relative to the [project](/guide/projects) root (which is `process.cwd()`, unless overriden manually). Previously, paths were resolved relative to the test file. 15 15 16 16 By default, Vitest uses `utf-8` encoding but you can override it with options. 17 17
+38 -34
docs/guide/browser/index.md
··· 8 8 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. 9 9 10 10 ::: tip 11 - 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/). 11 + If you are looking for documentation for `expect`, `vi` or any general API like test projects or type testing, refer to the ["Getting Started" guide](/guide/). 12 12 ::: 13 13 14 14 <img alt="Vitest UI" img-light src="/ui-browser-1-light.png"> ··· 209 209 ``` 210 210 ::: 211 211 212 - 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: 212 + If you need to run some tests using Node-based runner, you can define a [`projects`](/guide/projects) option with separate configurations for different testing strategies: 213 213 214 - {#workspace-config} 214 + {#projects-config} 215 215 216 - ```ts [vitest.workspace.ts] 217 - import { defineWorkspace } from 'vitest/config' 216 + ```ts [vitest.config.ts] 217 + import { defineConfig } from 'vitest/config' 218 218 219 - export default defineWorkspace([ 220 - { 221 - test: { 222 - // an example of file based convention, 223 - // you don't have to follow it 224 - include: [ 225 - 'tests/unit/**/*.{test,spec}.ts', 226 - 'tests/**/*.unit.{test,spec}.ts', 227 - ], 228 - name: 'unit', 229 - environment: 'node', 230 - }, 231 - }, 232 - { 233 - test: { 234 - // an example of file based convention, 235 - // you don't have to follow it 236 - include: [ 237 - 'tests/browser/**/*.{test,spec}.ts', 238 - 'tests/**/*.browser.{test,spec}.ts', 239 - ], 240 - name: 'browser', 241 - browser: { 242 - enabled: true, 243 - instances: [ 244 - { browser: 'chromium' }, 245 - ], 219 + export default defineConfig({ 220 + test: { 221 + projects: [ 222 + { 223 + test: { 224 + // an example of file based convention, 225 + // you don't have to follow it 226 + include: [ 227 + 'tests/unit/**/*.{test,spec}.ts', 228 + 'tests/**/*.unit.{test,spec}.ts', 229 + ], 230 + name: 'unit', 231 + environment: 'node', 232 + }, 246 233 }, 247 - }, 234 + { 235 + test: { 236 + // an example of file based convention, 237 + // you don't have to follow it 238 + include: [ 239 + 'tests/browser/**/*.{test,spec}.ts', 240 + 'tests/**/*.browser.{test,spec}.ts', 241 + ], 242 + name: 'browser', 243 + browser: { 244 + enabled: true, 245 + instances: [ 246 + { browser: 'chromium' }, 247 + ], 248 + }, 249 + }, 250 + }, 251 + ], 248 252 }, 249 - ]) 253 + }) 250 254 ``` 251 255 252 256 ## Browser Option Types
+1 -1
docs/guide/browser/multiple-setups.md
··· 2 2 3 3 Since Vitest 3, you can specify several different browser setups using the new [`browser.instances`](/guide/browser/config#browser-instances) option. 4 4 5 - The main advantage of using the `browser.instances` over the [workspace](/guide/workspace) is improved caching. Every project will use the same Vite server meaning the file transform and [dependency pre-bundling](https://vite.dev/guide/dep-pre-bundling.html) has to happen only once. 5 + The main advantage of using the `browser.instances` over the [test projects](/guide/projects) is improved caching. Every project will use the same Vite server meaning the file transform and [dependency pre-bundling](https://vite.dev/guide/dep-pre-bundling.html) has to happen only once. 6 6 7 7 ## Several Browsers 8 8
+4 -4
test/browser/test/commands.test.ts
··· 13 13 catch (err) { 14 14 expect(err.message).toMatch(`ENOENT: no such file or directory, open`) 15 15 if (server.platform === 'win32') { 16 - expect(err.message).toMatch('test\\browser\\test\\test.txt') 16 + expect(err.message).toMatch('test\\browser\\test.txt') 17 17 } 18 18 else { 19 - expect(err.message).toMatch('test/browser/test/test.txt') 19 + expect(err.message).toMatch('test/browser/test.txt') 20 20 } 21 21 } 22 22 ··· 34 34 catch (err) { 35 35 expect(err.message).toMatch(`ENOENT: no such file or directory, open`) 36 36 if (server.platform === 'win32') { 37 - expect(err.message).toMatch('test\\browser\\test\\test.txt') 37 + expect(err.message).toMatch('test\\browser\\test.txt') 38 38 } 39 39 else { 40 - expect(err.message).toMatch('test/browser/test/test.txt') 40 + expect(err.message).toMatch('test/browser/test.txt') 41 41 } 42 42 } 43 43 })
-2
test/cli/test/browser-multiple.test.ts
··· 6 6 7 7 it('automatically assigns the port', async () => { 8 8 const root = resolve(import.meta.dirname, '../fixtures/browser-multiple') 9 - const workspace = resolve(import.meta.dirname, '../fixtures/browser-multiple/vitest.workspace.ts') 10 9 const spy = vi.spyOn(console, 'log') 11 10 onTestFinished(() => spy.mockRestore()) 12 11 let ctx: Vitest 13 12 let urls: (string | undefined)[] = [] 14 13 const { stderr } = await runVitest({ 15 14 root, 16 - workspace, 17 15 dir: root, 18 16 watch: false, 19 17 reporters: [
+7 -7
test/config/test/browser-configs.test.ts
··· 68 68 69 69 test('assignes names as browsers in a custom project', async () => { 70 70 const { projects } = await vitest({ 71 - workspace: [ 71 + projects: [ 72 72 { 73 73 test: { 74 74 name: 'custom', ··· 207 207 test('coverage provider v8 works correctly in workspaced browser mode if instances are filtered', async () => { 208 208 const { projects } = await vitest({ 209 209 project: 'browser (chromium)', 210 - workspace: [ 210 + projects: [ 211 211 { 212 212 test: { 213 213 name: 'browser', ··· 236 236 test('filter for the global browser project includes all browser instances', async () => { 237 237 const { projects } = await vitest({ 238 238 project: 'myproject', 239 - workspace: [ 239 + projects: [ 240 240 { 241 241 test: { 242 242 name: 'myproject', ··· 275 275 }, 276 276 }, 277 277 { 278 - workspace: [ 278 + projects: [ 279 279 { 280 280 test: { 281 281 name: 'unit', ··· 373 373 describe('[e2e] workspace configs are affected by the CLI options', () => { 374 374 test('UI is not enabled by default in headless config', async () => { 375 375 const vitest = await getCliConfig({ 376 - workspace: [ 376 + projects: [ 377 377 { 378 378 test: { 379 379 name: 'unit', ··· 426 426 427 427 test('CLI options correctly override inline workspace options', async () => { 428 428 const vitest = await getCliConfig({ 429 - workspace: [ 429 + projects: [ 430 430 { 431 431 test: { 432 432 name: 'unit', ··· 481 481 test('CLI options correctly override config file workspace options', async () => { 482 482 const vitest = await getCliConfig( 483 483 { 484 - workspace: [ 484 + projects: [ 485 485 { 486 486 test: { 487 487 name: 'unit',
+10 -10
test/config/test/configureVitest.test.ts
··· 30 30 31 31 test('can change the project and the global configurations', async () => { 32 32 const v = await vitest({}, { 33 - workspace: [ 33 + projects: [ 34 34 { 35 35 plugins: [ 36 36 { ··· 59 59 const { projects } = await vitest({ 60 60 project: 'project-2', 61 61 }, { 62 - workspace: [ 62 + projects: [ 63 63 { 64 64 test: { 65 65 name: 'project-1', ··· 113 113 let newWorkspace: TestProject[] = [] 114 114 const { projects } = await vitest({ 115 115 project: 'project-1', 116 - workspace: [ 116 + projects: [ 117 117 { 118 118 test: { 119 119 name: 'project-1', ··· 143 143 let newWorkspace: TestProject[] = [] 144 144 const { projects } = await vitest({ 145 145 project: 'project-1', 146 - workspace: [ 146 + projects: [ 147 147 { 148 148 test: { 149 149 name: 'project-1', ··· 174 174 175 175 test('adding a plugin with existing name throws and error', async () => { 176 176 await expect(() => vitest({ 177 - workspace: [ 177 + projects: [ 178 178 { 179 179 test: { 180 180 name: 'project-1', ··· 194 194 }, 195 195 ], 196 196 }), 197 - ).rejects.toThrowError('Project name "project-1" is not unique. All projects in a workspace should have unique names. Make sure your configuration is correct.') 197 + ).rejects.toThrowError('Project name "project-1" is not unique. All projects should have unique names. Make sure your configuration is correct.') 198 198 199 199 await expect(() => vitest({ 200 - workspace: [ 200 + projects: [ 201 201 { 202 202 plugins: [ 203 203 { ··· 219 219 }, 220 220 ], 221 221 }), 222 - ).rejects.toThrowError('Project name "project-1" is not unique. All projects in a workspace should have unique names. Make sure your configuration is correct.') 222 + ).rejects.toThrowError('Project name "project-1" is not unique. All projects should have unique names. Make sure your configuration is correct.') 223 223 224 224 await expect(() => vitest({ 225 - workspace: [ 225 + projects: [ 226 226 { 227 227 plugins: [ 228 228 { ··· 246 246 }, 247 247 ], 248 248 }), 249 - ).rejects.toThrowError('Project name "project-1" is not unique. All projects in a workspace should have unique names. Make sure your configuration is correct.') 249 + ).rejects.toThrowError('Project name "project-1" is not unique. All projects should have unique names. Make sure your configuration is correct.') 250 250 })
+4 -4
test/config/test/failures.test.ts
··· 476 476 }, 477 477 }, 478 478 }) 479 - expect(stderr).toMatch('Cannot define a nested project for a firefox browser. The project name "firefox" was already defined. If you have multiple instances for the same browser, make sure to define a custom "name". All projects in a workspace should have unique names. Make sure your configuration is correct.') 479 + expect(stderr).toMatch('Cannot define a nested project for a firefox browser. The project name "firefox" was already defined. If you have multiple instances for the same browser, make sure to define a custom "name". All projects should have unique names. Make sure your configuration is correct.') 480 480 }) 481 481 482 482 test('browser.instances throws an error if no custom name is provided, but the config name is inherited', async () => { ··· 491 491 ], 492 492 }, 493 493 }) 494 - expect(stderr).toMatch('Cannot define a nested project for a firefox browser. The project name "custom (firefox)" was already defined. If you have multiple instances for the same browser, make sure to define a custom "name". All projects in a workspace should have unique names. Make sure your configuration is correct.') 494 + expect(stderr).toMatch('Cannot define a nested project for a firefox browser. The project name "custom (firefox)" was already defined. If you have multiple instances for the same browser, make sure to define a custom "name". All projects should have unique names. Make sure your configuration is correct.') 495 495 }) 496 496 497 497 test('throws an error if name conflicts with a workspace name', async () => { 498 498 const { stderr } = await runVitest({ 499 - workspace: [ 499 + projects: [ 500 500 { test: { name: '1 (firefox)' } }, 501 501 { 502 502 test: { ··· 511 511 }, 512 512 ], 513 513 }) 514 - expect(stderr).toMatch('Cannot define a nested project for a firefox browser. The project name "1 (firefox)" was already defined. If you have multiple instances for the same browser, make sure to define a custom "name". All projects in a workspace should have unique names. Make sure your configuration is correct.') 514 + expect(stderr).toMatch('Cannot define a nested project for a firefox browser. The project name "1 (firefox)" was already defined. If you have multiple instances for the same browser, make sure to define a custom "name". All projects should have unique names. Make sure your configuration is correct.') 515 515 }) 516 516 517 517 test('throws an error if several browsers are headed in nonTTY mode', async () => {
+150
test/config/test/projects.test.ts
··· 1 + import { resolve } from 'pathe' 2 + import { expect, it } from 'vitest' 3 + import { runVitest } from '../../test-utils' 4 + 5 + it('correctly runs workspace tests when workspace config path is specified', async () => { 6 + const { stderr, stdout } = await runVitest({ 7 + root: 'fixtures/workspace', 8 + workspace: 'nested/e2e.projects.js', 9 + }) 10 + expect(stderr).toBe('') 11 + expect(stdout).toContain('1 + 1 = 2') 12 + expect(stdout).not.toContain('2 + 2 = 4') 13 + }) 14 + 15 + it('runs the workspace if there are several vitest config files', async () => { 16 + const { stderr, stdout } = await runVitest({ 17 + root: 'fixtures/workspace/several-configs', 18 + }) 19 + expect(stderr).toBe('') 20 + expect(stdout).toContain('workspace/several-configs') 21 + expect(stdout).toContain('| 1_test') 22 + expect(stdout).toContain('| 2_test') 23 + expect(stdout).toContain('1 + 1 = 2') 24 + expect(stdout).toContain('2 + 2 = 4') 25 + }) 26 + 27 + it('correctly resolves workspace projects with a several folder globs', async () => { 28 + const { stderr, stdout } = await runVitest({ 29 + root: 'fixtures/workspace/several-folders', 30 + }) 31 + expect(stderr).toBe('') 32 + expect(stdout).toContain('test - a') 33 + expect(stdout).toContain('test - b') 34 + }) 35 + 36 + it('supports glob negation pattern', async () => { 37 + const { stderr, stdout } = await runVitest({ 38 + root: 'fixtures/workspace/negated', 39 + }) 40 + expect(stderr).toBe('') 41 + expect(stdout).toContain('test - a') 42 + expect(stdout).toContain('test - c') 43 + expect(stdout).not.toContain('test - b') 44 + }) 45 + 46 + it('fails if project names are identical with a nice error message', async () => { 47 + const { stderr } = await runVitest({ 48 + root: 'fixtures/workspace/invalid-duplicate-configs', 49 + }, [], 'test', {}, { fails: true }) 50 + expect(stderr).toContain( 51 + `Project name "test" from "vitest2.config.js" is not unique. The project is already defined by "vitest1.config.js". 52 + 53 + Your config matched these files: 54 + - vitest1.config.js 55 + - vitest2.config.js 56 + 57 + All projects should have unique names. Make sure your configuration is correct.`, 58 + ) 59 + }) 60 + 61 + it('fails if project names are identical inside the inline config', async () => { 62 + const { stderr } = await runVitest({ 63 + root: 'fixtures/workspace/invalid-duplicate-inline', 64 + }, [], 'test', {}, { fails: true }) 65 + expect(stderr).toContain( 66 + 'Project name "test" is not unique. All projects should have unique names. Make sure your configuration is correct.', 67 + ) 68 + }) 69 + 70 + it('fails if referenced file doesnt exist', async () => { 71 + const { stderr } = await runVitest({ 72 + root: 'fixtures/workspace/invalid-non-existing-config', 73 + }, [], 'test', {}, { fails: true }) 74 + expect(stderr).toContain( 75 + `Projects definition references a non-existing file or a directory: ${resolve('fixtures/workspace/invalid-non-existing-config/vitest.config.js')}`, 76 + ) 77 + }) 78 + 79 + it('vite import analysis is applied when loading workspace config', async () => { 80 + const { stderr, stdout } = await runVitest({ 81 + root: 'fixtures/workspace/config-import-analysis', 82 + }) 83 + expect(stderr).toBe('') 84 + expect(stdout).toContain('test - a') 85 + }) 86 + 87 + it('can define inline workspace config programmatically', async () => { 88 + const { stderr, stdout } = await runVitest({ 89 + root: 'fixtures/workspace/api', 90 + env: { 91 + TEST_ROOT: '1', 92 + }, 93 + projects: [ 94 + { 95 + extends: true, 96 + test: { 97 + name: 'project-1', 98 + }, 99 + }, 100 + { 101 + test: { 102 + name: 'project-2', 103 + env: { 104 + TEST_ROOT: '2', 105 + }, 106 + }, 107 + }, 108 + { 109 + extends: './vite.custom.config.js', 110 + test: { 111 + name: 'project-3', 112 + }, 113 + }, 114 + ], 115 + }) 116 + expect(stderr).toBe('') 117 + expect(stdout).toContain('project-1') 118 + expect(stdout).toContain('project-2') 119 + expect(stdout).toContain('project-3') 120 + expect(stdout).toContain('3 passed') 121 + }) 122 + 123 + it('correctly inherits the root config', async () => { 124 + const { stderr, stdout } = await runVitest({ 125 + root: 'fixtures/workspace/config-extends', 126 + }) 127 + expect(stderr).toBe('') 128 + expect(stdout).toContain('repro.test.js > importing a virtual module') 129 + }) 130 + 131 + it('fails if workspace is empty', async () => { 132 + const { stderr } = await runVitest({ 133 + projects: [], 134 + }) 135 + expect(stderr).toContain('No projects were found. Make sure your configuration is correct. The projects definition: [].') 136 + }) 137 + 138 + it('fails if workspace is filtered by the project', async () => { 139 + const { stderr } = await runVitest({ 140 + project: 'non-existing', 141 + root: 'fixtures/workspace/config-empty', 142 + config: './vitest.config.js', 143 + projects: [ 144 + './vitest.config.js', 145 + ], 146 + }) 147 + expect(stderr).toContain(`No projects were found. Make sure your configuration is correct. The filter matched no projects: non-existing. The projects definition: [ 148 + "./vitest.config.js" 149 + ].`) 150 + })
-157
test/config/test/workspace.test.ts
··· 1 - import { resolve } from 'pathe' 2 - import { expect, it } from 'vitest' 3 - import { runVitest } from '../../test-utils' 4 - 5 - it('correctly runs workspace tests when workspace config path is specified', async () => { 6 - const { stderr, stdout } = await runVitest({ 7 - root: 'fixtures/workspace', 8 - workspace: 'nested/e2e.projects.js', 9 - }) 10 - expect(stderr).toBe('') 11 - expect(stdout).toContain('1 + 1 = 2') 12 - expect(stdout).not.toContain('2 + 2 = 4') 13 - }) 14 - 15 - it('runs the workspace if there are several vitest config files', async () => { 16 - const { stderr, stdout } = await runVitest({ 17 - root: 'fixtures/workspace/several-configs', 18 - workspace: './fixtures/workspace/several-configs/vitest.workspace.ts', 19 - }) 20 - expect(stderr).toBe('') 21 - expect(stdout).toContain('workspace/several-configs') 22 - expect(stdout).toContain('| 1_test') 23 - expect(stdout).toContain('| 2_test') 24 - expect(stdout).toContain('1 + 1 = 2') 25 - expect(stdout).toContain('2 + 2 = 4') 26 - }) 27 - 28 - it('correctly resolves workspace projects with a several folder globs', async () => { 29 - const { stderr, stdout } = await runVitest({ 30 - root: 'fixtures/workspace/several-folders', 31 - workspace: './fixtures/workspace/several-folders/vitest.workspace.ts', 32 - }) 33 - expect(stderr).toBe('') 34 - expect(stdout).toContain('test - a') 35 - expect(stdout).toContain('test - b') 36 - }) 37 - 38 - it('supports glob negation pattern', async () => { 39 - const { stderr, stdout } = await runVitest({ 40 - root: 'fixtures/workspace/negated', 41 - workspace: './fixtures/workspace/negated/vitest.workspace.ts', 42 - }) 43 - expect(stderr).toBe('') 44 - expect(stdout).toContain('test - a') 45 - expect(stdout).toContain('test - c') 46 - expect(stdout).not.toContain('test - b') 47 - }) 48 - 49 - it('fails if project names are identical with a nice error message', async () => { 50 - const { stderr } = await runVitest({ 51 - root: 'fixtures/workspace/invalid-duplicate-configs', 52 - workspace: './fixtures/workspace/invalid-duplicate-configs/vitest.workspace.ts', 53 - }, [], 'test', {}, { fails: true }) 54 - expect(stderr).toContain( 55 - `Project name "test" from "vitest2.config.js" is not unique. The project is already defined by "vitest1.config.js". 56 - 57 - Your config matched these files: 58 - - vitest1.config.js 59 - - vitest2.config.js 60 - 61 - All projects in a workspace should have unique names. Make sure your configuration is correct.`, 62 - ) 63 - }) 64 - 65 - it('fails if project names are identical inside the inline config', async () => { 66 - const { stderr } = await runVitest({ 67 - root: 'fixtures/workspace/invalid-duplicate-inline', 68 - workspace: './fixtures/workspace/invalid-duplicate-inline/vitest.workspace.ts', 69 - }, [], 'test', {}, { fails: true }) 70 - expect(stderr).toContain( 71 - 'Project name "test" is not unique. All projects in a workspace should have unique names. Make sure your configuration is correct.', 72 - ) 73 - }) 74 - 75 - it('fails if referenced file doesnt exist', async () => { 76 - const { stderr } = await runVitest({ 77 - root: 'fixtures/workspace/invalid-non-existing-config', 78 - workspace: './fixtures/workspace/invalid-non-existing-config/vitest.workspace.ts', 79 - }, [], 'test', {}, { fails: true }) 80 - expect(stderr).toContain( 81 - `Workspace config file "vitest.workspace.ts" references a non-existing file or a directory: ${resolve('fixtures/workspace/invalid-non-existing-config/vitest.config.js')}`, 82 - ) 83 - }) 84 - 85 - it('vite import analysis is applied when loading workspace config', async () => { 86 - const { stderr, stdout } = await runVitest({ 87 - root: 'fixtures/workspace/config-import-analysis', 88 - workspace: './fixtures/workspace/config-import-analysis/vitest.workspace.ts', 89 - }) 90 - expect(stderr).toBe('') 91 - expect(stdout).toContain('test - a') 92 - }) 93 - 94 - it('can define inline workspace config programmatically', async () => { 95 - const { stderr, stdout } = await runVitest({ 96 - root: 'fixtures/workspace/api', 97 - env: { 98 - TEST_ROOT: '1', 99 - }, 100 - workspace: [ 101 - { 102 - extends: true, 103 - test: { 104 - name: 'project-1', 105 - }, 106 - }, 107 - { 108 - test: { 109 - name: 'project-2', 110 - env: { 111 - TEST_ROOT: '2', 112 - }, 113 - }, 114 - }, 115 - { 116 - extends: './vite.custom.config.js', 117 - test: { 118 - name: 'project-3', 119 - }, 120 - }, 121 - ], 122 - }) 123 - expect(stderr).toBe('') 124 - expect(stdout).toContain('project-1') 125 - expect(stdout).toContain('project-2') 126 - expect(stdout).toContain('project-3') 127 - expect(stdout).toContain('3 passed') 128 - }) 129 - 130 - it('correctly inherits the root config', async () => { 131 - const { stderr, stdout } = await runVitest({ 132 - root: 'fixtures/workspace/config-extends', 133 - }) 134 - expect(stderr).toBe('') 135 - expect(stdout).toContain('repro.test.js > importing a virtual module') 136 - }) 137 - 138 - it('fails if workspace is empty', async () => { 139 - const { stderr } = await runVitest({ 140 - workspace: [], 141 - }) 142 - expect(stderr).toContain('No projects were found. Make sure your configuration is correct. The workspace: [].') 143 - }) 144 - 145 - it('fails if workspace is filtered by the project', async () => { 146 - const { stderr } = await runVitest({ 147 - project: 'non-existing', 148 - root: 'fixtures/workspace/config-empty', 149 - config: './vitest.config.js', 150 - workspace: [ 151 - './vitest.config.js', 152 - ], 153 - }) 154 - expect(stderr).toContain(`No projects were found. Make sure your configuration is correct. The filter matched no projects: non-existing. The workspace: [ 155 - "./vitest.config.js" 156 - ].`) 157 - })
-1
test/snapshots/test/snapshots.test.ts
··· 23 23 const { stdout, exitCode } = await runVitest({ 24 24 update: true, 25 25 root: 'test/fixtures/workspace', 26 - workspace: 'vitest.workspace.ts', 27 26 }) 28 27 expect.soft(stdout).include('Snapshots 1 updated') 29 28 expect.soft(exitCode).toBe(0)
+1 -1
test/watch/test/change-project.test.ts
··· 10 10 11 11 export default { 12 12 test: { 13 - workspace: [ 13 + projects: [ 14 14 './project-1', 15 15 './project-2', 16 16 ],
+1 -1
test/workspaces/space_3/vite.config.ts
··· 5 5 { 6 6 name: 'throw-error', 7 7 config() { 8 - throw new Error('This file should not initiate a workspace project.') 8 + throw new Error('This file should not initiate a test project.') 9 9 }, 10 10 }, 11 11 ],
+21
examples/projects/packages/client/tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + "target": "esnext", 4 + "jsx": "react", 5 + "lib": ["esnext", "dom"], 6 + "module": "node16", 7 + "moduleResolution": "node16", 8 + "types": ["@testing-library/jest-dom"], 9 + "strict": true, 10 + "declaration": true, 11 + "noEmit": true, 12 + "esModuleInterop": true, 13 + "skipLibCheck": true 14 + }, 15 + "include": [ 16 + "**/*.ts", 17 + "**/*.tsx", 18 + "./vitest.setup.ts" 19 + ], 20 + "exclude": ["node_modules"] 21 + }
+8
examples/projects/packages/client/vitest.config.ts
··· 1 + import { defineConfig } from 'vitest/config' 2 + 3 + export default defineConfig({ 4 + test: { 5 + environment: 'jsdom', 6 + setupFiles: './vitest.setup.ts', 7 + }, 8 + })
+1
examples/projects/packages/client/vitest.setup.ts
··· 1 + import '@testing-library/jest-dom/vitest'
+18
examples/projects/packages/server/mockData.ts
··· 1 + const usersData = [{ 2 + id: 1, 3 + name: 'John Snow', 4 + email: 'john@got.com', 5 + }, { 6 + id: 2, 7 + name: 'Daenerys Targaryen', 8 + email: 'daenerys@got.com', 9 + }, { 10 + id: 3, 11 + name: 'Arya Stark', 12 + email: 'arya@got.com', 13 + }, { 14 + id: 4, 15 + name: 'Rhaenyra Targaryen', 16 + email: 'rhaenyra@hod.com', 17 + }] 18 + export { usersData }
-21
examples/workspace/packages/client/tsconfig.json
··· 1 - { 2 - "compilerOptions": { 3 - "target": "esnext", 4 - "jsx": "react", 5 - "lib": ["esnext", "dom"], 6 - "module": "node16", 7 - "moduleResolution": "node16", 8 - "types": ["@testing-library/jest-dom"], 9 - "strict": true, 10 - "declaration": true, 11 - "noEmit": true, 12 - "esModuleInterop": true, 13 - "skipLibCheck": true 14 - }, 15 - "include": [ 16 - "**/*.ts", 17 - "**/*.tsx", 18 - "./vitest.setup.ts" 19 - ], 20 - "exclude": ["node_modules"] 21 - }
-8
examples/workspace/packages/client/vitest.config.ts
··· 1 - import { defineConfig } from 'vitest/config' 2 - 3 - export default defineConfig({ 4 - test: { 5 - environment: 'jsdom', 6 - setupFiles: './vitest.setup.ts', 7 - }, 8 - })
-1
examples/workspace/packages/client/vitest.setup.ts
··· 1 - import '@testing-library/jest-dom/vitest'
-18
examples/workspace/packages/server/mockData.ts
··· 1 - const usersData = [{ 2 - id: 1, 3 - name: 'John Snow', 4 - email: 'john@got.com', 5 - }, { 6 - id: 2, 7 - name: 'Daenerys Targaryen', 8 - email: 'daenerys@got.com', 9 - }, { 10 - id: 3, 11 - name: 'Arya Stark', 12 - email: 'arya@got.com', 13 - }, { 14 - id: 4, 15 - name: 'Rhaenyra Targaryen', 16 - email: 'rhaenyra@hod.com', 17 - }] 18 - export { usersData }
+37 -10
packages/vitest/src/node/core.ts
··· 15 15 import { getTasks, hasFailed } from '@vitest/runner/utils' 16 16 import { SnapshotManager } from '@vitest/snapshot/manager' 17 17 import { noop, toArray } from '@vitest/utils' 18 - import { dirname, join, normalize, relative } from 'pathe' 18 + import { dirname, join, normalize, relative, resolve } from 'pathe' 19 19 import { ViteNodeRunner } from 'vite-node/client' 20 20 import { ViteNodeServer } from 'vite-node/server' 21 21 import { version } from '../../package.json' with { type: 'json' } ··· 33 33 import { VitestPackageInstaller } from './packageInstaller' 34 34 import { createPool } from './pool' 35 35 import { TestProject } from './project' 36 + import { getDefaultTestProject, resolveBrowserProjects, resolveProjects } from './projects/resolveProjects' 36 37 import { BlobReporter, readBlobs } from './reporters/blob' 37 38 import { HangingProcessReporter } from './reporters/hanging-process' 38 39 import { createBenchmarkReporters, createReporters } from './reporters/utils' ··· 40 41 import { StateManager } from './state' 41 42 import { TestRun } from './test-run' 42 43 import { VitestWatcher } from './watcher' 43 - import { getDefaultTestProject, resolveBrowserWorkspace, resolveWorkspace } from './workspace/resolveWorkspace' 44 44 45 45 const WATCHER_DEBOUNCE = 100 46 46 ··· 274 274 } 275 275 catch { } 276 276 277 - const projects = await this.resolveWorkspace(cliOptions) 277 + const projects = await this.resolveProjects(cliOptions) 278 278 this.resolvedProjects = projects 279 279 this.projects = projects 280 280 ··· 325 325 */ 326 326 private injectTestProject = async (config: TestProjectConfiguration | TestProjectConfiguration[]): Promise<TestProject[]> => { 327 327 const currentNames = new Set(this.projects.map(p => p.name)) 328 - const workspace = await resolveWorkspace( 328 + const projects = await resolveProjects( 329 329 this, 330 330 this._options, 331 331 undefined, 332 332 Array.isArray(config) ? config : [config], 333 333 currentNames, 334 334 ) 335 - this.projects.push(...workspace) 336 - return workspace 335 + this.projects.push(...projects) 336 + return projects 337 337 } 338 338 339 339 /** ··· 423 423 return join(configDir, workspaceConfigName) 424 424 } 425 425 426 - private async resolveWorkspace(cliOptions: UserConfig): Promise<TestProject[]> { 426 + private async resolveProjects(cliOptions: UserConfig): Promise<TestProject[]> { 427 427 const names = new Set<string>() 428 428 429 + if (this.config.projects) { 430 + if (typeof this.config.workspace !== 'undefined') { 431 + this.logger.warn( 432 + 'Both `config.projects` and `config.workspace` are defined. Ignoring the `workspace` option.', 433 + ) 434 + } 435 + 436 + return resolveProjects( 437 + this, 438 + cliOptions, 439 + undefined, 440 + this.config.projects, 441 + names, 442 + ) 443 + } 444 + 429 445 if (Array.isArray(this.config.workspace)) { 430 - return resolveWorkspace( 446 + this.logger.deprecate( 447 + 'The `workspace` option is deprecated and will be removed in the next major. To hide this warning, rename `workspace` option to `projects`.', 448 + ) 449 + return resolveProjects( 431 450 this, 432 451 cliOptions, 433 452 undefined, ··· 448 467 if (!project) { 449 468 return [] 450 469 } 451 - return resolveBrowserWorkspace(this, new Set([project.name]), [project]) 470 + return resolveBrowserProjects(this, new Set([project.name]), [project]) 452 471 } 472 + 473 + const configFile = this.vite.config.configFile 474 + ? resolve(this.vite.config.root, this.vite.config.configFile) 475 + : 'the root config file' 476 + 477 + this.logger.deprecate( 478 + `The workspace file is deprecated and will be removed in the next major. Please, use the \`projects\` field in ${configFile} instead.`, 479 + ) 453 480 454 481 const workspaceModule = await this.import<{ 455 482 default: ReturnType<typeof defineWorkspace> ··· 459 486 throw new TypeError(`Workspace config file "${workspaceConfigPath}" must export a default array of project paths.`) 460 487 } 461 488 462 - return resolveWorkspace( 489 + return resolveProjects( 463 490 this, 464 491 cliOptions, 465 492 workspaceConfigPath,
+4
packages/vitest/src/node/logger.ts
··· 110 110 printError(err, this.ctx, this, options) 111 111 } 112 112 113 + deprecate(message: string): void { 114 + this.log(c.bold(c.bgYellow(' DEPRECATED ')), c.yellow(message)) 115 + } 116 + 113 117 clearHighlightCache(filename?: string): void { 114 118 if (filename) { 115 119 this._highlights.delete(filename)
+1 -1
packages/vitest/src/node/pool.ts
··· 70 70 for (const [glob, pool] of project.config.poolMatchGlobs) { 71 71 if ((pool as Pool) === 'browser') { 72 72 throw new Error( 73 - 'Since Vitest 0.31.0 "browser" pool is not supported in "poolMatchGlobs". You can create a workspace to run some of your tests in browser in parallel. Read more: https://vitest.dev/guide/workspace', 73 + 'Since Vitest 0.31.0 "browser" pool is not supported in "poolMatchGlobs". You can create a project to run some of your tests in browser in parallel. Read more: https://vitest.dev/guide/projects', 74 74 ) 75 75 } 76 76 if (mm.isMatch(file, glob, { cwd: project.config.root })) {
+3
packages/vitest/src/public/config.ts
··· 66 66 return config 67 67 } 68 68 69 + /** 70 + * @deprecated use the `projects` field in the root config instead 71 + */ 69 72 export function defineWorkspace(config: TestProjectConfiguration[]): TestProjectConfiguration[] { 70 73 return config 71 74 }
+1 -1
test/browser/fixtures/inspect/vitest.config.with-workspace.ts
··· 5 5 test: { 6 6 watch: false, 7 7 8 - workspace: [ 8 + projects: [ 9 9 { 10 10 test: { 11 11 name: "Browser in workspace",
+37
test/cli/fixtures/browser-multiple/vitest.config.ts
··· 1 + import { resolve } from 'pathe'; 2 + import { defineConfig, defineWorkspace } from 'vitest/config'; 3 + 4 + export default defineConfig({ 5 + test: { 6 + projects: [ 7 + { 8 + cacheDir: resolve(import.meta.dirname, 'basic-1'), 9 + test: { 10 + name: 'basic-1', 11 + dir: import.meta.dirname, 12 + include: ['./basic.test.js'], 13 + browser: { 14 + enabled: true, 15 + instances: [{ browser: 'chromium' }], 16 + provider: 'playwright', 17 + headless: true, 18 + } 19 + } 20 + }, 21 + { 22 + cacheDir: resolve(import.meta.dirname, 'basic-2'), 23 + test: { 24 + name: 'basic-2', 25 + dir: import.meta.dirname, 26 + include: ['./basic.test.js'], 27 + browser: { 28 + enabled: true, 29 + instances: [{ browser: 'chromium' }], 30 + provider: 'playwright', 31 + headless: true, 32 + } 33 + } 34 + }, 35 + ], 36 + }, 37 + })
-33
test/cli/fixtures/browser-multiple/vitest.workspace.ts
··· 1 - import { resolve } from 'pathe'; 2 - import { defineWorkspace } from 'vitest/config'; 3 - 4 - export default defineWorkspace([ 5 - { 6 - cacheDir: resolve(import.meta.dirname, 'basic-1'), 7 - test: { 8 - name: 'basic-1', 9 - dir: import.meta.dirname, 10 - include: ['./basic.test.js'], 11 - browser: { 12 - enabled: true, 13 - instances: [{ browser: 'chromium' }], 14 - provider: 'playwright', 15 - headless: true, 16 - } 17 - } 18 - }, 19 - { 20 - cacheDir: resolve(import.meta.dirname, 'basic-2'), 21 - test: { 22 - name: 'basic-2', 23 - dir: import.meta.dirname, 24 - include: ['./basic.test.js'], 25 - browser: { 26 - enabled: true, 27 - instances: [{ browser: 'chromium' }], 28 - provider: 'playwright', 29 - headless: true, 30 - } 31 - } 32 - }, 33 - ])
+1 -1
test/cli/fixtures/config-loader/vitest.config.ts
··· 3 3 4 4 export default defineConfig({ 5 5 test: { 6 - workspace: [ 6 + projects: [ 7 7 "browser/vitest.config.ts", 8 8 "node/vitest.config.ts", 9 9 ],
+1 -1
test/cli/fixtures/custom-pool/vitest.config.ts
··· 8 8 array: [1, 2, 3], 9 9 }, 10 10 }, 11 - workspace: [ 11 + projects: [ 12 12 { 13 13 extends: true, 14 14 test: {
+21 -1
test/config/fixtures/mixed-environments/vitest.config.ts
··· 1 1 import { defineConfig } from 'vitest/config' 2 + import { resolve } from 'node:path' 3 + import { fileURLToPath } from 'node:url' 4 + 5 + const __filename = fileURLToPath(import.meta.url) 6 + const __dirname = resolve(__filename, '..') 2 7 3 8 export default defineConfig({ 4 - test: {}, 9 + test: { 10 + projects: [ 11 + { 12 + test: { 13 + name: 'Project #1', 14 + root: resolve(__dirname, './project'), 15 + }, 16 + }, 17 + { 18 + test: { 19 + name: 'Project #2', 20 + root: resolve(__dirname, './project'), 21 + }, 22 + }, 23 + ] 24 + }, 5 25 })
-21
test/config/fixtures/mixed-environments/vitest.workspace.ts
··· 1 - import { resolve } from 'node:path' 2 - import { fileURLToPath } from 'node:url' 3 - import { defineWorkspace } from 'vitest/config' 4 - 5 - const __filename = fileURLToPath(import.meta.url) 6 - const __dirname = resolve(__filename, '..') 7 - 8 - export default defineWorkspace([ 9 - { 10 - test: { 11 - name: 'Project #1', 12 - root: resolve(__dirname, './project'), 13 - }, 14 - }, 15 - { 16 - test: { 17 - name: 'Project #2', 18 - root: resolve(__dirname, './project'), 19 - }, 20 - }, 21 - ])
+1 -1
test/config/fixtures/no-browser-workspace/vitest.config.ts
··· 2 2 3 3 export default defineConfig({ 4 4 test: { 5 - workspace: [ 5 + projects: [ 6 6 { 7 7 test: { 8 8 name: 'unit',
+7 -1
test/config/fixtures/project/vitest.config.ts
··· 1 1 import { defineConfig } from "vitest/config"; 2 2 3 - export default defineConfig({}) 3 + export default defineConfig({ 4 + test: { 5 + projects: [ 6 + 'packages/*' 7 + ] 8 + } 9 + })
-3
test/config/fixtures/project/vitest.workspace.ts
··· 1 - export default [ 2 - 'packages/*' 3 - ]
+16
test/config/fixtures/snapshot-path-context/vitest.config.ts
··· 11 11 basename(path) + extension 12 12 ); 13 13 }, 14 + projects: [ 15 + { 16 + extends: './vitest.config.ts', 17 + test: { 18 + name: 'project1', 19 + root: import.meta.dirname, 20 + } 21 + }, 22 + { 23 + extends: './vitest.config.ts', 24 + test: { 25 + name: 'project2', 26 + root: import.meta.dirname, 27 + } 28 + } 29 + ], 14 30 }, 15 31 });
-18
test/config/fixtures/snapshot-path-context/vitest.workspace.ts
··· 1 - import { defineWorkspace } from 'vitest/config' 2 - 3 - export default defineWorkspace([ 4 - { 5 - extends: './vitest.config.ts', 6 - test: { 7 - name: 'project1', 8 - root: import.meta.dirname, 9 - } 10 - }, 11 - { 12 - extends: './vitest.config.ts', 13 - test: { 14 - name: 'project2', 15 - root: import.meta.dirname, 16 - } 17 - } 18 - ])
+7
test/config/fixtures/workspace-flags/vitest.config.js
··· 1 + import { defineConfig } from 'vitest/config' 2 + 3 + export default defineConfig({ 4 + test: { 5 + projects: ['projects'], 6 + }, 7 + })
-1
test/config/fixtures/workspace-flags/vitest.workspace.js
··· 1 - export default ['projects']
+5 -1
test/reporters/fixtures/better-testsuite-name/vitest.config.ts
··· 1 1 import { defineConfig } from 'vitest/config' 2 2 3 - export default defineConfig({}) 3 + export default defineConfig({ 4 + test: { 5 + projects: ['space-1', 'space-2'], 6 + }, 7 + })
-3
test/reporters/fixtures/better-testsuite-name/vitest.workspace.ts
··· 1 - import { defineWorkspace } from "vitest/config"; 2 - 3 - export default defineWorkspace(['space-1', 'space-2'])
+32
examples/projects/packages/client/components/Link.tsx
··· 1 + import React, { useState } from 'react' 2 + 3 + const STATUS = { 4 + HOVERED: 'hovered', 5 + NORMAL: 'normal', 6 + } 7 + 8 + function Link({ page, children }: React.PropsWithChildren<{ page: string }>) { 9 + const [status, setStatus] = useState(STATUS.NORMAL) 10 + 11 + const onMouseEnter = () => { 12 + setStatus(STATUS.HOVERED) 13 + } 14 + 15 + const onMouseLeave = () => { 16 + setStatus(STATUS.NORMAL) 17 + } 18 + 19 + return ( 20 + <a 21 + className={status} 22 + href={page || '#'} 23 + aria-label={`Link is ${status}`} 24 + onMouseEnter={onMouseEnter} 25 + onMouseLeave={onMouseLeave} 26 + > 27 + {children} 28 + </a> 29 + ) 30 + } 31 + 32 + export default Link
+23
examples/projects/packages/client/test/basic.test.tsx
··· 1 + import { render, screen } from '@testing-library/react' 2 + import { userEvent } from '@testing-library/user-event' 3 + import React from 'react' 4 + import { expect, test } from 'vitest' 5 + import Link from '../components/Link.js' 6 + 7 + test('Link changes the state when hovered', async () => { 8 + render( 9 + <Link page="http://antfu.me">Anthony Fu</Link>, 10 + ) 11 + 12 + const link = screen.getByText('Anthony Fu') 13 + 14 + expect(link).toHaveAccessibleName('Link is normal') 15 + 16 + await userEvent.hover(link) 17 + 18 + await expect.poll(() => link).toHaveAccessibleName('Link is hovered') 19 + 20 + await userEvent.unhover(link) 21 + 22 + await expect.poll(() => link).toHaveAccessibleName('Link is normal') 23 + })
+13
examples/projects/packages/server/src/app.ts
··· 1 + import type { FastifyInstance } from 'fastify' 2 + import Fastify from 'fastify' 3 + import { usersData } from '../mockData.js' 4 + 5 + const app: FastifyInstance = Fastify({ 6 + logger: process.env.NODE_ENV === 'development', 7 + }) 8 + 9 + app.get('/users', async () => { 10 + return usersData 11 + }) 12 + 13 + export default app
+13
examples/projects/packages/server/src/index.ts
··· 1 + import app from './app.js' 2 + 3 + async function start() { 4 + try { 5 + await app.listen({ port: 3000 }) 6 + } 7 + catch (err) { 8 + app.log.error(err) 9 + process.exit(1) 10 + } 11 + } 12 + 13 + start()
+44
examples/projects/packages/server/test/app.test.ts
··· 1 + import supertest from 'supertest' 2 + import { afterAll, expect, test } from 'vitest' 3 + 4 + import { usersData } from '../mockData.js' 5 + import app from '../src/app.js' 6 + 7 + test('with HTTP injection', async () => { 8 + const response = await app.inject({ 9 + method: 'GET', 10 + url: '/users', 11 + }) 12 + 13 + expect(response.statusCode).toBe(200) 14 + expect(JSON.parse(response.payload)).toHaveLength(4) 15 + expect(JSON.parse(response.payload)).toStrictEqual(usersData) 16 + }) 17 + 18 + test('with a running server', async () => { 19 + await app.ready() 20 + 21 + const response = await supertest(app.server) 22 + .get('/users') 23 + .expect(200) 24 + 25 + expect(response.body).toHaveLength(4) 26 + expect(response.body).toStrictEqual(usersData) 27 + }) 28 + 29 + test('with axios', async () => { 30 + await app.listen() 31 + await app.ready() 32 + 33 + const address = app.server.address() 34 + const port = typeof address === 'string' ? address : address?.port 35 + 36 + const response = await fetch(`http://localhost:${port}/users`).then(r => r.json()) 37 + 38 + expect(response).toHaveLength(4) 39 + expect(response).toStrictEqual(usersData) 40 + }) 41 + 42 + afterAll(async () => { 43 + await app.close() 44 + })
-32
examples/workspace/packages/client/components/Link.tsx
··· 1 - import React, { useState } from 'react' 2 - 3 - const STATUS = { 4 - HOVERED: 'hovered', 5 - NORMAL: 'normal', 6 - } 7 - 8 - function Link({ page, children }: React.PropsWithChildren<{ page: string }>) { 9 - const [status, setStatus] = useState(STATUS.NORMAL) 10 - 11 - const onMouseEnter = () => { 12 - setStatus(STATUS.HOVERED) 13 - } 14 - 15 - const onMouseLeave = () => { 16 - setStatus(STATUS.NORMAL) 17 - } 18 - 19 - return ( 20 - <a 21 - className={status} 22 - href={page || '#'} 23 - aria-label={`Link is ${status}`} 24 - onMouseEnter={onMouseEnter} 25 - onMouseLeave={onMouseLeave} 26 - > 27 - {children} 28 - </a> 29 - ) 30 - } 31 - 32 - export default Link
-23
examples/workspace/packages/client/test/basic.test.tsx
··· 1 - import { render, screen } from '@testing-library/react' 2 - import { userEvent } from '@testing-library/user-event' 3 - import React from 'react' 4 - import { expect, test } from 'vitest' 5 - import Link from '../components/Link.jsx' 6 - 7 - test('Link changes the state when hovered', async () => { 8 - render( 9 - <Link page="http://antfu.me">Anthony Fu</Link>, 10 - ) 11 - 12 - const link = screen.getByText('Anthony Fu') 13 - 14 - expect(link).toHaveAccessibleName('Link is normal') 15 - 16 - await userEvent.hover(link) 17 - 18 - await expect.poll(() => link).toHaveAccessibleName('Link is hovered') 19 - 20 - await userEvent.unhover(link) 21 - 22 - await expect.poll(() => link).toHaveAccessibleName('Link is normal') 23 - })
-13
examples/workspace/packages/server/src/app.ts
··· 1 - import type { FastifyInstance } from 'fastify' 2 - import Fastify from 'fastify' 3 - import { usersData } from '../mockData' 4 - 5 - const app: FastifyInstance = Fastify({ 6 - logger: process.env.NODE_ENV === 'development', 7 - }) 8 - 9 - app.get('/users', async () => { 10 - return usersData 11 - }) 12 - 13 - export default app
-13
examples/workspace/packages/server/src/index.ts
··· 1 - import app from './app' 2 - 3 - async function start() { 4 - try { 5 - await app.listen({ port: 3000 }) 6 - } 7 - catch (err) { 8 - app.log.error(err) 9 - process.exit(1) 10 - } 11 - } 12 - 13 - start()
-44
examples/workspace/packages/server/test/app.test.ts
··· 1 - import supertest from 'supertest' 2 - import { afterAll, expect, test } from 'vitest' 3 - 4 - import { usersData } from '../mockData' 5 - import app from '../src/app' 6 - 7 - test('with HTTP injection', async () => { 8 - const response = await app.inject({ 9 - method: 'GET', 10 - url: '/users', 11 - }) 12 - 13 - expect(response.statusCode).toBe(200) 14 - expect(JSON.parse(response.payload)).toHaveLength(4) 15 - expect(JSON.parse(response.payload)).toStrictEqual(usersData) 16 - }) 17 - 18 - test('with a running server', async () => { 19 - await app.ready() 20 - 21 - const response = await supertest(app.server) 22 - .get('/users') 23 - .expect(200) 24 - 25 - expect(response.body).toHaveLength(4) 26 - expect(response.body).toStrictEqual(usersData) 27 - }) 28 - 29 - test('with axios', async () => { 30 - await app.listen() 31 - await app.ready() 32 - 33 - const address = app.server.address() 34 - const port = typeof address === 'string' ? address : address?.port 35 - 36 - const response = await fetch(`http://localhost:${port}/users`).then(r => r.json()) 37 - 38 - expect(response).toHaveLength(4) 39 - expect(response).toStrictEqual(usersData) 40 - }) 41 - 42 - afterAll(async () => { 43 - await app.close() 44 - })
+2 -1
packages/vitest/src/node/cli/cli-config.ts
··· 332 332 argument: '<name>', 333 333 }, 334 334 workspace: { 335 - description: 'Path to a workspace configuration file', 335 + description: '[deprecated] Path to a workspace configuration file', 336 336 argument: '<path>', 337 337 normalize: true, 338 338 }, ··· 863 863 json: null, 864 864 provide: null, 865 865 filesOnly: null, 866 + projects: null, 866 867 watchTriggerPatterns: null, 867 868 } 868 869
+478
packages/vitest/src/node/projects/resolveProjects.ts
··· 1 + import type { GlobOptions } from 'tinyglobby' 2 + import type { Vitest } from '../core' 3 + import type { BrowserInstanceOption, ResolvedConfig, TestProjectConfiguration, UserConfig, UserWorkspaceConfig } from '../types/config' 4 + import { existsSync, promises as fs } from 'node:fs' 5 + import os from 'node:os' 6 + import { limitConcurrency } from '@vitest/runner/utils' 7 + import { deepClone } from '@vitest/utils' 8 + import { dirname, relative, resolve } from 'pathe' 9 + import { glob, isDynamicPattern } from 'tinyglobby' 10 + import { mergeConfig } from 'vite' 11 + import { configFiles as defaultConfigFiles } from '../../constants' 12 + import { isTTY } from '../../utils/env' 13 + import { VitestFilteredOutProjectError } from '../errors' 14 + import { initializeProject, TestProject } from '../project' 15 + import { withLabel } from '../reporters/renderers/utils' 16 + 17 + export async function resolveProjects( 18 + vitest: Vitest, 19 + cliOptions: UserConfig, 20 + workspaceConfigPath: string | undefined, 21 + projectsDefinition: TestProjectConfiguration[], 22 + names: Set<string>, 23 + ): Promise<TestProject[]> { 24 + const { configFiles, projectConfigs, nonConfigDirectories } = await resolveTestProjectConfigs( 25 + vitest, 26 + workspaceConfigPath, 27 + projectsDefinition, 28 + ) 29 + 30 + // cli options that affect the project config, 31 + // not all options are allowed to be overridden 32 + const overridesOptions = [ 33 + 'logHeapUsage', 34 + 'allowOnly', 35 + 'sequence', 36 + 'testTimeout', 37 + 'pool', 38 + 'update', 39 + 'globals', 40 + 'expandSnapshotDiff', 41 + 'disableConsoleIntercept', 42 + 'retry', 43 + 'testNamePattern', 44 + 'passWithNoTests', 45 + 'bail', 46 + 'isolate', 47 + 'printConsoleTrace', 48 + 'inspect', 49 + 'inspectBrk', 50 + 'fileParallelism', 51 + ] as const 52 + 53 + const cliOverrides = overridesOptions.reduce((acc, name) => { 54 + if (name in cliOptions) { 55 + acc[name] = cliOptions[name] as any 56 + } 57 + return acc 58 + }, {} as UserConfig) 59 + 60 + const projectPromises: Promise<TestProject>[] = [] 61 + const fileProjects = [...configFiles, ...nonConfigDirectories] 62 + const concurrent = limitConcurrency(os.availableParallelism?.() || os.cpus().length || 5) 63 + 64 + projectConfigs.forEach((options, index) => { 65 + const configRoot = workspaceConfigPath ? dirname(workspaceConfigPath) : vitest.config.root 66 + // if extends a config file, resolve the file path 67 + const configFile = typeof options.extends === 'string' 68 + ? resolve(configRoot, options.extends) 69 + : options.extends === true 70 + ? (vitest.vite.config.configFile || false) 71 + : false 72 + // if `root` is configured, resolve it relative to the workspace file or vite root (like other options) 73 + // if `root` is not specified, inline configs use the same root as the root project 74 + const root = options.root 75 + ? resolve(configRoot, options.root) 76 + : vitest.config.root 77 + projectPromises.push(concurrent(() => initializeProject( 78 + index, 79 + vitest, 80 + { ...options, root, configFile, test: { ...options.test, ...cliOverrides } }, 81 + ))) 82 + }) 83 + 84 + for (const path of fileProjects) { 85 + // if file leads to the root config, then we can just reuse it because we already initialized it 86 + if (vitest.vite.config.configFile === path) { 87 + const project = getDefaultTestProject(vitest) 88 + if (project) { 89 + projectPromises.push(Promise.resolve(project)) 90 + } 91 + continue 92 + } 93 + 94 + const configFile = path.endsWith('/') ? false : path 95 + const root = path.endsWith('/') ? path : dirname(path) 96 + 97 + projectPromises.push( 98 + concurrent(() => initializeProject( 99 + path, 100 + vitest, 101 + { root, configFile, test: cliOverrides }, 102 + )), 103 + ) 104 + } 105 + 106 + // pretty rare case - the glob didn't match anything and there are no inline configs 107 + if (!projectPromises.length) { 108 + throw new Error( 109 + [ 110 + 'No projects were found. Make sure your configuration is correct. ', 111 + vitest.config.project.length ? `The filter matched no projects: ${vitest.config.project.join(', ')}. ` : '', 112 + `The projects definition: ${JSON.stringify(projectsDefinition, null, 4)}.`, 113 + ].join(''), 114 + ) 115 + } 116 + 117 + const resolvedProjectsPromises = await Promise.allSettled(projectPromises) 118 + 119 + const errors: Error[] = [] 120 + const resolvedProjects: TestProject[] = [] 121 + 122 + for (const result of resolvedProjectsPromises) { 123 + if (result.status === 'rejected') { 124 + if (result.reason instanceof VitestFilteredOutProjectError) { 125 + // filter out filtered out projects 126 + continue 127 + } 128 + errors.push(result.reason) 129 + } 130 + else { 131 + resolvedProjects.push(result.value) 132 + } 133 + } 134 + 135 + if (errors.length) { 136 + throw new AggregateError( 137 + errors, 138 + 'Failed to initialize projects. There were errors during projects setup. See below for more details.', 139 + ) 140 + } 141 + 142 + // project names are guaranteed to be unique 143 + for (const project of resolvedProjects) { 144 + const name = project.name 145 + if (names.has(name)) { 146 + const duplicate = resolvedProjects.find(p => p.name === name && p !== project)! 147 + const filesError = fileProjects.length 148 + ? [ 149 + '\n\nYour config matched these files:\n', 150 + fileProjects.map(p => ` - ${relative(vitest.config.root, p)}`).join('\n'), 151 + '\n\n', 152 + ].join('') 153 + : ' ' 154 + throw new Error([ 155 + `Project name "${name}"`, 156 + project.vite.config.configFile ? ` from "${relative(vitest.config.root, project.vite.config.configFile)}"` : '', 157 + ' is not unique.', 158 + duplicate?.vite.config.configFile ? ` The project is already defined by "${relative(vitest.config.root, duplicate.vite.config.configFile)}".` : '', 159 + filesError, 160 + 'All projects should have unique names. Make sure your configuration is correct.', 161 + ].join('')) 162 + } 163 + names.add(name) 164 + } 165 + 166 + return resolveBrowserProjects(vitest, names, resolvedProjects) 167 + } 168 + 169 + export async function resolveBrowserProjects( 170 + vitest: Vitest, 171 + names: Set<string>, 172 + resolvedProjects: TestProject[], 173 + ): Promise<TestProject[]> { 174 + const removeProjects = new Set<TestProject>() 175 + 176 + resolvedProjects.forEach((project) => { 177 + if (!project.config.browser.enabled) { 178 + return 179 + } 180 + const instances = project.config.browser.instances || [] 181 + const browser = project.config.browser.name 182 + if (instances.length === 0 && browser) { 183 + instances.push({ 184 + browser, 185 + name: project.name ? `${project.name} (${browser})` : browser, 186 + }) 187 + vitest.logger.warn( 188 + withLabel( 189 + 'yellow', 190 + 'Vitest', 191 + [ 192 + `No browser "instances" were defined`, 193 + project.name ? ` for the "${project.name}" project. ` : '. ', 194 + `Running tests in "${project.config.browser.name}" browser. `, 195 + 'The "browser.name" field is deprecated since Vitest 3. ', 196 + 'Read more: https://vitest.dev/guide/browser/config#browser-instances', 197 + ].filter(Boolean).join(''), 198 + ), 199 + ) 200 + } 201 + const originalName = project.config.name 202 + // if original name is in the --project=name filter, keep all instances 203 + const filteredInstances = vitest.matchesProjectFilter(originalName) 204 + ? instances 205 + : instances.filter((instance) => { 206 + const newName = instance.name! // name is set in "workspace" plugin 207 + return vitest.matchesProjectFilter(newName) 208 + }) 209 + 210 + // every project was filtered out 211 + if (!filteredInstances.length) { 212 + removeProjects.add(project) 213 + return 214 + } 215 + 216 + if (project.config.browser.providerOptions) { 217 + vitest.logger.warn( 218 + withLabel('yellow', 'Vitest', `"providerOptions"${originalName ? ` in "${originalName}" project` : ''} is ignored because it's overridden by the configs. To hide this warning, remove the "providerOptions" property from the browser configuration.`), 219 + ) 220 + } 221 + 222 + filteredInstances.forEach((config, index) => { 223 + const browser = config.browser 224 + if (!browser) { 225 + const nth = index + 1 226 + const ending = nth === 2 ? 'nd' : nth === 3 ? 'rd' : 'th' 227 + throw new Error(`The browser configuration must have a "browser" property. The ${nth}${ending} item in "browser.instances" doesn't have it. Make sure your${originalName ? ` "${originalName}"` : ''} configuration is correct.`) 228 + } 229 + const name = config.name 230 + 231 + if (name == null) { 232 + throw new Error(`The browser configuration must have a "name" property. This is a bug in Vitest. Please, open a new issue with reproduction`) 233 + } 234 + 235 + if (names.has(name)) { 236 + throw new Error( 237 + [ 238 + `Cannot define a nested project for a ${browser} browser. The project name "${name}" was already defined. `, 239 + 'If you have multiple instances for the same browser, make sure to define a custom "name". ', 240 + 'All projects should have unique names. Make sure your configuration is correct.', 241 + ].join(''), 242 + ) 243 + } 244 + names.add(name) 245 + const clonedConfig = cloneConfig(project, config) 246 + clonedConfig.name = name 247 + const clone = TestProject._cloneBrowserProject(project, clonedConfig) 248 + resolvedProjects.push(clone) 249 + }) 250 + 251 + removeProjects.add(project) 252 + }) 253 + 254 + resolvedProjects = resolvedProjects.filter(project => !removeProjects.has(project)) 255 + 256 + const headedBrowserProjects = resolvedProjects.filter((project) => { 257 + return project.config.browser.enabled && !project.config.browser.headless 258 + }) 259 + if (headedBrowserProjects.length > 1) { 260 + const message = [ 261 + `Found multiple projects that run browser tests in headed mode: "${headedBrowserProjects.map(p => p.name).join('", "')}".`, 262 + ` Vitest cannot run multiple headed browsers at the same time.`, 263 + ].join('') 264 + if (!isTTY) { 265 + throw new Error(`${message} Please, filter projects with --browser=name or --project=name flag or run tests with "headless: true" option.`) 266 + } 267 + const prompts = await import('prompts') 268 + const { projectName } = await prompts.default({ 269 + type: 'select', 270 + name: 'projectName', 271 + choices: headedBrowserProjects.map(project => ({ 272 + title: project.name, 273 + value: project.name, 274 + })), 275 + message: `${message} Select a single project to run or cancel and run tests with "headless: true" option. Note that you can also start tests with --browser=name or --project=name flag.`, 276 + }) 277 + if (!projectName) { 278 + throw new Error('The test run was aborted.') 279 + } 280 + return resolvedProjects.filter(project => project.name === projectName) 281 + } 282 + 283 + return resolvedProjects 284 + } 285 + 286 + function cloneConfig(project: TestProject, { browser, ...config }: BrowserInstanceOption) { 287 + const { 288 + locators, 289 + viewport, 290 + testerHtmlPath, 291 + headless, 292 + screenshotDirectory, 293 + screenshotFailures, 294 + // @ts-expect-error remove just in case 295 + browser: _browser, 296 + name, 297 + ...overrideConfig 298 + } = config 299 + const currentConfig = project.config.browser 300 + return mergeConfig<any, any>({ 301 + ...deepClone(project.config), 302 + browser: { 303 + ...project.config.browser, 304 + locators: locators 305 + ? { 306 + testIdAttribute: locators.testIdAttribute ?? currentConfig.locators.testIdAttribute, 307 + } 308 + : project.config.browser.locators, 309 + viewport: viewport ?? currentConfig.viewport, 310 + testerHtmlPath: testerHtmlPath ?? currentConfig.testerHtmlPath, 311 + screenshotDirectory: screenshotDirectory ?? currentConfig.screenshotDirectory, 312 + screenshotFailures: screenshotFailures ?? currentConfig.screenshotFailures, 313 + headless: headless ?? currentConfig.headless, 314 + name: browser, 315 + providerOptions: config, 316 + instances: undefined, // projects cannot spawn more configs 317 + }, 318 + // TODO: should resolve, not merge/override 319 + } satisfies ResolvedConfig, overrideConfig) as ResolvedConfig 320 + } 321 + 322 + async function resolveTestProjectConfigs( 323 + vitest: Vitest, 324 + workspaceConfigPath: string | undefined, 325 + projectsDefinition: TestProjectConfiguration[], 326 + ) { 327 + // project configurations that were specified directly 328 + const projectsOptions: (UserWorkspaceConfig & { extends?: true | string })[] = [] 329 + 330 + // custom config files that were specified directly or resolved from a directory 331 + const projectsConfigFiles: string[] = [] 332 + 333 + // custom glob matches that should be resolved as directories or config files 334 + const projectsGlobMatches: string[] = [] 335 + 336 + // directories that don't have a config file inside, but should be treated as projects 337 + const nonConfigProjectDirectories: string[] = [] 338 + 339 + for (const definition of projectsDefinition) { 340 + if (typeof definition === 'string') { 341 + const stringOption = definition.replace('<rootDir>', vitest.config.root) 342 + // if the string doesn't contain a glob, we can resolve it directly 343 + // ['./vitest.config.js'] 344 + if (!isDynamicPattern(stringOption)) { 345 + const file = resolve(vitest.config.root, stringOption) 346 + 347 + if (!existsSync(file)) { 348 + const relativeWorkspaceConfigPath = workspaceConfigPath 349 + ? relative(vitest.config.root, workspaceConfigPath) 350 + : undefined 351 + const note = workspaceConfigPath ? `Workspace config file "${relativeWorkspaceConfigPath}"` : 'Projects definition' 352 + throw new Error(`${note} references a non-existing file or a directory: ${file}`) 353 + } 354 + 355 + const stats = await fs.stat(file) 356 + // user can specify a config file directly 357 + if (stats.isFile()) { 358 + projectsConfigFiles.push(file) 359 + } 360 + // user can specify a directory that should be used as a project 361 + else if (stats.isDirectory()) { 362 + const configFile = await resolveDirectoryConfig(file) 363 + if (configFile) { 364 + projectsConfigFiles.push(configFile) 365 + } 366 + else { 367 + const directory = file[file.length - 1] === '/' ? file : `${file}/` 368 + nonConfigProjectDirectories.push(directory) 369 + } 370 + } 371 + else { 372 + // should never happen 373 + throw new TypeError(`Unexpected file type: ${file}`) 374 + } 375 + } 376 + // if the string is a glob pattern, resolve it later 377 + // ['./packages/*'] 378 + else { 379 + projectsGlobMatches.push(stringOption) 380 + } 381 + } 382 + // if the config is inlined, we can resolve it immediately 383 + else if (typeof definition === 'function') { 384 + projectsOptions.push(await definition({ 385 + command: vitest.vite.config.command, 386 + mode: vitest.vite.config.mode, 387 + isPreview: false, 388 + isSsrBuild: false, 389 + })) 390 + } 391 + // the config is an object or a Promise that returns an object 392 + else { 393 + projectsOptions.push(await definition) 394 + } 395 + } 396 + 397 + if (projectsGlobMatches.length) { 398 + const globOptions: GlobOptions = { 399 + absolute: true, 400 + dot: true, 401 + onlyFiles: false, 402 + cwd: vitest.config.root, 403 + expandDirectories: false, 404 + ignore: [ 405 + '**/node_modules/**', 406 + // temporary vite config file 407 + '**/*.timestamp-*', 408 + // macOS directory metadata 409 + '**/.DS_Store', 410 + ], 411 + } 412 + 413 + const projectsFs = await glob(projectsGlobMatches, globOptions) 414 + 415 + await Promise.all(projectsFs.map(async (path) => { 416 + // directories are allowed with a glob like `packages/*` 417 + // in this case every directory is treated as a project 418 + if (path.endsWith('/')) { 419 + const configFile = await resolveDirectoryConfig(path) 420 + if (configFile) { 421 + projectsConfigFiles.push(configFile) 422 + } 423 + else { 424 + nonConfigProjectDirectories.push(path) 425 + } 426 + } 427 + else { 428 + projectsConfigFiles.push(path) 429 + } 430 + })) 431 + } 432 + 433 + const projectConfigFiles = Array.from(new Set(projectsConfigFiles)) 434 + 435 + return { 436 + projectConfigs: projectsOptions, 437 + nonConfigDirectories: nonConfigProjectDirectories, 438 + configFiles: projectConfigFiles, 439 + } 440 + } 441 + 442 + async function resolveDirectoryConfig(directory: string) { 443 + const files = new Set(await fs.readdir(directory)) 444 + // default resolution looks for vitest.config.* or vite.config.* files 445 + // this simulates how `findUp` works in packages/vitest/src/node/create.ts:29 446 + const configFile = defaultConfigFiles.find(file => files.has(file)) 447 + if (configFile) { 448 + return resolve(directory, configFile) 449 + } 450 + return null 451 + } 452 + 453 + export function getDefaultTestProject(vitest: Vitest): TestProject | null { 454 + const filter = vitest.config.project 455 + const project = vitest._ensureRootProject() 456 + if (!filter.length) { 457 + return project 458 + } 459 + // check for the project name and browser names 460 + const hasProjects = getPotentialProjectNames(project).some(p => 461 + vitest.matchesProjectFilter(p), 462 + ) 463 + if (hasProjects) { 464 + return project 465 + } 466 + return null 467 + } 468 + 469 + function getPotentialProjectNames(project: TestProject) { 470 + const names = [project.name] 471 + if (project.config.browser.instances) { 472 + names.push(...project.config.browser.instances.map(i => i.name!)) 473 + } 474 + else if (project.config.browser.name) { 475 + names.push(project.config.browser.name) 476 + } 477 + return names 478 + }
+2 -3
packages/vitest/src/node/reporters/basic.ts
··· 1 1 import type { File } from '@vitest/runner' 2 2 import type { Vitest } from '../core' 3 - import c from 'tinyrainbow' 4 3 import { BaseReporter } from './base' 5 4 6 5 export class BasicReporter extends BaseReporter { ··· 12 11 onInit(ctx: Vitest): void { 13 12 super.onInit(ctx) 14 13 15 - ctx.logger.log(c.bold(c.bgYellow(' DEPRECATED ')), c.yellow( 14 + ctx.logger.deprecate( 16 15 `'basic' reporter is deprecated and will be removed in Vitest v3.\n` 17 16 + `Remove 'basic' from 'reporters' option. To match 'basic' reporter 100%, use configuration:\n${ 18 17 JSON.stringify({ test: { reporters: [['default', { summary: false }]] } }, null, 2)}`, 19 - )) 18 + ) 20 19 } 21 20 22 21 reportSummary(files: File[], errors: unknown[]): void {
+8 -2
packages/vitest/src/node/types/config.ts
··· 320 320 * 321 321 * Format: [glob, environment-name] 322 322 * 323 - * @deprecated use [`workspace`](https://vitest.dev/config/#environmentmatchglobs) instead 323 + * @deprecated use [`projects`](https://vitest.dev/config/#projects) instead 324 324 * @default [] 325 325 * @example [ 326 326 * // all tests in tests/dom will run in jsdom ··· 377 377 * 378 378 * Format: [glob, pool-name] 379 379 * 380 - * @deprecated use [`workspace`](https://vitest.dev/config/#poolmatchglobs) instead 380 + * @deprecated use [`projects`](https://vitest.dev/config/#projects) instead 381 381 * @default [] 382 382 * @example [ 383 383 * // all tests in "forks" directory will run using "poolOptions.forks" API ··· 389 389 poolMatchGlobs?: [string, Exclude<Pool, 'browser'>][] 390 390 391 391 /** 392 + * Options for projects 393 + */ 394 + projects?: TestProjectConfiguration[] 395 + 396 + /** 392 397 * Path to a workspace configuration file 398 + * @deprecated use `projects` instead 393 399 */ 394 400 workspace?: string | TestProjectConfiguration[] 395 401
-478
packages/vitest/src/node/workspace/resolveWorkspace.ts
··· 1 - import type { GlobOptions } from 'tinyglobby' 2 - import type { Vitest } from '../core' 3 - import type { BrowserInstanceOption, ResolvedConfig, TestProjectConfiguration, UserConfig, UserWorkspaceConfig } from '../types/config' 4 - import { existsSync, promises as fs } from 'node:fs' 5 - import os from 'node:os' 6 - import { limitConcurrency } from '@vitest/runner/utils' 7 - import { deepClone } from '@vitest/utils' 8 - import { dirname, relative, resolve } from 'pathe' 9 - import { glob, isDynamicPattern } from 'tinyglobby' 10 - import { mergeConfig } from 'vite' 11 - import { configFiles as defaultConfigFiles } from '../../constants' 12 - import { isTTY } from '../../utils/env' 13 - import { VitestFilteredOutProjectError } from '../errors' 14 - import { initializeProject, TestProject } from '../project' 15 - import { withLabel } from '../reporters/renderers/utils' 16 - 17 - export async function resolveWorkspace( 18 - vitest: Vitest, 19 - cliOptions: UserConfig, 20 - workspaceConfigPath: string | undefined, 21 - workspaceDefinition: TestProjectConfiguration[], 22 - names: Set<string>, 23 - ): Promise<TestProject[]> { 24 - const { configFiles, projectConfigs, nonConfigDirectories } = await resolveTestProjectConfigs( 25 - vitest, 26 - workspaceConfigPath, 27 - workspaceDefinition, 28 - ) 29 - 30 - // cli options that affect the project config, 31 - // not all options are allowed to be overridden 32 - const overridesOptions = [ 33 - 'logHeapUsage', 34 - 'allowOnly', 35 - 'sequence', 36 - 'testTimeout', 37 - 'pool', 38 - 'update', 39 - 'globals', 40 - 'expandSnapshotDiff', 41 - 'disableConsoleIntercept', 42 - 'retry', 43 - 'testNamePattern', 44 - 'passWithNoTests', 45 - 'bail', 46 - 'isolate', 47 - 'printConsoleTrace', 48 - 'inspect', 49 - 'inspectBrk', 50 - 'fileParallelism', 51 - ] as const 52 - 53 - const cliOverrides = overridesOptions.reduce((acc, name) => { 54 - if (name in cliOptions) { 55 - acc[name] = cliOptions[name] as any 56 - } 57 - return acc 58 - }, {} as UserConfig) 59 - 60 - const projectPromises: Promise<TestProject>[] = [] 61 - const fileProjects = [...configFiles, ...nonConfigDirectories] 62 - const concurrent = limitConcurrency(os.availableParallelism?.() || os.cpus().length || 5) 63 - 64 - projectConfigs.forEach((options, index) => { 65 - const configRoot = workspaceConfigPath ? dirname(workspaceConfigPath) : vitest.config.root 66 - // if extends a config file, resolve the file path 67 - const configFile = typeof options.extends === 'string' 68 - ? resolve(configRoot, options.extends) 69 - : options.extends === true 70 - ? (vitest.vite.config.configFile || false) 71 - : false 72 - // if `root` is configured, resolve it relative to the workspace file or vite root (like other options) 73 - // if `root` is not specified, inline configs use the same root as the root project 74 - const root = options.root 75 - ? resolve(configRoot, options.root) 76 - : vitest.config.root 77 - projectPromises.push(concurrent(() => initializeProject( 78 - index, 79 - vitest, 80 - { ...options, root, configFile, test: { ...options.test, ...cliOverrides } }, 81 - ))) 82 - }) 83 - 84 - for (const path of fileProjects) { 85 - // if file leads to the root config, then we can just reuse it because we already initialized it 86 - if (vitest.vite.config.configFile === path) { 87 - const project = getDefaultTestProject(vitest) 88 - if (project) { 89 - projectPromises.push(Promise.resolve(project)) 90 - } 91 - continue 92 - } 93 - 94 - const configFile = path.endsWith('/') ? false : path 95 - const root = path.endsWith('/') ? path : dirname(path) 96 - 97 - projectPromises.push( 98 - concurrent(() => initializeProject( 99 - path, 100 - vitest, 101 - { root, configFile, test: cliOverrides }, 102 - )), 103 - ) 104 - } 105 - 106 - // pretty rare case - the glob didn't match anything and there are no inline configs 107 - if (!projectPromises.length) { 108 - throw new Error( 109 - [ 110 - 'No projects were found. Make sure your configuration is correct. ', 111 - vitest.config.project.length ? `The filter matched no projects: ${vitest.config.project.join(', ')}. ` : '', 112 - `The workspace: ${JSON.stringify(workspaceDefinition, null, 4)}.`, 113 - ].join(''), 114 - ) 115 - } 116 - 117 - const resolvedProjectsPromises = await Promise.allSettled(projectPromises) 118 - 119 - const errors: Error[] = [] 120 - const resolvedProjects: TestProject[] = [] 121 - 122 - for (const result of resolvedProjectsPromises) { 123 - if (result.status === 'rejected') { 124 - if (result.reason instanceof VitestFilteredOutProjectError) { 125 - // filter out filtered out projects 126 - continue 127 - } 128 - errors.push(result.reason) 129 - } 130 - else { 131 - resolvedProjects.push(result.value) 132 - } 133 - } 134 - 135 - if (errors.length) { 136 - throw new AggregateError( 137 - errors, 138 - 'Failed to initialize projects. There were errors during workspace setup. See below for more details.', 139 - ) 140 - } 141 - 142 - // project names are guaranteed to be unique 143 - for (const project of resolvedProjects) { 144 - const name = project.name 145 - if (names.has(name)) { 146 - const duplicate = resolvedProjects.find(p => p.name === name && p !== project)! 147 - const filesError = fileProjects.length 148 - ? [ 149 - '\n\nYour config matched these files:\n', 150 - fileProjects.map(p => ` - ${relative(vitest.config.root, p)}`).join('\n'), 151 - '\n\n', 152 - ].join('') 153 - : ' ' 154 - throw new Error([ 155 - `Project name "${name}"`, 156 - project.vite.config.configFile ? ` from "${relative(vitest.config.root, project.vite.config.configFile)}"` : '', 157 - ' is not unique.', 158 - duplicate?.vite.config.configFile ? ` The project is already defined by "${relative(vitest.config.root, duplicate.vite.config.configFile)}".` : '', 159 - filesError, 160 - 'All projects in a workspace should have unique names. Make sure your configuration is correct.', 161 - ].join('')) 162 - } 163 - names.add(name) 164 - } 165 - 166 - return resolveBrowserWorkspace(vitest, names, resolvedProjects) 167 - } 168 - 169 - export async function resolveBrowserWorkspace( 170 - vitest: Vitest, 171 - names: Set<string>, 172 - resolvedProjects: TestProject[], 173 - ): Promise<TestProject[]> { 174 - const removeProjects = new Set<TestProject>() 175 - 176 - resolvedProjects.forEach((project) => { 177 - if (!project.config.browser.enabled) { 178 - return 179 - } 180 - const instances = project.config.browser.instances || [] 181 - const browser = project.config.browser.name 182 - if (instances.length === 0 && browser) { 183 - instances.push({ 184 - browser, 185 - name: project.name ? `${project.name} (${browser})` : browser, 186 - }) 187 - console.warn( 188 - withLabel( 189 - 'yellow', 190 - 'Vitest', 191 - [ 192 - `No browser "instances" were defined`, 193 - project.name ? ` for the "${project.name}" project. ` : '. ', 194 - `Running tests in "${project.config.browser.name}" browser. `, 195 - 'The "browser.name" field is deprecated since Vitest 3. ', 196 - 'Read more: https://vitest.dev/guide/browser/config#browser-instances', 197 - ].filter(Boolean).join(''), 198 - ), 199 - ) 200 - } 201 - const originalName = project.config.name 202 - // if original name is in the --project=name filter, keep all instances 203 - const filteredInstances = vitest.matchesProjectFilter(originalName) 204 - ? instances 205 - : instances.filter((instance) => { 206 - const newName = instance.name! // name is set in "workspace" plugin 207 - return vitest.matchesProjectFilter(newName) 208 - }) 209 - 210 - // every project was filtered out 211 - if (!filteredInstances.length) { 212 - removeProjects.add(project) 213 - return 214 - } 215 - 216 - if (project.config.browser.providerOptions) { 217 - vitest.logger.warn( 218 - withLabel('yellow', 'Vitest', `"providerOptions"${originalName ? ` in "${originalName}" project` : ''} is ignored because it's overridden by the configs. To hide this warning, remove the "providerOptions" property from the browser configuration.`), 219 - ) 220 - } 221 - 222 - filteredInstances.forEach((config, index) => { 223 - const browser = config.browser 224 - if (!browser) { 225 - const nth = index + 1 226 - const ending = nth === 2 ? 'nd' : nth === 3 ? 'rd' : 'th' 227 - throw new Error(`The browser configuration must have a "browser" property. The ${nth}${ending} item in "browser.instances" doesn't have it. Make sure your${originalName ? ` "${originalName}"` : ''} configuration is correct.`) 228 - } 229 - const name = config.name 230 - 231 - if (name == null) { 232 - throw new Error(`The browser configuration must have a "name" property. This is a bug in Vitest. Please, open a new issue with reproduction`) 233 - } 234 - 235 - if (names.has(name)) { 236 - throw new Error( 237 - [ 238 - `Cannot define a nested project for a ${browser} browser. The project name "${name}" was already defined. `, 239 - 'If you have multiple instances for the same browser, make sure to define a custom "name". ', 240 - 'All projects in a workspace should have unique names. Make sure your configuration is correct.', 241 - ].join(''), 242 - ) 243 - } 244 - names.add(name) 245 - const clonedConfig = cloneConfig(project, config) 246 - clonedConfig.name = name 247 - const clone = TestProject._cloneBrowserProject(project, clonedConfig) 248 - resolvedProjects.push(clone) 249 - }) 250 - 251 - removeProjects.add(project) 252 - }) 253 - 254 - resolvedProjects = resolvedProjects.filter(project => !removeProjects.has(project)) 255 - 256 - const headedBrowserProjects = resolvedProjects.filter((project) => { 257 - return project.config.browser.enabled && !project.config.browser.headless 258 - }) 259 - if (headedBrowserProjects.length > 1) { 260 - const message = [ 261 - `Found multiple projects that run browser tests in headed mode: "${headedBrowserProjects.map(p => p.name).join('", "')}".`, 262 - ` Vitest cannot run multiple headed browsers at the same time.`, 263 - ].join('') 264 - if (!isTTY) { 265 - throw new Error(`${message} Please, filter projects with --browser=name or --project=name flag or run tests with "headless: true" option.`) 266 - } 267 - const prompts = await import('prompts') 268 - const { projectName } = await prompts.default({ 269 - type: 'select', 270 - name: 'projectName', 271 - choices: headedBrowserProjects.map(project => ({ 272 - title: project.name, 273 - value: project.name, 274 - })), 275 - message: `${message} Select a single project to run or cancel and run tests with "headless: true" option. Note that you can also start tests with --browser=name or --project=name flag.`, 276 - }) 277 - if (!projectName) { 278 - throw new Error('The test run was aborted.') 279 - } 280 - return resolvedProjects.filter(project => project.name === projectName) 281 - } 282 - 283 - return resolvedProjects 284 - } 285 - 286 - function cloneConfig(project: TestProject, { browser, ...config }: BrowserInstanceOption) { 287 - const { 288 - locators, 289 - viewport, 290 - testerHtmlPath, 291 - headless, 292 - screenshotDirectory, 293 - screenshotFailures, 294 - // @ts-expect-error remove just in case 295 - browser: _browser, 296 - name, 297 - ...overrideConfig 298 - } = config 299 - const currentConfig = project.config.browser 300 - return mergeConfig<any, any>({ 301 - ...deepClone(project.config), 302 - browser: { 303 - ...project.config.browser, 304 - locators: locators 305 - ? { 306 - testIdAttribute: locators.testIdAttribute ?? currentConfig.locators.testIdAttribute, 307 - } 308 - : project.config.browser.locators, 309 - viewport: viewport ?? currentConfig.viewport, 310 - testerHtmlPath: testerHtmlPath ?? currentConfig.testerHtmlPath, 311 - screenshotDirectory: screenshotDirectory ?? currentConfig.screenshotDirectory, 312 - screenshotFailures: screenshotFailures ?? currentConfig.screenshotFailures, 313 - headless: headless ?? currentConfig.headless, 314 - name: browser, 315 - providerOptions: config, 316 - instances: undefined, // projects cannot spawn more configs 317 - }, 318 - // TODO: should resolve, not merge/override 319 - } satisfies ResolvedConfig, overrideConfig) as ResolvedConfig 320 - } 321 - 322 - async function resolveTestProjectConfigs( 323 - vitest: Vitest, 324 - workspaceConfigPath: string | undefined, 325 - workspaceDefinition: TestProjectConfiguration[], 326 - ) { 327 - // project configurations that were specified directly 328 - const projectsOptions: (UserWorkspaceConfig & { extends?: true | string })[] = [] 329 - 330 - // custom config files that were specified directly or resolved from a directory 331 - const workspaceConfigFiles: string[] = [] 332 - 333 - // custom glob matches that should be resolved as directories or config files 334 - const workspaceGlobMatches: string[] = [] 335 - 336 - // directories that don't have a config file inside, but should be treated as projects 337 - const nonConfigProjectDirectories: string[] = [] 338 - 339 - for (const definition of workspaceDefinition) { 340 - if (typeof definition === 'string') { 341 - const stringOption = definition.replace('<rootDir>', vitest.config.root) 342 - // if the string doesn't contain a glob, we can resolve it directly 343 - // ['./vitest.config.js'] 344 - if (!isDynamicPattern(stringOption)) { 345 - const file = resolve(vitest.config.root, stringOption) 346 - 347 - if (!existsSync(file)) { 348 - const relativeWorkSpaceConfigPath = workspaceConfigPath 349 - ? relative(vitest.config.root, workspaceConfigPath) 350 - : undefined 351 - const note = workspaceConfigPath ? `Workspace config file "${relativeWorkSpaceConfigPath}"` : 'Inline workspace' 352 - throw new Error(`${note} references a non-existing file or a directory: ${file}`) 353 - } 354 - 355 - const stats = await fs.stat(file) 356 - // user can specify a config file directly 357 - if (stats.isFile()) { 358 - workspaceConfigFiles.push(file) 359 - } 360 - // user can specify a directory that should be used as a project 361 - else if (stats.isDirectory()) { 362 - const configFile = await resolveDirectoryConfig(file) 363 - if (configFile) { 364 - workspaceConfigFiles.push(configFile) 365 - } 366 - else { 367 - const directory = file[file.length - 1] === '/' ? file : `${file}/` 368 - nonConfigProjectDirectories.push(directory) 369 - } 370 - } 371 - else { 372 - // should never happen 373 - throw new TypeError(`Unexpected file type: ${file}`) 374 - } 375 - } 376 - // if the string is a glob pattern, resolve it later 377 - // ['./packages/*'] 378 - else { 379 - workspaceGlobMatches.push(stringOption) 380 - } 381 - } 382 - // if the config is inlined, we can resolve it immediately 383 - else if (typeof definition === 'function') { 384 - projectsOptions.push(await definition({ 385 - command: vitest.vite.config.command, 386 - mode: vitest.vite.config.mode, 387 - isPreview: false, 388 - isSsrBuild: false, 389 - })) 390 - } 391 - // the config is an object or a Promise that returns an object 392 - else { 393 - projectsOptions.push(await definition) 394 - } 395 - } 396 - 397 - if (workspaceGlobMatches.length) { 398 - const globOptions: GlobOptions = { 399 - absolute: true, 400 - dot: true, 401 - onlyFiles: false, 402 - cwd: vitest.config.root, 403 - expandDirectories: false, 404 - ignore: [ 405 - '**/node_modules/**', 406 - // temporary vite config file 407 - '**/*.timestamp-*', 408 - // macOS directory metadata 409 - '**/.DS_Store', 410 - ], 411 - } 412 - 413 - const workspacesFs = await glob(workspaceGlobMatches, globOptions) 414 - 415 - await Promise.all(workspacesFs.map(async (path) => { 416 - // directories are allowed with a glob like `packages/*` 417 - // in this case every directory is treated as a project 418 - if (path.endsWith('/')) { 419 - const configFile = await resolveDirectoryConfig(path) 420 - if (configFile) { 421 - workspaceConfigFiles.push(configFile) 422 - } 423 - else { 424 - nonConfigProjectDirectories.push(path) 425 - } 426 - } 427 - else { 428 - workspaceConfigFiles.push(path) 429 - } 430 - })) 431 - } 432 - 433 - const projectConfigFiles = Array.from(new Set(workspaceConfigFiles)) 434 - 435 - return { 436 - projectConfigs: projectsOptions, 437 - nonConfigDirectories: nonConfigProjectDirectories, 438 - configFiles: projectConfigFiles, 439 - } 440 - } 441 - 442 - async function resolveDirectoryConfig(directory: string) { 443 - const files = new Set(await fs.readdir(directory)) 444 - // default resolution looks for vitest.config.* or vite.config.* files 445 - // this simulates how `findUp` works in packages/vitest/src/node/create.ts:29 446 - const configFile = defaultConfigFiles.find(file => files.has(file)) 447 - if (configFile) { 448 - return resolve(directory, configFile) 449 - } 450 - return null 451 - } 452 - 453 - export function getDefaultTestProject(vitest: Vitest): TestProject | null { 454 - const filter = vitest.config.project 455 - const project = vitest._ensureRootProject() 456 - if (!filter.length) { 457 - return project 458 - } 459 - // check for the project name and browser names 460 - const hasProjects = getPotentialProjectNames(project).some(p => 461 - vitest.matchesProjectFilter(p), 462 - ) 463 - if (hasProjects) { 464 - return project 465 - } 466 - return null 467 - } 468 - 469 - function getPotentialProjectNames(project: TestProject) { 470 - const names = [project.name] 471 - if (project.config.browser.instances) { 472 - names.push(...project.config.browser.instances.map(i => i.name!)) 473 - } 474 - else if (project.config.browser.name) { 475 - names.push(project.config.browser.name) 476 - } 477 - return names 478 - }
+9 -1
test/cli/fixtures/git-changed/workspace/vitest.config.mjs
··· 1 - export default {} 1 + import { defineConfig } from 'vitest/config' 2 + 3 + export default defineConfig({ 4 + test: { 5 + projects: [ 6 + "packages/*/vitest.config.mjs", 7 + ] 8 + }, 9 + })
-3
test/cli/fixtures/git-changed/workspace/vitest.workspace.js
··· 1 - export default [ 2 - "packages/*/vitest.config.mjs", 3 - ];
+1 -1
test/config/fixtures/workspace/config-extends/vitest.config.ts
··· 17 17 }, 18 18 ], 19 19 test: { 20 - workspace: [ 20 + projects: [ 21 21 { 22 22 extends: true, 23 23 test: {
+11
test/config/fixtures/workspace/config-import-analysis/vitest.config.ts
··· 1 + import { defineConfig } from 'vitest/config' 2 + import depAsJs from "./dep.js" 3 + 4 + export default defineConfig({ 5 + test: { 6 + projects: [ 7 + "./packages/*", 8 + ...depAsJs, 9 + ], 10 + } 11 + })
-6
test/config/fixtures/workspace/config-import-analysis/vitest.workspace.ts
··· 1 - import depAsJs from "./dep.js" 2 - 3 - export default [ 4 - "./packages/*", 5 - ...depAsJs, 6 - ]
+10
test/config/fixtures/workspace/invalid-duplicate-configs/vitest.config.ts
··· 1 + import { defineConfig } from 'vitest/config' 2 + 3 + export default defineConfig({ 4 + test: { 5 + projects: [ 6 + './vitest1.config.js', 7 + './vitest2.config.js', 8 + ], 9 + } 10 + })
-6
test/config/fixtures/workspace/invalid-duplicate-configs/vitest.workspace.ts
··· 1 - import { defineWorkspace } from 'vitest/config' 2 - 3 - export default defineWorkspace([ 4 - './vitest1.config.js', 5 - './vitest2.config.js', 6 - ])
+18
test/config/fixtures/workspace/invalid-duplicate-inline/vitest.config.ts
··· 1 + import { defineConfig } from 'vitest/config' 2 + 3 + export default defineConfig({ 4 + test: { 5 + projects: [ 6 + { 7 + test: { 8 + name: 'test', 9 + }, 10 + }, 11 + { 12 + test: { 13 + name: 'test', 14 + }, 15 + }, 16 + ] 17 + } 18 + })
-14
test/config/fixtures/workspace/invalid-duplicate-inline/vitest.workspace.ts
··· 1 - import { defineWorkspace } from 'vitest/config' 2 - 3 - export default defineWorkspace([ 4 - { 5 - test: { 6 - name: 'test', 7 - }, 8 - }, 9 - { 10 - test: { 11 - name: 'test', 12 - }, 13 - }, 14 - ])
+9
test/config/fixtures/workspace/invalid-non-existing-config/vitest.config.ts
··· 1 + import { defineConfig } from 'vitest/config' 2 + 3 + export default defineConfig({ 4 + test: { 5 + projects: [ 6 + './vitest.config.js' 7 + ], 8 + }, 9 + })
-5
test/config/fixtures/workspace/invalid-non-existing-config/vitest.workspace.ts
··· 1 - import { defineWorkspace } from 'vitest/config' 2 - 3 - export default defineWorkspace([ 4 - './vitest.config.js' 5 - ])
+10
test/config/fixtures/workspace/negated/vitest.config.ts
··· 1 + import { defineConfig } from 'vitest/config'; 2 + 3 + export default defineConfig({ 4 + test: { 5 + projects: [ 6 + 'packages/*', 7 + '!packages/b' 8 + ], 9 + }, 10 + })
-4
test/config/fixtures/workspace/negated/vitest.workspace.ts
··· 1 - export default [ 2 - 'packages/*', 3 - '!packages/b' 4 - ]
+9
test/config/fixtures/workspace/several-configs/vitest.config.ts
··· 1 + import { defineConfig } from 'vitest/config' 2 + 3 + export default defineConfig({ 4 + test: { 5 + projects: [ 6 + './test/*.config.*.ts', 7 + ], 8 + }, 9 + })
-3
test/config/fixtures/workspace/several-configs/vitest.workspace.ts
··· 1 - export default [ 2 - './test/*.config.*.ts' 3 - ]
+10
test/config/fixtures/workspace/several-folders/vitest.config.ts
··· 1 + import { defineConfig } from 'vitest/config'; 2 + 3 + export default defineConfig({ 4 + test: { 5 + projects: [ 6 + 'projects/*', 7 + 'apps/*' 8 + ], 9 + }, 10 + })
-4
test/config/fixtures/workspace/several-folders/vitest.workspace.ts
··· 1 - export default [ 2 - 'projects/*', 3 - 'apps/*' 4 - ]
+7
test/snapshots/test/fixtures/workspace/vitest.config.ts
··· 1 + import { defineConfig, defineWorkspace } from 'vitest/config' 2 + 3 + export default defineConfig({ 4 + test: { 5 + projects: ['packages/*'], 6 + }, 7 + })
-5
test/snapshots/test/fixtures/workspace/vitest.workspace.ts
··· 1 - import { defineWorkspace } from 'vitest/config' 2 - 3 - export default defineWorkspace([ 4 - 'packages/*', 5 - ])