mirror of
https://github.com/vitest-dev/vitest.git
synced 2025-12-08 18:26:03 +00:00
Co-authored-by: Vladimir <sleuths.slews0s@icloud.com>
This commit is contained in:
parent
1326c6efb7
commit
480d866aaa
@ -1,9 +1,11 @@
|
||||
import readline from 'node:readline'
|
||||
import c from 'picocolors'
|
||||
import prompt from 'prompts'
|
||||
import { isWindows, stdout } from '../utils'
|
||||
import { relative } from 'pathe'
|
||||
import { getTests, isWindows, stdout } from '../utils'
|
||||
import { toArray } from '../utils/base'
|
||||
import type { Vitest } from './core'
|
||||
import { WatchFilter } from './watch-filter'
|
||||
|
||||
const keys = [
|
||||
[['a', 'return'], 'rerun all tests'],
|
||||
@ -95,14 +97,22 @@ export function registerConsoleShortcuts(ctx: Vitest) {
|
||||
|
||||
async function inputNamePattern() {
|
||||
off()
|
||||
const { filter = '' }: { filter: string } = await prompt([{
|
||||
name: 'filter',
|
||||
type: 'text',
|
||||
message: 'Input test name pattern (RegExp)',
|
||||
initial: ctx.configOverride.testNamePattern?.source || '',
|
||||
}])
|
||||
const watchFilter = new WatchFilter('Input test name pattern (RegExp)')
|
||||
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 []
|
||||
}
|
||||
})
|
||||
|
||||
on()
|
||||
await ctx.changeNamePattern(filter.trim(), undefined, 'change pattern')
|
||||
await ctx.changeNamePattern(filter?.trim() || '', undefined, 'change pattern')
|
||||
}
|
||||
|
||||
async function inputProjectName() {
|
||||
@ -119,15 +129,21 @@ export function registerConsoleShortcuts(ctx: Vitest) {
|
||||
|
||||
async function inputFilePattern() {
|
||||
off()
|
||||
const { filter = '' }: { filter: string } = await prompt([{
|
||||
name: 'filter',
|
||||
type: 'text',
|
||||
message: 'Input filename pattern',
|
||||
initial: latestFilename,
|
||||
}])
|
||||
latestFilename = filter.trim()
|
||||
|
||||
const watchFilter = new WatchFilter('Input filename pattern')
|
||||
|
||||
const filter = await watchFilter.filter(async (str: string) => {
|
||||
const files = await ctx.globTestFiles([str])
|
||||
return files.map(file =>
|
||||
relative(ctx.config.root, file[1]),
|
||||
)
|
||||
})
|
||||
|
||||
on()
|
||||
await ctx.changeFilenamePattern(filter.trim())
|
||||
|
||||
latestFilename = filter?.trim() || ''
|
||||
|
||||
await ctx.changeFilenamePattern(latestFilename)
|
||||
}
|
||||
|
||||
let rl: readline.Interface | undefined
|
||||
|
||||
168
packages/vitest/src/node/watch-filter.ts
Normal file
168
packages/vitest/src/node/watch-filter.ts
Normal file
@ -0,0 +1,168 @@
|
||||
import readline from 'node:readline'
|
||||
import c from 'picocolors'
|
||||
import stripAnsi from 'strip-ansi'
|
||||
import { createDefer } from '@vitest/utils'
|
||||
import { stdout } from '../utils'
|
||||
|
||||
const MAX_RESULT_COUNT = 10
|
||||
const SELECTION_MAX_INDEX = 7
|
||||
const ESC = '\u001B['
|
||||
|
||||
type FilterFunc = (keyword: string) => Promise<string[]> | string[]
|
||||
|
||||
export class WatchFilter {
|
||||
private filterRL: readline.Interface
|
||||
private currentKeyword: string | undefined = undefined
|
||||
private message: string
|
||||
private results: string[] = []
|
||||
private selectionIndex = -1
|
||||
private onKeyPress?: (str: string, key: any) => void
|
||||
|
||||
constructor(message: string) {
|
||||
this.message = message
|
||||
this.filterRL = readline.createInterface({ input: process.stdin, escapeCodeTimeout: 50 })
|
||||
readline.emitKeypressEvents(process.stdin, this.filterRL)
|
||||
if (process.stdin.isTTY)
|
||||
process.stdin.setRawMode(true)
|
||||
}
|
||||
|
||||
public async filter(filterFunc: FilterFunc): Promise<string | undefined> {
|
||||
stdout().write(this.promptLine())
|
||||
|
||||
const resultPromise = createDefer<string | undefined>()
|
||||
|
||||
this.onKeyPress = this.filterHandler(filterFunc, (result) => {
|
||||
resultPromise.resolve(result)
|
||||
})
|
||||
process.stdin.on('keypress', this.onKeyPress)
|
||||
try {
|
||||
return await resultPromise
|
||||
}
|
||||
finally {
|
||||
this.close()
|
||||
}
|
||||
}
|
||||
|
||||
private filterHandler(filterFunc: FilterFunc, onSubmit: (result?: string) => void) {
|
||||
return async (str: string | undefined, key: any) => {
|
||||
switch (true) {
|
||||
case key.sequence === '\x7F':
|
||||
if (this.currentKeyword && this.currentKeyword?.length > 1)
|
||||
this.currentKeyword = this.currentKeyword?.slice(0, -1)
|
||||
|
||||
else
|
||||
this.currentKeyword = undefined
|
||||
|
||||
break
|
||||
case key?.ctrl && key?.name === 'c':
|
||||
case key?.name === 'escape':
|
||||
this.cancel()
|
||||
onSubmit(undefined)
|
||||
break
|
||||
case key?.name === 'enter':
|
||||
case key?.name === 'return':
|
||||
onSubmit(this.results[this.selectionIndex] || this.currentKeyword || '')
|
||||
this.currentKeyword = undefined
|
||||
break
|
||||
case key?.name === 'up':
|
||||
if (this.selectionIndex && this.selectionIndex > 0)
|
||||
this.selectionIndex--
|
||||
else
|
||||
this.selectionIndex = -1
|
||||
|
||||
break
|
||||
case key?.name === 'down':
|
||||
if (this.selectionIndex < this.results.length - 1)
|
||||
this.selectionIndex++
|
||||
else if (this.selectionIndex >= this.results.length - 1)
|
||||
this.selectionIndex = this.results.length - 1
|
||||
|
||||
break
|
||||
case !key?.ctrl && !key?.meta:
|
||||
if (this.currentKeyword === undefined)
|
||||
this.currentKeyword = str
|
||||
|
||||
else
|
||||
this.currentKeyword += str || ''
|
||||
break
|
||||
}
|
||||
|
||||
if (this.currentKeyword)
|
||||
this.results = await filterFunc(this.currentKeyword)
|
||||
|
||||
this.render()
|
||||
}
|
||||
}
|
||||
|
||||
private render() {
|
||||
let printStr = this.promptLine()
|
||||
if (!this.currentKeyword) {
|
||||
printStr += '\nPlease input filter pattern'
|
||||
}
|
||||
else if (this.currentKeyword && this.results.length === 0) {
|
||||
printStr += '\nPattern matches no results'
|
||||
}
|
||||
else {
|
||||
const resultCountLine = this.results.length === 1 ? `Pattern matches ${this.results.length} result` : `Pattern matches ${this.results.length} results`
|
||||
|
||||
let resultBody = ''
|
||||
|
||||
if (this.results.length > MAX_RESULT_COUNT) {
|
||||
const offset = this.selectionIndex > SELECTION_MAX_INDEX ? this.selectionIndex - SELECTION_MAX_INDEX : 0
|
||||
const displayResults = this.results.slice(offset, MAX_RESULT_COUNT + offset)
|
||||
const remainingResultCount = this.results.length - offset - displayResults.length
|
||||
|
||||
resultBody = `${displayResults.map((result, index) => (index + offset === this.selectionIndex) ? c.green(` › ${result}`) : c.dim(` › ${result}`)).join('\n')}`
|
||||
if (remainingResultCount > 0)
|
||||
resultBody += '\n' + `${c.dim(` ...and ${remainingResultCount} more ${remainingResultCount === 1 ? 'result' : 'results'}`)}`
|
||||
}
|
||||
else {
|
||||
resultBody = this.results.map((result, index) => (index === this.selectionIndex) ? c.green(` › ${result}`) : c.dim(` › ${result}`))
|
||||
.join('\n')
|
||||
}
|
||||
|
||||
printStr += `\n${resultCountLine}\n${resultBody}`
|
||||
}
|
||||
this.eraseAndPrint(printStr)
|
||||
this.restoreCursor()
|
||||
}
|
||||
|
||||
private keywordOffset() {
|
||||
return `? ${this.message} › `.length + 1
|
||||
}
|
||||
|
||||
private promptLine() {
|
||||
return `${c.cyan('?')} ${c.bold(this.message)} › ${this.currentKeyword || ''}`
|
||||
}
|
||||
|
||||
private eraseAndPrint(str: string) {
|
||||
let rows = 0
|
||||
const lines = str.split(/\r?\n/)
|
||||
for (const line of lines)
|
||||
// We have to take care of screen width in case of long lines
|
||||
rows += 1 + Math.floor(Math.max(stripAnsi(line).length - 1, 0) / stdout().columns)
|
||||
|
||||
stdout().write(`${ESC}1G`) // move to the beginning of the line
|
||||
stdout().write(`${ESC}J`) // erase down
|
||||
stdout().write(str)
|
||||
stdout().write(`${ESC}${rows - 1}A`) // moving up lines
|
||||
}
|
||||
|
||||
private close() {
|
||||
this.filterRL.close()
|
||||
if (this.onKeyPress)
|
||||
process.stdin.removeListener('keypress', this.onKeyPress)
|
||||
|
||||
if (process.stdin.isTTY)
|
||||
process.stdin.setRawMode(false)
|
||||
}
|
||||
|
||||
private restoreCursor() {
|
||||
const cursortPos = this.keywordOffset() + (this.currentKeyword?.length || 0)
|
||||
stdout().write(`${ESC}${cursortPos}G`)
|
||||
}
|
||||
|
||||
private cancel() {
|
||||
stdout().write(`${ESC}J`) // erase down
|
||||
}
|
||||
}
|
||||
@ -45,7 +45,12 @@ test('filter by filename', async () => {
|
||||
|
||||
await vitest.waitForStdout('Input filename pattern')
|
||||
|
||||
vitest.write('math\n')
|
||||
vitest.write('math')
|
||||
|
||||
await vitest.waitForStdout('Pattern matches 1 results')
|
||||
await vitest.waitForStdout('› math.test.ts')
|
||||
|
||||
vitest.write('\n')
|
||||
|
||||
await vitest.waitForStdout('Filename pattern: math')
|
||||
await vitest.waitForStdout('1 passed')
|
||||
@ -58,7 +63,11 @@ test('filter by test name', async () => {
|
||||
|
||||
await vitest.waitForStdout('Input test name pattern')
|
||||
|
||||
vitest.write('sum\n')
|
||||
vitest.write('sum')
|
||||
await vitest.waitForStdout('Pattern matches 1 results')
|
||||
await vitest.waitForStdout('› sum')
|
||||
|
||||
vitest.write('\n')
|
||||
|
||||
await vitest.waitForStdout('Test name pattern: /sum/')
|
||||
await vitest.waitForStdout('1 passed')
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user