From 2e49189549aca4134bafdc1e3a6419b249d2ce55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20S=C3=A1nchez?= Date: Mon, 8 Sep 2025 17:29:02 +0200 Subject: [PATCH] feat: display test "path" when filtering (#8547) --- packages/vitest/src/node/stdin.ts | 51 ++++++++++++++----- packages/vitest/src/node/watch-filter.ts | 27 +++++++--- test/cli/fixtures/path-filter/basic.test.ts | 10 ++++ .../cli/fixtures/path-filter/vitest.config.ts | 3 ++ test/cli/test/path-filter.test.ts | 16 ++++++ 5 files changed, 85 insertions(+), 22 deletions(-) create mode 100644 test/cli/fixtures/path-filter/basic.test.ts create mode 100644 test/cli/fixtures/path-filter/vitest.config.ts create mode 100644 test/cli/test/path-filter.test.ts diff --git a/packages/vitest/src/node/stdin.ts b/packages/vitest/src/node/stdin.ts index 066d60fdc..0fffd9537 100644 --- a/packages/vitest/src/node/stdin.ts +++ b/packages/vitest/src/node/stdin.ts @@ -1,7 +1,9 @@ +import type { File, Task } from '@vitest/runner' import type { Writable } from 'node:stream' import type { Vitest } from './core' +import type { FilterObject } from './watch-filter' import readline from 'node:readline' -import { getTests } from '@vitest/runner/utils' +import { isTestCase } from '@vitest/runner/utils' import { relative, resolve } from 'pathe' import prompt from 'prompts' import c from 'tinyrainbow' @@ -38,6 +40,38 @@ ${keys ) } +function* traverseFilteredTestNames(parentName: string, filter: RegExp, t: Task): Generator { + if (isTestCase(t)) { + if (t.name.match(filter)) { + const displayName = `${parentName} > ${t.name}` + yield { key: t.name, toString: () => displayName } + } + } + else { + parentName = parentName.length ? `${parentName} > ${t.name}` : t.name + for (const task of t.tasks) { + yield* traverseFilteredTestNames(parentName, filter, task) + } + } +} + +function* getFilteredTestNames(pattern: string, suite: File[]): Generator { + try { + const reg = new RegExp(pattern) + // TODO: we cannot run tests per workspace yet: filtering files + const files = new Set() + for (const file of suite) { + if (!files.has(file.name)) { + files.add(file.name) + yield* traverseFilteredTestNames('', reg, file) + } + } + } + catch { + // `new RegExp` may throw error when input is invalid regexp + } +} + export function registerConsoleShortcuts( ctx: Vitest, stdin: NodeJS.ReadStream | undefined = process.stdin, @@ -134,24 +168,13 @@ export function registerConsoleShortcuts( async function inputNamePattern() { off() - const watchFilter = new WatchFilter( + const watchFilter = new WatchFilter<'object'>( 'Input test name pattern (RegExp)', stdin, stdout, ) const filter = await watchFilter.filter((str: string) => { - const files = ctx.state.getFiles() - const tests = getTests(files) - try { - const reg = new RegExp(str) - return tests - .map(test => test.name) - .filter(testName => testName.match(reg)) - } - catch { - // `new RegExp` may throw error when input is invalid regexp - return [] - } + return [...getFilteredTestNames(str, ctx.state.getFiles())] }) on() diff --git a/packages/vitest/src/node/watch-filter.ts b/packages/vitest/src/node/watch-filter.ts index 24d5a85c0..e595169ca 100644 --- a/packages/vitest/src/node/watch-filter.ts +++ b/packages/vitest/src/node/watch-filter.ts @@ -9,13 +9,19 @@ const MAX_RESULT_COUNT = 10 const SELECTION_MAX_INDEX = 7 const ESC = '\u001B[' -type FilterFunc = (keyword: string) => Promise | string[] +export interface FilterObject { + key: string + toString: () => string +} + +type FilterItemType = T extends 'string' ? string : FilterObject +type FilterFuncType = (keyword: string) => Promise[]> | FilterItemType[] -export class WatchFilter { +export class WatchFilter { private filterRL: readline.Interface private currentKeyword: string | undefined = undefined private message: string - private results: string[] = [] + private results: FilterItemType[] = [] private selectionIndex = -1 private onKeyPress?: (str: string, key: any) => void private stdin: NodeJS.ReadStream @@ -40,7 +46,7 @@ export class WatchFilter { } } - public async filter(filterFunc: FilterFunc): Promise { + public async filter(filterFunc: FilterFuncType): Promise { this.write(this.promptLine()) const resultPromise = createDefer() @@ -58,7 +64,7 @@ export class WatchFilter { } private filterHandler( - filterFunc: FilterFunc, + filterFunc: FilterFuncType, onSubmit: (result?: string) => void, ) { return async (str: string | undefined, key: any) => { @@ -78,12 +84,17 @@ export class WatchFilter { onSubmit(undefined) return case key?.name === 'enter': - case key?.name === 'return': + case key?.name === 'return': { + const selection = this.results[this.selectionIndex] + const result = typeof selection === 'string' + ? selection + : selection?.key onSubmit( - this.results[this.selectionIndex] || this.currentKeyword || '', + result || this.currentKeyword || '', ) this.currentKeyword = undefined break + } case key?.name === 'up': if (this.selectionIndex && this.selectionIndex > 0) { this.selectionIndex-- @@ -229,6 +240,6 @@ export class WatchFilter { } public getLastResults(): string[] { - return this.results + return this.results.map(r => (typeof r === 'string' ? r : r.toString())) } } diff --git a/test/cli/fixtures/path-filter/basic.test.ts b/test/cli/fixtures/path-filter/basic.test.ts new file mode 100644 index 000000000..76f8bf7b3 --- /dev/null +++ b/test/cli/fixtures/path-filter/basic.test.ts @@ -0,0 +1,10 @@ +import { describe, expect, test } from "vitest"; + +describe('basic path filter', () => { + test("foo", () => { + expect(1).toBe(1); + }) + test("bar", () => { + expect(1).toBe(1); + }) +}) diff --git a/test/cli/fixtures/path-filter/vitest.config.ts b/test/cli/fixtures/path-filter/vitest.config.ts new file mode 100644 index 000000000..abed6b211 --- /dev/null +++ b/test/cli/fixtures/path-filter/vitest.config.ts @@ -0,0 +1,3 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({}) diff --git a/test/cli/test/path-filter.test.ts b/test/cli/test/path-filter.test.ts new file mode 100644 index 000000000..c2a658a80 --- /dev/null +++ b/test/cli/test/path-filter.test.ts @@ -0,0 +1,16 @@ +import { test } from 'vitest' +import { runVitest } from '../../test-utils' + +test('test path is shown when filtering', async () => { + const { vitest } = await runVitest({ + root: 'fixtures/path-filter', + watch: true, + }) + + await vitest.waitForStdout('press h to show help, press q to quit') + vitest.write('t') + await vitest.waitForStdout(`? Input test name pattern (RegExp)`) + vitest.write('foo') + await vitest.waitForStdout('Pattern matches 1 result') + await vitest.waitForStdout('basic.test.ts > basic path filter > foo') +}) -- 2.51.2