Skip to content

Commit 583dd8a

Browse files
authored
feat(vitest)!: add "vitest list" API to print collected tests without running them (#6013)
1 parent f645e48 commit 583dd8a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+815
-108
lines changed

docs/advanced/pool.md

+3
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import { ProcessPool, WorkspaceProject } from 'vitest/node'
4545
export interface ProcessPool {
4646
name: string
4747
runTests: (files: [project: WorkspaceProject, testFile: string][], invalidates?: string[]) => Promise<void>
48+
collectTests: (files: [project: WorkspaceProject, testFile: string][], invalidates?: string[]) => Promise<void>
4849
close?: () => Promise<void>
4950
}
5051
```
@@ -57,6 +58,8 @@ Vitest will wait until `runTests` is executed before finishing a run (i.e., it w
5758

5859
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.
5960

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)`.
62+
6063
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:
6164

6265
```ts

docs/guide/cli.md

+30
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,36 @@ export default {
5555

5656
Run only [benchmark](https://vitest.dev/guide/features.html#benchmarking-experimental) tests, which compare performance results.
5757

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+
5888
## Options
5989

6090
<!--@include: ./cli-table.md-->

packages/browser/src/client/tester/tester.ts

+12-7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { SpyModule, setupCommonEnv, startTests } from 'vitest/browser'
1+
import { SpyModule, collectTests, setupCommonEnv, startTests } from 'vitest/browser'
22
import { getBrowserState, getConfig, getWorkerState } from '../utils'
33
import { channel, client, onCancel } from '../client'
44
import { setupDialogsSpy } from './dialog'
@@ -65,8 +65,6 @@ async function prepareTestEnvironment(files: string[]) {
6565
runner,
6666
config,
6767
state,
68-
setupCommonEnv,
69-
startTests,
7068
}
7169
}
7270

@@ -78,7 +76,7 @@ function done(files: string[]) {
7876
})
7977
}
8078

81-
async function runTests(files: string[]) {
79+
async function executeTests(method: 'run' | 'collect', files: string[]) {
8280
await client.waitForConnection()
8381

8482
debug('client is connected to ws server')
@@ -107,7 +105,7 @@ async function runTests(files: string[]) {
107105

108106
debug('runner resolved successfully')
109107

110-
const { config, runner, state, setupCommonEnv, startTests } = preparedData
108+
const { config, runner, state } = preparedData
111109

112110
state.durations.prepare = performance.now() - state.durations.prepare
113111

@@ -116,7 +114,12 @@ async function runTests(files: string[]) {
116114
try {
117115
await setupCommonEnv(config)
118116
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+
}
120123
}
121124
}
122125
finally {
@@ -127,4 +130,6 @@ async function runTests(files: string[]) {
127130
}
128131

129132
// @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)

packages/browser/src/node/pool.ts

+9-7
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,16 @@ export function createBrowserPool(ctx: Vitest): ProcessPool {
1010
const providers = new Set<BrowserProvider>()
1111

1212
const waitForTests = async (
13+
method: 'run' | 'collect',
1314
contextId: string,
1415
project: WorkspaceProject,
1516
files: string[],
1617
) => {
17-
const context = project.browser!.state.createAsyncContext(contextId, files)
18+
const context = project.browser!.state.createAsyncContext(method, contextId, files)
1819
return await context
1920
}
2021

21-
const runTests = async (project: WorkspaceProject, files: string[]) => {
22+
const executeTests = async (method: 'run' | 'collect', project: WorkspaceProject, files: string[]) => {
2223
ctx.state.clearFiles(project, files)
2324
const browser = project.browser!
2425

@@ -67,13 +68,13 @@ export function createBrowserPool(ctx: Vitest): ProcessPool {
6768
contextId,
6869
[...files.map(f => relative(project.config.root, f))].join(', '),
6970
)
70-
const promise = waitForTests(contextId, project, files)
71+
const promise = waitForTests(method, contextId, project, files)
7172
promises.push(promise)
7273
orchestrator.createTesters(files)
7374
}
7475
else {
7576
const contextId = crypto.randomUUID()
76-
const waitPromise = waitForTests(contextId, project, files)
77+
const waitPromise = waitForTests(method, contextId, project, files)
7778
debug?.(
7879
'Opening a new context %s for files: %s',
7980
contextId,
@@ -91,7 +92,7 @@ export function createBrowserPool(ctx: Vitest): ProcessPool {
9192
await Promise.all(promises)
9293
}
9394

94-
const runWorkspaceTests = async (specs: [WorkspaceProject, string][]) => {
95+
const runWorkspaceTests = async (method: 'run' | 'collect', specs: [WorkspaceProject, string][]) => {
9596
const groupedFiles = new Map<WorkspaceProject, string[]>()
9697
for (const [project, file] of specs) {
9798
const files = groupedFiles.get(project) || []
@@ -110,7 +111,7 @@ export function createBrowserPool(ctx: Vitest): ProcessPool {
110111
break
111112
}
112113

113-
await runTests(project, files)
114+
await executeTests(method, project, files)
114115
}
115116
}
116117

@@ -140,6 +141,7 @@ export function createBrowserPool(ctx: Vitest): ProcessPool {
140141
await Promise.all([...providers].map(provider => provider.close()))
141142
providers.clear()
142143
},
143-
runTests: runWorkspaceTests,
144+
runTests: files => runWorkspaceTests('run', files),
145+
collectTests: files => runWorkspaceTests('collect', files),
144146
}
145147
}

packages/browser/src/node/serverTester.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ export async function resolveTester(
2929
? '__vitest_browser_runner__.files'
3030
: JSON.stringify([testFile])
3131
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'
3335

3436
const injectorJs = typeof server.injectorJs === 'string'
3537
? server.injectorJs
@@ -74,7 +76,7 @@ export async function resolveTester(
7476
`<script type="module">
7577
__vitest_browser_runner__.runningFiles = ${tests}
7678
__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)
7880
</script>`,
7981
})
8082
}

packages/browser/src/node/state.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@ export class BrowserServerState implements IBrowserServerState {
1414
return this.contexts.get(contextId)
1515
}
1616

17-
createAsyncContext(contextId: string, files: string[]): Promise<void> {
17+
createAsyncContext(method: 'run' | 'collect', contextId: string, files: string[]): Promise<void> {
1818
const defer = createDefer<void>()
1919
this.contexts.set(contextId, {
2020
files,
21+
method,
2122
resolve: () => {
2223
defer.resolve()
2324
this.contexts.delete(contextId)

packages/runner/src/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export { startTests, updateTask } from './run'
1+
export { startTests, updateTask, collectTests } from './run'
22
export {
33
test,
44
it,

packages/runner/src/run.ts

+11
Original file line numberDiff line numberDiff line change
@@ -512,3 +512,14 @@ export async function startTests(paths: string[], runner: VitestRunner) {
512512

513513
return files
514514
}
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 }

packages/vitest/LICENSE.md

-21
Original file line numberDiff line numberDiff line change
@@ -438,27 +438,6 @@ License: MIT
438438
By: Mathias Bynens
439439
Repository: https://github.com/mathiasbynens/emoji-regex.git
440440

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-
462441
---------------------------------------
463442

464443
## expect-type

packages/vitest/src/browser.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export { startTests, processError } from '@vitest/runner'
1+
export { startTests, collectTests, processError } from '@vitest/runner'
22
export {
33
setupCommonEnv,
44
loadDiffConfig,

packages/vitest/src/node/cli/cac.ts

+51-4
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ import cac, { type CAC, type Command } from 'cac'
33
import c from 'picocolors'
44
import { version } from '../../../package.json' with { type: 'json' }
55
import { toArray } from '../../utils/base'
6-
import type { Vitest, VitestRunMode } from '../../types'
6+
import type { VitestRunMode } from '../../types'
77
import type { CliOptions } from './cli-api'
88
import type { CLIOption, CLIOptions as CLIOptionsConfig } from './cli-config'
9-
import { benchCliOptionsConfig, cliOptionsConfig } from './cli-config'
9+
import { benchCliOptionsConfig, cliOptionsConfig, collectCliOptionsConfig } from './cli-config'
1010

1111
function addCommand(cli: CAC | Command, name: string, option: CLIOption<any>) {
1212
const commandName = option.alias || name
@@ -182,6 +182,13 @@ export function createCLI(options: CLIOptions = {}) {
182182
.command('init <project>', undefined, options)
183183
.action(init)
184184

185+
addCliOptions(
186+
cli
187+
.command('list [...filters]', undefined, options)
188+
.action((filters, options) => collect('test', filters, options)),
189+
collectCliOptionsConfig,
190+
)
191+
185192
cli
186193
.command('[...filters]', undefined, options)
187194
.action((filters, options) => start('test', filters, options))
@@ -249,7 +256,7 @@ function normalizeCliOptions(argv: CliOptions): CliOptions {
249256
return argv
250257
}
251258

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> {
253260
try {
254261
process.title = 'node (vitest)'
255262
}
@@ -261,7 +268,6 @@ async function start(mode: VitestRunMode, cliFilters: string[], options: CliOpti
261268
if (!ctx?.shouldKeepServer()) {
262269
await ctx?.exit()
263270
}
264-
return ctx
265271
}
266272
catch (e) {
267273
const { divider } = await import('../reporters/renderers/utils')
@@ -286,3 +292,44 @@ async function init(project: string) {
286292
const { create } = await import('../../create/browser/creator')
287293
await create()
288294
}
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+
}
335+
}

0 commit comments

Comments
 (0)