diff --git a/docs/guide/cli.md b/docs/guide/cli.md index afff5f82a3f3..5dd6429693d2 100644 --- a/docs/guide/cli.md +++ b/docs/guide/cli.md @@ -121,6 +121,12 @@ tests/test1.test.ts tests/test2.test.ts ``` +Use `--related` to list only the tests that import the specified source files: + +```bash +vitest list --related --filesOnly src/index.ts src/utils.ts +``` + Since Vitest 4.1, you may pass `--static-parse` to [parse test files](/api/advanced/vitest#parsespecifications) instead of running them to collect tests. Vitest parses test files with limited concurrency, defaulting to `os.availableParallelism()`. You can change it via the `--static-parse-concurrency` option. ### `vitest doctor` diff --git a/packages/vitest/src/node/cli/cac.ts b/packages/vitest/src/node/cli/cac.ts index 5ef682197bd3..f7be566cbc35 100644 --- a/packages/vitest/src/node/cli/cac.ts +++ b/packages/vitest/src/node/cli/cac.ts @@ -193,7 +193,13 @@ export function createCLI(options: CliParseOptions = {}): CAC { addCliOptions( cli .command('list [...filters]', undefined, options) - .action((filters, options) => collect(filters, options)), + .action((filters, options) => { + if (options.related === true) { + const { related, ...cliOptions } = options + return collect([], { ...cliOptions, related: filters }) + } + return collect(filters, options) + }), collectCliOptionsConfig, ) @@ -254,6 +260,10 @@ export function parseCLI(argv: string | string[], config: CliParseOptions = {}): options.passWithNoTests ??= true args = [] } + if (arrayArgs[2] === 'list' && options.related === true) { + options.related = args + args = [] + } return { filter: args as string[], options, diff --git a/packages/vitest/src/node/cli/cli-config.ts b/packages/vitest/src/node/cli/cli-config.ts index 9ebfbe0797ef..9f4b38a5f385 100644 --- a/packages/vitest/src/node/cli/cli-config.ts +++ b/packages/vitest/src/node/cli/cli-config.ts @@ -30,6 +30,7 @@ export type CLIOptions = { } type VitestCLIOptions = CLIOptions +type CollectCliOptions = Omit & { related?: boolean } const apiConfig: (port: number) => CLIOptions = (port: number) => ({ port: { @@ -1013,7 +1014,7 @@ export const cliOptionsConfig: VitestCLIOptions = { taskTitleValueFormatTruncate: null, } -export const collectCliOptionsConfig: VitestCLIOptions = { +export const collectCliOptionsConfig: CLIOptions = { ...cliOptionsConfig, json: { description: 'Print collected tests as JSON or write to a file (Default: false)', @@ -1029,6 +1030,9 @@ export const collectCliOptionsConfig: VitestCLIOptions = { description: 'How many tests to process at the same time (default: os.availableParallelism())', argument: '', }, + related: { + description: 'Print only tests related to the specified source files', + }, changed: { description: 'Print only tests that are affected by the changed files (default: `false`)', argument: '[since]', diff --git a/test/e2e/test/list-related.test.ts b/test/e2e/test/list-related.test.ts new file mode 100644 index 000000000000..87c79818ecc3 --- /dev/null +++ b/test/e2e/test/list-related.test.ts @@ -0,0 +1,147 @@ +import { expect, test } from 'vitest' +import { replaceRoot, runInlineTests, runVitestCli } from '#test-utils' + +const structure = { + 'src/shared.ts': 'export const shared = true', + 'src/intermediate.ts': ` + export { shared } from './shared' + `, + 'src/other.ts': 'export const other = true', + 'src/unrelated.ts': 'export const unrelated = true', + 'tests/direct.test.ts': ` + import { expect, test } from 'vitest' + import { shared } from '../src/shared' + + test('direct dependency', () => { + expect(shared).toBe(true) + }) + `, + 'tests/transitive.test.ts': ` + import { expect, test } from 'vitest' + import { shared } from '../src/intermediate' + + test('transitive dependency', () => { + expect(shared).toBe(true) + }) + `, + 'tests/other.test.ts': ` + import { expect, test } from 'vitest' + import { other } from '../src/other' + + test('other dependency', () => { + expect(other).toBe(true) + }) + `, + 'tests/unrelated.test.ts': ` + import { expect, test } from 'vitest' + import { unrelated } from '../src/unrelated' + + test('unrelated dependency', () => { + expect(unrelated).toBe(true) + }) + `, +} + +async function setupRelatedTests() { + const result = await runInlineTests(structure) + + expect(result.stderr).toBe('') + expect(result.testTree()).toMatchInlineSnapshot(` + { + "tests/direct.test.ts": { + "direct dependency": "passed", + }, + "tests/other.test.ts": { + "other dependency": "passed", + }, + "tests/transitive.test.ts": { + "transitive dependency": "passed", + }, + "tests/unrelated.test.ts": { + "unrelated dependency": "passed", + }, + } + `) + + return result +} + +test('list --related includes direct and transitive dependents', async () => { + const { root } = await setupRelatedTests() + const { stdout, stderr, exitCode } = await runVitestCli( + 'list', + `--root=${root}`, + '--related', + 'src/shared.ts', + ) + + expect(stderr).toBe('') + expect(stdout).toMatchInlineSnapshot(` + "tests/direct.test.ts > direct dependency + tests/transitive.test.ts > transitive dependency + " + `) + expect(exitCode).toBe(0) +}) + +test('list --related combines multiple source files', async () => { + const { root } = await setupRelatedTests() + const { stdout, stderr, exitCode } = await runVitestCli( + 'list', + `--root=${root}`, + '--related', + 'src/shared.ts', + 'src/other.ts', + ) + + expect(stderr).toBe('') + expect(stdout).toMatchInlineSnapshot(` + "tests/direct.test.ts > direct dependency + tests/other.test.ts > other dependency + tests/transitive.test.ts > transitive dependency + " + `) + expect(exitCode).toBe(0) +}) + +test('list --related supports files-only and JSON output', async () => { + const { root } = await setupRelatedTests() + const filesResult = await runVitestCli( + 'list', + `--root=${root}`, + '--related', + '--filesOnly', + 'src/shared.ts', + ) + const jsonResult = await runVitestCli( + 'list', + `--root=${root}`, + '--related', + 'src/shared.ts', + '--json', + ) + + expect(filesResult.stderr).toBe('') + expect(filesResult.stdout).toMatchInlineSnapshot(` + "tests/direct.test.ts + tests/transitive.test.ts + " + `) + expect(filesResult.exitCode).toBe(0) + + expect(jsonResult.stderr).toBe('') + expect(replaceRoot(jsonResult.stdout, root)).toMatchInlineSnapshot(` + "[ + { + "name": "direct dependency", + "file": "/tests/direct.test.ts" + }, + { + "name": "transitive dependency", + "file": "/tests/transitive.test.ts" + } + ] + " + `) + expect(jsonResult.exitCode).toBe(0) +}) diff --git a/test/unit/test/cli-test.test.ts b/test/unit/test/cli-test.test.ts index e5c84f89f7ed..92a35201c08e 100644 --- a/test/unit/test/cli-test.test.ts +++ b/test/unit/test/cli-test.test.ts @@ -420,6 +420,23 @@ test('public parseCLI works correctly', () => { 'color': true, }, }) + expect(parseCLI('vitest list --related ./source-a.ts ./source-b.ts')).toEqual({ + filter: [], + options: { + 'related': ['./source-a.ts', './source-b.ts'], + '--': [], + 'color': true, + }, + }) + expect(parseCLI('vitest list --related --filesOnly ./source-a.ts ./source-b.ts')).toEqual({ + filter: [], + options: { + 'related': ['./source-a.ts', './source-b.ts'], + 'filesOnly': true, + '--': [], + 'color': true, + }, + }) expect(parseCLI('vitest --coverage --browser=chrome')).toEqual({ filter: [],