Skip to content

Commit 2e49189

Browse files
authored
feat: display test "path" when filtering (#8547)
1 parent 1050a37 commit 2e49189

File tree

5 files changed

+85
-22
lines changed

5 files changed

+85
-22
lines changed

packages/vitest/src/node/stdin.ts

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import type { File, Task } from '@vitest/runner'
12
import type { Writable } from 'node:stream'
23
import type { Vitest } from './core'
4+
import type { FilterObject } from './watch-filter'
35
import readline from 'node:readline'
4-
import { getTests } from '@vitest/runner/utils'
6+
import { isTestCase } from '@vitest/runner/utils'
57
import { relative, resolve } from 'pathe'
68
import prompt from 'prompts'
79
import c from 'tinyrainbow'
@@ -38,6 +40,38 @@ ${keys
3840
)
3941
}
4042

43+
function* traverseFilteredTestNames(parentName: string, filter: RegExp, t: Task): Generator<FilterObject> {
44+
if (isTestCase(t)) {
45+
if (t.name.match(filter)) {
46+
const displayName = `${parentName} > ${t.name}`
47+
yield { key: t.name, toString: () => displayName }
48+
}
49+
}
50+
else {
51+
parentName = parentName.length ? `${parentName} > ${t.name}` : t.name
52+
for (const task of t.tasks) {
53+
yield* traverseFilteredTestNames(parentName, filter, task)
54+
}
55+
}
56+
}
57+
58+
function* getFilteredTestNames(pattern: string, suite: File[]): Generator<FilterObject> {
59+
try {
60+
const reg = new RegExp(pattern)
61+
// TODO: we cannot run tests per workspace yet: filtering files
62+
const files = new Set<string>()
63+
for (const file of suite) {
64+
if (!files.has(file.name)) {
65+
files.add(file.name)
66+
yield* traverseFilteredTestNames('', reg, file)
67+
}
68+
}
69+
}
70+
catch {
71+
// `new RegExp` may throw error when input is invalid regexp
72+
}
73+
}
74+
4175
export function registerConsoleShortcuts(
4276
ctx: Vitest,
4377
stdin: NodeJS.ReadStream | undefined = process.stdin,
@@ -134,24 +168,13 @@ export function registerConsoleShortcuts(
134168

135169
async function inputNamePattern() {
136170
off()
137-
const watchFilter = new WatchFilter(
171+
const watchFilter = new WatchFilter<'object'>(
138172
'Input test name pattern (RegExp)',
139173
stdin,
140174
stdout,
141175
)
142176
const filter = await watchFilter.filter((str: string) => {
143-
const files = ctx.state.getFiles()
144-
const tests = getTests(files)
145-
try {
146-
const reg = new RegExp(str)
147-
return tests
148-
.map(test => test.name)
149-
.filter(testName => testName.match(reg))
150-
}
151-
catch {
152-
// `new RegExp` may throw error when input is invalid regexp
153-
return []
154-
}
177+
return [...getFilteredTestNames(str, ctx.state.getFiles())]
155178
})
156179

157180
on()

packages/vitest/src/node/watch-filter.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,19 @@ const MAX_RESULT_COUNT = 10
99
const SELECTION_MAX_INDEX = 7
1010
const ESC = '\u001B['
1111

12-
type FilterFunc = (keyword: string) => Promise<string[]> | string[]
12+
export interface FilterObject {
13+
key: string
14+
toString: () => string
15+
}
16+
17+
type FilterItemType<T extends 'string' | 'object' = 'string'> = T extends 'string' ? string : FilterObject
18+
type FilterFuncType<T extends 'string' | 'object' = 'string'> = (keyword: string) => Promise<FilterItemType<T>[]> | FilterItemType<T>[]
1319

14-
export class WatchFilter {
20+
export class WatchFilter<T extends 'string' | 'object' = 'string'> {
1521
private filterRL: readline.Interface
1622
private currentKeyword: string | undefined = undefined
1723
private message: string
18-
private results: string[] = []
24+
private results: FilterItemType<T>[] = []
1925
private selectionIndex = -1
2026
private onKeyPress?: (str: string, key: any) => void
2127
private stdin: NodeJS.ReadStream
@@ -40,7 +46,7 @@ export class WatchFilter {
4046
}
4147
}
4248

43-
public async filter(filterFunc: FilterFunc): Promise<string | undefined> {
49+
public async filter(filterFunc: FilterFuncType<T>): Promise<string | undefined> {
4450
this.write(this.promptLine())
4551

4652
const resultPromise = createDefer<string | undefined>()
@@ -58,7 +64,7 @@ export class WatchFilter {
5864
}
5965

6066
private filterHandler(
61-
filterFunc: FilterFunc,
67+
filterFunc: FilterFuncType<T>,
6268
onSubmit: (result?: string) => void,
6369
) {
6470
return async (str: string | undefined, key: any) => {
@@ -78,12 +84,17 @@ export class WatchFilter {
7884
onSubmit(undefined)
7985
return
8086
case key?.name === 'enter':
81-
case key?.name === 'return':
87+
case key?.name === 'return': {
88+
const selection = this.results[this.selectionIndex]
89+
const result = typeof selection === 'string'
90+
? selection
91+
: selection?.key
8292
onSubmit(
83-
this.results[this.selectionIndex] || this.currentKeyword || '',
93+
result || this.currentKeyword || '',
8494
)
8595
this.currentKeyword = undefined
8696
break
97+
}
8798
case key?.name === 'up':
8899
if (this.selectionIndex && this.selectionIndex > 0) {
89100
this.selectionIndex--
@@ -229,6 +240,6 @@ export class WatchFilter {
229240
}
230241

231242
public getLastResults(): string[] {
232-
return this.results
243+
return this.results.map(r => (typeof r === 'string' ? r : r.toString()))
233244
}
234245
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { describe, expect, test } from "vitest";
2+
3+
describe('basic path filter', () => {
4+
test("foo", () => {
5+
expect(1).toBe(1);
6+
})
7+
test("bar", () => {
8+
expect(1).toBe(1);
9+
})
10+
})
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { defineConfig } from 'vitest/config'
2+
3+
export default defineConfig({})

test/cli/test/path-filter.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { test } from 'vitest'
2+
import { runVitest } from '../../test-utils'
3+
4+
test('test path is shown when filtering', async () => {
5+
const { vitest } = await runVitest({
6+
root: 'fixtures/path-filter',
7+
watch: true,
8+
})
9+
10+
await vitest.waitForStdout('press h to show help, press q to quit')
11+
vitest.write('t')
12+
await vitest.waitForStdout(`? Input test name pattern (RegExp)`)
13+
vitest.write('foo')
14+
await vitest.waitForStdout('Pattern matches 1 result')
15+
await vitest.waitForStdout('basic.test.ts > basic path filter > foo')
16+
})

0 commit comments

Comments
 (0)