[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
12

Configure Feed

Select the types of activity you want to include in your feed.

feat(api): `startVitest()` to accept `stdout` and `stdin` (#5493)

authored by

Ari Perkkiö and committed by
GitHub
(Apr 23, 2024, 3:09 PM +0200) 780b187f 23bd3cd2

+377 -352
+2
eslint.config.js
··· 13 13 '**/assets/**', 14 14 '**/*.timestamp-*', 15 15 'test/core/src/self', 16 + 'test/cache/cache/.vitest-base/results.json', 16 17 'test/wasm-modules/src/wasm-bindgen-no-cyclic', 17 18 'test/workspaces/results.json', 19 + 'test/workspaces-browser/results.json', 18 20 'test/reporters/fixtures/with-syntax-error.test.js', 19 21 'test/network-imports/public/slash@3.0.0.js', 20 22 'test/coverage-test/src/transpiled.js',
+1
packages/vitest/rollup.config.js
··· 65 65 'worker_threads', 66 66 'node:worker_threads', 67 67 'node:fs', 68 + 'node:stream', 68 69 'node:vm', 69 70 'inspector', 70 71 'vite-node/source-map',
+95
test/test-utils/cli.ts
··· 1 + import type { Readable, Writable } from 'node:stream' 2 + import stripAnsi from 'strip-ansi' 3 + 4 + type Listener = (() => void) 5 + type ReadableOrWritable = Readable | Writable 6 + type Source = 'stdout' | 'stderr' 7 + 8 + export class Cli { 9 + stdout = '' 10 + stderr = '' 11 + 12 + private stdoutListeners: Listener[] = [] 13 + private stderrListeners: Listener[] = [] 14 + private stdin: ReadableOrWritable 15 + 16 + constructor(options: { stdin: ReadableOrWritable; stdout: ReadableOrWritable; stderr: ReadableOrWritable }) { 17 + this.stdin = options.stdin 18 + 19 + for (const source of (['stdout', 'stderr'] as const)) { 20 + const stream = options[source] 21 + 22 + if ((stream as Readable).readable) { 23 + stream.on('data', (data) => { 24 + this.capture(source, data) 25 + }) 26 + } 27 + else if (isWritable(stream)) { 28 + const original = stream.write.bind(stream) 29 + 30 + // @ts-expect-error -- Is there a better way to detect when a Writable is being written into? 31 + stream.write = (data, encoding, callback) => { 32 + this.capture(source, data) 33 + return original(data, encoding, callback) 34 + } 35 + } 36 + } 37 + } 38 + 39 + private capture(source: Source, data: any) { 40 + this[source] += stripAnsi(data.toString()) 41 + this[`${source}Listeners`].forEach(fn => fn()) 42 + } 43 + 44 + write(data: string) { 45 + this.resetOutput() 46 + 47 + if (((this.stdin as Readable).readable)) 48 + this.stdin.emit('data', data) 49 + else if (isWritable(this.stdin)) 50 + this.stdin.write(data) 51 + } 52 + 53 + resetOutput() { 54 + this.stdout = '' 55 + this.stderr = '' 56 + } 57 + 58 + waitForStdout(expected: string) { 59 + return this.waitForOutput(expected, 'stdout', this.waitForStdout) 60 + } 61 + 62 + waitForStderr(expected: string) { 63 + return this.waitForOutput(expected, 'stderr', this.waitForStderr) 64 + } 65 + 66 + private waitForOutput(expected: string, source: Source, caller: Parameters<typeof Error.captureStackTrace>[1]) { 67 + const error = new Error('Timeout') 68 + Error.captureStackTrace(error, caller) 69 + 70 + return new Promise<void>((resolve, reject) => { 71 + if (this[source].includes(expected)) 72 + return resolve() 73 + 74 + const timeout = setTimeout(() => { 75 + error.message = `Timeout when waiting for error "${expected}".\nReceived:\n${this[source]}` 76 + reject(error) 77 + }, process.env.CI ? 20_000 : 4_000) 78 + 79 + const listener = () => { 80 + if (this[source].includes(expected)) { 81 + if (timeout) 82 + clearTimeout(timeout) 83 + 84 + resolve() 85 + } 86 + } 87 + 88 + this[`${source}Listeners`].push(listener) 89 + }) 90 + } 91 + } 92 + 93 + function isWritable(stream: any): stream is Writable { 94 + return stream && typeof stream.write === 'function' 95 + }
+53 -136
test/test-utils/index.ts
··· 1 - import { Console } from 'node:console' 2 - import { Writable } from 'node:stream' 1 + import { Readable, Writable } from 'node:stream' 3 2 import fs from 'node:fs' 4 3 import { fileURLToPath } from 'node:url' 5 4 import type { UserConfig as ViteUserConfig } from 'vite' ··· 7 6 import type { Vitest } from 'vitest/node' 8 7 import { startVitest } from 'vitest/node' 9 8 import { type Options, execa } from 'execa' 10 - import stripAnsi from 'strip-ansi' 11 9 import { dirname, resolve } from 'pathe' 10 + import { Cli } from './cli' 12 11 13 12 export async function runVitest(config: UserConfig, cliFilters: string[] = [], mode: VitestRunMode = 'test', viteOverrides: ViteUserConfig = {}) { 14 13 // Reset possible previous runs ··· 19 18 const exit = process.exit 20 19 process.exit = (() => { }) as never 21 20 22 - const { getLogs, restore } = captureLogs() 21 + const stdout = new Writable({ write: (_, __, callback) => callback() }) 22 + const stderr = new Writable({ write: (_, __, callback) => callback() }) 23 23 24 - let vitest: Vitest | undefined 24 + // "node:tty".ReadStream doesn't work on Github Windows CI, let's simulate it 25 + const stdin = new Readable({ read: () => '' }) as NodeJS.ReadStream 26 + stdin.isTTY = true 27 + stdin.setRawMode = () => stdin 28 + const cli = new Cli({ stdin, stdout, stderr }) 29 + 30 + let ctx: Vitest | undefined 25 31 try { 26 32 const { reporters, ...rest } = config 27 33 28 - vitest = await startVitest(mode, cliFilters, { 34 + ctx = await startVitest(mode, cliFilters, { 29 35 watch: false, 30 36 // "none" can be used to disable passing "reporter" option so that default value is used (it's not same as reporters: ["default"]) 31 37 ...(reporters === 'none' ? {} : reporters ? { reporters } : { reporters: ['verbose'] }), 32 38 ...rest, 33 - }, viteOverrides) 39 + }, viteOverrides, { 40 + stdin, 41 + stdout, 42 + stderr, 43 + }) 34 44 } 35 45 catch (e: any) { 36 - return { 37 - stderr: `${getLogs().stderr}\n${e.message}`, 38 - stdout: getLogs().stdout, 39 - exitCode, 40 - vitest, 41 - } 46 + console.error(e.message) 47 + cli.stderr += e.message 42 48 } 43 49 finally { 44 50 exitCode = process.exitCode 45 51 process.exitCode = 0 46 - process.exit = exit 47 52 48 - restore() 53 + afterEach(async () => { 54 + await ctx?.close() 55 + await ctx?.closingPromise 56 + process.exit = exit 57 + }) 49 58 } 50 - 51 - return { ...getLogs(), exitCode, vitest } 52 - } 53 - 54 - function captureLogs() { 55 - const stdout: string[] = [] 56 - const stderr: string[] = [] 57 - 58 - const streams = { 59 - stdout: new Writable({ 60 - write(chunk, _, callback) { 61 - stdout.push(chunk.toString()) 62 - callback() 63 - }, 64 - }), 65 - stderr: new Writable({ 66 - write(chunk, _, callback) { 67 - stderr.push(chunk.toString()) 68 - callback() 69 - }, 70 - }), 71 - } 72 - 73 - const originalConsole = globalThis.console 74 - globalThis.console = new Console(streams) 75 - 76 - const originalStdoutWrite = process.stdout.write 77 - process.stdout.write = streams.stdout.write.bind(streams.stdout) as any 78 - 79 - const originalStderrWrite = process.stderr.write 80 - process.stderr.write = streams.stderr.write.bind(streams.stderr) as any 81 59 82 60 return { 83 - restore: () => { 84 - globalThis.console = originalConsole 85 - process.stdout.write = originalStdoutWrite 86 - process.stderr.write = originalStderrWrite 87 - }, 88 - getLogs() { 89 - return { 90 - stdout: stripAnsi(stdout.join('')), 91 - stderr: stripAnsi(stderr.join('')), 92 - } 61 + ctx, 62 + exitCode, 63 + vitest: cli, 64 + stdout: cli.stdout, 65 + stderr: cli.stderr, 66 + waitForClose: async () => { 67 + await new Promise<void>(resolve => ctx!.onClose(resolve)) 68 + return ctx?.closingPromise 93 69 }, 94 70 } 95 71 } ··· 103 79 } 104 80 105 81 const subprocess = execa(command, args, options as Options) 82 + const cli = new Cli({ 83 + stdin: subprocess.stdin!, 84 + stdout: subprocess.stdout!, 85 + stderr: subprocess.stderr!, 86 + }) 106 87 107 88 let setDone: (value?: unknown) => void 108 89 const isDone = new Promise(resolve => (setDone = resolve)) 109 - 110 - const cli = { 111 - stdout: '', 112 - stderr: '', 113 - stdoutListeners: [] as (() => void)[], 114 - stderrListeners: [] as (() => void)[], 115 - isDone, 116 - write(text: string) { 117 - this.resetOutput() 118 - subprocess.stdin!.write(text) 119 - }, 120 - waitForStdout(expected: string) { 121 - const error = new Error('Timeout error') 122 - Error.captureStackTrace(error, this.waitForStdout) 123 - return new Promise<void>((resolve, reject) => { 124 - if (this.stdout.includes(expected)) 125 - return resolve() 126 - 127 - const timeout = setTimeout(() => { 128 - error.message = `Timeout when waiting for output "${expected}".\nReceived:\n${this.stdout} \nStderr:\n${this.stderr}` 129 - reject(error) 130 - }, process.env.CI ? 20_000 : 4_000) 131 - 132 - const listener = () => { 133 - if (this.stdout.includes(expected)) { 134 - if (timeout) 135 - clearTimeout(timeout) 136 - 137 - resolve() 138 - } 139 - } 140 - 141 - this.stdoutListeners.push(listener) 142 - }) 143 - }, 144 - waitForStderr(expected: string) { 145 - const error = new Error('Timeout') 146 - Error.captureStackTrace(error, this.waitForStderr) 147 - return new Promise<void>((resolve, reject) => { 148 - if (this.stderr.includes(expected)) 149 - return resolve() 150 - 151 - const timeout = setTimeout(() => { 152 - error.message = `Timeout when waiting for error "${expected}".\nReceived:\n${this.stderr}\nStdout:\n${this.stdout}` 153 - reject(error) 154 - }, process.env.CI ? 20_000 : 4_000) 155 - 156 - const listener = () => { 157 - if (this.stderr.includes(expected)) { 158 - if (timeout) 159 - clearTimeout(timeout) 160 - 161 - resolve() 162 - } 163 - } 164 - 165 - this.stderrListeners.push(listener) 166 - }) 167 - }, 168 - resetOutput() { 169 - this.stdout = '' 170 - this.stderr = '' 171 - }, 172 - } 173 - 174 - subprocess.stdout!.on('data', (data) => { 175 - cli.stdout += stripAnsi(data.toString()) 176 - cli.stdoutListeners.forEach(fn => fn()) 177 - }) 178 - 179 - subprocess.stderr!.on('data', (data) => { 180 - cli.stderr += stripAnsi(data.toString()) 181 - cli.stderrListeners.forEach(fn => fn()) 182 - }) 183 - 184 90 subprocess.on('exit', () => setDone()) 91 + 92 + function output() { 93 + return { 94 + vitest: cli, 95 + stdout: cli.stdout || '', 96 + stderr: cli.stderr || '', 97 + waitForClose: () => isDone, 98 + } 99 + } 185 100 186 101 // Manually stop the processes so that each test don't have to do this themselves 187 102 afterEach(async () => { 188 103 if (subprocess.exitCode === null) 189 104 subprocess.kill() 190 105 191 - await cli.isDone 106 + await isDone 192 107 }) 193 108 194 109 if (args.includes('--inspect') || args.includes('--inspect-brk')) 195 - return cli 110 + return output() 196 111 197 112 if (args.includes('--watch')) { 198 113 if (command === 'vitest') // Wait for initial test run to complete ··· 202 117 cli.stdout = cli.stdout.replace('[debug] watcher is ready\n', '') 203 118 } 204 119 else { 205 - await cli.isDone 120 + await isDone 206 121 } 207 122 208 - return cli 123 + return output() 209 124 } 210 125 211 126 export async function runVitestCli(_options?: Options | string, ...args: string[]) { ··· 215 130 216 131 export async function runViteNodeCli(_options?: Options | string, ...args: string[]) { 217 132 process.env.VITE_TEST_WATCHER_DEBUG = 'true' 218 - return runCli('vite-node', _options, ...args) 133 + const { vitest, ...rest } = await runCli('vite-node', _options, ...args) 134 + 135 + return { viteNode: vitest, ...rest } 219 136 } 220 137 221 138 const originalFiles = new Map<string, string>()
+12 -12
test/config/test/cache.test.ts
··· 6 6 const project = resolve(__dirname, '../') 7 7 8 8 test('default', async () => { 9 - const { vitest, stdout, stderr } = await runVitest({ 9 + const { ctx, stdout, stderr } = await runVitest({ 10 10 root, 11 11 include: ['*.test.ts'], 12 12 }) ··· 14 14 expect(stdout).toContain('✓ basic.test.ts >') 15 15 expect(stderr).toBe('') 16 16 17 - const cachePath = vitest!.cache.results.getCachePath() 17 + const cachePath = ctx!.cache.results.getCachePath() 18 18 const path = resolve(project, 'node_modules/.vite/vitest/results.json') 19 19 expect(cachePath).toMatch(path) 20 20 }) 21 21 22 22 test('use cache.dir', async () => { 23 - const { vitest, stdout, stderr } = await runVitest( 23 + const { ctx, stdout, stderr } = await runVitest( 24 24 { 25 25 root, 26 26 include: ['*.test.ts'], ··· 33 33 expect(stdout).toContain('✓ basic.test.ts >') 34 34 expect(stderr).toContain('"cache.dir" is deprecated') 35 35 36 - const cachePath = vitest!.cache.results.getCachePath() 36 + const cachePath = ctx!.cache.results.getCachePath() 37 37 const path = resolve(root, 'node_modules/.vitest-custom/results.json') 38 38 expect(cachePath).toMatch(path) 39 39 }) 40 40 41 41 test('use cacheDir', async () => { 42 - const { vitest, stdout, stderr } = await runVitest( 42 + const { ctx, stdout, stderr } = await runVitest( 43 43 { 44 44 root, 45 45 include: ['*.test.ts'], ··· 52 52 expect(stdout).toContain('✓ basic.test.ts >') 53 53 expect(stderr).toBe('') 54 54 55 - const cachePath = vitest!.cache.results.getCachePath() 55 + const cachePath = ctx!.cache.results.getCachePath() 56 56 const path = resolve(root, 'node_modules/.vite-custom/vitest/results.json') 57 57 expect(cachePath).toMatch(path) 58 58 }) ··· 67 67 } 68 68 69 69 test('default', async () => { 70 - const { vitest, stdout, stderr } = await runVitest({ 70 + const { ctx, stdout, stderr } = await runVitest({ 71 71 root, 72 72 include: ['*.test.ts'], 73 73 deps, ··· 76 76 expect(stdout).toContain('✓ basic.test.ts >') 77 77 expect(stderr).toBe('') 78 78 79 - const cachePath = vitest!.cache.results.getCachePath() 79 + const cachePath = ctx!.cache.results.getCachePath() 80 80 const path = resolve(project, 'node_modules/.vite/vitest/results.json') 81 81 expect(cachePath).toBe(path) 82 82 }) 83 83 84 84 test('use cache.dir', async () => { 85 - const { vitest, stdout, stderr } = await runVitest( 85 + const { ctx, stdout, stderr } = await runVitest( 86 86 { 87 87 root, 88 88 include: ['*.test.ts'], ··· 96 96 expect(stdout).toContain('✓ basic.test.ts >') 97 97 expect(stderr).toContain('"cache.dir" is deprecated') 98 98 99 - const cachePath = vitest!.cache.results.getCachePath() 99 + const cachePath = ctx!.cache.results.getCachePath() 100 100 const path = resolve(root, 'node_modules/.vitest-custom/results.json') 101 101 expect(cachePath).toBe(path) 102 102 }) 103 103 104 104 test('use cacheDir', async () => { 105 - const { vitest, stdout, stderr } = await runVitest( 105 + const { ctx, stdout, stderr } = await runVitest( 106 106 { 107 107 root, 108 108 include: ['*.test.ts'], ··· 116 116 expect(stdout).toContain('✓ basic.test.ts >') 117 117 expect(stderr).toBe('') 118 118 119 - const cachePath = vitest!.cache.results.getCachePath() 119 + const cachePath = ctx!.cache.results.getCachePath() 120 120 const path = resolve(root, 'node_modules/.vite-custom/vitest/results.json') 121 121 expect(cachePath).toBe(path) 122 122 })
+8 -3
test/config/test/console.test.ts
··· 1 - import { expect, test } from 'vitest' 1 + import { expect, test, vi } from 'vitest' 2 2 import { runVitest } from '../../test-utils' 3 3 4 4 test('default intercept', async () => { ··· 9 9 }) 10 10 11 11 test.each(['threads', 'vmThreads'] as const)(`disable intercept pool=%s`, async (pool) => { 12 - const { stderr } = await runVitest({ 12 + // `disableConsoleIntercept: true` forwards workers console.error to main thread's stderr 13 + const spy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true) 14 + 15 + await runVitest({ 13 16 root: './fixtures/console', 14 17 disableConsoleIntercept: true, 15 18 pool, 16 19 }) 17 - expect(stderr).toBe('__test_console__\n') 20 + 21 + const call = spy.mock.lastCall![0] 22 + expect(call.toString()).toBe('__test_console__\n') 18 23 })
+2 -2
test/config/test/flags.test.ts
··· 2 2 import { runVitest } from '../../test-utils' 3 3 4 4 it('correctly inherit from the cli', async () => { 5 - const { vitest } = await runVitest({ 5 + const { ctx } = await runVitest({ 6 6 root: 'fixtures/workspace-flags', 7 7 logHeapUsage: true, 8 8 allowOnly: true, ··· 18 18 passWithNoTests: true, 19 19 bail: 100, 20 20 }) 21 - const project = vitest!.projects[0] 21 + const project = ctx!.projects[0] 22 22 const config = project.getSerializableConfig() 23 23 expect(config).toMatchObject({ 24 24 logHeapUsage: true,
+17 -12
test/config/test/resolution.test.ts
··· 1 + import { Writable } from 'node:stream' 1 2 import type { UserConfig } from 'vitest' 2 3 import type { UserConfig as ViteUserConfig } from 'vite' 3 - import { describe, expect, it, onTestFinished, vi } from 'vitest' 4 + import { describe, expect, it } from 'vitest' 4 5 import { createVitest, parseCLI } from 'vitest/node' 5 6 import { extraInlineDeps } from 'vitest/config' 6 7 7 - async function vitest(cliOptions: UserConfig, configValue: UserConfig = {}, viteConfig: ViteUserConfig = {}) { 8 - const vitest = await createVitest('test', { ...cliOptions, watch: false }, { ...viteConfig, test: configValue as any }) 8 + type VitestOptions = Parameters<typeof createVitest>[3] 9 + 10 + async function vitest(cliOptions: UserConfig, configValue: UserConfig = {}, viteConfig: ViteUserConfig = {}, vitestOptions: VitestOptions = {}) { 11 + const vitest = await createVitest('test', { ...cliOptions, watch: false }, { ...viteConfig, test: configValue as any }, vitestOptions) 9 12 return vitest 10 13 } 11 14 12 - async function config(cliOptions: UserConfig, configValue: UserConfig = {}, viteConfig: ViteUserConfig = {}) { 13 - const v = await vitest(cliOptions, configValue, viteConfig) 15 + async function config(cliOptions: UserConfig, configValue: UserConfig = {}, viteConfig: ViteUserConfig = {}, vitestOptions: VitestOptions = {}) { 16 + const v = await vitest(cliOptions, configValue, viteConfig, vitestOptions) 14 17 return v.config 15 18 } 16 19 ··· 296 299 inspectFlagName, 297 300 url, 298 301 ]) 299 - const error = vi.fn() 300 - const originalError = console.error 301 - console.error = error 302 - onTestFinished(() => { 303 - console.error = originalError 302 + const errors: string[] = [] 303 + const stderr = new Writable({ 304 + write(chunk, _encoding, callback) { 305 + errors.push(chunk.toString()) 306 + callback() 307 + }, 304 308 }) 305 309 await expect(async () => { 306 - await config(rawConfig.options) 310 + await config(rawConfig.options, {}, {}, { stderr }) 307 311 }).rejects.toThrowError() 308 - expect(error.mock.lastCall[0]).toEqual( 312 + 313 + expect(errors[0]).toEqual( 309 314 expect.stringContaining(`Inspector host cannot be a URL. Use "host:port" instead of "${url}"`), 310 315 ) 311 316 })
+6 -6
test/config/test/shuffle-options.test.ts
··· 25 25 { files: false, tests: true }, 26 26 ], 27 27 )('should use BaseSequencer if shuffle is %o', async (shuffle) => { 28 - const { vitest } = await run({ shuffle }) 29 - expect(vitest?.config.sequence.sequencer.name).toBe('BaseSequencer') 28 + const { ctx } = await run({ shuffle }) 29 + expect(ctx?.config.sequence.sequencer.name).toBe('BaseSequencer') 30 30 }) 31 31 32 32 test.each([ ··· 34 34 { files: true, tests: false }, 35 35 { files: true, tests: true }, 36 36 ])('should use RandomSequencer if shuffle is %o', async (shuffle) => { 37 - const { vitest } = await run({ shuffle }) 38 - expect(vitest?.config.sequence.sequencer.name).toBe('RandomSequencer') 37 + const { ctx } = await run({ shuffle }) 38 + expect(ctx?.config.sequence.sequencer.name).toBe('RandomSequencer') 39 39 }) 40 40 41 41 test.each([ ··· 44 44 { files: true, tests: false }, 45 45 { files: true, tests: true }, 46 46 ])('should always use CustomSequencer if passed', async (shuffle) => { 47 - const { vitest } = await run({ shuffle, sequencer: CustomSequencer }) 48 - expect(vitest?.config.sequence.sequencer.name).toBe('CustomSequencer') 47 + const { ctx } = await run({ shuffle, sequencer: CustomSequencer }) 48 + expect(ctx?.config.sequence.sequencer.name).toBe('CustomSequencer') 49 49 })
+1 -1
test/core/test/cli-test.test.ts
··· 274 274 clearScreen: viteClearScreen, 275 275 } 276 276 const vitestConfig = getCLIOptions(vitestClearScreen) 277 - const config = resolveConfig('test', vitestConfig, viteConfig) 277 + const config = resolveConfig('test', vitestConfig, viteConfig, undefined as any) 278 278 return config.clearScreen 279 279 }) 280 280 expect(results).toMatchInlineSnapshot(`
+2 -2
test/inspect/test/inspect.test.ts
··· 8 8 type Message = Partial<InspectorNotification<any>> 9 9 10 10 test.skipIf(isWindows)('--inspect-brk stops at test file', async () => { 11 - const vitest = await runVitestCli('--root', 'fixtures', '--inspect-brk', '--no-file-parallelism') 11 + const { vitest, waitForClose } = await runVitestCli('--root', 'fixtures', '--inspect-brk', '--no-file-parallelism') 12 12 13 13 await vitest.waitForStderr('Debugger listening on ') 14 14 const url = vitest.stderr.split('\n')[0].replace('Debugger listening on ', '') ··· 36 36 send({ method: 'Debugger.resume' }) 37 37 38 38 await vitest.waitForStdout('Test Files 1 passed (1)') 39 - await vitest.isDone 39 + await waitForClose() 40 40 }) 41 41 42 42 async function createChannel(url: string) {
+2 -2
test/public-api/tests/runner.spec.ts
··· 19 19 const taskUpdate: TaskResultPack[] = [] 20 20 const finishedFiles: File[] = [] 21 21 const collectedFiles: File[] = [] 22 - const { vitest, stdout, stderr } = await runVitest({ 22 + const { ctx, stdout, stderr } = await runVitest({ 23 23 root: resolve(__dirname, '..', 'fixtures'), 24 24 include: ['**/*.spec.ts'], 25 25 reporters: [ ··· 50 50 expect(taskUpdate).toHaveLength(4) 51 51 expect(finishedFiles).toHaveLength(1) 52 52 53 - const files = vitest?.state.getFiles() || [] 53 + const files = ctx?.state.getFiles() || [] 54 54 expect(files).toHaveLength(1) 55 55 56 56 expect(taskUpdate).toContainEqual(
+7 -9
test/reporters/tests/default.test.ts
··· 1 1 import { describe, expect, test } from 'vitest' 2 - import { runVitest, runVitestCli } from '../../test-utils' 2 + import { runVitest } from '../../test-utils' 3 3 4 4 describe('default reporter', async () => { 5 5 test('normal', async () => { ··· 28 28 }) 29 29 30 30 test('rerun should undo', async () => { 31 - const vitest = await runVitestCli( 32 - '--root', 33 - 'fixtures/default', 34 - '--watch', 35 - '-t', 36 - 'passed', 37 - ) 38 - vitest.resetOutput() 31 + const { vitest } = await runVitest({ 32 + root: 'fixtures/default', 33 + watch: true, 34 + testNamePattern: 'passed', 35 + reporters: 'none', 36 + }) 39 37 40 38 // one file 41 39 vitest.write('p')
+4 -5
test/run/test/tty.test.ts
··· 2 2 import { runVitestCli } from '../../test-utils' 3 3 4 4 test('run mode does not get stuck when TTY', async () => { 5 - const vitest = await runVitestCli('--root', 'fixtures') 6 - await vitest.isDone 5 + const { vitest } = await runVitestCli('--root', 'fixtures') 7 6 8 - expect(vitest.stdout).toContain('✓ example.test.ts') 9 - expect(vitest.stdout).toContain('✓ math.test.ts') 10 - expect(vitest.stdout).toContain('2 passed') 7 + await vitest.waitForStdout('✓ example.test.ts') 8 + await vitest.waitForStdout('✓ math.test.ts') 9 + await vitest.waitForStdout('2 passed') 11 10 12 11 // Regression #3642 13 12 expect(vitest.stderr).not.toContain('close timed out')
+26 -38
test/typescript/test/runner.test.ts
··· 2 2 import fg from 'fast-glob' 3 3 import { describe, expect, it } from 'vitest' 4 4 5 - import { runVitest, runVitestCli } from '../../test-utils' 5 + import { runVitest } from '../../test-utils' 6 6 7 7 describe('should fail', async () => { 8 8 const root = resolve(__dirname, '../failing') ··· 17 17 allowJs: true, 18 18 include: ['**/*.test-d.*'], 19 19 }, 20 - }, []) 20 + }) 21 21 22 22 expect(stderr).toBeTruthy() 23 23 const lines = String(stderr).split(/\n/g) ··· 43 43 }) 44 44 45 45 it('typecheks with custom tsconfig', async () => { 46 - const { stderr } = await runVitestCli( 47 - { cwd: root, env: { ...process.env, CI: 'true' } }, 48 - '--run', 49 - '--dir', 50 - resolve(__dirname, '..', './failing'), 51 - '--config', 52 - resolve(__dirname, './vitest.custom.config.ts'), 53 - '--typecheck.enabled', 54 - ) 46 + const { stderr } = await runVitest({ 47 + root, 48 + dir: resolve(__dirname, '..', './failing'), 49 + config: resolve('./test/vitest.custom.config.ts'), 50 + typecheck: { enabled: true }, 51 + }) 55 52 56 - expect(stderr).toBeTruthy() 57 53 const lines = String(stderr).split(/\n/g) 58 54 const msg = lines 59 55 .filter(i => i.includes('TypeCheckError: ')) ··· 67 63 expect(msg).toMatchSnapshot() 68 64 69 65 expect(stderr).toContain('FAIL fail.test-d.ts') // included in tsconfig 70 - expect(stderr).toContain('FAIL only.test-d.ts') // .only 66 + 67 + // TODO: Why should this be picked as well? 68 + // expect(stderr).toContain('FAIL only.test-d.ts') // .only 71 69 72 70 // not included in tsconfig 73 71 expect(stderr).not.toContain('expect-error.test-d.ts') ··· 77 75 }) 78 76 79 77 it('typechecks empty "include" but with tests', async () => { 80 - const { stderr } = await runVitestCli( 81 - { 82 - cwd: root, 83 - env: { 84 - ...process.env, 85 - CI: 'true', 86 - NO_COLOR: 'true', 87 - }, 88 - }, 89 - '--run', 90 - '--dir', 91 - resolve(__dirname, '..', './failing'), 92 - '--config', 93 - resolve(__dirname, './vitest.empty.config.ts'), 94 - '--typecheck.enabled', 78 + const { stderr } = await runVitest({ 79 + root, 80 + dir: resolve(__dirname, '..', './failing'), 81 + config: resolve(__dirname, './vitest.empty.config.ts'), 82 + typecheck: { enabled: true }, 83 + }, 95 84 ) 96 85 97 86 expect(stderr.replace(resolve(__dirname, '..'), '<root>')).toMatchSnapshot() ··· 100 89 101 90 describe('ignoreSourceErrors', () => { 102 91 it('disabled', async () => { 103 - const vitest = await runVitestCli( 104 - { 105 - cwd: resolve(__dirname, '../fixtures/source-error'), 106 - }, 107 - '--run', 108 - ) 92 + const vitest = await runVitest({ 93 + root: resolve(__dirname, '../fixtures/source-error'), 94 + }) 109 95 expect(vitest.stdout).toContain('Unhandled Errors') 110 96 expect(vitest.stderr).toContain('Unhandled Source Error') 111 97 expect(vitest.stderr).toContain('TypeCheckError: Cannot find name \'thisIsSourceError\'') 112 98 }) 113 99 114 100 it('enabled', async () => { 115 - const vitest = await runVitestCli( 101 + const vitest = await runVitest( 116 102 { 117 - cwd: resolve(__dirname, '../fixtures/source-error'), 103 + root: resolve(__dirname, '../fixtures/source-error'), 104 + typecheck: { 105 + ignoreSourceErrors: true, 106 + enabled: true, 107 + }, 118 108 }, 119 - '--run', 120 - '--typecheck.ignoreSourceErrors', 121 109 ) 122 110 expect(vitest.stdout).not.toContain('Unhandled Errors') 123 111 expect(vitest.stderr).not.toContain('Unhandled Source Error')
+3 -3
test/vite-node/test/cli.test.ts
··· 41 41 42 42 it.each(['index.js', 'index.cjs', 'index.mjs'])('correctly runs --watch %s', async (file) => { 43 43 const entryPath = resolve(__dirname, '../src/watch', file) 44 - const cli = await runViteNodeCli('--watch', entryPath) 45 - await cli.waitForStdout('test 1') 44 + const { viteNode } = await runViteNodeCli('--watch', entryPath) 45 + await viteNode.waitForStdout('test 1') 46 46 editFile(entryPath, c => c.replace('test 1', 'test 2')) 47 - await cli.waitForStdout('test 2') 47 + await viteNode.waitForStdout('test 2') 48 48 })
+1 -1
test/vite-node/test/hmr.test.ts
··· 5 5 test('hmr.accept works correctly', async () => { 6 6 const scriptFile = resolve(__dirname, '../src/hmr-script.js') 7 7 8 - const viteNode = await runViteNodeCli('--watch', scriptFile) 8 + const { viteNode } = await runViteNodeCli('--watch', scriptFile) 9 9 10 10 await viteNode.waitForStderr('Hello!') 11 11
+4 -4
test/vite-node/test/self-export.test.ts
··· 11 11 12 12 it('example 1', async () => { 13 13 const entryPath = resolve(__dirname, '../src/self-export-example1.ts') 14 - const cli = await runViteNodeCli(entryPath) 15 - await cli.waitForStdout('Function') 14 + const { viteNode } = await runViteNodeCli(entryPath) 15 + await viteNode.waitForStdout('Function') 16 16 }, 60_000) 17 17 18 18 it('example 2', async () => { 19 19 const entryPath = resolve(__dirname, '../src/self-export-example2.ts') 20 - const cli = await runViteNodeCli(entryPath) 21 - await cli.waitForStdout('HelloWorld: 1') 20 + const { viteNode } = await runViteNodeCli(entryPath) 21 + await viteNode.waitForStdout('HelloWorld: 1') 22 22 }, 60_000)
+24 -27
test/watch/test/file-watching.test.ts
··· 15 15 const forceTriggerFile = 'fixtures/force-watch/trigger.js' 16 16 const forceTriggerFileContent = readFileSync(forceTriggerFile, 'utf-8') 17 17 18 - const cliArgs = ['--root', 'fixtures', '--watch'] 18 + const options = { root: 'fixtures', watch: true } 19 19 const cleanups: (() => void)[] = [] 20 - 21 - async function runVitestCli(...args: string[]) { 22 - const vitest = await testUtils.runVitestCli(...args) 23 - if (args.includes('--watch')) 24 - vitest.resetOutput() 25 - return vitest 26 - } 27 20 28 21 function editFile(fileContent: string) { 29 22 return `// Modified by file-watching.test.ts ··· 45 38 test.only('skip tests on CI', () => {}) 46 39 47 40 test('editing source file triggers re-run', async () => { 48 - const vitest = await runVitestCli(...cliArgs) 41 + const { vitest } = await testUtils.runVitest(options) 49 42 50 43 writeFileSync(sourceFile, editFile(sourceFileContent), 'utf8') 51 44 ··· 55 48 }) 56 49 57 50 test('editing file that was imported with a query reruns suite', async () => { 58 - const vitest = await runVitestCli(...cliArgs) 51 + const { vitest } = await testUtils.runVitest(options) 59 52 60 53 testUtils.editFile( 61 54 testUtils.resolvePath(import.meta.url, '../fixtures/42.txt'), ··· 67 60 }) 68 61 69 62 test('editing force rerun trigger reruns all tests', async () => { 70 - const vitest = await runVitestCli(...cliArgs) 63 + const { vitest } = await testUtils.runVitest(options) 71 64 72 65 writeFileSync(forceTriggerFile, editFile(forceTriggerFileContent), 'utf8') 73 66 ··· 79 72 }) 80 73 81 74 test('editing test file triggers re-run', async () => { 82 - const vitest = await runVitestCli(...cliArgs) 75 + const { vitest } = await testUtils.runVitest(options) 83 76 84 77 writeFileSync(testFile, editFile(testFileContent), 'utf8') 85 78 ··· 89 82 }) 90 83 91 84 test('editing config file triggers re-run', async () => { 92 - const vitest = await runVitestCli(...cliArgs) 85 + const { vitest } = await testUtils.runVitest(options) 93 86 94 87 writeFileSync(configFile, editFile(configFileContent), 'utf8') 95 88 96 - await vitest.waitForStdout('New code running') 97 89 await vitest.waitForStdout('Restarting due to config changes') 98 90 await vitest.waitForStdout('2 passed') 99 91 }) 100 92 101 93 test('editing config file reloads new changes', async () => { 102 - const vitest = await runVitestCli(...cliArgs) 94 + const { vitest } = await testUtils.runVitest({ ...options, reporters: 'none' }) 103 95 104 96 writeFileSync(configFile, configFileContent.replace('reporters: \'verbose\'', 'reporters: \'tap\''), 'utf8') 105 97 ··· 108 100 }) 109 101 110 102 test('adding a new test file triggers re-run', async () => { 111 - const vitest = await runVitestCli(...cliArgs) 103 + const { vitest } = await testUtils.runVitest(options) 112 104 113 105 const testFile = 'fixtures/new-dynamic.test.ts' 114 106 const testFileContent = ` ··· 135 127 // Test report should not be present before test run 136 128 expect(existsSync(report)).toBe(false) 137 129 138 - const vitest = await runVitestCli( 139 - ...cliArgs, 140 - '--reporter', 141 - 'verbose', 142 - '--reporter', 143 - 'junit', 144 - '--output-file', 145 - 'test-results/junit.xml', 130 + const { vitest } = await testUtils.runVitest({ 131 + ...options, 132 + reporters: ['verbose', 'junit'], 133 + outputFile: './test-results/junit.xml', 134 + }, 146 135 ) 147 136 148 137 // Test report should be generated on initial test run ··· 152 141 rmSync(report) 153 142 expect(existsSync(report)).toBe(false) 154 143 144 + vitest.resetOutput() 155 145 writeFileSync(sourceFile, editFile(sourceFileContent), 'utf8') 156 146 157 147 await vitest.waitForStdout('JUNIT report written') ··· 160 150 }) 161 151 162 152 describe('browser', () => { 163 - test.runIf((process.platform !== 'win32'))('editing source file triggers re-run', async () => { 164 - const vitest = await runVitestCli(...cliArgs, '--browser.enabled', '--browser.headless', '--browser.name=chrome') 153 + test.runIf((process.platform !== 'win32'))('editing source file triggers re-run', { retry: 3 }, async () => { 154 + const { vitest } = await testUtils.runVitest({ 155 + ...options, 156 + browser: { 157 + name: 'chrome', 158 + enabled: true, 159 + headless: true, 160 + }, 161 + }) 165 162 166 163 writeFileSync(sourceFile, editFile(sourceFileContent), 'utf8') 167 164 ··· 170 167 await vitest.waitForStdout('1 passed') 171 168 172 169 vitest.write('q') 173 - }, { retry: 3 }) 170 + }) 174 171 })
+6 -4
test/watch/test/related.test.ts
··· 1 1 import { test } from 'vitest' 2 2 import { resolve } from 'pathe' 3 - import { editFile, runVitestCli } from '../../test-utils' 4 - 5 - const cliArgs = ['--root', 'fixtures', '--watch', '--changed'] 3 + import { editFile, runVitest } from '../../test-utils' 6 4 7 5 test('when nothing is changed, run nothing but keep watching', async () => { 8 - const vitest = await runVitestCli(...cliArgs) 6 + const { vitest } = await runVitest({ 7 + root: 'fixtures', 8 + watch: true, 9 + changed: true, 10 + }) 9 11 10 12 await vitest.waitForStdout('No affected test files found') 11 13 await vitest.waitForStdout('Waiting for file changes...')
+12 -27
test/watch/test/stdin.test.ts
··· 1 1 import { rmSync, writeFileSync } from 'node:fs' 2 - import { afterEach, expect, test } from 'vitest' 2 + import { expect, onTestFinished, test } from 'vitest' 3 3 4 - import * as testUtils from '../../test-utils' 4 + import { runVitest } from '../../test-utils' 5 5 6 - async function runVitestCli(...args: string[]) { 7 - const vitest = await testUtils.runVitestCli(...args) 8 - if (args.includes('--watch')) 9 - vitest.resetOutput() 10 - return vitest 11 - } 12 - 13 - const cliArgs = ['--root', 'fixtures', '--watch'] 14 - const cleanups: (() => void)[] = [] 15 - 16 - afterEach(() => { 17 - cleanups.splice(0).forEach(fn => fn()) 18 - }) 19 - 20 - // TODO: Fix flakiness and enable on CI 21 - if (process.env.GITHUB_ACTIONS) 22 - test.only('skip tests on CI', () => {}) 6 + const options = { root: 'fixtures', watch: true } 23 7 24 8 test('quit watch mode', async () => { 25 - const vitest = await runVitestCli(...cliArgs) 9 + const { vitest, waitForClose } = await runVitest(options) 26 10 27 11 vitest.write('q') 28 12 29 - await vitest.isDone 13 + await waitForClose() 30 14 }) 31 15 32 16 test('rerun current pattern tests', async () => { 33 - const vitest = await runVitestCli(...cliArgs, '-t', 'sum') 17 + const { vitest } = await runVitest({ ...options, testNamePattern: 'sum' }) 34 18 35 19 vitest.write('r') 36 20 21 + await vitest.waitForStdout('RERUN') 37 22 await vitest.waitForStdout('Test name pattern: /sum/') 38 23 await vitest.waitForStdout('1 passed') 39 24 }) 40 25 41 26 test('filter by filename', async () => { 42 - const vitest = await runVitestCli(...cliArgs) 27 + const { vitest } = await runVitest(options) 43 28 44 29 vitest.write('p') 45 30 ··· 57 42 }) 58 43 59 44 test('filter by test name', async () => { 60 - const vitest = await runVitestCli(...cliArgs) 45 + const { vitest } = await runVitest(options) 61 46 62 47 vitest.write('t') 63 48 ··· 73 58 await vitest.waitForStdout('1 passed') 74 59 }) 75 60 76 - test('cancel test run', async () => { 77 - const vitest = await runVitestCli(...cliArgs) 61 + test.skipIf(process.env.GITHUB_ACTIONS)('cancel test run', async () => { 62 + const { vitest } = await runVitest(options) 78 63 79 64 const testPath = 'fixtures/cancel.test.ts' 80 65 const testCase = `// Dynamic test case ··· 95 80 }) 96 81 ` 97 82 98 - cleanups.push(() => rmSync(testPath)) 83 + onTestFinished(() => rmSync(testPath)) 99 84 writeFileSync(testPath, testCase, 'utf8') 100 85 101 86 // Test case is running, cancel it
+3 -4
test/watch/test/stdout.test.ts
··· 1 1 import { readFileSync, writeFileSync } from 'node:fs' 2 2 import { afterEach, test } from 'vitest' 3 - 4 - import { runVitestCli } from '../../test-utils' 3 + import { runVitest } from '../../test-utils' 5 4 6 5 const testFile = 'fixtures/math.test.ts' 7 6 const testFileContent = readFileSync(testFile, 'utf-8') ··· 11 10 }) 12 11 13 12 test('console.log is visible on test re-run', async () => { 14 - const vitest = await runVitestCli('--root', 'fixtures', '--watch') 15 - vitest.resetOutput() 13 + const { vitest } = await runVitest({ root: 'fixtures', watch: true }) 14 + 16 15 const testCase = ` 17 16 test('test with logging', () => { 18 17 console.log('First')
+1 -1
test/watch/test/workspaces.test.ts
··· 27 27 ` 28 28 29 29 async function startVitest() { 30 - const vitest = await runVitestCli( 30 + const { vitest } = await runVitestCli( 31 31 { cwd: root, env: { TEST_WATCH: 'true' } }, 32 32 '--root', 33 33 root,
+7 -5
packages/vitest/src/node/config.ts
··· 12 12 import { RandomSequencer } from './sequencers/RandomSequencer' 13 13 import type { BenchmarkBuiltinReporters } from './reporters' 14 14 import { builtinPools } from './pool' 15 + import type { Logger } from './logger' 15 16 16 17 function resolvePath(path: string, root: string) { 17 18 return normalize( ··· 76 77 mode: VitestRunMode, 77 78 options: UserConfig, 78 79 viteConfig: ResolvedViteConfig, 80 + logger: Logger, 79 81 ): ResolvedConfig { 80 82 if (options.dom) { 81 83 if ( 82 84 viteConfig.test?.environment != null 83 85 && viteConfig.test!.environment !== 'happy-dom' 84 86 ) { 85 - console.warn( 87 + logger.console.warn( 86 88 c.yellow( 87 89 `${c.inverse(c.yellow(' Vitest '))} Your config.test.environment ("${ 88 90 viteConfig.test.environment ··· 209 211 return 210 212 211 213 if (option === 'fallbackCJS') { 212 - console.warn(c.yellow(`${c.inverse(c.yellow(' Vitest '))} "deps.${option}" is deprecated. Use "server.deps.${option}" instead`)) 214 + logger.console.warn(c.yellow(`${c.inverse(c.yellow(' Vitest '))} "deps.${option}" is deprecated. Use "server.deps.${option}" instead`)) 213 215 } 214 216 else { 215 217 const transformMode = resolved.environment === 'happy-dom' || resolved.environment === 'jsdom' ? 'web' : 'ssr' 216 - console.warn( 218 + logger.console.warn( 217 219 c.yellow( 218 220 `${c.inverse(c.yellow(' Vitest '))} "deps.${option}" is deprecated. If you rely on vite-node directly, use "server.deps.${option}" instead. Otherwise, consider using "deps.optimizer.${transformMode}.${option === 'external' ? 'exclude' : 'include'}"`, 219 221 ), ··· 475 477 let cacheDir = VitestCache.resolveCacheDir('', resolve(viteConfig.cacheDir, 'vitest'), resolved.name) 476 478 477 479 if (resolved.cache && resolved.cache.dir) { 478 - console.warn( 480 + logger.console.warn( 479 481 c.yellow( 480 482 `${c.inverse(c.yellow(' Vitest '))} "cache.dir" is deprecated, use Vite's "cacheDir" instead if you want to change the cache director. Note caches will be written to "cacheDir\/vitest"`, 481 483 ), ··· 514 516 resolved.typecheck.enabled ??= false 515 517 516 518 if (resolved.typecheck.enabled) 517 - console.warn(c.yellow('Testing types with tsc and vue-tsc is an experimental feature.\nBreaking changes might not follow SemVer, please pin Vitest\'s version when using it.')) 519 + logger.console.warn(c.yellow('Testing types with tsc and vue-tsc is an experimental feature.\nBreaking changes might not follow SemVer, please pin Vitest\'s version when using it.')) 518 520 519 521 resolved.browser ??= {} as any 520 522 resolved.browser.enabled ??= false
+6 -2
packages/vitest/src/node/core.ts
··· 1 1 import { existsSync, promises as fs } from 'node:fs' 2 + import type { Writable } from 'node:stream' 2 3 import { isMainThread } from 'node:worker_threads' 3 4 import type { ViteDevServer } from 'vite' 4 5 import { mergeConfig } from 'vite' ··· 31 32 32 33 export interface VitestOptions { 33 34 packageInstaller?: VitestPackageInstaller 35 + stdin?: NodeJS.ReadStream 36 + stdout?: NodeJS.WriteStream | Writable 37 + stderr?: NodeJS.WriteStream | Writable 34 38 } 35 39 36 40 export class Vitest { ··· 74 78 public readonly mode: VitestRunMode, 75 79 options: VitestOptions = {}, 76 80 ) { 77 - this.logger = new Logger(this) 81 + this.logger = new Logger(this, options.stdout, options.stderr) 78 82 this.packageInstaller = options.packageInstaller || new VitestPackageInstaller() 79 83 } 80 84 ··· 93 97 this.runningPromise = undefined 94 98 this.projectsTestFiles.clear() 95 99 96 - const resolved = resolveConfig(this.mode, options, server.config) 100 + const resolved = resolveConfig(this.mode, options, server.config, this.logger) 97 101 98 102 this.server = server 99 103 this.config = resolved
+8 -4
packages/vitest/src/node/logger.ts
··· 1 + import { Console } from 'node:console' 2 + import type { Writable } from 'node:stream' 1 3 import { createLogUpdate } from 'log-update' 2 4 import c from 'picocolors' 3 5 import { version } from '../../../../package.json' ··· 24 26 const CLEAR_SCREEN = '\x1Bc' 25 27 26 28 export class Logger { 27 - outputStream = process.stdout 28 - errorStream = process.stderr 29 - logUpdate = createLogUpdate(process.stdout) 29 + logUpdate: ReturnType<typeof createLogUpdate> 30 30 31 31 private _clearScreenPending: string | undefined 32 32 private _highlights = new Map<string, string>() 33 + public console: Console 33 34 34 35 constructor( 35 36 public ctx: Vitest, 36 - public console = globalThis.console, 37 + public outputStream: NodeJS.WriteStream | Writable = process.stdout, 38 + public errorStream: NodeJS.WriteStream | Writable = process.stderr, 37 39 ) { 40 + this.console = new Console({ stdout: outputStream, stderr: errorStream }) 41 + this.logUpdate = createLogUpdate(this.outputStream) 38 42 this._highlights.clear() 39 43 } 40 44
+12 -11
packages/vitest/src/node/stdin.ts
··· 1 1 import readline from 'node:readline' 2 + import type { Writable } from 'node:stream' 2 3 import c from 'picocolors' 3 4 import prompt from 'prompts' 4 5 import { relative } from 'pathe' ··· 28 29 ) 29 30 } 30 31 31 - export function registerConsoleShortcuts(ctx: Vitest) { 32 + export function registerConsoleShortcuts(ctx: Vitest, stdin: NodeJS.ReadStream = process.stdin, stdout: NodeJS.WriteStream | Writable) { 32 33 let latestFilename = '' 33 34 34 35 async function _keypressHandler(str: string, key: any) { ··· 97 98 98 99 async function inputNamePattern() { 99 100 off() 100 - const watchFilter = new WatchFilter('Input test name pattern (RegExp)') 101 + const watchFilter = new WatchFilter('Input test name pattern (RegExp)', stdin, stdout) 101 102 const filter = await watchFilter.filter((str: string) => { 102 103 const files = ctx.state.getFiles() 103 104 const tests = getTests(files) ··· 130 131 async function inputFilePattern() { 131 132 off() 132 133 133 - const watchFilter = new WatchFilter('Input filename pattern') 134 + const watchFilter = new WatchFilter('Input filename pattern', stdin, stdout) 134 135 135 136 const filter = await watchFilter.filter(async (str: string) => { 136 137 const files = await ctx.globTestFiles([str]) ··· 149 150 let rl: readline.Interface | undefined 150 151 function on() { 151 152 off() 152 - rl = readline.createInterface({ input: process.stdin, escapeCodeTimeout: 50 }) 153 - readline.emitKeypressEvents(process.stdin, rl) 154 - if (process.stdin.isTTY) 155 - process.stdin.setRawMode(true) 156 - process.stdin.on('keypress', keypressHandler) 153 + rl = readline.createInterface({ input: stdin, escapeCodeTimeout: 50 }) 154 + readline.emitKeypressEvents(stdin, rl) 155 + if (stdin.isTTY) 156 + stdin.setRawMode(true) 157 + stdin.on('keypress', keypressHandler) 157 158 } 158 159 159 160 function off() { 160 161 rl?.close() 161 162 rl = undefined 162 - process.stdin.removeListener('keypress', keypressHandler) 163 - if (process.stdin.isTTY) 164 - process.stdin.setRawMode(false) 163 + stdin.removeListener('keypress', keypressHandler) 164 + if (stdin.isTTY) 165 + stdin.setRawMode(false) 165 166 } 166 167 167 168 on()
+34 -20
packages/vitest/src/node/watch-filter.ts
··· 1 1 import readline from 'node:readline' 2 + import type { Writable } from 'node:stream' 2 3 import c from 'picocolors' 3 4 import stripAnsi from 'strip-ansi' 4 5 import { createDefer } from '@vitest/utils' 5 - import { stdout } from '../utils' 6 + import { stdout as getStdout } from '../utils' 6 7 7 8 const MAX_RESULT_COUNT = 10 8 9 const SELECTION_MAX_INDEX = 7 ··· 17 18 private results: string[] = [] 18 19 private selectionIndex = -1 19 20 private onKeyPress?: (str: string, key: any) => void 21 + private stdin: NodeJS.ReadStream 22 + private stdout: NodeJS.WriteStream | Writable 20 23 21 - constructor(message: string) { 24 + constructor(message: string, stdin: NodeJS.ReadStream = process.stdin, stdout: NodeJS.WriteStream | Writable = getStdout()) { 22 25 this.message = message 23 - this.filterRL = readline.createInterface({ input: process.stdin, escapeCodeTimeout: 50 }) 24 - readline.emitKeypressEvents(process.stdin, this.filterRL) 25 - if (process.stdin.isTTY) 26 - process.stdin.setRawMode(true) 26 + this.stdin = stdin 27 + this.stdout = stdout 28 + 29 + this.filterRL = readline.createInterface({ input: this.stdin, escapeCodeTimeout: 50 }) 30 + readline.emitKeypressEvents(this.stdin, this.filterRL) 31 + if (this.stdin.isTTY) 32 + this.stdin.setRawMode(true) 27 33 } 28 34 29 35 public async filter(filterFunc: FilterFunc): Promise<string | undefined> { 30 - stdout().write(this.promptLine()) 36 + this.write(this.promptLine()) 31 37 32 38 const resultPromise = createDefer<string | undefined>() 33 39 34 40 this.onKeyPress = this.filterHandler(filterFunc, (result) => { 35 41 resultPromise.resolve(result) 36 42 }) 37 - process.stdin.on('keypress', this.onKeyPress) 43 + this.stdin.on('keypress', this.onKeyPress) 38 44 try { 39 45 return await resultPromise 40 46 } ··· 138 144 private eraseAndPrint(str: string) { 139 145 let rows = 0 140 146 const lines = str.split(/\r?\n/) 141 - for (const line of lines) 142 - // We have to take care of screen width in case of long lines 143 - rows += 1 + Math.floor(Math.max(stripAnsi(line).length - 1, 0) / stdout().columns) 147 + for (const line of lines) { 148 + const columns = 'columns' in this.stdout ? this.stdout.columns : 80 144 149 145 - stdout().write(`${ESC}1G`) // move to the beginning of the line 146 - stdout().write(`${ESC}J`) // erase down 147 - stdout().write(str) 148 - stdout().write(`${ESC}${rows - 1}A`) // moving up lines 150 + // We have to take care of screen width in case of long lines 151 + rows += 1 + Math.floor(Math.max(stripAnsi(line).length - 1, 0) / columns) 152 + } 153 + 154 + this.write(`${ESC}1G`) // move to the beginning of the line 155 + this.write(`${ESC}J`) // erase down 156 + this.write(str) 157 + this.write(`${ESC}${rows - 1}A`) // moving up lines 149 158 } 150 159 151 160 private close() { 152 161 this.filterRL.close() 153 162 if (this.onKeyPress) 154 - process.stdin.removeListener('keypress', this.onKeyPress) 163 + this.stdin.removeListener('keypress', this.onKeyPress) 155 164 156 - if (process.stdin.isTTY) 157 - process.stdin.setRawMode(false) 165 + if (this.stdin.isTTY) 166 + this.stdin.setRawMode(false) 158 167 } 159 168 160 169 private restoreCursor() { 161 170 const cursortPos = this.keywordOffset() + (this.currentKeyword?.length || 0) 162 - stdout().write(`${ESC}${cursortPos}G`) 171 + this.write(`${ESC}${cursortPos}G`) 163 172 } 164 173 165 174 private cancel() { 166 - stdout().write(`${ESC}J`) // erase down 175 + this.write(`${ESC}J`) // erase down 176 + } 177 + 178 + private write(data: string) { 179 + // @ts-expect-error -- write() method has different signature on the union type 180 + this.stdout.write(data) 167 181 } 168 182 }
+1
packages/vitest/src/node/workspace.ts
··· 308 308 coverage: this.ctx.config.coverage, 309 309 }, 310 310 server.config, 311 + this.ctx.logger, 311 312 ) 312 313 313 314 this.server = server
+4 -4
test/run/pool-custom-fixtures/pool/custom-pool.ts
··· 9 9 return { 10 10 name: 'custom', 11 11 async runTests(specs) { 12 - console.warn('[pool] printing:', options.print) 13 - console.warn('[pool] array option', options.array) 12 + ctx.logger.console.warn('[pool] printing:', options.print) 13 + ctx.logger.console.warn('[pool] array option', options.array) 14 14 for await (const [project, file] of specs) { 15 15 ctx.state.clearFiles(project) 16 16 const methods = createMethodsRPC(project) 17 - console.warn('[pool] running tests for', project.getName(), 'in', normalize(file).toLowerCase().replace(normalize(process.cwd()).toLowerCase(), '')) 17 + ctx.logger.console.warn('[pool] running tests for', project.getName(), 'in', normalize(file).toLowerCase().replace(normalize(process.cwd()).toLowerCase(), '')) 18 18 const path = relative(project.config.root, file) 19 19 const taskFile: File = { 20 20 id: `${path}${project.getName()}`, ··· 47 47 } 48 48 }, 49 49 close() { 50 - console.warn('[pool] custom pool is closed!') 50 + ctx.logger.console.warn('[pool] custom pool is closed!') 51 51 }, 52 52 } 53 53 }
+4 -2
packages/vitest/src/node/cli/cli-api.ts
··· 73 73 return ctx 74 74 } 75 75 76 + const stdin = vitestOptions?.stdin || process.stdin 77 + const stdout = vitestOptions?.stdout || process.stdout 76 78 let stdinCleanup 77 - if (process.stdin.isTTY && ctx.config.watch) 78 - stdinCleanup = registerConsoleShortcuts(ctx) 79 + if (stdin.isTTY && ctx.config.watch) 80 + stdinCleanup = registerConsoleShortcuts(ctx, stdin, stdout) 79 81 80 82 ctx.onServerRestart((reason) => { 81 83 ctx.report('onServerRestart', reason)
+5 -1
packages/vitest/src/node/reporters/base.ts
··· 191 191 return 192 192 const task = log.taskId ? this.ctx.state.idMap.get(log.taskId) : undefined 193 193 const header = c.gray(log.type + c.dim(` | ${task ? getFullName(task, c.dim(' > ')) : log.taskId !== UNKNOWN_TEST_ID ? log.taskId : 'unknown test'}`)) 194 - process[log.type].write(`${header}\n${log.content}\n`) 194 + 195 + const output = log.type === 'stdout' ? this.ctx.logger.outputStream : this.ctx.logger.errorStream 196 + 197 + // @ts-expect-error -- write() method has different signature on the union type 198 + output.write(`${header}\n${log.content}\n`) 195 199 } 196 200 197 201 shouldLog(log: UserConsoleLog) {
+1 -2
packages/vitest/src/node/reporters/github-actions.ts
··· 1 - import { Console } from 'node:console' 2 1 import { Writable } from 'node:stream' 3 2 import { getTasks } from '@vitest/runner/utils' 4 3 import stripAnsi from 'strip-ansi' ··· 76 75 }) 77 76 const result = await printError(error, project, { 78 77 showCodeFrame: false, 79 - logger: new Logger(ctx, new Console(writable, writable)), 78 + logger: new Logger(ctx, writable, writable), 80 79 }) 81 80 return { nearest: result?.nearest, output } 82 81 }
+3 -2
packages/vitest/src/node/reporters/renderers/dotRenderer.ts
··· 92 92 let timer: any 93 93 94 94 const { logUpdate: log, outputStream } = options.logger 95 + const columns = 'columns' in outputStream ? outputStream.columns : 80 95 96 96 97 function update() { 97 - log(render(tasks, outputStream.columns)) 98 + log(render(tasks, columns)) 98 99 } 99 100 100 101 return { ··· 114 115 timer = undefined 115 116 } 116 117 log.clear() 117 - options.logger.log(render(tasks, outputStream.columns)) 118 + options.logger.log(render(tasks, columns)) 118 119 return this 119 120 }, 120 121 clear() {