diff --git a/docs/advanced/pool.md b/docs/advanced/pool.md index d4f8d9c56c88..0024e60d280e 100644 --- a/docs/advanced/pool.md +++ b/docs/advanced/pool.md @@ -45,6 +45,7 @@ import { ProcessPool, WorkspaceProject } from 'vitest/node' export interface ProcessPool { name: string runTests: (files: [project: WorkspaceProject, testFile: string][], invalidates?: string[]) => Promise + collectTests: (files: [project: WorkspaceProject, testFile: string][], invalidates?: string[]) => Promise close?: () => Promise } ``` @@ -57,6 +58,8 @@ Vitest will wait until `runTests` is executed before finishing a run (i.e., it w 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. +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)`. + 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: ```ts diff --git a/docs/guide/cli.md b/docs/guide/cli.md index 0b4807ef5a6a..f6d4270e8705 100644 --- a/docs/guide/cli.md +++ b/docs/guide/cli.md @@ -55,6 +55,36 @@ export default { Run only [benchmark](https://vitest.dev/guide/features.html#benchmarking-experimental) tests, which compare performance results. +### `vitest init` + +`vitest init ` can be used to setup project configuration. At the moment, it only supports [`browser`](/guide/browser) value: + +```bash +vitest init browser +``` + +### `vitest list` + +`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: + +```shell +vitest list filename.spec.ts -t="some-test" +``` + +```txt +describe > some-test +describe > some-test > test 1 +describe > some-test > test 2 +``` + +You can pass down `--json` flag to print tests in JSON format or save it in a separate file: + +```bash +vitest list filename.spec.ts -t="some-test" --json=./file.json +``` + +If `--json` flag doesn't receive a value, it will output the JSON into stdout. + ## Options diff --git a/packages/browser/src/client/tester/tester.ts b/packages/browser/src/client/tester/tester.ts index 07732a53e96b..7be42128f1c9 100644 --- a/packages/browser/src/client/tester/tester.ts +++ b/packages/browser/src/client/tester/tester.ts @@ -1,4 +1,4 @@ -import { SpyModule, setupCommonEnv, startTests } from 'vitest/browser' +import { SpyModule, collectTests, setupCommonEnv, startTests } from 'vitest/browser' import { getBrowserState, getConfig, getWorkerState } from '../utils' import { channel, client, onCancel } from '../client' import { setupDialogsSpy } from './dialog' @@ -65,8 +65,6 @@ async function prepareTestEnvironment(files: string[]) { runner, config, state, - setupCommonEnv, - startTests, } } @@ -78,7 +76,7 @@ function done(files: string[]) { }) } -async function runTests(files: string[]) { +async function executeTests(method: 'run' | 'collect', files: string[]) { await client.waitForConnection() debug('client is connected to ws server') @@ -107,7 +105,7 @@ async function runTests(files: string[]) { debug('runner resolved successfully') - const { config, runner, state, setupCommonEnv, startTests } = preparedData + const { config, runner, state } = preparedData state.durations.prepare = performance.now() - state.durations.prepare @@ -116,7 +114,12 @@ async function runTests(files: string[]) { try { await setupCommonEnv(config) for (const file of files) { - await startTests([file], runner) + if (method === 'run') { + await startTests([file], runner) + } + else { + await collectTests([file], runner) + } } } finally { @@ -127,4 +130,6 @@ async function runTests(files: string[]) { } // @ts-expect-error untyped global for internal use -window.__vitest_browser_runner__.runTests = runTests +window.__vitest_browser_runner__.runTests = files => executeTests('run', files) +// @ts-expect-error untyped global for internal use +window.__vitest_browser_runner__.collectTests = files => executeTests('collect', files) diff --git a/packages/browser/src/node/pool.ts b/packages/browser/src/node/pool.ts index e0b6b53a6bcc..e1a0b5ea61ae 100644 --- a/packages/browser/src/node/pool.ts +++ b/packages/browser/src/node/pool.ts @@ -10,15 +10,16 @@ export function createBrowserPool(ctx: Vitest): ProcessPool { const providers = new Set() const waitForTests = async ( + method: 'run' | 'collect', contextId: string, project: WorkspaceProject, files: string[], ) => { - const context = project.browser!.state.createAsyncContext(contextId, files) + const context = project.browser!.state.createAsyncContext(method, contextId, files) return await context } - const runTests = async (project: WorkspaceProject, files: string[]) => { + const executeTests = async (method: 'run' | 'collect', project: WorkspaceProject, files: string[]) => { ctx.state.clearFiles(project, files) const browser = project.browser! @@ -67,13 +68,13 @@ export function createBrowserPool(ctx: Vitest): ProcessPool { contextId, [...files.map(f => relative(project.config.root, f))].join(', '), ) - const promise = waitForTests(contextId, project, files) + const promise = waitForTests(method, contextId, project, files) promises.push(promise) orchestrator.createTesters(files) } else { const contextId = crypto.randomUUID() - const waitPromise = waitForTests(contextId, project, files) + const waitPromise = waitForTests(method, contextId, project, files) debug?.( 'Opening a new context %s for files: %s', contextId, @@ -91,7 +92,7 @@ export function createBrowserPool(ctx: Vitest): ProcessPool { await Promise.all(promises) } - const runWorkspaceTests = async (specs: [WorkspaceProject, string][]) => { + const runWorkspaceTests = async (method: 'run' | 'collect', specs: [WorkspaceProject, string][]) => { const groupedFiles = new Map() for (const [project, file] of specs) { const files = groupedFiles.get(project) || [] @@ -110,7 +111,7 @@ export function createBrowserPool(ctx: Vitest): ProcessPool { break } - await runTests(project, files) + await executeTests(method, project, files) } } @@ -140,6 +141,7 @@ export function createBrowserPool(ctx: Vitest): ProcessPool { await Promise.all([...providers].map(provider => provider.close())) providers.clear() }, - runTests: runWorkspaceTests, + runTests: files => runWorkspaceTests('run', files), + collectTests: files => runWorkspaceTests('collect', files), } } diff --git a/packages/browser/src/node/serverTester.ts b/packages/browser/src/node/serverTester.ts index 9b1ba337a32f..d756efa0b8ad 100644 --- a/packages/browser/src/node/serverTester.ts +++ b/packages/browser/src/node/serverTester.ts @@ -29,7 +29,9 @@ export async function resolveTester( ? '__vitest_browser_runner__.files' : JSON.stringify([testFile]) const iframeId = JSON.stringify(testFile) - const files = state.getContext(contextId)?.files ?? [] + const context = state.getContext(contextId) + const files = context?.files ?? [] + const method = context?.method ?? 'run' const injectorJs = typeof server.injectorJs === 'string' ? server.injectorJs @@ -74,7 +76,7 @@ export async function resolveTester( ``, }) } diff --git a/packages/browser/src/node/state.ts b/packages/browser/src/node/state.ts index 9f008a6ff83b..03d8c8f02ba7 100644 --- a/packages/browser/src/node/state.ts +++ b/packages/browser/src/node/state.ts @@ -14,10 +14,11 @@ export class BrowserServerState implements IBrowserServerState { return this.contexts.get(contextId) } - createAsyncContext(contextId: string, files: string[]): Promise { + createAsyncContext(method: 'run' | 'collect', contextId: string, files: string[]): Promise { const defer = createDefer() this.contexts.set(contextId, { files, + method, resolve: () => { defer.resolve() this.contexts.delete(contextId) diff --git a/packages/runner/src/index.ts b/packages/runner/src/index.ts index 0d8cd906401e..56c5cc5927e3 100644 --- a/packages/runner/src/index.ts +++ b/packages/runner/src/index.ts @@ -1,4 +1,4 @@ -export { startTests, updateTask } from './run' +export { startTests, updateTask, collectTests } from './run' export { test, it, diff --git a/packages/runner/src/run.ts b/packages/runner/src/run.ts index 308ef41a7224..68b6623c8a5d 100644 --- a/packages/runner/src/run.ts +++ b/packages/runner/src/run.ts @@ -512,3 +512,14 @@ export async function startTests(paths: string[], runner: VitestRunner) { return files } + +async function publicCollect(paths: string[], runner: VitestRunner) { + await runner.onBeforeCollect?.(paths) + + const files = await collectTests(paths, runner) + + await runner.onCollected?.(files) + return files +} + +export { publicCollect as collectTests } diff --git a/packages/vitest/LICENSE.md b/packages/vitest/LICENSE.md index e012ba88698a..c98f8251bc14 100644 --- a/packages/vitest/LICENSE.md +++ b/packages/vitest/LICENSE.md @@ -438,27 +438,6 @@ License: MIT By: Mathias Bynens Repository: https://github.com/mathiasbynens/emoji-regex.git -> Copyright Mathias Bynens -> -> Permission is hereby granted, free of charge, to any person obtaining -> a copy of this software and associated documentation files (the -> "Software"), to deal in the Software without restriction, including -> without limitation the rights to use, copy, modify, merge, publish, -> distribute, sublicense, and/or sell copies of the Software, and to -> permit persons to whom the Software is furnished to do so, subject to -> the following conditions: -> -> The above copyright notice and this permission notice shall be -> included in all copies or substantial portions of the Software. -> -> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -> EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -> MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -> NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -> LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -> OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -> WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - --------------------------------------- ## expect-type diff --git a/packages/vitest/src/browser.ts b/packages/vitest/src/browser.ts index e9eb3ea0cb3c..2aedb49fb4f2 100644 --- a/packages/vitest/src/browser.ts +++ b/packages/vitest/src/browser.ts @@ -1,4 +1,4 @@ -export { startTests, processError } from '@vitest/runner' +export { startTests, collectTests, processError } from '@vitest/runner' export { setupCommonEnv, loadDiffConfig, diff --git a/packages/vitest/src/node/cli/cac.ts b/packages/vitest/src/node/cli/cac.ts index 6c11444ce76b..fe3c61c8f61a 100644 --- a/packages/vitest/src/node/cli/cac.ts +++ b/packages/vitest/src/node/cli/cac.ts @@ -3,10 +3,10 @@ import cac, { type CAC, type Command } from 'cac' import c from 'picocolors' import { version } from '../../../package.json' with { type: 'json' } import { toArray } from '../../utils/base' -import type { Vitest, VitestRunMode } from '../../types' +import type { VitestRunMode } from '../../types' import type { CliOptions } from './cli-api' import type { CLIOption, CLIOptions as CLIOptionsConfig } from './cli-config' -import { benchCliOptionsConfig, cliOptionsConfig } from './cli-config' +import { benchCliOptionsConfig, cliOptionsConfig, collectCliOptionsConfig } from './cli-config' function addCommand(cli: CAC | Command, name: string, option: CLIOption) { const commandName = option.alias || name @@ -182,6 +182,13 @@ export function createCLI(options: CLIOptions = {}) { .command('init ', undefined, options) .action(init) + addCliOptions( + cli + .command('list [...filters]', undefined, options) + .action((filters, options) => collect('test', filters, options)), + collectCliOptionsConfig, + ) + cli .command('[...filters]', undefined, options) .action((filters, options) => start('test', filters, options)) @@ -249,7 +256,7 @@ function normalizeCliOptions(argv: CliOptions): CliOptions { return argv } -async function start(mode: VitestRunMode, cliFilters: string[], options: CliOptions): Promise { +async function start(mode: VitestRunMode, cliFilters: string[], options: CliOptions): Promise { try { process.title = 'node (vitest)' } @@ -261,7 +268,6 @@ async function start(mode: VitestRunMode, cliFilters: string[], options: CliOpti if (!ctx?.shouldKeepServer()) { await ctx?.exit() } - return ctx } catch (e) { const { divider } = await import('../reporters/renderers/utils') @@ -286,3 +292,44 @@ async function init(project: string) { const { create } = await import('../../create/browser/creator') await create() } + +async function collect(mode: VitestRunMode, cliFilters: string[], options: CliOptions): Promise { + try { + process.title = 'node (vitest)' + } + catch {} + + try { + const { prepareVitest, processCollected } = await import('./cli-api') + const ctx = await prepareVitest(mode, { + ...normalizeCliOptions(options), + watch: false, + run: true, + }) + + const { tests, errors } = await ctx.collect(cliFilters.map(normalize)) + + if (errors.length) { + console.error('\nThere were unhandled errors during test collection') + errors.forEach(e => console.error(e)) + console.error('\n\n') + await ctx.close() + return + } + + processCollected(ctx, tests, options) + await ctx.close() + } + catch (e) { + const { divider } = await import('../reporters/renderers/utils') + console.error(`\n${c.red(divider(c.bold(c.inverse(' Collect Error '))))}`) + console.error(e) + console.error('\n\n') + + if (process.exitCode == null) { + process.exitCode = 1 + } + + process.exit() + } +} diff --git a/packages/vitest/src/node/cli/cli-api.ts b/packages/vitest/src/node/cli/cli-api.ts index 9d255807bf86..197ae804f25f 100644 --- a/packages/vitest/src/node/cli/cli-api.ts +++ b/packages/vitest/src/node/cli/cli-api.ts @@ -1,5 +1,9 @@ -import { resolve } from 'pathe' +/* eslint-disable no-console */ + +import { mkdirSync, writeFileSync } from 'node:fs' +import { dirname, resolve } from 'pathe' import type { UserConfig as ViteUserConfig } from 'vite' +import type { File, Suite, Task } from '@vitest/runner' import { CoverageProviderMap } from '../../integrations/coverage' import { getEnvPackageName } from '../../integrations/env' import type { UserConfig, Vitest, VitestRunMode } from '../../types' @@ -7,6 +11,7 @@ import { createVitest } from '../create' import { registerConsoleShortcuts } from '../stdin' import type { VitestOptions } from '../core' import { FilesNotFoundError, GitNotFoundError } from '../errors' +import { getNames, getTests } from '../../utils' export interface CliOptions extends UserConfig { /** @@ -17,6 +22,10 @@ export interface CliOptions extends UserConfig { * Removes colors from the console output */ color?: boolean + /** + * Output collected tests as JSON or to a file + */ + json?: string | boolean } /** @@ -31,27 +40,14 @@ export async function startVitest( viteOverrides?: ViteUserConfig, vitestOptions?: VitestOptions, ): Promise { - process.env.TEST = 'true' - process.env.VITEST = 'true' - process.env.NODE_ENV ??= 'test' - - if (options.run) { - options.watch = false - } - - // this shouldn't affect _application root_ that can be changed inside config const root = resolve(options.root || process.cwd()) - // running "vitest --browser.headless" - if (typeof options.browser === 'object' && !('enabled' in options.browser)) { - options.browser.enabled = true - } - - if (typeof options.typecheck?.only === 'boolean') { - options.typecheck.enabled ??= true - } - - const ctx = await createVitest(mode, options, viteOverrides, vitestOptions) + const ctx = await prepareVitest( + mode, + options, + viteOverrides, + vitestOptions, + ) if (mode === 'test' && ctx.config.coverage.enabled) { const provider = ctx.config.coverage.provider || 'v8' @@ -67,16 +63,6 @@ export async function startVitest( } } - const environmentPackage = getEnvPackageName(ctx.config.environment) - - if ( - environmentPackage - && !(await ctx.packageInstaller.ensureInstalled(environmentPackage, root)) - ) { - process.exitCode = 1 - return ctx - } - const stdin = vitestOptions?.stdin || process.stdin const stdout = vitestOptions?.stdout || process.stdout let stdinCleanup @@ -132,3 +118,121 @@ export async function startVitest( await ctx.close() return ctx } + +export async function prepareVitest( + mode: VitestRunMode, + options: CliOptions = {}, + viteOverrides?: ViteUserConfig, + vitestOptions?: VitestOptions, +): Promise { + process.env.TEST = 'true' + process.env.VITEST = 'true' + process.env.NODE_ENV ??= 'test' + + if (options.run) { + options.watch = false + } + + // this shouldn't affect _application root_ that can be changed inside config + const root = resolve(options.root || process.cwd()) + + // running "vitest --browser.headless" + if (typeof options.browser === 'object' && !('enabled' in options.browser)) { + options.browser.enabled = true + } + + if (typeof options.typecheck?.only === 'boolean') { + options.typecheck.enabled ??= true + } + + const ctx = await createVitest(mode, options, viteOverrides, vitestOptions) + + const environmentPackage = getEnvPackageName(ctx.config.environment) + + if ( + environmentPackage + && !(await ctx.packageInstaller.ensureInstalled(environmentPackage, root)) + ) { + process.exitCode = 1 + return ctx + } + + return ctx +} + +export function processCollected(ctx: Vitest, files: File[], options: CliOptions) { + let errorsPrinted = false + + forEachSuite(files, (suite) => { + const errors = suite.result?.errors || [] + errors.forEach((error) => { + errorsPrinted = true + ctx.logger.printError(error, { + project: ctx.getProjectByName(suite.file.projectName), + }) + }) + }) + + if (errorsPrinted) { + return + } + + if (typeof options.json !== 'undefined') { + return processJsonOutput(files, options) + } + + return formatCollectedAsString(files).forEach(test => console.log(test)) +} + +function processJsonOutput(files: File[], options: CliOptions) { + if (typeof options.json === 'boolean') { + return console.log(JSON.stringify(formatCollectedAsJSON(files), null, 2)) + } + + if (typeof options.json === 'string') { + const jsonPath = resolve(options.root || process.cwd(), options.json) + mkdirSync(dirname(jsonPath), { recursive: true }) + writeFileSync(jsonPath, JSON.stringify(formatCollectedAsJSON(files), null, 2)) + } +} + +function forEachSuite(tasks: Task[], callback: (suite: Suite) => void) { + tasks.forEach((task) => { + if (task.type === 'suite') { + callback(task) + forEachSuite(task.tasks, callback) + } + }) +} + +export function formatCollectedAsJSON(files: File[]) { + return files.map((file) => { + const tests = getTests(file).filter(test => test.mode === 'run' || test.mode === 'only') + return tests.map((test) => { + const result: any = { + name: getNames(test).slice(1).join(' > '), + file: file.filepath, + } + if (test.file.projectName) { + result.projectName = test.file.projectName + } + if (test.location) { + result.location = test.location + } + return result + }) + }).flat() +} + +export function formatCollectedAsString(files: File[]) { + return files.map((file) => { + const tests = getTests(file).filter(test => test.mode === 'run' || test.mode === 'only') + return tests.map((test) => { + const name = getNames(test).join(' > ') + if (test.file.projectName) { + return `[${test.file.projectName}] ${name}` + } + return name + }) + }).flat() +} diff --git a/packages/vitest/src/node/cli/cli-config.ts b/packages/vitest/src/node/cli/cli-config.ts index c6b28c0785d5..3a99814317e2 100644 --- a/packages/vitest/src/node/cli/cli-config.ts +++ b/packages/vitest/src/node/cli/cli-config.ts @@ -792,6 +792,7 @@ export const cliOptionsConfig: VitestCLIOptions = { snapshotEnvironment: null, compare: null, outputJson: null, + json: null, } export const benchCliOptionsConfig: Pick< @@ -799,11 +800,21 @@ export const benchCliOptionsConfig: Pick< 'compare' | 'outputJson' > = { compare: { - description: 'benchmark output file to compare against', + description: 'Benchmark output file to compare against', argument: '', }, outputJson: { - description: 'benchmark output file', + description: 'Benchmark output file', argument: '', }, } + +export const collectCliOptionsConfig: Pick< + VitestCLIOptions, + 'json' +> = { + json: { + description: 'Print collected tests as JSON or write to a file (Default: false)', + argument: '[true/path]', + }, +} diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index 41ca9c8b74d9..8d3b88e2dd6e 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -466,6 +466,28 @@ export class Vitest { await this.coverageProvider?.mergeReports?.(coverages) } + async collect(filters?: string[]) { + this._onClose = [] + + await this.initBrowserProviders() + + const files = await this.filterTestsBySource( + await this.globTestFiles(filters), + ) + + // if run with --changed, don't exit if no tests are found + if (!files.length) { + return { tests: [], errors: [] } + } + + await this.collectFiles(files) + + return { + tests: this.state.getFiles(), + errors: this.state.getUnhandledErrors(), + } + } + async start(filters?: string[]) { this._onClose = [] @@ -705,6 +727,56 @@ export class Vitest { return await this.runningPromise } + async collectFiles(specs: WorkspaceSpec[]) { + await this.initializeDistPath() + + const filepaths = specs.map(([, file]) => file) + this.state.collectPaths(filepaths) + + // previous run + await this.runningPromise + this._onCancelListeners = [] + this.isCancelling = false + + // schedule the new run + this.runningPromise = (async () => { + if (!this.pool) { + this.pool = createPool(this) + } + + const invalidates = Array.from(this.invalidates) + this.invalidates.clear() + this.snapshot.clear() + this.state.clearErrors() + + await this.initializeGlobalSetup(specs) + + try { + await this.pool.collectTests(specs, invalidates) + } + catch (err) { + this.state.catchError(err, 'Unhandled Error') + } + + const files = this.state.getFiles() + + // can only happen if there was a syntax error in describe block + // or there was an error importing a file + if (hasFailed(files)) { + process.exitCode = 1 + } + })() + .finally(async () => { + this.runningPromise = undefined + + // all subsequent runs will treat this as a fresh run + this.config.changed = false + this.config.related = undefined + }) + + return await this.runningPromise + } + async cancelCurrentRun(reason: CancelReason) { this.isCancelling = true await Promise.all(this._onCancelListeners.splice(0).map(listener => listener(reason))) diff --git a/packages/vitest/src/node/pool.ts b/packages/vitest/src/node/pool.ts index ef183ef1081c..5dd244fea431 100644 --- a/packages/vitest/src/node/pool.ts +++ b/packages/vitest/src/node/pool.ts @@ -21,6 +21,7 @@ type LocalPool = Exclude export interface ProcessPool { name: string runTests: RunWithFiles + collectTests: RunWithFiles close?: () => Awaitable } @@ -104,7 +105,7 @@ export function createPool(ctx: Vitest): ProcessPool { || execArg.startsWith('--diagnostic-dir'), ) - async function runTests(files: WorkspaceSpec[], invalidate?: string[]) { + async function executeTests(method: 'runTests' | 'collectTests', files: WorkspaceSpec[], invalidate?: string[]) { const options: PoolProcessOptions = { execArgv: [...execArgv, ...conditions], env: { @@ -144,9 +145,9 @@ export function createPool(ctx: Vitest): ProcessPool { `Custom pool "${filepath}" should return an object with "name" property`, ) } - if (typeof poolInstance?.runTests !== 'function') { + if (typeof poolInstance?.[method] !== 'function') { throw new TypeError( - `Custom pool "${filepath}" should return an object with "runTests" method`, + `Custom pool "${filepath}" should return an object with "${method}" method`, ) } @@ -201,7 +202,7 @@ export function createPool(ctx: Vitest): ProcessPool { if (pool in factories) { const factory = factories[pool] pools[pool] ??= factory() - return pools[pool]!.runTests(specs, invalidate) + return pools[pool]![method](specs, invalidate) } if (pool === 'browser') { @@ -209,19 +210,20 @@ export function createPool(ctx: Vitest): ProcessPool { const { createBrowserPool } = await import('@vitest/browser') return createBrowserPool(ctx) })() - return pools[pool]!.runTests(specs, invalidate) + return pools[pool]![method](specs, invalidate) } const poolHandler = await resolveCustomPool(pool) pools[poolHandler.name] ??= poolHandler - return poolHandler.runTests(specs, invalidate) + return poolHandler[method](specs, invalidate) }), ) } return { name: 'default', - runTests, + runTests: (files, invalidates) => executeTests('runTests', files, invalidates), + collectTests: (files, invalidates) => executeTests('collectTests', files, invalidates), async close() { await Promise.all(Object.values(pools).map(p => p?.close?.())) }, diff --git a/packages/vitest/src/node/pools/forks.ts b/packages/vitest/src/node/pools/forks.ts index bfc739cc5503..5d54d911bef7 100644 --- a/packages/vitest/src/node/pools/forks.ts +++ b/packages/vitest/src/node/pools/forks.ts @@ -290,6 +290,7 @@ export function createForksPool( return { name: 'forks', runTests: runWithFiles('run'), + collectTests: runWithFiles('collect'), close: () => pool.destroy(), } } diff --git a/packages/vitest/src/node/pools/threads.ts b/packages/vitest/src/node/pools/threads.ts index 38bdec730055..0fcf5c02ab97 100644 --- a/packages/vitest/src/node/pools/threads.ts +++ b/packages/vitest/src/node/pools/threads.ts @@ -286,6 +286,7 @@ export function createThreadsPool( return { name: 'threads', runTests: runWithFiles('run'), + collectTests: runWithFiles('collect'), close: () => pool.destroy(), } } diff --git a/packages/vitest/src/node/pools/typecheck.ts b/packages/vitest/src/node/pools/typecheck.ts index 3c86225e5db6..a88c29ae5912 100644 --- a/packages/vitest/src/node/pools/typecheck.ts +++ b/packages/vitest/src/node/pools/typecheck.ts @@ -87,11 +87,31 @@ export function createTypecheckPool(ctx: Vitest): ProcessPool { }) await checker.prepare() - await checker.collectTests() - checker.start() return checker } + async function startTypechecker(project: WorkspaceProject, files: string[]) { + if (project.typechecker) { + return project.typechecker + } + const checker = await createWorkspaceTypechecker(project, files) + await checker.collectTests() + await checker.start() + } + + async function collectTests(specs: WorkspaceSpec[]) { + const specsByProject = groupBy(specs, ([project]) => project.getName()) + for (const name in specsByProject) { + const project = specsByProject[name][0][0] + const files = specsByProject[name].map(([_, file]) => file) + const checker = await createWorkspaceTypechecker(project, files) + checker.setFiles(files) + await checker.collectTests() + ctx.state.collectFiles(checker.getTestFiles()) + await ctx.report('onCollected') + } + } + async function runTests(specs: WorkspaceSpec[]) { const specsByProject = groupBy(specs, ([project]) => project.getName()) const promises: Promise[] = [] @@ -122,7 +142,7 @@ export function createTypecheckPool(ctx: Vitest): ProcessPool { } promises.push(promise) promisesMap.set(project, promise) - createWorkspaceTypechecker(project, files) + startTypechecker(project, files) } await Promise.all(promises) @@ -131,6 +151,7 @@ export function createTypecheckPool(ctx: Vitest): ProcessPool { return { name: 'typescript', runTests, + collectTests, async close() { const promises = ctx.projects.map(project => project.typechecker?.stop(), diff --git a/packages/vitest/src/node/pools/vmForks.ts b/packages/vitest/src/node/pools/vmForks.ts index 5c55a3181311..c34bdb990bab 100644 --- a/packages/vitest/src/node/pools/vmForks.ts +++ b/packages/vitest/src/node/pools/vmForks.ts @@ -208,6 +208,7 @@ export function createVmForksPool( return { name: 'vmForks', runTests: runWithFiles('run'), + collectTests: runWithFiles('collect'), close: () => pool.destroy(), } } diff --git a/packages/vitest/src/node/pools/vmThreads.ts b/packages/vitest/src/node/pools/vmThreads.ts index 92f982cd7bce..09654cf2f574 100644 --- a/packages/vitest/src/node/pools/vmThreads.ts +++ b/packages/vitest/src/node/pools/vmThreads.ts @@ -200,6 +200,7 @@ export function createVmThreadsPool( return { name: 'vmThreads', runTests: runWithFiles('run'), + collectTests: runWithFiles('collect'), close: () => pool.destroy(), } } diff --git a/packages/vitest/src/node/workspace.ts b/packages/vitest/src/node/workspace.ts index e2f5b8002f68..d5e66e54b706 100644 --- a/packages/vitest/src/node/workspace.ts +++ b/packages/vitest/src/node/workspace.ts @@ -407,14 +407,14 @@ export class WorkspaceProject { return isBrowserEnabled(this.config) } - getSerializableConfig() { + getSerializableConfig(method: 'run' | 'collect' = 'run') { const optimizer = this.config.deps?.optimizer const poolOptions = this.config.poolOptions // Resolve from server.config to avoid comparing against default value const isolate = this.server?.config?.test?.isolate - return deepMerge( + const config = deepMerge( { ...this.config, @@ -500,6 +500,18 @@ export class WorkspaceProject { }, this.ctx.configOverride || ({} as any), ) as ResolvedConfig + + // disable heavy features when collecting because they are not needed + if (method === 'collect') { + config.coverage.enabled = false + if (config.browser.provider && config.browser.provider !== 'preview') { + config.browser.headless = true + } + config.snapshotSerializers = [] + config.diff = undefined + } + + return config } close() { diff --git a/packages/vitest/src/runtime/runBaseTests.ts b/packages/vitest/src/runtime/runBaseTests.ts index 2e2acf279ff1..4bcc9c04c912 100644 --- a/packages/vitest/src/runtime/runBaseTests.ts +++ b/packages/vitest/src/runtime/runBaseTests.ts @@ -1,5 +1,5 @@ import { performance } from 'node:perf_hooks' -import { startTests } from '@vitest/runner' +import { collectTests, startTests } from '@vitest/runner' import type { ResolvedConfig, ResolvedTestEnvironment } from '../types' import { getWorkerState, resetModules } from '../utils' import { vi } from '../integrations/vi' @@ -15,6 +15,7 @@ import { closeInspector } from './inspector' // browser shouldn't call this! export async function run( + method: 'run' | 'collect', files: string[], config: ResolvedConfig, environment: ResolvedTestEnvironment, @@ -62,7 +63,12 @@ export async function run( workerState.filepath = file - await startTests([file], runner) + if (method === 'run') { + await startTests([file], runner) + } + else { + await collectTests([file], runner) + } // reset after tests, because user might call `vi.setConfig` in setupFile vi.resetConfig() diff --git a/packages/vitest/src/runtime/runVmTests.ts b/packages/vitest/src/runtime/runVmTests.ts index 57a651b4ea67..a989d7d6ff46 100644 --- a/packages/vitest/src/runtime/runVmTests.ts +++ b/packages/vitest/src/runtime/runVmTests.ts @@ -3,7 +3,7 @@ import { createRequire } from 'node:module' import util from 'node:util' import timers from 'node:timers' import { performance } from 'node:perf_hooks' -import { startTests } from '@vitest/runner' +import { collectTests, startTests } from '@vitest/runner' import { createColors, setupColors } from '@vitest/utils' import { installSourcemapsSupport } from 'vite-node/source-map' import { setupChaiConfig } from '../integrations/chai/config' @@ -21,6 +21,7 @@ import { setupCommonEnv } from './setup-common' import { closeInspector } from './inspector' export async function run( + method: 'run' | 'collect', files: string[], config: ResolvedConfig, executor: VitestExecutor, @@ -81,7 +82,12 @@ export async function run( for (const file of files) { workerState.filepath = file - await startTests([file], runner) + if (method === 'run') { + await startTests([file], runner) + } + else { + await collectTests([file], runner) + } // reset after tests, because user might call `vi.setConfig` in setupFile vi.resetConfig() diff --git a/packages/vitest/src/runtime/worker.ts b/packages/vitest/src/runtime/worker.ts index f5ce651b3b69..f55a5b8ad6c4 100644 --- a/packages/vitest/src/runtime/worker.ts +++ b/packages/vitest/src/runtime/worker.ts @@ -13,7 +13,7 @@ if (isChildProcess()) { } // this is what every pool executes when running tests -export async function run(ctx: ContextRPC) { +async function execute(mehtod: 'run' | 'collect', ctx: ContextRPC) { const prepareStart = performance.now() const inspectorCleanup = setupInspect(ctx) @@ -75,16 +75,26 @@ export async function run(ctx: ContextRPC) { providedContext: ctx.providedContext, } - if (!worker.runTests || typeof worker.runTests !== 'function') { + const methodName = mehtod === 'collect' ? 'collectTests' : 'runTests' + + if (!worker[methodName] || typeof worker[methodName] !== 'function') { throw new TypeError( `Test worker should expose "runTests" method. Received "${typeof worker.runTests}".`, ) } - await worker.runTests(state) + await worker[methodName](state) } finally { await rpcDone().catch(() => {}) inspectorCleanup() } } + +export function run(ctx: ContextRPC) { + return execute('run', ctx) +} + +export function collect(ctx: ContextRPC) { + return execute('collect', ctx) +} diff --git a/packages/vitest/src/runtime/workers/base.ts b/packages/vitest/src/runtime/workers/base.ts index a8cdc7562708..740f170eff23 100644 --- a/packages/vitest/src/runtime/workers/base.ts +++ b/packages/vitest/src/runtime/workers/base.ts @@ -19,7 +19,7 @@ async function startViteNode(options: ContextExecutorOptions) { return _viteNode } -export async function runBaseTests(state: WorkerGlobalState) { +export async function runBaseTests(method: 'run' | 'collect', state: WorkerGlobalState) { const { ctx } = state // state has new context, but we want to reuse existing ones state.moduleCache = moduleCache @@ -40,6 +40,7 @@ export async function runBaseTests(state: WorkerGlobalState) { import('../runBaseTests'), ]) await run( + method, ctx.files, ctx.config, { environment: state.environment, options: ctx.environment.options }, diff --git a/packages/vitest/src/runtime/workers/forks.ts b/packages/vitest/src/runtime/workers/forks.ts index 65b0e919776a..5d4e8d564551 100644 --- a/packages/vitest/src/runtime/workers/forks.ts +++ b/packages/vitest/src/runtime/workers/forks.ts @@ -9,19 +9,27 @@ class ForksBaseWorker implements VitestWorker { return createForksRpcOptions(v8) } - async runTests(state: WorkerGlobalState) { + async executeTests(method: 'run' | 'collect', state: WorkerGlobalState) { // TODO: don't rely on reassigning process.exit // https://github.com/vitest-dev/vitest/pull/4441#discussion_r1443771486 const exit = process.exit state.ctx.config = unwrapSerializableConfig(state.ctx.config) try { - await runBaseTests(state) + await runBaseTests(method, state) } finally { process.exit = exit } } + + runTests(state: WorkerGlobalState) { + return this.executeTests('run', state) + } + + collectTests(state: WorkerGlobalState) { + return this.executeTests('collect', state) + } } export default new ForksBaseWorker() diff --git a/packages/vitest/src/runtime/workers/threads.ts b/packages/vitest/src/runtime/workers/threads.ts index 38fb1a5abff0..77fb7eb96dd5 100644 --- a/packages/vitest/src/runtime/workers/threads.ts +++ b/packages/vitest/src/runtime/workers/threads.ts @@ -10,7 +10,11 @@ class ThreadsBaseWorker implements VitestWorker { } runTests(state: WorkerGlobalState): unknown { - return runBaseTests(state) + return runBaseTests('run', state) + } + + collectTests(state: WorkerGlobalState): unknown { + return runBaseTests('collect', state) } } diff --git a/packages/vitest/src/runtime/workers/types.ts b/packages/vitest/src/runtime/workers/types.ts index 577cbdeb4c10..0cbdc515d70d 100644 --- a/packages/vitest/src/runtime/workers/types.ts +++ b/packages/vitest/src/runtime/workers/types.ts @@ -11,4 +11,5 @@ export type WorkerRpcOptions = Pick< export interface VitestWorker { getRpcOptions: (ctx: ContextRPC) => WorkerRpcOptions runTests: (state: WorkerGlobalState) => Awaitable + collectTests: (state: WorkerGlobalState) => Awaitable } diff --git a/packages/vitest/src/runtime/workers/vm.ts b/packages/vitest/src/runtime/workers/vm.ts index 6553e2f8e863..ebd9e75f55aa 100644 --- a/packages/vitest/src/runtime/workers/vm.ts +++ b/packages/vitest/src/runtime/workers/vm.ts @@ -15,7 +15,7 @@ const entryFile = pathToFileURL(resolve(distDir, 'workers/runVmTests.js')).href const fileMap = new FileMap() const packageCache = new Map() -export async function runVmTests(state: WorkerGlobalState) { +export async function runVmTests(method: 'run' | 'collect', state: WorkerGlobalState) { const { environment, ctx, rpc } = state if (!environment.setupVM) { @@ -90,7 +90,7 @@ export async function runVmTests(state: WorkerGlobalState) { )) as typeof import('../runVmTests') try { - await run(ctx.files, ctx.config, executor) + await run(method, ctx.files, ctx.config, executor) } finally { await vm.teardown?.() diff --git a/packages/vitest/src/runtime/workers/vmForks.ts b/packages/vitest/src/runtime/workers/vmForks.ts index 365c7634851c..d93a9052f201 100644 --- a/packages/vitest/src/runtime/workers/vmForks.ts +++ b/packages/vitest/src/runtime/workers/vmForks.ts @@ -9,17 +9,25 @@ class ForksVmWorker implements VitestWorker { return createForksRpcOptions(v8) } - async runTests(state: WorkerGlobalState) { + async executeTests(method: 'run' | 'collect', state: WorkerGlobalState) { const exit = process.exit state.ctx.config = unwrapSerializableConfig(state.ctx.config) try { - await runVmTests(state) + await runVmTests(method, state) } finally { process.exit = exit } } + + runTests(state: WorkerGlobalState) { + return this.executeTests('run', state) + } + + collectTests(state: WorkerGlobalState) { + return this.executeTests('collect', state) + } } export default new ForksVmWorker() diff --git a/packages/vitest/src/runtime/workers/vmThreads.ts b/packages/vitest/src/runtime/workers/vmThreads.ts index 8040b8dfcc55..3197cd34ba1a 100644 --- a/packages/vitest/src/runtime/workers/vmThreads.ts +++ b/packages/vitest/src/runtime/workers/vmThreads.ts @@ -10,7 +10,11 @@ class ThreadsVmWorker implements VitestWorker { } runTests(state: WorkerGlobalState): unknown { - return runVmTests(state) + return runVmTests('run', state) + } + + collectTests(state: WorkerGlobalState): unknown { + return runVmTests('collect', state) } } diff --git a/packages/vitest/src/typecheck/typechecker.ts b/packages/vitest/src/typecheck/typechecker.ts index d50d239c87ad..b4bc264d38be 100644 --- a/packages/vitest/src/typecheck/typechecker.ts +++ b/packages/vitest/src/typecheck/typechecker.ts @@ -266,6 +266,7 @@ export class Typechecker { public async stop() { await this.clear() this.process?.kill() + this.process = undefined } protected async ensurePackageInstalled(ctx: Vitest, checker: string) { @@ -294,6 +295,10 @@ export class Typechecker { } public async start() { + if (this.process) { + return + } + if (!this.tempConfigPath) { throw new Error('tsconfig was not initialized') } diff --git a/packages/vitest/src/types/browser.ts b/packages/vitest/src/types/browser.ts index a6de0e7595ff..4f8ed9cb85ba 100644 --- a/packages/vitest/src/types/browser.ts +++ b/packages/vitest/src/types/browser.ts @@ -170,6 +170,7 @@ export interface BrowserCommandContext { export interface BrowserServerStateContext { files: string[] + method: 'run' | 'collect' resolve: () => void reject: (v: unknown) => void } @@ -182,7 +183,7 @@ export interface BrowserOrchestrator { export interface BrowserServerState { orchestrators: Map getContext: (contextId: string) => BrowserServerStateContext | undefined - createAsyncContext: (contextId: string, files: string[]) => Promise + createAsyncContext: (method: 'collect' | 'run', contextId: string, files: string[]) => Promise } export interface BrowserServer { diff --git a/packages/vitest/src/types/config.ts b/packages/vitest/src/types/config.ts index 6b230a80c18e..b564e061af85 100644 --- a/packages/vitest/src/types/config.ts +++ b/packages/vitest/src/types/config.ts @@ -954,10 +954,12 @@ export interface ResolvedConfig | 'poolOptions' | 'pool' | 'cliExclude' + | 'diff' > { mode: VitestRunMode base?: string + diff?: string config?: string filters?: string[] diff --git a/packages/vitest/src/workers.ts b/packages/vitest/src/workers.ts index 1a6e839c2c49..5601196ce6f4 100644 --- a/packages/vitest/src/workers.ts +++ b/packages/vitest/src/workers.ts @@ -4,7 +4,7 @@ export { unwrapSerializableConfig, } from './runtime/workers/utils' export { provideWorkerState } from './utils/global' -export { run as runVitestWorker } from './runtime/worker' +export { run as runVitestWorker, collect as collectVitestWorkerTests } from './runtime/worker' export { runVmTests } from './runtime/workers/vm' export { runBaseTests } from './runtime/workers/base' export type { WorkerRpcOptions, VitestWorker } from './runtime/workers/types' diff --git a/test/cli/fixtures/custom-pool/pool/custom-pool.ts b/test/cli/fixtures/custom-pool/pool/custom-pool.ts index eaaf2d54d10c..5f40830acbea 100644 --- a/test/cli/fixtures/custom-pool/pool/custom-pool.ts +++ b/test/cli/fixtures/custom-pool/pool/custom-pool.ts @@ -8,6 +8,9 @@ export default (ctx: Vitest): ProcessPool => { const options = ctx.config.poolOptions?.custom as any return { name: 'custom', + async collectTests() { + throw new Error('Not implemented') + }, async runTests(specs) { ctx.logger.console.warn('[pool] printing:', options.print) ctx.logger.console.warn('[pool] array option', options.array) diff --git a/test/cli/fixtures/list/basic.test.ts b/test/cli/fixtures/list/basic.test.ts new file mode 100644 index 000000000000..37f68c1bd622 --- /dev/null +++ b/test/cli/fixtures/list/basic.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from 'vitest' + +describe('basic suite', () => { + describe('inner suite', () => { + it('some test', () => { + expect(1).toBe(1) + }) + + it('another test', () => { + expect(1).toBe(1) + }) + }) + + it('basic test', () => { + expect(1).toBe(1) + }) +}) + +it('outside test', () => { + expect(1).toBe(1) +}) diff --git a/test/cli/fixtures/list/custom.config.ts b/test/cli/fixtures/list/custom.config.ts new file mode 100644 index 000000000000..841db817e7b8 --- /dev/null +++ b/test/cli/fixtures/list/custom.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + include: ['basic.test.ts', 'math.test.ts'], + name: 'custom', + includeTaskLocation: true, + }, +}) diff --git a/test/cli/fixtures/list/describe-error.test.ts b/test/cli/fixtures/list/describe-error.test.ts new file mode 100644 index 000000000000..3238cceabdb6 --- /dev/null +++ b/test/cli/fixtures/list/describe-error.test.ts @@ -0,0 +1,9 @@ +import { describe, expect, it } from 'vitest'; + +describe('describe error', () => { + throw new Error('describe error') + + it('wont run', () => { + expect(true).toBe(true) + }) +}) \ No newline at end of file diff --git a/test/cli/fixtures/list/fail.config.ts b/test/cli/fixtures/list/fail.config.ts new file mode 100644 index 000000000000..16d254a9aaab --- /dev/null +++ b/test/cli/fixtures/list/fail.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + include: ['top-level-error.test.ts', 'describe-error.test.ts'], + }, +}) diff --git a/test/cli/fixtures/list/math.test.ts b/test/cli/fixtures/list/math.test.ts new file mode 100644 index 000000000000..c39601812ac5 --- /dev/null +++ b/test/cli/fixtures/list/math.test.ts @@ -0,0 +1,9 @@ +import { expect, it } from 'vitest' + +it('1 plus 1', () => { + expect(1 + 1).toBe(2) +}) + +it('failing test', () => { + expect(1 + 1).toBe(3) +}) diff --git a/test/cli/fixtures/list/top-level-error.test.ts b/test/cli/fixtures/list/top-level-error.test.ts new file mode 100644 index 000000000000..59922e3fd360 --- /dev/null +++ b/test/cli/fixtures/list/top-level-error.test.ts @@ -0,0 +1 @@ +throw new Error('top level error') diff --git a/test/cli/fixtures/list/vitest.config.ts b/test/cli/fixtures/list/vitest.config.ts new file mode 100644 index 000000000000..e64489065ea4 --- /dev/null +++ b/test/cli/fixtures/list/vitest.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + include: ['basic.test.ts', 'math.test.ts'], + browser: { + name: 'chromium', + provider: 'playwright', + headless: true, + api: 7523, + } + }, +}) diff --git a/test/cli/test/__snapshots__/list.test.ts.snap b/test/cli/test/__snapshots__/list.test.ts.snap new file mode 100644 index 000000000000..efc6f0b467ad --- /dev/null +++ b/test/cli/test/__snapshots__/list.test.ts.snap @@ -0,0 +1,98 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`basic output shows error 1`] = ` +"Error: top level error + ❯ top-level-error.test.ts:1:7 + 1| throw new Error('top level error') + | ^ + 2| + +Error: describe error + ❯ describe-error.test.ts:4:9 + 2| + 3| describe('describe error', () => { + 4| throw new Error('describe error') + | ^ + 5| + 6| it('wont run', () => { + +" +`; + +exports[`correctly outputs all tests with args: "--browser.enabled" 1`] = ` +"basic.test.ts > basic suite > inner suite > some test +basic.test.ts > basic suite > inner suite > another test +basic.test.ts > basic suite > basic test +basic.test.ts > outside test +math.test.ts > 1 plus 1 +math.test.ts > failing test +" +`; + +exports[`correctly outputs all tests with args: "--pool=forks" 1`] = ` +"basic.test.ts > basic suite > inner suite > some test +basic.test.ts > basic suite > inner suite > another test +basic.test.ts > basic suite > basic test +basic.test.ts > outside test +math.test.ts > 1 plus 1 +math.test.ts > failing test +" +`; + +exports[`correctly outputs all tests with args: "--pool=threads" 1`] = ` +"basic.test.ts > basic suite > inner suite > some test +basic.test.ts > basic suite > inner suite > another test +basic.test.ts > basic suite > basic test +basic.test.ts > outside test +math.test.ts > 1 plus 1 +math.test.ts > failing test +" +`; + +exports[`correctly outputs all tests with args: "--pool=vmForks" 1`] = ` +"basic.test.ts > basic suite > inner suite > some test +basic.test.ts > basic suite > inner suite > another test +basic.test.ts > basic suite > basic test +basic.test.ts > outside test +math.test.ts > 1 plus 1 +math.test.ts > failing test +" +`; + +exports[`json output shows error 1`] = ` +"Error: top level error + ❯ top-level-error.test.ts:1:7 + 1| throw new Error('top level error') + | ^ + 2| + +Error: describe error + ❯ describe-error.test.ts:4:9 + 2| + 3| describe('describe error', () => { + 4| throw new Error('describe error') + | ^ + 5| + 6| it('wont run', () => { + +" +`; + +exports[`json with a file output shows error 1`] = ` +"Error: top level error + ❯ top-level-error.test.ts:1:7 + 1| throw new Error('top level error') + | ^ + 2| + +Error: describe error + ❯ describe-error.test.ts:4:9 + 2| + 3| describe('describe error', () => { + 4| throw new Error('describe error') + | ^ + 5| + 6| it('wont run', () => { + +" +`; diff --git a/test/cli/test/list.test.ts b/test/cli/test/list.test.ts new file mode 100644 index 000000000000..6fc3597cb2c2 --- /dev/null +++ b/test/cli/test/list.test.ts @@ -0,0 +1,174 @@ +import { readFileSync, rmSync } from 'node:fs' +import { expect, onTestFinished, test } from 'vitest' +import { runVitestCli } from '../../test-utils' + +test.each([ + ['--pool=threads'], + ['--pool=forks'], + ['--pool=vmForks'], + ['--browser.enabled'], +])('correctly outputs all tests with args: "%s"', async (...args) => { + const { stdout, exitCode } = await runVitestCli('list', '-r=./fixtures/list', ...args) + expect(stdout).toMatchSnapshot() + expect(exitCode).toBe(0) +}) + +test.each([ + ['basic'], + ['json', '--json'], + ['json with a file', '--json=./list.json'], +])('%s output shows error', async () => { + const { stderr, stdout, exitCode } = await runVitestCli('list', '-r=./fixtures/list', '-c=fail.config.ts') + expect(stdout).toBe('') + expect(stderr).toMatchSnapshot() + expect(exitCode).toBe(1) +}) + +test('correctly outputs json', async () => { + const { stdout, exitCode } = await runVitestCli('list', '-r=./fixtures/list', '--json') + expect(relative(stdout)).toMatchInlineSnapshot(` + "[ + { + "name": "basic suite > inner suite > some test", + "file": "/fixtures/list/basic.test.ts" + }, + { + "name": "basic suite > inner suite > another test", + "file": "/fixtures/list/basic.test.ts" + }, + { + "name": "basic suite > basic test", + "file": "/fixtures/list/basic.test.ts" + }, + { + "name": "outside test", + "file": "/fixtures/list/basic.test.ts" + }, + { + "name": "1 plus 1", + "file": "/fixtures/list/math.test.ts" + }, + { + "name": "failing test", + "file": "/fixtures/list/math.test.ts" + } + ] + " + `) + expect(exitCode).toBe(0) +}) + +test('correctly saves json', async () => { + const { stdout, exitCode } = await runVitestCli('list', '-r=./fixtures/list', '--json=./list.json') + onTestFinished(() => { + rmSync('./fixtures/list/list.json') + }) + const json = readFileSync('./fixtures/list/list.json', 'utf-8') + expect(stdout).toBe('') + expect(relative(json)).toMatchInlineSnapshot(` + "[ + { + "name": "basic suite > inner suite > some test", + "file": "/fixtures/list/basic.test.ts" + }, + { + "name": "basic suite > inner suite > another test", + "file": "/fixtures/list/basic.test.ts" + }, + { + "name": "basic suite > basic test", + "file": "/fixtures/list/basic.test.ts" + }, + { + "name": "outside test", + "file": "/fixtures/list/basic.test.ts" + }, + { + "name": "1 plus 1", + "file": "/fixtures/list/math.test.ts" + }, + { + "name": "failing test", + "file": "/fixtures/list/math.test.ts" + } + ]" + `) + expect(exitCode).toBe(0) +}) + +test('correctly filters by file', async () => { + const { stdout, exitCode } = await runVitestCli('list', 'math.test.ts', '-r=./fixtures/list') + expect(stdout).toMatchInlineSnapshot(` + "math.test.ts > 1 plus 1 + math.test.ts > failing test + " + `) + expect(exitCode).toBe(0) +}) + +test('correctly prints project name in basic report', async () => { + const { stdout } = await runVitestCli('list', 'math.test.ts', '-r=./fixtures/list', '--config=./custom.config.ts') + expect(stdout).toMatchInlineSnapshot(` + "[custom] math.test.ts > 1 plus 1 + [custom] math.test.ts > failing test + " + `) +}) + +test('correctly prints project name and locations in json report', async () => { + const { stdout } = await runVitestCli('list', 'math.test.ts', '-r=./fixtures/list', '--json', '--config=./custom.config.ts') + expect(relative(stdout)).toMatchInlineSnapshot(` + "[ + { + "name": "1 plus 1", + "file": "/fixtures/list/math.test.ts", + "projectName": "custom", + "location": { + "line": 3, + "column": 1 + } + }, + { + "name": "failing test", + "file": "/fixtures/list/math.test.ts", + "projectName": "custom", + "location": { + "line": 7, + "column": 1 + } + } + ] + " + `) +}) + +test('correctly filters by test name', async () => { + const { stdout } = await runVitestCli('list', '-t=inner', '-r=./fixtures/list') + expect(stdout).toMatchInlineSnapshot(` + "basic.test.ts > basic suite > inner suite > some test + basic.test.ts > basic suite > inner suite > another test + " + `) +}) + +test('ignores watch flag', async () => { + // if it ends, it works - otherwise it will hang + const { stdout } = await runVitestCli('list', '-r=./fixtures/list', '--watch') + expect(stdout).toMatchInlineSnapshot(` + "basic.test.ts > basic suite > inner suite > some test + basic.test.ts > basic suite > inner suite > another test + basic.test.ts > basic suite > basic test + basic.test.ts > outside test + math.test.ts > 1 plus 1 + math.test.ts > failing test + " + `) +}) + +function relative(stdout: string) { + return stdout.replace(new RegExp(slash(process.cwd()), 'gi'), '') +} + +function slash(stdout: string) { + return stdout.replace(/\\/g, '/') +} diff --git a/test/core/package.json b/test/core/package.json index b06f3139a57c..ce41f04ea557 100644 --- a/test/core/package.json +++ b/test/core/package.json @@ -8,7 +8,8 @@ "test:forks": "vitest --project forks", "test:vmThreads": "vitest --project vmThreads", "dev": "vite", - "coverage": "vitest run --coverage" + "coverage": "vitest run --coverage", + "collect": "vitest list" }, "devDependencies": { "@types/debug": "^4.1.12", diff --git a/test/test-utils/index.ts b/test/test-utils/index.ts index b1e212139237..ce8980f1e918 100644 --- a/test/test-utils/index.ts +++ b/test/test-utils/index.ts @@ -136,7 +136,7 @@ export async function runCli(command: string, _options?: Options | string, ...ar return output() } - if (args.includes('--watch')) { + if (args[0] !== 'list' && args.includes('--watch')) { if (command === 'vitest') { // Wait for initial test run to complete await cli.waitForStdout('Waiting for file changes')