[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(vitest)!: add "vitest list" API to print collected tests without running them (#6013)

authored by

Vladimir and committed by
GitHub
(Jul 3, 2024, 5:51 PM +0200) 583dd8a9 f645e48c

+815 -108
+3
docs/advanced/pool.md
··· 45 45 export interface ProcessPool { 46 46 name: string 47 47 runTests: (files: [project: WorkspaceProject, testFile: string][], invalidates?: string[]) => Promise<void> 48 + collectTests: (files: [project: WorkspaceProject, testFile: string][], invalidates?: string[]) => Promise<void> 48 49 close?: () => Promise<void> 49 50 } 50 51 ``` ··· 56 57 Vitest will wait until `runTests` is executed before finishing a run (i.e., it will emit [`onFinished`](/guide/reporters) only after `runTests` is resolved). 57 58 58 59 If you are using a custom pool, you will have to provide test files and their results yourself - you can reference [`vitest.state`](https://github.com/vitest-dev/vitest/blob/main/packages/vitest/src/node/state.ts) for that (most important are `collectFiles` and `updateTasks`). Vitest uses `startTests` function from `@vitest/runner` package to do that. 60 + 61 + Vitest will call `collectTests` if `vitest.collect` is called or `vitest list` is invoked via a CLI command. It works the same way as `runTests`, but you don't have to run test callbacks, only report their tasks by calling `vitest.state.collectFiles(files)`. 59 62 60 63 To communicate between different processes, you can create methods object using `createMethodsRPC` from `vitest/node`, and use any form of communication that you prefer. For example, to use WebSockets with `birpc` you can write something like this: 61 64
+30
docs/guide/cli.md
··· 55 55 56 56 Run only [benchmark](https://vitest.dev/guide/features.html#benchmarking-experimental) tests, which compare performance results. 57 57 58 + ### `vitest init` 59 + 60 + `vitest init <name>` can be used to setup project configuration. At the moment, it only supports [`browser`](/guide/browser) value: 61 + 62 + ```bash 63 + vitest init browser 64 + ``` 65 + 66 + ### `vitest list` 67 + 68 + `vitest list` command inherits all `vitest` options to print the list of all matching tests. This command ignores `reporters` option. By default, it will print the names of all tests that matched the file filter and name pattern: 69 + 70 + ```shell 71 + vitest list filename.spec.ts -t="some-test" 72 + ``` 73 + 74 + ```txt 75 + describe > some-test 76 + describe > some-test > test 1 77 + describe > some-test > test 2 78 + ``` 79 + 80 + You can pass down `--json` flag to print tests in JSON format or save it in a separate file: 81 + 82 + ```bash 83 + vitest list filename.spec.ts -t="some-test" --json=./file.json 84 + ``` 85 + 86 + If `--json` flag doesn't receive a value, it will output the JSON into stdout. 87 + 58 88 ## Options 59 89 60 90 <!--@include: ./cli-table.md-->
-21
packages/vitest/LICENSE.md
··· 438 438 By: Mathias Bynens 439 439 Repository: https://github.com/mathiasbynens/emoji-regex.git 440 440 441 - > Copyright Mathias Bynens <https://mathiasbynens.be/> 442 - > 443 - > Permission is hereby granted, free of charge, to any person obtaining 444 - > a copy of this software and associated documentation files (the 445 - > "Software"), to deal in the Software without restriction, including 446 - > without limitation the rights to use, copy, modify, merge, publish, 447 - > distribute, sublicense, and/or sell copies of the Software, and to 448 - > permit persons to whom the Software is furnished to do so, subject to 449 - > the following conditions: 450 - > 451 - > The above copyright notice and this permission notice shall be 452 - > included in all copies or substantial portions of the Software. 453 - > 454 - > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 455 - > EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 456 - > MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 457 - > NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 458 - > LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 459 - > OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 460 - > WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 461 - 462 441 --------------------------------------- 463 442 464 443 ## expect-type
+2 -1
test/core/package.json
··· 8 8 "test:forks": "vitest --project forks", 9 9 "test:vmThreads": "vitest --project vmThreads", 10 10 "dev": "vite", 11 - "coverage": "vitest run --coverage" 11 + "coverage": "vitest run --coverage", 12 + "collect": "vitest list" 12 13 }, 13 14 "devDependencies": { 14 15 "@types/debug": "^4.1.12",
+1 -1
test/test-utils/index.ts
··· 136 136 return output() 137 137 } 138 138 139 - if (args.includes('--watch')) { 139 + if (args[0] !== 'list' && args.includes('--watch')) { 140 140 if (command === 'vitest') { 141 141 // Wait for initial test run to complete 142 142 await cli.waitForStdout('Waiting for file changes')
+1 -1
packages/runner/src/index.ts
··· 1 - export { startTests, updateTask } from './run' 1 + export { startTests, updateTask, collectTests } from './run' 2 2 export { 3 3 test, 4 4 it,
+11
packages/runner/src/run.ts
··· 512 512 513 513 return files 514 514 } 515 + 516 + async function publicCollect(paths: string[], runner: VitestRunner) { 517 + await runner.onBeforeCollect?.(paths) 518 + 519 + const files = await collectTests(paths, runner) 520 + 521 + await runner.onCollected?.(files) 522 + return files 523 + } 524 + 525 + export { publicCollect as collectTests }
+1 -1
packages/vitest/src/browser.ts
··· 1 - export { startTests, processError } from '@vitest/runner' 1 + export { startTests, collectTests, processError } from '@vitest/runner' 2 2 export { 3 3 setupCommonEnv, 4 4 loadDiffConfig,
+1 -1
packages/vitest/src/workers.ts
··· 4 4 unwrapSerializableConfig, 5 5 } from './runtime/workers/utils' 6 6 export { provideWorkerState } from './utils/global' 7 - export { run as runVitestWorker } from './runtime/worker' 7 + export { run as runVitestWorker, collect as collectVitestWorkerTests } from './runtime/worker' 8 8 export { runVmTests } from './runtime/workers/vm' 9 9 export { runBaseTests } from './runtime/workers/base' 10 10 export type { WorkerRpcOptions, VitestWorker } from './runtime/workers/types'
+174
test/cli/test/list.test.ts
··· 1 + import { readFileSync, rmSync } from 'node:fs' 2 + import { expect, onTestFinished, test } from 'vitest' 3 + import { runVitestCli } from '../../test-utils' 4 + 5 + test.each([ 6 + ['--pool=threads'], 7 + ['--pool=forks'], 8 + ['--pool=vmForks'], 9 + ['--browser.enabled'], 10 + ])('correctly outputs all tests with args: "%s"', async (...args) => { 11 + const { stdout, exitCode } = await runVitestCli('list', '-r=./fixtures/list', ...args) 12 + expect(stdout).toMatchSnapshot() 13 + expect(exitCode).toBe(0) 14 + }) 15 + 16 + test.each([ 17 + ['basic'], 18 + ['json', '--json'], 19 + ['json with a file', '--json=./list.json'], 20 + ])('%s output shows error', async () => { 21 + const { stderr, stdout, exitCode } = await runVitestCli('list', '-r=./fixtures/list', '-c=fail.config.ts') 22 + expect(stdout).toBe('') 23 + expect(stderr).toMatchSnapshot() 24 + expect(exitCode).toBe(1) 25 + }) 26 + 27 + test('correctly outputs json', async () => { 28 + const { stdout, exitCode } = await runVitestCli('list', '-r=./fixtures/list', '--json') 29 + expect(relative(stdout)).toMatchInlineSnapshot(` 30 + "[ 31 + { 32 + "name": "basic suite > inner suite > some test", 33 + "file": "<root>/fixtures/list/basic.test.ts" 34 + }, 35 + { 36 + "name": "basic suite > inner suite > another test", 37 + "file": "<root>/fixtures/list/basic.test.ts" 38 + }, 39 + { 40 + "name": "basic suite > basic test", 41 + "file": "<root>/fixtures/list/basic.test.ts" 42 + }, 43 + { 44 + "name": "outside test", 45 + "file": "<root>/fixtures/list/basic.test.ts" 46 + }, 47 + { 48 + "name": "1 plus 1", 49 + "file": "<root>/fixtures/list/math.test.ts" 50 + }, 51 + { 52 + "name": "failing test", 53 + "file": "<root>/fixtures/list/math.test.ts" 54 + } 55 + ] 56 + " 57 + `) 58 + expect(exitCode).toBe(0) 59 + }) 60 + 61 + test('correctly saves json', async () => { 62 + const { stdout, exitCode } = await runVitestCli('list', '-r=./fixtures/list', '--json=./list.json') 63 + onTestFinished(() => { 64 + rmSync('./fixtures/list/list.json') 65 + }) 66 + const json = readFileSync('./fixtures/list/list.json', 'utf-8') 67 + expect(stdout).toBe('') 68 + expect(relative(json)).toMatchInlineSnapshot(` 69 + "[ 70 + { 71 + "name": "basic suite > inner suite > some test", 72 + "file": "<root>/fixtures/list/basic.test.ts" 73 + }, 74 + { 75 + "name": "basic suite > inner suite > another test", 76 + "file": "<root>/fixtures/list/basic.test.ts" 77 + }, 78 + { 79 + "name": "basic suite > basic test", 80 + "file": "<root>/fixtures/list/basic.test.ts" 81 + }, 82 + { 83 + "name": "outside test", 84 + "file": "<root>/fixtures/list/basic.test.ts" 85 + }, 86 + { 87 + "name": "1 plus 1", 88 + "file": "<root>/fixtures/list/math.test.ts" 89 + }, 90 + { 91 + "name": "failing test", 92 + "file": "<root>/fixtures/list/math.test.ts" 93 + } 94 + ]" 95 + `) 96 + expect(exitCode).toBe(0) 97 + }) 98 + 99 + test('correctly filters by file', async () => { 100 + const { stdout, exitCode } = await runVitestCli('list', 'math.test.ts', '-r=./fixtures/list') 101 + expect(stdout).toMatchInlineSnapshot(` 102 + "math.test.ts > 1 plus 1 103 + math.test.ts > failing test 104 + " 105 + `) 106 + expect(exitCode).toBe(0) 107 + }) 108 + 109 + test('correctly prints project name in basic report', async () => { 110 + const { stdout } = await runVitestCli('list', 'math.test.ts', '-r=./fixtures/list', '--config=./custom.config.ts') 111 + expect(stdout).toMatchInlineSnapshot(` 112 + "[custom] math.test.ts > 1 plus 1 113 + [custom] math.test.ts > failing test 114 + " 115 + `) 116 + }) 117 + 118 + test('correctly prints project name and locations in json report', async () => { 119 + const { stdout } = await runVitestCli('list', 'math.test.ts', '-r=./fixtures/list', '--json', '--config=./custom.config.ts') 120 + expect(relative(stdout)).toMatchInlineSnapshot(` 121 + "[ 122 + { 123 + "name": "1 plus 1", 124 + "file": "<root>/fixtures/list/math.test.ts", 125 + "projectName": "custom", 126 + "location": { 127 + "line": 3, 128 + "column": 1 129 + } 130 + }, 131 + { 132 + "name": "failing test", 133 + "file": "<root>/fixtures/list/math.test.ts", 134 + "projectName": "custom", 135 + "location": { 136 + "line": 7, 137 + "column": 1 138 + } 139 + } 140 + ] 141 + " 142 + `) 143 + }) 144 + 145 + test('correctly filters by test name', async () => { 146 + const { stdout } = await runVitestCli('list', '-t=inner', '-r=./fixtures/list') 147 + expect(stdout).toMatchInlineSnapshot(` 148 + "basic.test.ts > basic suite > inner suite > some test 149 + basic.test.ts > basic suite > inner suite > another test 150 + " 151 + `) 152 + }) 153 + 154 + test('ignores watch flag', async () => { 155 + // if it ends, it works - otherwise it will hang 156 + const { stdout } = await runVitestCli('list', '-r=./fixtures/list', '--watch') 157 + expect(stdout).toMatchInlineSnapshot(` 158 + "basic.test.ts > basic suite > inner suite > some test 159 + basic.test.ts > basic suite > inner suite > another test 160 + basic.test.ts > basic suite > basic test 161 + basic.test.ts > outside test 162 + math.test.ts > 1 plus 1 163 + math.test.ts > failing test 164 + " 165 + `) 166 + }) 167 + 168 + function relative(stdout: string) { 169 + return stdout.replace(new RegExp(slash(process.cwd()), 'gi'), '<root>') 170 + } 171 + 172 + function slash(stdout: string) { 173 + return stdout.replace(/\\/g, '/') 174 + }
+9 -7
packages/browser/src/node/pool.ts
··· 10 10 const providers = new Set<BrowserProvider>() 11 11 12 12 const waitForTests = async ( 13 + method: 'run' | 'collect', 13 14 contextId: string, 14 15 project: WorkspaceProject, 15 16 files: string[], 16 17 ) => { 17 - const context = project.browser!.state.createAsyncContext(contextId, files) 18 + const context = project.browser!.state.createAsyncContext(method, contextId, files) 18 19 return await context 19 20 } 20 21 21 - const runTests = async (project: WorkspaceProject, files: string[]) => { 22 + const executeTests = async (method: 'run' | 'collect', project: WorkspaceProject, files: string[]) => { 22 23 ctx.state.clearFiles(project, files) 23 24 const browser = project.browser! 24 25 ··· 67 68 contextId, 68 69 [...files.map(f => relative(project.config.root, f))].join(', '), 69 70 ) 70 - const promise = waitForTests(contextId, project, files) 71 + const promise = waitForTests(method, contextId, project, files) 71 72 promises.push(promise) 72 73 orchestrator.createTesters(files) 73 74 } 74 75 else { 75 76 const contextId = crypto.randomUUID() 76 - const waitPromise = waitForTests(contextId, project, files) 77 + const waitPromise = waitForTests(method, contextId, project, files) 77 78 debug?.( 78 79 'Opening a new context %s for files: %s', 79 80 contextId, ··· 91 92 await Promise.all(promises) 92 93 } 93 94 94 - const runWorkspaceTests = async (specs: [WorkspaceProject, string][]) => { 95 + const runWorkspaceTests = async (method: 'run' | 'collect', specs: [WorkspaceProject, string][]) => { 95 96 const groupedFiles = new Map<WorkspaceProject, string[]>() 96 97 for (const [project, file] of specs) { 97 98 const files = groupedFiles.get(project) || [] ··· 110 111 break 111 112 } 112 113 113 - await runTests(project, files) 114 + await executeTests(method, project, files) 114 115 } 115 116 } 116 117 ··· 140 141 await Promise.all([...providers].map(provider => provider.close())) 141 142 providers.clear() 142 143 }, 143 - runTests: runWorkspaceTests, 144 + runTests: files => runWorkspaceTests('run', files), 145 + collectTests: files => runWorkspaceTests('collect', files), 144 146 } 145 147 }
+4 -2
packages/browser/src/node/serverTester.ts
··· 29 29 ? '__vitest_browser_runner__.files' 30 30 : JSON.stringify([testFile]) 31 31 const iframeId = JSON.stringify(testFile) 32 - const files = state.getContext(contextId)?.files ?? [] 32 + const context = state.getContext(contextId) 33 + const files = context?.files ?? [] 34 + const method = context?.method ?? 'run' 33 35 34 36 const injectorJs = typeof server.injectorJs === 'string' 35 37 ? server.injectorJs ··· 74 76 `<script type="module"> 75 77 __vitest_browser_runner__.runningFiles = ${tests} 76 78 __vitest_browser_runner__.iframeId = ${iframeId} 77 - __vitest_browser_runner__.runTests(__vitest_browser_runner__.runningFiles) 79 + __vitest_browser_runner__.${method === 'run' ? 'runTests' : 'collectTests'}(__vitest_browser_runner__.runningFiles) 78 80 </script>`, 79 81 }) 80 82 }
+2 -1
packages/browser/src/node/state.ts
··· 14 14 return this.contexts.get(contextId) 15 15 } 16 16 17 - createAsyncContext(contextId: string, files: string[]): Promise<void> { 17 + createAsyncContext(method: 'run' | 'collect', contextId: string, files: string[]): Promise<void> { 18 18 const defer = createDefer<void>() 19 19 this.contexts.set(contextId, { 20 20 files, 21 + method, 21 22 resolve: () => { 22 23 defer.resolve() 23 24 this.contexts.delete(contextId)
+72
packages/vitest/src/node/core.ts
··· 466 466 await this.coverageProvider?.mergeReports?.(coverages) 467 467 } 468 468 469 + async collect(filters?: string[]) { 470 + this._onClose = [] 471 + 472 + await this.initBrowserProviders() 473 + 474 + const files = await this.filterTestsBySource( 475 + await this.globTestFiles(filters), 476 + ) 477 + 478 + // if run with --changed, don't exit if no tests are found 479 + if (!files.length) { 480 + return { tests: [], errors: [] } 481 + } 482 + 483 + await this.collectFiles(files) 484 + 485 + return { 486 + tests: this.state.getFiles(), 487 + errors: this.state.getUnhandledErrors(), 488 + } 489 + } 490 + 469 491 async start(filters?: string[]) { 470 492 this._onClose = [] 471 493 ··· 696 718 697 719 this.runningPromise = undefined 698 720 this.isFirstRun = false 721 + 722 + // all subsequent runs will treat this as a fresh run 723 + this.config.changed = false 724 + this.config.related = undefined 725 + }) 726 + 727 + return await this.runningPromise 728 + } 729 + 730 + async collectFiles(specs: WorkspaceSpec[]) { 731 + await this.initializeDistPath() 732 + 733 + const filepaths = specs.map(([, file]) => file) 734 + this.state.collectPaths(filepaths) 735 + 736 + // previous run 737 + await this.runningPromise 738 + this._onCancelListeners = [] 739 + this.isCancelling = false 740 + 741 + // schedule the new run 742 + this.runningPromise = (async () => { 743 + if (!this.pool) { 744 + this.pool = createPool(this) 745 + } 746 + 747 + const invalidates = Array.from(this.invalidates) 748 + this.invalidates.clear() 749 + this.snapshot.clear() 750 + this.state.clearErrors() 751 + 752 + await this.initializeGlobalSetup(specs) 753 + 754 + try { 755 + await this.pool.collectTests(specs, invalidates) 756 + } 757 + catch (err) { 758 + this.state.catchError(err, 'Unhandled Error') 759 + } 760 + 761 + const files = this.state.getFiles() 762 + 763 + // can only happen if there was a syntax error in describe block 764 + // or there was an error importing a file 765 + if (hasFailed(files)) { 766 + process.exitCode = 1 767 + } 768 + })() 769 + .finally(async () => { 770 + this.runningPromise = undefined 699 771 700 772 // all subsequent runs will treat this as a fresh run 701 773 this.config.changed = false
+9 -7
packages/vitest/src/node/pool.ts
··· 21 21 export interface ProcessPool { 22 22 name: string 23 23 runTests: RunWithFiles 24 + collectTests: RunWithFiles 24 25 close?: () => Awaitable<void> 25 26 } 26 27 ··· 104 105 || execArg.startsWith('--diagnostic-dir'), 105 106 ) 106 107 107 - async function runTests(files: WorkspaceSpec[], invalidate?: string[]) { 108 + async function executeTests(method: 'runTests' | 'collectTests', files: WorkspaceSpec[], invalidate?: string[]) { 108 109 const options: PoolProcessOptions = { 109 110 execArgv: [...execArgv, ...conditions], 110 111 env: { ··· 144 145 `Custom pool "${filepath}" should return an object with "name" property`, 145 146 ) 146 147 } 147 - if (typeof poolInstance?.runTests !== 'function') { 148 + if (typeof poolInstance?.[method] !== 'function') { 148 149 throw new TypeError( 149 - `Custom pool "${filepath}" should return an object with "runTests" method`, 150 + `Custom pool "${filepath}" should return an object with "${method}" method`, 150 151 ) 151 152 } 152 153 ··· 201 202 if (pool in factories) { 202 203 const factory = factories[pool] 203 204 pools[pool] ??= factory() 204 - return pools[pool]!.runTests(specs, invalidate) 205 + return pools[pool]![method](specs, invalidate) 205 206 } 206 207 207 208 if (pool === 'browser') { ··· 209 210 const { createBrowserPool } = await import('@vitest/browser') 210 211 return createBrowserPool(ctx) 211 212 })() 212 - return pools[pool]!.runTests(specs, invalidate) 213 + return pools[pool]![method](specs, invalidate) 213 214 } 214 215 215 216 const poolHandler = await resolveCustomPool(pool) 216 217 pools[poolHandler.name] ??= poolHandler 217 - return poolHandler.runTests(specs, invalidate) 218 + return poolHandler[method](specs, invalidate) 218 219 }), 219 220 ) 220 221 } 221 222 222 223 return { 223 224 name: 'default', 224 - runTests, 225 + runTests: (files, invalidates) => executeTests('runTests', files, invalidates), 226 + collectTests: (files, invalidates) => executeTests('collectTests', files, invalidates), 225 227 async close() { 226 228 await Promise.all(Object.values(pools).map(p => p?.close?.())) 227 229 },
+14 -2
packages/vitest/src/node/workspace.ts
··· 407 407 return isBrowserEnabled(this.config) 408 408 } 409 409 410 - getSerializableConfig() { 410 + getSerializableConfig(method: 'run' | 'collect' = 'run') { 411 411 const optimizer = this.config.deps?.optimizer 412 412 const poolOptions = this.config.poolOptions 413 413 414 414 // Resolve from server.config to avoid comparing against default value 415 415 const isolate = this.server?.config?.test?.isolate 416 416 417 - return deepMerge( 417 + const config = deepMerge( 418 418 { 419 419 ...this.config, 420 420 ··· 500 500 }, 501 501 this.ctx.configOverride || ({} as any), 502 502 ) as ResolvedConfig 503 + 504 + // disable heavy features when collecting because they are not needed 505 + if (method === 'collect') { 506 + config.coverage.enabled = false 507 + if (config.browser.provider && config.browser.provider !== 'preview') { 508 + config.browser.headless = true 509 + } 510 + config.snapshotSerializers = [] 511 + config.diff = undefined 512 + } 513 + 514 + return config 503 515 } 504 516 505 517 close() {
+8 -2
packages/vitest/src/runtime/runBaseTests.ts
··· 1 1 import { performance } from 'node:perf_hooks' 2 - import { startTests } from '@vitest/runner' 2 + import { collectTests, startTests } from '@vitest/runner' 3 3 import type { ResolvedConfig, ResolvedTestEnvironment } from '../types' 4 4 import { getWorkerState, resetModules } from '../utils' 5 5 import { vi } from '../integrations/vi' ··· 15 15 16 16 // browser shouldn't call this! 17 17 export async function run( 18 + method: 'run' | 'collect', 18 19 files: string[], 19 20 config: ResolvedConfig, 20 21 environment: ResolvedTestEnvironment, ··· 62 63 63 64 workerState.filepath = file 64 65 65 - await startTests([file], runner) 66 + if (method === 'run') { 67 + await startTests([file], runner) 68 + } 69 + else { 70 + await collectTests([file], runner) 71 + } 66 72 67 73 // reset after tests, because user might call `vi.setConfig` in setupFile 68 74 vi.resetConfig()
+8 -2
packages/vitest/src/runtime/runVmTests.ts
··· 3 3 import util from 'node:util' 4 4 import timers from 'node:timers' 5 5 import { performance } from 'node:perf_hooks' 6 - import { startTests } from '@vitest/runner' 6 + import { collectTests, startTests } from '@vitest/runner' 7 7 import { createColors, setupColors } from '@vitest/utils' 8 8 import { installSourcemapsSupport } from 'vite-node/source-map' 9 9 import { setupChaiConfig } from '../integrations/chai/config' ··· 21 21 import { closeInspector } from './inspector' 22 22 23 23 export async function run( 24 + method: 'run' | 'collect', 24 25 files: string[], 25 26 config: ResolvedConfig, 26 27 executor: VitestExecutor, ··· 81 82 for (const file of files) { 82 83 workerState.filepath = file 83 84 84 - await startTests([file], runner) 85 + if (method === 'run') { 86 + await startTests([file], runner) 87 + } 88 + else { 89 + await collectTests([file], runner) 90 + } 85 91 86 92 // reset after tests, because user might call `vi.setConfig` in setupFile 87 93 vi.resetConfig()
+13 -3
packages/vitest/src/runtime/worker.ts
··· 13 13 } 14 14 15 15 // this is what every pool executes when running tests 16 - export async function run(ctx: ContextRPC) { 16 + async function execute(mehtod: 'run' | 'collect', ctx: ContextRPC) { 17 17 const prepareStart = performance.now() 18 18 19 19 const inspectorCleanup = setupInspect(ctx) ··· 75 75 providedContext: ctx.providedContext, 76 76 } 77 77 78 - if (!worker.runTests || typeof worker.runTests !== 'function') { 78 + const methodName = mehtod === 'collect' ? 'collectTests' : 'runTests' 79 + 80 + if (!worker[methodName] || typeof worker[methodName] !== 'function') { 79 81 throw new TypeError( 80 82 `Test worker should expose "runTests" method. Received "${typeof worker.runTests}".`, 81 83 ) 82 84 } 83 85 84 - await worker.runTests(state) 86 + await worker[methodName](state) 85 87 } 86 88 finally { 87 89 await rpcDone().catch(() => {}) 88 90 inspectorCleanup() 89 91 } 92 + } 93 + 94 + export function run(ctx: ContextRPC) { 95 + return execute('run', ctx) 96 + } 97 + 98 + export function collect(ctx: ContextRPC) { 99 + return execute('collect', ctx) 90 100 }
+5
packages/vitest/src/typecheck/typechecker.ts
··· 266 266 public async stop() { 267 267 await this.clear() 268 268 this.process?.kill() 269 + this.process = undefined 269 270 } 270 271 271 272 protected async ensurePackageInstalled(ctx: Vitest, checker: string) { ··· 294 295 } 295 296 296 297 public async start() { 298 + if (this.process) { 299 + return 300 + } 301 + 297 302 if (!this.tempConfigPath) { 298 303 throw new Error('tsconfig was not initialized') 299 304 }
+2 -1
packages/vitest/src/types/browser.ts
··· 170 170 171 171 export interface BrowserServerStateContext { 172 172 files: string[] 173 + method: 'run' | 'collect' 173 174 resolve: () => void 174 175 reject: (v: unknown) => void 175 176 } ··· 182 183 export interface BrowserServerState { 183 184 orchestrators: Map<string, BrowserOrchestrator> 184 185 getContext: (contextId: string) => BrowserServerStateContext | undefined 185 - createAsyncContext: (contextId: string, files: string[]) => Promise<void> 186 + createAsyncContext: (method: 'collect' | 'run', contextId: string, files: string[]) => Promise<void> 186 187 } 187 188 188 189 export interface BrowserServer {
+2
packages/vitest/src/types/config.ts
··· 954 954 | 'poolOptions' 955 955 | 'pool' 956 956 | 'cliExclude' 957 + | 'diff' 957 958 > { 958 959 mode: VitestRunMode 959 960 960 961 base?: string 962 + diff?: string 961 963 962 964 config?: string 963 965 filters?: string[]
+21
test/cli/fixtures/list/basic.test.ts
··· 1 + import { describe, expect, it } from 'vitest' 2 + 3 + describe('basic suite', () => { 4 + describe('inner suite', () => { 5 + it('some test', () => { 6 + expect(1).toBe(1) 7 + }) 8 + 9 + it('another test', () => { 10 + expect(1).toBe(1) 11 + }) 12 + }) 13 + 14 + it('basic test', () => { 15 + expect(1).toBe(1) 16 + }) 17 + }) 18 + 19 + it('outside test', () => { 20 + expect(1).toBe(1) 21 + })
+9
test/cli/fixtures/list/custom.config.ts
··· 1 + import { defineConfig } from 'vitest/config' 2 + 3 + export default defineConfig({ 4 + test: { 5 + include: ['basic.test.ts', 'math.test.ts'], 6 + name: 'custom', 7 + includeTaskLocation: true, 8 + }, 9 + })
+9
test/cli/fixtures/list/describe-error.test.ts
··· 1 + import { describe, expect, it } from 'vitest'; 2 + 3 + describe('describe error', () => { 4 + throw new Error('describe error') 5 + 6 + it('wont run', () => { 7 + expect(true).toBe(true) 8 + }) 9 + })
+7
test/cli/fixtures/list/fail.config.ts
··· 1 + import { defineConfig } from 'vitest/config' 2 + 3 + export default defineConfig({ 4 + test: { 5 + include: ['top-level-error.test.ts', 'describe-error.test.ts'], 6 + }, 7 + })
+9
test/cli/fixtures/list/math.test.ts
··· 1 + import { expect, it } from 'vitest' 2 + 3 + it('1 plus 1', () => { 4 + expect(1 + 1).toBe(2) 5 + }) 6 + 7 + it('failing test', () => { 8 + expect(1 + 1).toBe(3) 9 + })
+1
test/cli/fixtures/list/top-level-error.test.ts
··· 1 + throw new Error('top level error')
+13
test/cli/fixtures/list/vitest.config.ts
··· 1 + import { defineConfig } from 'vitest/config' 2 + 3 + export default defineConfig({ 4 + test: { 5 + include: ['basic.test.ts', 'math.test.ts'], 6 + browser: { 7 + name: 'chromium', 8 + provider: 'playwright', 9 + headless: true, 10 + api: 7523, 11 + } 12 + }, 13 + })
+98
test/cli/test/__snapshots__/list.test.ts.snap
··· 1 + // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 + 3 + exports[`basic output shows error 1`] = ` 4 + "Error: top level error 5 + ❯ top-level-error.test.ts:1:7 6 + 1| throw new Error('top level error') 7 + | ^ 8 + 2| 9 + 10 + Error: describe error 11 + ❯ describe-error.test.ts:4:9 12 + 2| 13 + 3| describe('describe error', () => { 14 + 4| throw new Error('describe error') 15 + | ^ 16 + 5| 17 + 6| it('wont run', () => { 18 + 19 + " 20 + `; 21 + 22 + exports[`correctly outputs all tests with args: "--browser.enabled" 1`] = ` 23 + "basic.test.ts > basic suite > inner suite > some test 24 + basic.test.ts > basic suite > inner suite > another test 25 + basic.test.ts > basic suite > basic test 26 + basic.test.ts > outside test 27 + math.test.ts > 1 plus 1 28 + math.test.ts > failing test 29 + " 30 + `; 31 + 32 + exports[`correctly outputs all tests with args: "--pool=forks" 1`] = ` 33 + "basic.test.ts > basic suite > inner suite > some test 34 + basic.test.ts > basic suite > inner suite > another test 35 + basic.test.ts > basic suite > basic test 36 + basic.test.ts > outside test 37 + math.test.ts > 1 plus 1 38 + math.test.ts > failing test 39 + " 40 + `; 41 + 42 + exports[`correctly outputs all tests with args: "--pool=threads" 1`] = ` 43 + "basic.test.ts > basic suite > inner suite > some test 44 + basic.test.ts > basic suite > inner suite > another test 45 + basic.test.ts > basic suite > basic test 46 + basic.test.ts > outside test 47 + math.test.ts > 1 plus 1 48 + math.test.ts > failing test 49 + " 50 + `; 51 + 52 + exports[`correctly outputs all tests with args: "--pool=vmForks" 1`] = ` 53 + "basic.test.ts > basic suite > inner suite > some test 54 + basic.test.ts > basic suite > inner suite > another test 55 + basic.test.ts > basic suite > basic test 56 + basic.test.ts > outside test 57 + math.test.ts > 1 plus 1 58 + math.test.ts > failing test 59 + " 60 + `; 61 + 62 + exports[`json output shows error 1`] = ` 63 + "Error: top level error 64 + ❯ top-level-error.test.ts:1:7 65 + 1| throw new Error('top level error') 66 + | ^ 67 + 2| 68 + 69 + Error: describe error 70 + ❯ describe-error.test.ts:4:9 71 + 2| 72 + 3| describe('describe error', () => { 73 + 4| throw new Error('describe error') 74 + | ^ 75 + 5| 76 + 6| it('wont run', () => { 77 + 78 + " 79 + `; 80 + 81 + exports[`json with a file output shows error 1`] = ` 82 + "Error: top level error 83 + ❯ top-level-error.test.ts:1:7 84 + 1| throw new Error('top level error') 85 + | ^ 86 + 2| 87 + 88 + Error: describe error 89 + ❯ describe-error.test.ts:4:9 90 + 2| 91 + 3| describe('describe error', () => { 92 + 4| throw new Error('describe error') 93 + | ^ 94 + 5| 95 + 6| it('wont run', () => { 96 + 97 + " 98 + `;
+12 -7
packages/browser/src/client/tester/tester.ts
··· 1 - import { SpyModule, setupCommonEnv, startTests } from 'vitest/browser' 1 + import { SpyModule, collectTests, setupCommonEnv, startTests } from 'vitest/browser' 2 2 import { getBrowserState, getConfig, getWorkerState } from '../utils' 3 3 import { channel, client, onCancel } from '../client' 4 4 import { setupDialogsSpy } from './dialog' ··· 65 65 runner, 66 66 config, 67 67 state, 68 - setupCommonEnv, 69 - startTests, 70 68 } 71 69 } 72 70 ··· 78 76 }) 79 77 } 80 78 81 - async function runTests(files: string[]) { 79 + async function executeTests(method: 'run' | 'collect', files: string[]) { 82 80 await client.waitForConnection() 83 81 84 82 debug('client is connected to ws server') ··· 107 105 108 106 debug('runner resolved successfully') 109 107 110 - const { config, runner, state, setupCommonEnv, startTests } = preparedData 108 + const { config, runner, state } = preparedData 111 109 112 110 state.durations.prepare = performance.now() - state.durations.prepare 113 111 ··· 116 114 try { 117 115 await setupCommonEnv(config) 118 116 for (const file of files) { 119 - await startTests([file], runner) 117 + if (method === 'run') { 118 + await startTests([file], runner) 119 + } 120 + else { 121 + await collectTests([file], runner) 122 + } 120 123 } 121 124 } 122 125 finally { ··· 127 130 } 128 131 129 132 // @ts-expect-error untyped global for internal use 130 - window.__vitest_browser_runner__.runTests = runTests 133 + window.__vitest_browser_runner__.runTests = files => executeTests('run', files) 134 + // @ts-expect-error untyped global for internal use 135 + window.__vitest_browser_runner__.collectTests = files => executeTests('collect', files)
+51 -4
packages/vitest/src/node/cli/cac.ts
··· 3 3 import c from 'picocolors' 4 4 import { version } from '../../../package.json' with { type: 'json' } 5 5 import { toArray } from '../../utils/base' 6 - import type { Vitest, VitestRunMode } from '../../types' 6 + import type { VitestRunMode } from '../../types' 7 7 import type { CliOptions } from './cli-api' 8 8 import type { CLIOption, CLIOptions as CLIOptionsConfig } from './cli-config' 9 - import { benchCliOptionsConfig, cliOptionsConfig } from './cli-config' 9 + import { benchCliOptionsConfig, cliOptionsConfig, collectCliOptionsConfig } from './cli-config' 10 10 11 11 function addCommand(cli: CAC | Command, name: string, option: CLIOption<any>) { 12 12 const commandName = option.alias || name ··· 182 182 .command('init <project>', undefined, options) 183 183 .action(init) 184 184 185 + addCliOptions( 186 + cli 187 + .command('list [...filters]', undefined, options) 188 + .action((filters, options) => collect('test', filters, options)), 189 + collectCliOptionsConfig, 190 + ) 191 + 185 192 cli 186 193 .command('[...filters]', undefined, options) 187 194 .action((filters, options) => start('test', filters, options)) ··· 249 256 return argv 250 257 } 251 258 252 - async function start(mode: VitestRunMode, cliFilters: string[], options: CliOptions): Promise<Vitest | undefined> { 259 + async function start(mode: VitestRunMode, cliFilters: string[], options: CliOptions): Promise<void> { 253 260 try { 254 261 process.title = 'node (vitest)' 255 262 } ··· 261 268 if (!ctx?.shouldKeepServer()) { 262 269 await ctx?.exit() 263 270 } 264 - return ctx 265 271 } 266 272 catch (e) { 267 273 const { divider } = await import('../reporters/renderers/utils') ··· 285 291 286 292 const { create } = await import('../../create/browser/creator') 287 293 await create() 294 + } 295 + 296 + async function collect(mode: VitestRunMode, cliFilters: string[], options: CliOptions): Promise<void> { 297 + try { 298 + process.title = 'node (vitest)' 299 + } 300 + catch {} 301 + 302 + try { 303 + const { prepareVitest, processCollected } = await import('./cli-api') 304 + const ctx = await prepareVitest(mode, { 305 + ...normalizeCliOptions(options), 306 + watch: false, 307 + run: true, 308 + }) 309 + 310 + const { tests, errors } = await ctx.collect(cliFilters.map(normalize)) 311 + 312 + if (errors.length) { 313 + console.error('\nThere were unhandled errors during test collection') 314 + errors.forEach(e => console.error(e)) 315 + console.error('\n\n') 316 + await ctx.close() 317 + return 318 + } 319 + 320 + processCollected(ctx, tests, options) 321 + await ctx.close() 322 + } 323 + catch (e) { 324 + const { divider } = await import('../reporters/renderers/utils') 325 + console.error(`\n${c.red(divider(c.bold(c.inverse(' Collect Error '))))}`) 326 + console.error(e) 327 + console.error('\n\n') 328 + 329 + if (process.exitCode == null) { 330 + process.exitCode = 1 331 + } 332 + 333 + process.exit() 334 + } 288 335 }
+134 -30
packages/vitest/src/node/cli/cli-api.ts
··· 1 - import { resolve } from 'pathe' 1 + /* eslint-disable no-console */ 2 + 3 + import { mkdirSync, writeFileSync } from 'node:fs' 4 + import { dirname, resolve } from 'pathe' 2 5 import type { UserConfig as ViteUserConfig } from 'vite' 6 + import type { File, Suite, Task } from '@vitest/runner' 3 7 import { CoverageProviderMap } from '../../integrations/coverage' 4 8 import { getEnvPackageName } from '../../integrations/env' 5 9 import type { UserConfig, Vitest, VitestRunMode } from '../../types' ··· 7 11 import { registerConsoleShortcuts } from '../stdin' 8 12 import type { VitestOptions } from '../core' 9 13 import { FilesNotFoundError, GitNotFoundError } from '../errors' 14 + import { getNames, getTests } from '../../utils' 10 15 11 16 export interface CliOptions extends UserConfig { 12 17 /** ··· 17 22 * Removes colors from the console output 18 23 */ 19 24 color?: boolean 25 + /** 26 + * Output collected tests as JSON or to a file 27 + */ 28 + json?: string | boolean 20 29 } 21 30 22 31 /** ··· 31 40 viteOverrides?: ViteUserConfig, 32 41 vitestOptions?: VitestOptions, 33 42 ): Promise<Vitest | undefined> { 34 - process.env.TEST = 'true' 35 - process.env.VITEST = 'true' 36 - process.env.NODE_ENV ??= 'test' 37 - 38 - if (options.run) { 39 - options.watch = false 40 - } 41 - 42 - // this shouldn't affect _application root_ that can be changed inside config 43 43 const root = resolve(options.root || process.cwd()) 44 44 45 - // running "vitest --browser.headless" 46 - if (typeof options.browser === 'object' && !('enabled' in options.browser)) { 47 - options.browser.enabled = true 48 - } 49 - 50 - if (typeof options.typecheck?.only === 'boolean') { 51 - options.typecheck.enabled ??= true 52 - } 53 - 54 - const ctx = await createVitest(mode, options, viteOverrides, vitestOptions) 45 + const ctx = await prepareVitest( 46 + mode, 47 + options, 48 + viteOverrides, 49 + vitestOptions, 50 + ) 55 51 56 52 if (mode === 'test' && ctx.config.coverage.enabled) { 57 53 const provider = ctx.config.coverage.provider || 'v8' ··· 65 61 return ctx 66 62 } 67 63 } 68 - } 69 - 70 - const environmentPackage = getEnvPackageName(ctx.config.environment) 71 - 72 - if ( 73 - environmentPackage 74 - && !(await ctx.packageInstaller.ensureInstalled(environmentPackage, root)) 75 - ) { 76 - process.exitCode = 1 77 - return ctx 78 64 } 79 65 80 66 const stdin = vitestOptions?.stdin || process.stdin ··· 131 117 stdinCleanup?.() 132 118 await ctx.close() 133 119 return ctx 120 + } 121 + 122 + export async function prepareVitest( 123 + mode: VitestRunMode, 124 + options: CliOptions = {}, 125 + viteOverrides?: ViteUserConfig, 126 + vitestOptions?: VitestOptions, 127 + ): Promise<Vitest> { 128 + process.env.TEST = 'true' 129 + process.env.VITEST = 'true' 130 + process.env.NODE_ENV ??= 'test' 131 + 132 + if (options.run) { 133 + options.watch = false 134 + } 135 + 136 + // this shouldn't affect _application root_ that can be changed inside config 137 + const root = resolve(options.root || process.cwd()) 138 + 139 + // running "vitest --browser.headless" 140 + if (typeof options.browser === 'object' && !('enabled' in options.browser)) { 141 + options.browser.enabled = true 142 + } 143 + 144 + if (typeof options.typecheck?.only === 'boolean') { 145 + options.typecheck.enabled ??= true 146 + } 147 + 148 + const ctx = await createVitest(mode, options, viteOverrides, vitestOptions) 149 + 150 + const environmentPackage = getEnvPackageName(ctx.config.environment) 151 + 152 + if ( 153 + environmentPackage 154 + && !(await ctx.packageInstaller.ensureInstalled(environmentPackage, root)) 155 + ) { 156 + process.exitCode = 1 157 + return ctx 158 + } 159 + 160 + return ctx 161 + } 162 + 163 + export function processCollected(ctx: Vitest, files: File[], options: CliOptions) { 164 + let errorsPrinted = false 165 + 166 + forEachSuite(files, (suite) => { 167 + const errors = suite.result?.errors || [] 168 + errors.forEach((error) => { 169 + errorsPrinted = true 170 + ctx.logger.printError(error, { 171 + project: ctx.getProjectByName(suite.file.projectName), 172 + }) 173 + }) 174 + }) 175 + 176 + if (errorsPrinted) { 177 + return 178 + } 179 + 180 + if (typeof options.json !== 'undefined') { 181 + return processJsonOutput(files, options) 182 + } 183 + 184 + return formatCollectedAsString(files).forEach(test => console.log(test)) 185 + } 186 + 187 + function processJsonOutput(files: File[], options: CliOptions) { 188 + if (typeof options.json === 'boolean') { 189 + return console.log(JSON.stringify(formatCollectedAsJSON(files), null, 2)) 190 + } 191 + 192 + if (typeof options.json === 'string') { 193 + const jsonPath = resolve(options.root || process.cwd(), options.json) 194 + mkdirSync(dirname(jsonPath), { recursive: true }) 195 + writeFileSync(jsonPath, JSON.stringify(formatCollectedAsJSON(files), null, 2)) 196 + } 197 + } 198 + 199 + function forEachSuite(tasks: Task[], callback: (suite: Suite) => void) { 200 + tasks.forEach((task) => { 201 + if (task.type === 'suite') { 202 + callback(task) 203 + forEachSuite(task.tasks, callback) 204 + } 205 + }) 206 + } 207 + 208 + export function formatCollectedAsJSON(files: File[]) { 209 + return files.map((file) => { 210 + const tests = getTests(file).filter(test => test.mode === 'run' || test.mode === 'only') 211 + return tests.map((test) => { 212 + const result: any = { 213 + name: getNames(test).slice(1).join(' > '), 214 + file: file.filepath, 215 + } 216 + if (test.file.projectName) { 217 + result.projectName = test.file.projectName 218 + } 219 + if (test.location) { 220 + result.location = test.location 221 + } 222 + return result 223 + }) 224 + }).flat() 225 + } 226 + 227 + export function formatCollectedAsString(files: File[]) { 228 + return files.map((file) => { 229 + const tests = getTests(file).filter(test => test.mode === 'run' || test.mode === 'only') 230 + return tests.map((test) => { 231 + const name = getNames(test).join(' > ') 232 + if (test.file.projectName) { 233 + return `[${test.file.projectName}] ${name}` 234 + } 235 + return name 236 + }) 237 + }).flat() 134 238 }
+13 -2
packages/vitest/src/node/cli/cli-config.ts
··· 792 792 snapshotEnvironment: null, 793 793 compare: null, 794 794 outputJson: null, 795 + json: null, 795 796 } 796 797 797 798 export const benchCliOptionsConfig: Pick< ··· 799 800 'compare' | 'outputJson' 800 801 > = { 801 802 compare: { 802 - description: 'benchmark output file to compare against', 803 + description: 'Benchmark output file to compare against', 803 804 argument: '<filename>', 804 805 }, 805 806 outputJson: { 806 - description: 'benchmark output file', 807 + description: 'Benchmark output file', 807 808 argument: '<filename>', 809 + }, 810 + } 811 + 812 + export const collectCliOptionsConfig: Pick< 813 + VitestCLIOptions, 814 + 'json' 815 + > = { 816 + json: { 817 + description: 'Print collected tests as JSON or write to a file (Default: false)', 818 + argument: '[true/path]', 808 819 }, 809 820 }
+1
packages/vitest/src/node/pools/forks.ts
··· 290 290 return { 291 291 name: 'forks', 292 292 runTests: runWithFiles('run'), 293 + collectTests: runWithFiles('collect'), 293 294 close: () => pool.destroy(), 294 295 } 295 296 }
+1
packages/vitest/src/node/pools/threads.ts
··· 286 286 return { 287 287 name: 'threads', 288 288 runTests: runWithFiles('run'), 289 + collectTests: runWithFiles('collect'), 289 290 close: () => pool.destroy(), 290 291 } 291 292 }
+24 -3
packages/vitest/src/node/pools/typecheck.ts
··· 87 87 }) 88 88 89 89 await checker.prepare() 90 - await checker.collectTests() 91 - checker.start() 92 90 return checker 91 + } 92 + 93 + async function startTypechecker(project: WorkspaceProject, files: string[]) { 94 + if (project.typechecker) { 95 + return project.typechecker 96 + } 97 + const checker = await createWorkspaceTypechecker(project, files) 98 + await checker.collectTests() 99 + await checker.start() 100 + } 101 + 102 + async function collectTests(specs: WorkspaceSpec[]) { 103 + const specsByProject = groupBy(specs, ([project]) => project.getName()) 104 + for (const name in specsByProject) { 105 + const project = specsByProject[name][0][0] 106 + const files = specsByProject[name].map(([_, file]) => file) 107 + const checker = await createWorkspaceTypechecker(project, files) 108 + checker.setFiles(files) 109 + await checker.collectTests() 110 + ctx.state.collectFiles(checker.getTestFiles()) 111 + await ctx.report('onCollected') 112 + } 93 113 } 94 114 95 115 async function runTests(specs: WorkspaceSpec[]) { ··· 122 142 } 123 143 promises.push(promise) 124 144 promisesMap.set(project, promise) 125 - createWorkspaceTypechecker(project, files) 145 + startTypechecker(project, files) 126 146 } 127 147 128 148 await Promise.all(promises) ··· 131 151 return { 132 152 name: 'typescript', 133 153 runTests, 154 + collectTests, 134 155 async close() { 135 156 const promises = ctx.projects.map(project => 136 157 project.typechecker?.stop(),
+1
packages/vitest/src/node/pools/vmForks.ts
··· 208 208 return { 209 209 name: 'vmForks', 210 210 runTests: runWithFiles('run'), 211 + collectTests: runWithFiles('collect'), 211 212 close: () => pool.destroy(), 212 213 } 213 214 }
+1
packages/vitest/src/node/pools/vmThreads.ts
··· 200 200 return { 201 201 name: 'vmThreads', 202 202 runTests: runWithFiles('run'), 203 + collectTests: runWithFiles('collect'), 203 204 close: () => pool.destroy(), 204 205 } 205 206 }
+2 -1
packages/vitest/src/runtime/workers/base.ts
··· 19 19 return _viteNode 20 20 } 21 21 22 - export async function runBaseTests(state: WorkerGlobalState) { 22 + export async function runBaseTests(method: 'run' | 'collect', state: WorkerGlobalState) { 23 23 const { ctx } = state 24 24 // state has new context, but we want to reuse existing ones 25 25 state.moduleCache = moduleCache ··· 40 40 import('../runBaseTests'), 41 41 ]) 42 42 await run( 43 + method, 43 44 ctx.files, 44 45 ctx.config, 45 46 { environment: state.environment, options: ctx.environment.options },
+10 -2
packages/vitest/src/runtime/workers/forks.ts
··· 9 9 return createForksRpcOptions(v8) 10 10 } 11 11 12 - async runTests(state: WorkerGlobalState) { 12 + async executeTests(method: 'run' | 'collect', state: WorkerGlobalState) { 13 13 // TODO: don't rely on reassigning process.exit 14 14 // https://github.com/vitest-dev/vitest/pull/4441#discussion_r1443771486 15 15 const exit = process.exit 16 16 state.ctx.config = unwrapSerializableConfig(state.ctx.config) 17 17 18 18 try { 19 - await runBaseTests(state) 19 + await runBaseTests(method, state) 20 20 } 21 21 finally { 22 22 process.exit = exit 23 23 } 24 + } 25 + 26 + runTests(state: WorkerGlobalState) { 27 + return this.executeTests('run', state) 28 + } 29 + 30 + collectTests(state: WorkerGlobalState) { 31 + return this.executeTests('collect', state) 24 32 } 25 33 } 26 34
+5 -1
packages/vitest/src/runtime/workers/threads.ts
··· 10 10 } 11 11 12 12 runTests(state: WorkerGlobalState): unknown { 13 - return runBaseTests(state) 13 + return runBaseTests('run', state) 14 + } 15 + 16 + collectTests(state: WorkerGlobalState): unknown { 17 + return runBaseTests('collect', state) 14 18 } 15 19 } 16 20
+1
packages/vitest/src/runtime/workers/types.ts
··· 11 11 export interface VitestWorker { 12 12 getRpcOptions: (ctx: ContextRPC) => WorkerRpcOptions 13 13 runTests: (state: WorkerGlobalState) => Awaitable<unknown> 14 + collectTests: (state: WorkerGlobalState) => Awaitable<unknown> 14 15 }
+2 -2
packages/vitest/src/runtime/workers/vm.ts
··· 15 15 const fileMap = new FileMap() 16 16 const packageCache = new Map<string, string>() 17 17 18 - export async function runVmTests(state: WorkerGlobalState) { 18 + export async function runVmTests(method: 'run' | 'collect', state: WorkerGlobalState) { 19 19 const { environment, ctx, rpc } = state 20 20 21 21 if (!environment.setupVM) { ··· 90 90 )) as typeof import('../runVmTests') 91 91 92 92 try { 93 - await run(ctx.files, ctx.config, executor) 93 + await run(method, ctx.files, ctx.config, executor) 94 94 } 95 95 finally { 96 96 await vm.teardown?.()
+10 -2
packages/vitest/src/runtime/workers/vmForks.ts
··· 9 9 return createForksRpcOptions(v8) 10 10 } 11 11 12 - async runTests(state: WorkerGlobalState) { 12 + async executeTests(method: 'run' | 'collect', state: WorkerGlobalState) { 13 13 const exit = process.exit 14 14 state.ctx.config = unwrapSerializableConfig(state.ctx.config) 15 15 16 16 try { 17 - await runVmTests(state) 17 + await runVmTests(method, state) 18 18 } 19 19 finally { 20 20 process.exit = exit 21 21 } 22 + } 23 + 24 + runTests(state: WorkerGlobalState) { 25 + return this.executeTests('run', state) 26 + } 27 + 28 + collectTests(state: WorkerGlobalState) { 29 + return this.executeTests('collect', state) 22 30 } 23 31 } 24 32
+5 -1
packages/vitest/src/runtime/workers/vmThreads.ts
··· 10 10 } 11 11 12 12 runTests(state: WorkerGlobalState): unknown { 13 - return runVmTests(state) 13 + return runVmTests('run', state) 14 + } 15 + 16 + collectTests(state: WorkerGlobalState): unknown { 17 + return runVmTests('collect', state) 14 18 } 15 19 } 16 20
+3
test/cli/fixtures/custom-pool/pool/custom-pool.ts
··· 8 8 const options = ctx.config.poolOptions?.custom as any 9 9 return { 10 10 name: 'custom', 11 + async collectTests() { 12 + throw new Error('Not implemented') 13 + }, 11 14 async runTests(specs) { 12 15 ctx.logger.console.warn('[pool] printing:', options.print) 13 16 ctx.logger.console.warn('[pool] array option', options.array)