diff --git a/eslint.config.js b/eslint.config.js index 8bab15c5cb95..394db5c50f86 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -13,8 +13,10 @@ export default antfu( '**/assets/**', '**/*.timestamp-*', 'test/core/src/self', + 'test/cache/cache/.vitest-base/results.json', 'test/wasm-modules/src/wasm-bindgen-no-cyclic', 'test/workspaces/results.json', + 'test/workspaces-browser/results.json', 'test/reporters/fixtures/with-syntax-error.test.js', 'test/network-imports/public/slash@3.0.0.js', 'test/coverage-test/src/transpiled.js', diff --git a/packages/vitest/rollup.config.js b/packages/vitest/rollup.config.js index c5542c49a153..8030ae847fd8 100644 --- a/packages/vitest/rollup.config.js +++ b/packages/vitest/rollup.config.js @@ -65,6 +65,7 @@ const external = [ 'worker_threads', 'node:worker_threads', 'node:fs', + 'node:stream', 'node:vm', 'inspector', 'vite-node/source-map', diff --git a/packages/vitest/src/node/cli/cli-api.ts b/packages/vitest/src/node/cli/cli-api.ts index 991f54d59f44..739321a30b9b 100644 --- a/packages/vitest/src/node/cli/cli-api.ts +++ b/packages/vitest/src/node/cli/cli-api.ts @@ -73,9 +73,11 @@ export async function startVitest( return ctx } + const stdin = vitestOptions?.stdin || process.stdin + const stdout = vitestOptions?.stdout || process.stdout let stdinCleanup - if (process.stdin.isTTY && ctx.config.watch) - stdinCleanup = registerConsoleShortcuts(ctx) + if (stdin.isTTY && ctx.config.watch) + stdinCleanup = registerConsoleShortcuts(ctx, stdin, stdout) ctx.onServerRestart((reason) => { ctx.report('onServerRestart', reason) diff --git a/packages/vitest/src/node/config.ts b/packages/vitest/src/node/config.ts index 6ca6a4139fbb..167b90e5073c 100644 --- a/packages/vitest/src/node/config.ts +++ b/packages/vitest/src/node/config.ts @@ -12,6 +12,7 @@ import { BaseSequencer } from './sequencers/BaseSequencer' import { RandomSequencer } from './sequencers/RandomSequencer' import type { BenchmarkBuiltinReporters } from './reporters' import { builtinPools } from './pool' +import type { Logger } from './logger' function resolvePath(path: string, root: string) { return normalize( @@ -76,13 +77,14 @@ export function resolveConfig( mode: VitestRunMode, options: UserConfig, viteConfig: ResolvedViteConfig, + logger: Logger, ): ResolvedConfig { if (options.dom) { if ( viteConfig.test?.environment != null && viteConfig.test!.environment !== 'happy-dom' ) { - console.warn( + logger.console.warn( c.yellow( `${c.inverse(c.yellow(' Vitest '))} Your config.test.environment ("${ viteConfig.test.environment @@ -209,11 +211,11 @@ export function resolveConfig( return if (option === 'fallbackCJS') { - console.warn(c.yellow(`${c.inverse(c.yellow(' Vitest '))} "deps.${option}" is deprecated. Use "server.deps.${option}" instead`)) + logger.console.warn(c.yellow(`${c.inverse(c.yellow(' Vitest '))} "deps.${option}" is deprecated. Use "server.deps.${option}" instead`)) } else { const transformMode = resolved.environment === 'happy-dom' || resolved.environment === 'jsdom' ? 'web' : 'ssr' - console.warn( + logger.console.warn( c.yellow( `${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'}"`, ), @@ -475,7 +477,7 @@ export function resolveConfig( let cacheDir = VitestCache.resolveCacheDir('', resolve(viteConfig.cacheDir, 'vitest'), resolved.name) if (resolved.cache && resolved.cache.dir) { - console.warn( + logger.console.warn( c.yellow( `${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"`, ), @@ -514,7 +516,7 @@ export function resolveConfig( resolved.typecheck.enabled ??= false if (resolved.typecheck.enabled) - 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.')) + 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.')) resolved.browser ??= {} as any resolved.browser.enabled ??= false diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index 46f9e0f80d97..4cfed6786c2f 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -1,4 +1,5 @@ import { existsSync, promises as fs } from 'node:fs' +import type { Writable } from 'node:stream' import { isMainThread } from 'node:worker_threads' import type { ViteDevServer } from 'vite' import { mergeConfig } from 'vite' @@ -31,6 +32,9 @@ const WATCHER_DEBOUNCE = 100 export interface VitestOptions { packageInstaller?: VitestPackageInstaller + stdin?: NodeJS.ReadStream + stdout?: NodeJS.WriteStream | Writable + stderr?: NodeJS.WriteStream | Writable } export class Vitest { @@ -74,7 +78,7 @@ export class Vitest { public readonly mode: VitestRunMode, options: VitestOptions = {}, ) { - this.logger = new Logger(this) + this.logger = new Logger(this, options.stdout, options.stderr) this.packageInstaller = options.packageInstaller || new VitestPackageInstaller() } @@ -93,7 +97,7 @@ export class Vitest { this.runningPromise = undefined this.projectsTestFiles.clear() - const resolved = resolveConfig(this.mode, options, server.config) + const resolved = resolveConfig(this.mode, options, server.config, this.logger) this.server = server this.config = resolved diff --git a/packages/vitest/src/node/logger.ts b/packages/vitest/src/node/logger.ts index a494c9b619ee..130794c4d7d5 100644 --- a/packages/vitest/src/node/logger.ts +++ b/packages/vitest/src/node/logger.ts @@ -1,3 +1,5 @@ +import { Console } from 'node:console' +import type { Writable } from 'node:stream' import { createLogUpdate } from 'log-update' import c from 'picocolors' import { version } from '../../../../package.json' @@ -24,17 +26,19 @@ const CURSOR_TO_START = `${ESC}1;1H` const CLEAR_SCREEN = '\x1Bc' export class Logger { - outputStream = process.stdout - errorStream = process.stderr - logUpdate = createLogUpdate(process.stdout) + logUpdate: ReturnType private _clearScreenPending: string | undefined private _highlights = new Map() + public console: Console constructor( public ctx: Vitest, - public console = globalThis.console, + public outputStream: NodeJS.WriteStream | Writable = process.stdout, + public errorStream: NodeJS.WriteStream | Writable = process.stderr, ) { + this.console = new Console({ stdout: outputStream, stderr: errorStream }) + this.logUpdate = createLogUpdate(this.outputStream) this._highlights.clear() } diff --git a/packages/vitest/src/node/reporters/base.ts b/packages/vitest/src/node/reporters/base.ts index f1b149b76378..7f791108b723 100644 --- a/packages/vitest/src/node/reporters/base.ts +++ b/packages/vitest/src/node/reporters/base.ts @@ -191,7 +191,11 @@ export abstract class BaseReporter implements Reporter { return const task = log.taskId ? this.ctx.state.idMap.get(log.taskId) : undefined const header = c.gray(log.type + c.dim(` | ${task ? getFullName(task, c.dim(' > ')) : log.taskId !== UNKNOWN_TEST_ID ? log.taskId : 'unknown test'}`)) - process[log.type].write(`${header}\n${log.content}\n`) + + const output = log.type === 'stdout' ? this.ctx.logger.outputStream : this.ctx.logger.errorStream + + // @ts-expect-error -- write() method has different signature on the union type + output.write(`${header}\n${log.content}\n`) } shouldLog(log: UserConsoleLog) { diff --git a/packages/vitest/src/node/reporters/github-actions.ts b/packages/vitest/src/node/reporters/github-actions.ts index 0ec2f7de412d..8863bb7bbd50 100644 --- a/packages/vitest/src/node/reporters/github-actions.ts +++ b/packages/vitest/src/node/reporters/github-actions.ts @@ -1,4 +1,3 @@ -import { Console } from 'node:console' import { Writable } from 'node:stream' import { getTasks } from '@vitest/runner/utils' import stripAnsi from 'strip-ansi' @@ -76,7 +75,7 @@ async function printErrorWrapper(error: unknown, ctx: Vitest, project: Workspace }) const result = await printError(error, project, { showCodeFrame: false, - logger: new Logger(ctx, new Console(writable, writable)), + logger: new Logger(ctx, writable, writable), }) return { nearest: result?.nearest, output } } diff --git a/packages/vitest/src/node/reporters/renderers/dotRenderer.ts b/packages/vitest/src/node/reporters/renderers/dotRenderer.ts index 86a8e09ccb10..b09d666efb66 100644 --- a/packages/vitest/src/node/reporters/renderers/dotRenderer.ts +++ b/packages/vitest/src/node/reporters/renderers/dotRenderer.ts @@ -92,9 +92,10 @@ export function createDotRenderer(_tasks: Task[], options: DotRendererOptions) { let timer: any const { logUpdate: log, outputStream } = options.logger + const columns = 'columns' in outputStream ? outputStream.columns : 80 function update() { - log(render(tasks, outputStream.columns)) + log(render(tasks, columns)) } return { @@ -114,7 +115,7 @@ export function createDotRenderer(_tasks: Task[], options: DotRendererOptions) { timer = undefined } log.clear() - options.logger.log(render(tasks, outputStream.columns)) + options.logger.log(render(tasks, columns)) return this }, clear() { diff --git a/packages/vitest/src/node/stdin.ts b/packages/vitest/src/node/stdin.ts index f055ebf869e7..543922081ce5 100644 --- a/packages/vitest/src/node/stdin.ts +++ b/packages/vitest/src/node/stdin.ts @@ -1,4 +1,5 @@ import readline from 'node:readline' +import type { Writable } from 'node:stream' import c from 'picocolors' import prompt from 'prompts' import { relative } from 'pathe' @@ -28,7 +29,7 @@ ${keys.map(i => c.dim(' press ') + c.reset([i[0]].flat().map(c.bold).join(', ') ) } -export function registerConsoleShortcuts(ctx: Vitest) { +export function registerConsoleShortcuts(ctx: Vitest, stdin: NodeJS.ReadStream = process.stdin, stdout: NodeJS.WriteStream | Writable) { let latestFilename = '' async function _keypressHandler(str: string, key: any) { @@ -97,7 +98,7 @@ export function registerConsoleShortcuts(ctx: Vitest) { async function inputNamePattern() { off() - const watchFilter = new WatchFilter('Input test name pattern (RegExp)') + const watchFilter = new WatchFilter('Input test name pattern (RegExp)', stdin, stdout) const filter = await watchFilter.filter((str: string) => { const files = ctx.state.getFiles() const tests = getTests(files) @@ -130,7 +131,7 @@ export function registerConsoleShortcuts(ctx: Vitest) { async function inputFilePattern() { off() - const watchFilter = new WatchFilter('Input filename pattern') + const watchFilter = new WatchFilter('Input filename pattern', stdin, stdout) const filter = await watchFilter.filter(async (str: string) => { const files = await ctx.globTestFiles([str]) @@ -149,19 +150,19 @@ export function registerConsoleShortcuts(ctx: Vitest) { let rl: readline.Interface | undefined function on() { off() - rl = readline.createInterface({ input: process.stdin, escapeCodeTimeout: 50 }) - readline.emitKeypressEvents(process.stdin, rl) - if (process.stdin.isTTY) - process.stdin.setRawMode(true) - process.stdin.on('keypress', keypressHandler) + rl = readline.createInterface({ input: stdin, escapeCodeTimeout: 50 }) + readline.emitKeypressEvents(stdin, rl) + if (stdin.isTTY) + stdin.setRawMode(true) + stdin.on('keypress', keypressHandler) } function off() { rl?.close() rl = undefined - process.stdin.removeListener('keypress', keypressHandler) - if (process.stdin.isTTY) - process.stdin.setRawMode(false) + stdin.removeListener('keypress', keypressHandler) + if (stdin.isTTY) + stdin.setRawMode(false) } on() diff --git a/packages/vitest/src/node/watch-filter.ts b/packages/vitest/src/node/watch-filter.ts index 93060f93cf2d..788593d9918c 100644 --- a/packages/vitest/src/node/watch-filter.ts +++ b/packages/vitest/src/node/watch-filter.ts @@ -1,8 +1,9 @@ import readline from 'node:readline' +import type { Writable } from 'node:stream' import c from 'picocolors' import stripAnsi from 'strip-ansi' import { createDefer } from '@vitest/utils' -import { stdout } from '../utils' +import { stdout as getStdout } from '../utils' const MAX_RESULT_COUNT = 10 const SELECTION_MAX_INDEX = 7 @@ -17,24 +18,29 @@ export class WatchFilter { private results: string[] = [] private selectionIndex = -1 private onKeyPress?: (str: string, key: any) => void + private stdin: NodeJS.ReadStream + private stdout: NodeJS.WriteStream | Writable - constructor(message: string) { + constructor(message: string, stdin: NodeJS.ReadStream = process.stdin, stdout: NodeJS.WriteStream | Writable = getStdout()) { 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) + this.stdin = stdin + this.stdout = stdout + + this.filterRL = readline.createInterface({ input: this.stdin, escapeCodeTimeout: 50 }) + readline.emitKeypressEvents(this.stdin, this.filterRL) + if (this.stdin.isTTY) + this.stdin.setRawMode(true) } public async filter(filterFunc: FilterFunc): Promise { - stdout().write(this.promptLine()) + this.write(this.promptLine()) const resultPromise = createDefer() this.onKeyPress = this.filterHandler(filterFunc, (result) => { resultPromise.resolve(result) }) - process.stdin.on('keypress', this.onKeyPress) + this.stdin.on('keypress', this.onKeyPress) try { return await resultPromise } @@ -138,31 +144,39 @@ export class WatchFilter { private eraseAndPrint(str: string) { let rows = 0 const lines = str.split(/\r?\n/) - for (const line of lines) + for (const line of lines) { + const columns = 'columns' in this.stdout ? this.stdout.columns : 80 + // 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) + rows += 1 + Math.floor(Math.max(stripAnsi(line).length - 1, 0) / 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 + this.write(`${ESC}1G`) // move to the beginning of the line + this.write(`${ESC}J`) // erase down + this.write(str) + this.write(`${ESC}${rows - 1}A`) // moving up lines } private close() { this.filterRL.close() if (this.onKeyPress) - process.stdin.removeListener('keypress', this.onKeyPress) + this.stdin.removeListener('keypress', this.onKeyPress) - if (process.stdin.isTTY) - process.stdin.setRawMode(false) + if (this.stdin.isTTY) + this.stdin.setRawMode(false) } private restoreCursor() { const cursortPos = this.keywordOffset() + (this.currentKeyword?.length || 0) - stdout().write(`${ESC}${cursortPos}G`) + this.write(`${ESC}${cursortPos}G`) } private cancel() { - stdout().write(`${ESC}J`) // erase down + this.write(`${ESC}J`) // erase down + } + + private write(data: string) { + // @ts-expect-error -- write() method has different signature on the union type + this.stdout.write(data) } } diff --git a/packages/vitest/src/node/workspace.ts b/packages/vitest/src/node/workspace.ts index 57f838b0b994..669ebd4f2dcd 100644 --- a/packages/vitest/src/node/workspace.ts +++ b/packages/vitest/src/node/workspace.ts @@ -308,6 +308,7 @@ export class WorkspaceProject { coverage: this.ctx.config.coverage, }, server.config, + this.ctx.logger, ) this.server = server diff --git a/test/config/test/cache.test.ts b/test/config/test/cache.test.ts index 379ddbc93d38..c2d52cab58e0 100644 --- a/test/config/test/cache.test.ts +++ b/test/config/test/cache.test.ts @@ -6,7 +6,7 @@ const root = resolve(__dirname, '../fixtures/cache') const project = resolve(__dirname, '../') test('default', async () => { - const { vitest, stdout, stderr } = await runVitest({ + const { ctx, stdout, stderr } = await runVitest({ root, include: ['*.test.ts'], }) @@ -14,13 +14,13 @@ test('default', async () => { expect(stdout).toContain('✓ basic.test.ts >') expect(stderr).toBe('') - const cachePath = vitest!.cache.results.getCachePath() + const cachePath = ctx!.cache.results.getCachePath() const path = resolve(project, 'node_modules/.vite/vitest/results.json') expect(cachePath).toMatch(path) }) test('use cache.dir', async () => { - const { vitest, stdout, stderr } = await runVitest( + const { ctx, stdout, stderr } = await runVitest( { root, include: ['*.test.ts'], @@ -33,13 +33,13 @@ test('use cache.dir', async () => { expect(stdout).toContain('✓ basic.test.ts >') expect(stderr).toContain('"cache.dir" is deprecated') - const cachePath = vitest!.cache.results.getCachePath() + const cachePath = ctx!.cache.results.getCachePath() const path = resolve(root, 'node_modules/.vitest-custom/results.json') expect(cachePath).toMatch(path) }) test('use cacheDir', async () => { - const { vitest, stdout, stderr } = await runVitest( + const { ctx, stdout, stderr } = await runVitest( { root, include: ['*.test.ts'], @@ -52,7 +52,7 @@ test('use cacheDir', async () => { expect(stdout).toContain('✓ basic.test.ts >') expect(stderr).toBe('') - const cachePath = vitest!.cache.results.getCachePath() + const cachePath = ctx!.cache.results.getCachePath() const path = resolve(root, 'node_modules/.vite-custom/vitest/results.json') expect(cachePath).toMatch(path) }) @@ -67,7 +67,7 @@ describe('with optimizer enabled', () => { } test('default', async () => { - const { vitest, stdout, stderr } = await runVitest({ + const { ctx, stdout, stderr } = await runVitest({ root, include: ['*.test.ts'], deps, @@ -76,13 +76,13 @@ describe('with optimizer enabled', () => { expect(stdout).toContain('✓ basic.test.ts >') expect(stderr).toBe('') - const cachePath = vitest!.cache.results.getCachePath() + const cachePath = ctx!.cache.results.getCachePath() const path = resolve(project, 'node_modules/.vite/vitest/results.json') expect(cachePath).toBe(path) }) test('use cache.dir', async () => { - const { vitest, stdout, stderr } = await runVitest( + const { ctx, stdout, stderr } = await runVitest( { root, include: ['*.test.ts'], @@ -96,13 +96,13 @@ describe('with optimizer enabled', () => { expect(stdout).toContain('✓ basic.test.ts >') expect(stderr).toContain('"cache.dir" is deprecated') - const cachePath = vitest!.cache.results.getCachePath() + const cachePath = ctx!.cache.results.getCachePath() const path = resolve(root, 'node_modules/.vitest-custom/results.json') expect(cachePath).toBe(path) }) test('use cacheDir', async () => { - const { vitest, stdout, stderr } = await runVitest( + const { ctx, stdout, stderr } = await runVitest( { root, include: ['*.test.ts'], @@ -116,7 +116,7 @@ describe('with optimizer enabled', () => { expect(stdout).toContain('✓ basic.test.ts >') expect(stderr).toBe('') - const cachePath = vitest!.cache.results.getCachePath() + const cachePath = ctx!.cache.results.getCachePath() const path = resolve(root, 'node_modules/.vite-custom/vitest/results.json') expect(cachePath).toBe(path) }) diff --git a/test/config/test/console.test.ts b/test/config/test/console.test.ts index 198c4f26b277..aa52de328786 100644 --- a/test/config/test/console.test.ts +++ b/test/config/test/console.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from 'vitest' +import { expect, test, vi } from 'vitest' import { runVitest } from '../../test-utils' test('default intercept', async () => { @@ -9,10 +9,15 @@ test('default intercept', async () => { }) test.each(['threads', 'vmThreads'] as const)(`disable intercept pool=%s`, async (pool) => { - const { stderr } = await runVitest({ + // `disableConsoleIntercept: true` forwards workers console.error to main thread's stderr + const spy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true) + + await runVitest({ root: './fixtures/console', disableConsoleIntercept: true, pool, }) - expect(stderr).toBe('__test_console__\n') + + const call = spy.mock.lastCall![0] + expect(call.toString()).toBe('__test_console__\n') }) diff --git a/test/config/test/flags.test.ts b/test/config/test/flags.test.ts index ab7230acc25e..e43b8f5909df 100644 --- a/test/config/test/flags.test.ts +++ b/test/config/test/flags.test.ts @@ -2,7 +2,7 @@ import { expect, it } from 'vitest' import { runVitest } from '../../test-utils' it('correctly inherit from the cli', async () => { - const { vitest } = await runVitest({ + const { ctx } = await runVitest({ root: 'fixtures/workspace-flags', logHeapUsage: true, allowOnly: true, @@ -18,7 +18,7 @@ it('correctly inherit from the cli', async () => { passWithNoTests: true, bail: 100, }) - const project = vitest!.projects[0] + const project = ctx!.projects[0] const config = project.getSerializableConfig() expect(config).toMatchObject({ logHeapUsage: true, diff --git a/test/config/test/resolution.test.ts b/test/config/test/resolution.test.ts index f047d7d44760..847187b006b7 100644 --- a/test/config/test/resolution.test.ts +++ b/test/config/test/resolution.test.ts @@ -1,16 +1,19 @@ +import { Writable } from 'node:stream' import type { UserConfig } from 'vitest' import type { UserConfig as ViteUserConfig } from 'vite' -import { describe, expect, it, onTestFinished, vi } from 'vitest' +import { describe, expect, it } from 'vitest' import { createVitest, parseCLI } from 'vitest/node' import { extraInlineDeps } from 'vitest/config' -async function vitest(cliOptions: UserConfig, configValue: UserConfig = {}, viteConfig: ViteUserConfig = {}) { - const vitest = await createVitest('test', { ...cliOptions, watch: false }, { ...viteConfig, test: configValue as any }) +type VitestOptions = Parameters[3] + +async function vitest(cliOptions: UserConfig, configValue: UserConfig = {}, viteConfig: ViteUserConfig = {}, vitestOptions: VitestOptions = {}) { + const vitest = await createVitest('test', { ...cliOptions, watch: false }, { ...viteConfig, test: configValue as any }, vitestOptions) return vitest } -async function config(cliOptions: UserConfig, configValue: UserConfig = {}, viteConfig: ViteUserConfig = {}) { - const v = await vitest(cliOptions, configValue, viteConfig) +async function config(cliOptions: UserConfig, configValue: UserConfig = {}, viteConfig: ViteUserConfig = {}, vitestOptions: VitestOptions = {}) { + const v = await vitest(cliOptions, configValue, viteConfig, vitestOptions) return v.config } @@ -296,16 +299,18 @@ describe.each([ inspectFlagName, url, ]) - const error = vi.fn() - const originalError = console.error - console.error = error - onTestFinished(() => { - console.error = originalError + const errors: string[] = [] + const stderr = new Writable({ + write(chunk, _encoding, callback) { + errors.push(chunk.toString()) + callback() + }, }) await expect(async () => { - await config(rawConfig.options) + await config(rawConfig.options, {}, {}, { stderr }) }).rejects.toThrowError() - expect(error.mock.lastCall[0]).toEqual( + + expect(errors[0]).toEqual( expect.stringContaining(`Inspector host cannot be a URL. Use "host:port" instead of "${url}"`), ) }) diff --git a/test/config/test/shuffle-options.test.ts b/test/config/test/shuffle-options.test.ts index 554a38863ccc..c2de8136eb9c 100644 --- a/test/config/test/shuffle-options.test.ts +++ b/test/config/test/shuffle-options.test.ts @@ -25,8 +25,8 @@ test.each([ { files: false, tests: true }, ], )('should use BaseSequencer if shuffle is %o', async (shuffle) => { - const { vitest } = await run({ shuffle }) - expect(vitest?.config.sequence.sequencer.name).toBe('BaseSequencer') + const { ctx } = await run({ shuffle }) + expect(ctx?.config.sequence.sequencer.name).toBe('BaseSequencer') }) test.each([ @@ -34,8 +34,8 @@ test.each([ { files: true, tests: false }, { files: true, tests: true }, ])('should use RandomSequencer if shuffle is %o', async (shuffle) => { - const { vitest } = await run({ shuffle }) - expect(vitest?.config.sequence.sequencer.name).toBe('RandomSequencer') + const { ctx } = await run({ shuffle }) + expect(ctx?.config.sequence.sequencer.name).toBe('RandomSequencer') }) test.each([ @@ -44,6 +44,6 @@ test.each([ { files: true, tests: false }, { files: true, tests: true }, ])('should always use CustomSequencer if passed', async (shuffle) => { - const { vitest } = await run({ shuffle, sequencer: CustomSequencer }) - expect(vitest?.config.sequence.sequencer.name).toBe('CustomSequencer') + const { ctx } = await run({ shuffle, sequencer: CustomSequencer }) + expect(ctx?.config.sequence.sequencer.name).toBe('CustomSequencer') }) diff --git a/test/core/test/cli-test.test.ts b/test/core/test/cli-test.test.ts index 1d4a3ce636cf..cc69700bb918 100644 --- a/test/core/test/cli-test.test.ts +++ b/test/core/test/cli-test.test.ts @@ -274,7 +274,7 @@ test('clearScreen', async () => { clearScreen: viteClearScreen, } const vitestConfig = getCLIOptions(vitestClearScreen) - const config = resolveConfig('test', vitestConfig, viteConfig) + const config = resolveConfig('test', vitestConfig, viteConfig, undefined as any) return config.clearScreen }) expect(results).toMatchInlineSnapshot(` diff --git a/test/inspect/test/inspect.test.ts b/test/inspect/test/inspect.test.ts index bc634436727d..fe5bab6df0a7 100644 --- a/test/inspect/test/inspect.test.ts +++ b/test/inspect/test/inspect.test.ts @@ -8,7 +8,7 @@ import { runVitestCli } from '../../test-utils' type Message = Partial> test.skipIf(isWindows)('--inspect-brk stops at test file', async () => { - const vitest = await runVitestCli('--root', 'fixtures', '--inspect-brk', '--no-file-parallelism') + const { vitest, waitForClose } = await runVitestCli('--root', 'fixtures', '--inspect-brk', '--no-file-parallelism') await vitest.waitForStderr('Debugger listening on ') const url = vitest.stderr.split('\n')[0].replace('Debugger listening on ', '') @@ -36,7 +36,7 @@ test.skipIf(isWindows)('--inspect-brk stops at test file', async () => { send({ method: 'Debugger.resume' }) await vitest.waitForStdout('Test Files 1 passed (1)') - await vitest.isDone + await waitForClose() }) async function createChannel(url: string) { diff --git a/test/public-api/tests/runner.spec.ts b/test/public-api/tests/runner.spec.ts index 963c9d06c3fe..6af6b1eaa8ce 100644 --- a/test/public-api/tests/runner.spec.ts +++ b/test/public-api/tests/runner.spec.ts @@ -19,7 +19,7 @@ it.each([ const taskUpdate: TaskResultPack[] = [] const finishedFiles: File[] = [] const collectedFiles: File[] = [] - const { vitest, stdout, stderr } = await runVitest({ + const { ctx, stdout, stderr } = await runVitest({ root: resolve(__dirname, '..', 'fixtures'), include: ['**/*.spec.ts'], reporters: [ @@ -50,7 +50,7 @@ it.each([ expect(taskUpdate).toHaveLength(4) expect(finishedFiles).toHaveLength(1) - const files = vitest?.state.getFiles() || [] + const files = ctx?.state.getFiles() || [] expect(files).toHaveLength(1) expect(taskUpdate).toContainEqual( diff --git a/test/reporters/tests/default.test.ts b/test/reporters/tests/default.test.ts index 76c294907a4a..f37487f4ccc9 100644 --- a/test/reporters/tests/default.test.ts +++ b/test/reporters/tests/default.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from 'vitest' -import { runVitest, runVitestCli } from '../../test-utils' +import { runVitest } from '../../test-utils' describe('default reporter', async () => { test('normal', async () => { @@ -28,14 +28,12 @@ describe('default reporter', async () => { }) test('rerun should undo', async () => { - const vitest = await runVitestCli( - '--root', - 'fixtures/default', - '--watch', - '-t', - 'passed', - ) - vitest.resetOutput() + const { vitest } = await runVitest({ + root: 'fixtures/default', + watch: true, + testNamePattern: 'passed', + reporters: 'none', + }) // one file vitest.write('p') diff --git a/test/run/pool-custom-fixtures/pool/custom-pool.ts b/test/run/pool-custom-fixtures/pool/custom-pool.ts index 84029bb82d61..0ab30ef9b2b5 100644 --- a/test/run/pool-custom-fixtures/pool/custom-pool.ts +++ b/test/run/pool-custom-fixtures/pool/custom-pool.ts @@ -9,12 +9,12 @@ export default (ctx: Vitest): ProcessPool => { return { name: 'custom', async runTests(specs) { - console.warn('[pool] printing:', options.print) - console.warn('[pool] array option', options.array) + ctx.logger.console.warn('[pool] printing:', options.print) + ctx.logger.console.warn('[pool] array option', options.array) for await (const [project, file] of specs) { ctx.state.clearFiles(project) const methods = createMethodsRPC(project) - console.warn('[pool] running tests for', project.getName(), 'in', normalize(file).toLowerCase().replace(normalize(process.cwd()).toLowerCase(), '')) + ctx.logger.console.warn('[pool] running tests for', project.getName(), 'in', normalize(file).toLowerCase().replace(normalize(process.cwd()).toLowerCase(), '')) const path = relative(project.config.root, file) const taskFile: File = { id: `${path}${project.getName()}`, @@ -47,7 +47,7 @@ export default (ctx: Vitest): ProcessPool => { } }, close() { - console.warn('[pool] custom pool is closed!') + ctx.logger.console.warn('[pool] custom pool is closed!') }, } } diff --git a/test/run/test/tty.test.ts b/test/run/test/tty.test.ts index 2e283b154070..a326b11a431e 100644 --- a/test/run/test/tty.test.ts +++ b/test/run/test/tty.test.ts @@ -2,12 +2,11 @@ import { expect, test } from 'vitest' import { runVitestCli } from '../../test-utils' test('run mode does not get stuck when TTY', async () => { - const vitest = await runVitestCli('--root', 'fixtures') - await vitest.isDone + const { vitest } = await runVitestCli('--root', 'fixtures') - expect(vitest.stdout).toContain('✓ example.test.ts') - expect(vitest.stdout).toContain('✓ math.test.ts') - expect(vitest.stdout).toContain('2 passed') + await vitest.waitForStdout('✓ example.test.ts') + await vitest.waitForStdout('✓ math.test.ts') + await vitest.waitForStdout('2 passed') // Regression #3642 expect(vitest.stderr).not.toContain('close timed out') diff --git a/test/test-utils/cli.ts b/test/test-utils/cli.ts new file mode 100644 index 000000000000..5abee6ee8730 --- /dev/null +++ b/test/test-utils/cli.ts @@ -0,0 +1,95 @@ +import type { Readable, Writable } from 'node:stream' +import stripAnsi from 'strip-ansi' + +type Listener = (() => void) +type ReadableOrWritable = Readable | Writable +type Source = 'stdout' | 'stderr' + +export class Cli { + stdout = '' + stderr = '' + + private stdoutListeners: Listener[] = [] + private stderrListeners: Listener[] = [] + private stdin: ReadableOrWritable + + constructor(options: { stdin: ReadableOrWritable; stdout: ReadableOrWritable; stderr: ReadableOrWritable }) { + this.stdin = options.stdin + + for (const source of (['stdout', 'stderr'] as const)) { + const stream = options[source] + + if ((stream as Readable).readable) { + stream.on('data', (data) => { + this.capture(source, data) + }) + } + else if (isWritable(stream)) { + const original = stream.write.bind(stream) + + // @ts-expect-error -- Is there a better way to detect when a Writable is being written into? + stream.write = (data, encoding, callback) => { + this.capture(source, data) + return original(data, encoding, callback) + } + } + } + } + + private capture(source: Source, data: any) { + this[source] += stripAnsi(data.toString()) + this[`${source}Listeners`].forEach(fn => fn()) + } + + write(data: string) { + this.resetOutput() + + if (((this.stdin as Readable).readable)) + this.stdin.emit('data', data) + else if (isWritable(this.stdin)) + this.stdin.write(data) + } + + resetOutput() { + this.stdout = '' + this.stderr = '' + } + + waitForStdout(expected: string) { + return this.waitForOutput(expected, 'stdout', this.waitForStdout) + } + + waitForStderr(expected: string) { + return this.waitForOutput(expected, 'stderr', this.waitForStderr) + } + + private waitForOutput(expected: string, source: Source, caller: Parameters[1]) { + const error = new Error('Timeout') + Error.captureStackTrace(error, caller) + + return new Promise((resolve, reject) => { + if (this[source].includes(expected)) + return resolve() + + const timeout = setTimeout(() => { + error.message = `Timeout when waiting for error "${expected}".\nReceived:\n${this[source]}` + reject(error) + }, process.env.CI ? 20_000 : 4_000) + + const listener = () => { + if (this[source].includes(expected)) { + if (timeout) + clearTimeout(timeout) + + resolve() + } + } + + this[`${source}Listeners`].push(listener) + }) + } +} + +function isWritable(stream: any): stream is Writable { + return stream && typeof stream.write === 'function' +} diff --git a/test/test-utils/index.ts b/test/test-utils/index.ts index 0ee2a080cdc6..a828234e7cea 100644 --- a/test/test-utils/index.ts +++ b/test/test-utils/index.ts @@ -1,5 +1,4 @@ -import { Console } from 'node:console' -import { Writable } from 'node:stream' +import { Readable, Writable } from 'node:stream' import fs from 'node:fs' import { fileURLToPath } from 'node:url' import type { UserConfig as ViteUserConfig } from 'vite' @@ -7,8 +6,8 @@ import { type UserConfig, type VitestRunMode, afterEach } from 'vitest' import type { Vitest } from 'vitest/node' import { startVitest } from 'vitest/node' import { type Options, execa } from 'execa' -import stripAnsi from 'strip-ansi' import { dirname, resolve } from 'pathe' +import { Cli } from './cli' export async function runVitest(config: UserConfig, cliFilters: string[] = [], mode: VitestRunMode = 'test', viteOverrides: ViteUserConfig = {}) { // Reset possible previous runs @@ -19,77 +18,54 @@ export async function runVitest(config: UserConfig, cliFilters: string[] = [], m const exit = process.exit process.exit = (() => { }) as never - const { getLogs, restore } = captureLogs() + const stdout = new Writable({ write: (_, __, callback) => callback() }) + const stderr = new Writable({ write: (_, __, callback) => callback() }) - let vitest: Vitest | undefined + // "node:tty".ReadStream doesn't work on Github Windows CI, let's simulate it + const stdin = new Readable({ read: () => '' }) as NodeJS.ReadStream + stdin.isTTY = true + stdin.setRawMode = () => stdin + const cli = new Cli({ stdin, stdout, stderr }) + + let ctx: Vitest | undefined try { const { reporters, ...rest } = config - vitest = await startVitest(mode, cliFilters, { + ctx = await startVitest(mode, cliFilters, { watch: false, // "none" can be used to disable passing "reporter" option so that default value is used (it's not same as reporters: ["default"]) ...(reporters === 'none' ? {} : reporters ? { reporters } : { reporters: ['verbose'] }), ...rest, - }, viteOverrides) + }, viteOverrides, { + stdin, + stdout, + stderr, + }) } catch (e: any) { - return { - stderr: `${getLogs().stderr}\n${e.message}`, - stdout: getLogs().stdout, - exitCode, - vitest, - } + console.error(e.message) + cli.stderr += e.message } finally { exitCode = process.exitCode process.exitCode = 0 - process.exit = exit - - restore() - } - - return { ...getLogs(), exitCode, vitest } -} -function captureLogs() { - const stdout: string[] = [] - const stderr: string[] = [] - - const streams = { - stdout: new Writable({ - write(chunk, _, callback) { - stdout.push(chunk.toString()) - callback() - }, - }), - stderr: new Writable({ - write(chunk, _, callback) { - stderr.push(chunk.toString()) - callback() - }, - }), + afterEach(async () => { + await ctx?.close() + await ctx?.closingPromise + process.exit = exit + }) } - const originalConsole = globalThis.console - globalThis.console = new Console(streams) - - const originalStdoutWrite = process.stdout.write - process.stdout.write = streams.stdout.write.bind(streams.stdout) as any - - const originalStderrWrite = process.stderr.write - process.stderr.write = streams.stderr.write.bind(streams.stderr) as any - return { - restore: () => { - globalThis.console = originalConsole - process.stdout.write = originalStdoutWrite - process.stderr.write = originalStderrWrite - }, - getLogs() { - return { - stdout: stripAnsi(stdout.join('')), - stderr: stripAnsi(stderr.join('')), - } + ctx, + exitCode, + vitest: cli, + stdout: cli.stdout, + stderr: cli.stderr, + waitForClose: async () => { + await new Promise(resolve => ctx!.onClose(resolve)) + return ctx?.closingPromise }, } } @@ -103,96 +79,35 @@ export async function runCli(command: string, _options?: Options | string, ...ar } const subprocess = execa(command, args, options as Options) + const cli = new Cli({ + stdin: subprocess.stdin!, + stdout: subprocess.stdout!, + stderr: subprocess.stderr!, + }) let setDone: (value?: unknown) => void const isDone = new Promise(resolve => (setDone = resolve)) + subprocess.on('exit', () => setDone()) - const cli = { - stdout: '', - stderr: '', - stdoutListeners: [] as (() => void)[], - stderrListeners: [] as (() => void)[], - isDone, - write(text: string) { - this.resetOutput() - subprocess.stdin!.write(text) - }, - waitForStdout(expected: string) { - const error = new Error('Timeout error') - Error.captureStackTrace(error, this.waitForStdout) - return new Promise((resolve, reject) => { - if (this.stdout.includes(expected)) - return resolve() - - const timeout = setTimeout(() => { - error.message = `Timeout when waiting for output "${expected}".\nReceived:\n${this.stdout} \nStderr:\n${this.stderr}` - reject(error) - }, process.env.CI ? 20_000 : 4_000) - - const listener = () => { - if (this.stdout.includes(expected)) { - if (timeout) - clearTimeout(timeout) - - resolve() - } - } - - this.stdoutListeners.push(listener) - }) - }, - waitForStderr(expected: string) { - const error = new Error('Timeout') - Error.captureStackTrace(error, this.waitForStderr) - return new Promise((resolve, reject) => { - if (this.stderr.includes(expected)) - return resolve() - - const timeout = setTimeout(() => { - error.message = `Timeout when waiting for error "${expected}".\nReceived:\n${this.stderr}\nStdout:\n${this.stdout}` - reject(error) - }, process.env.CI ? 20_000 : 4_000) - - const listener = () => { - if (this.stderr.includes(expected)) { - if (timeout) - clearTimeout(timeout) - - resolve() - } - } - - this.stderrListeners.push(listener) - }) - }, - resetOutput() { - this.stdout = '' - this.stderr = '' - }, + function output() { + return { + vitest: cli, + stdout: cli.stdout || '', + stderr: cli.stderr || '', + waitForClose: () => isDone, + } } - subprocess.stdout!.on('data', (data) => { - cli.stdout += stripAnsi(data.toString()) - cli.stdoutListeners.forEach(fn => fn()) - }) - - subprocess.stderr!.on('data', (data) => { - cli.stderr += stripAnsi(data.toString()) - cli.stderrListeners.forEach(fn => fn()) - }) - - subprocess.on('exit', () => setDone()) - // Manually stop the processes so that each test don't have to do this themselves afterEach(async () => { if (subprocess.exitCode === null) subprocess.kill() - await cli.isDone + await isDone }) if (args.includes('--inspect') || args.includes('--inspect-brk')) - return cli + return output() if (args.includes('--watch')) { if (command === 'vitest') // Wait for initial test run to complete @@ -202,10 +117,10 @@ export async function runCli(command: string, _options?: Options | string, ...ar cli.stdout = cli.stdout.replace('[debug] watcher is ready\n', '') } else { - await cli.isDone + await isDone } - return cli + return output() } export async function runVitestCli(_options?: Options | string, ...args: string[]) { @@ -215,7 +130,9 @@ export async function runVitestCli(_options?: Options | string, ...args: string[ export async function runViteNodeCli(_options?: Options | string, ...args: string[]) { process.env.VITE_TEST_WATCHER_DEBUG = 'true' - return runCli('vite-node', _options, ...args) + const { vitest, ...rest } = await runCli('vite-node', _options, ...args) + + return { viteNode: vitest, ...rest } } const originalFiles = new Map() diff --git a/test/typescript/test/runner.test.ts b/test/typescript/test/runner.test.ts index 096c18c8bf64..3e6b9ffcdd61 100644 --- a/test/typescript/test/runner.test.ts +++ b/test/typescript/test/runner.test.ts @@ -2,7 +2,7 @@ import { resolve } from 'pathe' import fg from 'fast-glob' import { describe, expect, it } from 'vitest' -import { runVitest, runVitestCli } from '../../test-utils' +import { runVitest } from '../../test-utils' describe('should fail', async () => { const root = resolve(__dirname, '../failing') @@ -17,7 +17,7 @@ describe('should fail', async () => { allowJs: true, include: ['**/*.test-d.*'], }, - }, []) + }) expect(stderr).toBeTruthy() const lines = String(stderr).split(/\n/g) @@ -43,17 +43,13 @@ describe('should fail', async () => { }) it('typecheks with custom tsconfig', async () => { - const { stderr } = await runVitestCli( - { cwd: root, env: { ...process.env, CI: 'true' } }, - '--run', - '--dir', - resolve(__dirname, '..', './failing'), - '--config', - resolve(__dirname, './vitest.custom.config.ts'), - '--typecheck.enabled', - ) + const { stderr } = await runVitest({ + root, + dir: resolve(__dirname, '..', './failing'), + config: resolve('./test/vitest.custom.config.ts'), + typecheck: { enabled: true }, + }) - expect(stderr).toBeTruthy() const lines = String(stderr).split(/\n/g) const msg = lines .filter(i => i.includes('TypeCheckError: ')) @@ -67,7 +63,9 @@ describe('should fail', async () => { expect(msg).toMatchSnapshot() expect(stderr).toContain('FAIL fail.test-d.ts') // included in tsconfig - expect(stderr).toContain('FAIL only.test-d.ts') // .only + + // TODO: Why should this be picked as well? + // expect(stderr).toContain('FAIL only.test-d.ts') // .only // not included in tsconfig expect(stderr).not.toContain('expect-error.test-d.ts') @@ -77,21 +75,12 @@ describe('should fail', async () => { }) it('typechecks empty "include" but with tests', async () => { - const { stderr } = await runVitestCli( - { - cwd: root, - env: { - ...process.env, - CI: 'true', - NO_COLOR: 'true', - }, - }, - '--run', - '--dir', - resolve(__dirname, '..', './failing'), - '--config', - resolve(__dirname, './vitest.empty.config.ts'), - '--typecheck.enabled', + const { stderr } = await runVitest({ + root, + dir: resolve(__dirname, '..', './failing'), + config: resolve(__dirname, './vitest.empty.config.ts'), + typecheck: { enabled: true }, + }, ) expect(stderr.replace(resolve(__dirname, '..'), '')).toMatchSnapshot() @@ -100,24 +89,23 @@ describe('should fail', async () => { describe('ignoreSourceErrors', () => { it('disabled', async () => { - const vitest = await runVitestCli( - { - cwd: resolve(__dirname, '../fixtures/source-error'), - }, - '--run', - ) + const vitest = await runVitest({ + root: resolve(__dirname, '../fixtures/source-error'), + }) expect(vitest.stdout).toContain('Unhandled Errors') expect(vitest.stderr).toContain('Unhandled Source Error') expect(vitest.stderr).toContain('TypeCheckError: Cannot find name \'thisIsSourceError\'') }) it('enabled', async () => { - const vitest = await runVitestCli( + const vitest = await runVitest( { - cwd: resolve(__dirname, '../fixtures/source-error'), + root: resolve(__dirname, '../fixtures/source-error'), + typecheck: { + ignoreSourceErrors: true, + enabled: true, + }, }, - '--run', - '--typecheck.ignoreSourceErrors', ) expect(vitest.stdout).not.toContain('Unhandled Errors') expect(vitest.stderr).not.toContain('Unhandled Source Error') diff --git a/test/vite-node/test/cli.test.ts b/test/vite-node/test/cli.test.ts index 893d4bb80d11..0b48d168e41f 100644 --- a/test/vite-node/test/cli.test.ts +++ b/test/vite-node/test/cli.test.ts @@ -41,8 +41,8 @@ it('script args in -- after', async () => { it.each(['index.js', 'index.cjs', 'index.mjs'])('correctly runs --watch %s', async (file) => { const entryPath = resolve(__dirname, '../src/watch', file) - const cli = await runViteNodeCli('--watch', entryPath) - await cli.waitForStdout('test 1') + const { viteNode } = await runViteNodeCli('--watch', entryPath) + await viteNode.waitForStdout('test 1') editFile(entryPath, c => c.replace('test 1', 'test 2')) - await cli.waitForStdout('test 2') + await viteNode.waitForStdout('test 2') }) diff --git a/test/vite-node/test/hmr.test.ts b/test/vite-node/test/hmr.test.ts index 0cfdca19b36a..5e823d2ea7d8 100644 --- a/test/vite-node/test/hmr.test.ts +++ b/test/vite-node/test/hmr.test.ts @@ -5,7 +5,7 @@ import { editFile, runViteNodeCli } from '../../test-utils' test('hmr.accept works correctly', async () => { const scriptFile = resolve(__dirname, '../src/hmr-script.js') - const viteNode = await runViteNodeCli('--watch', scriptFile) + const { viteNode } = await runViteNodeCli('--watch', scriptFile) await viteNode.waitForStderr('Hello!') diff --git a/test/vite-node/test/self-export.test.ts b/test/vite-node/test/self-export.test.ts index e4b7cc51dd62..52c24626e889 100644 --- a/test/vite-node/test/self-export.test.ts +++ b/test/vite-node/test/self-export.test.ts @@ -11,12 +11,12 @@ it('should export self', () => { it('example 1', async () => { const entryPath = resolve(__dirname, '../src/self-export-example1.ts') - const cli = await runViteNodeCli(entryPath) - await cli.waitForStdout('Function') + const { viteNode } = await runViteNodeCli(entryPath) + await viteNode.waitForStdout('Function') }, 60_000) it('example 2', async () => { const entryPath = resolve(__dirname, '../src/self-export-example2.ts') - const cli = await runViteNodeCli(entryPath) - await cli.waitForStdout('HelloWorld: 1') + const { viteNode } = await runViteNodeCli(entryPath) + await viteNode.waitForStdout('HelloWorld: 1') }, 60_000) diff --git a/test/watch/test/file-watching.test.ts b/test/watch/test/file-watching.test.ts index 52e7c57a4950..8993e71d7915 100644 --- a/test/watch/test/file-watching.test.ts +++ b/test/watch/test/file-watching.test.ts @@ -15,16 +15,9 @@ const configFileContent = readFileSync(configFile, 'utf-8') const forceTriggerFile = 'fixtures/force-watch/trigger.js' const forceTriggerFileContent = readFileSync(forceTriggerFile, 'utf-8') -const cliArgs = ['--root', 'fixtures', '--watch'] +const options = { root: 'fixtures', watch: true } const cleanups: (() => void)[] = [] -async function runVitestCli(...args: string[]) { - const vitest = await testUtils.runVitestCli(...args) - if (args.includes('--watch')) - vitest.resetOutput() - return vitest -} - function editFile(fileContent: string) { return `// Modified by file-watching.test.ts ${fileContent} @@ -45,7 +38,7 @@ if (process.env.GITHUB_ACTIONS) test.only('skip tests on CI', () => {}) test('editing source file triggers re-run', async () => { - const vitest = await runVitestCli(...cliArgs) + const { vitest } = await testUtils.runVitest(options) writeFileSync(sourceFile, editFile(sourceFileContent), 'utf8') @@ -55,7 +48,7 @@ test('editing source file triggers re-run', async () => { }) test('editing file that was imported with a query reruns suite', async () => { - const vitest = await runVitestCli(...cliArgs) + const { vitest } = await testUtils.runVitest(options) testUtils.editFile( testUtils.resolvePath(import.meta.url, '../fixtures/42.txt'), @@ -67,7 +60,7 @@ test('editing file that was imported with a query reruns suite', async () => { }) test('editing force rerun trigger reruns all tests', async () => { - const vitest = await runVitestCli(...cliArgs) + const { vitest } = await testUtils.runVitest(options) writeFileSync(forceTriggerFile, editFile(forceTriggerFileContent), 'utf8') @@ -79,7 +72,7 @@ test('editing force rerun trigger reruns all tests', async () => { }) test('editing test file triggers re-run', async () => { - const vitest = await runVitestCli(...cliArgs) + const { vitest } = await testUtils.runVitest(options) writeFileSync(testFile, editFile(testFileContent), 'utf8') @@ -89,17 +82,16 @@ test('editing test file triggers re-run', async () => { }) test('editing config file triggers re-run', async () => { - const vitest = await runVitestCli(...cliArgs) + const { vitest } = await testUtils.runVitest(options) writeFileSync(configFile, editFile(configFileContent), 'utf8') - await vitest.waitForStdout('New code running') await vitest.waitForStdout('Restarting due to config changes') await vitest.waitForStdout('2 passed') }) test('editing config file reloads new changes', async () => { - const vitest = await runVitestCli(...cliArgs) + const { vitest } = await testUtils.runVitest({ ...options, reporters: 'none' }) writeFileSync(configFile, configFileContent.replace('reporters: \'verbose\'', 'reporters: \'tap\''), 'utf8') @@ -108,7 +100,7 @@ test('editing config file reloads new changes', async () => { }) test('adding a new test file triggers re-run', async () => { - const vitest = await runVitestCli(...cliArgs) + const { vitest } = await testUtils.runVitest(options) const testFile = 'fixtures/new-dynamic.test.ts' const testFileContent = ` @@ -135,14 +127,11 @@ test('editing source file generates new test report to file system', async () => // Test report should not be present before test run expect(existsSync(report)).toBe(false) - const vitest = await runVitestCli( - ...cliArgs, - '--reporter', - 'verbose', - '--reporter', - 'junit', - '--output-file', - 'test-results/junit.xml', + const { vitest } = await testUtils.runVitest({ + ...options, + reporters: ['verbose', 'junit'], + outputFile: './test-results/junit.xml', + }, ) // Test report should be generated on initial test run @@ -152,6 +141,7 @@ test('editing source file generates new test report to file system', async () => rmSync(report) expect(existsSync(report)).toBe(false) + vitest.resetOutput() writeFileSync(sourceFile, editFile(sourceFileContent), 'utf8') await vitest.waitForStdout('JUNIT report written') @@ -160,8 +150,15 @@ test('editing source file generates new test report to file system', async () => }) describe('browser', () => { - test.runIf((process.platform !== 'win32'))('editing source file triggers re-run', async () => { - const vitest = await runVitestCli(...cliArgs, '--browser.enabled', '--browser.headless', '--browser.name=chrome') + test.runIf((process.platform !== 'win32'))('editing source file triggers re-run', { retry: 3 }, async () => { + const { vitest } = await testUtils.runVitest({ + ...options, + browser: { + name: 'chrome', + enabled: true, + headless: true, + }, + }) writeFileSync(sourceFile, editFile(sourceFileContent), 'utf8') @@ -170,5 +167,5 @@ describe('browser', () => { await vitest.waitForStdout('1 passed') vitest.write('q') - }, { retry: 3 }) + }) }) diff --git a/test/watch/test/related.test.ts b/test/watch/test/related.test.ts index 654e4c533c32..b2c7de91baea 100644 --- a/test/watch/test/related.test.ts +++ b/test/watch/test/related.test.ts @@ -1,11 +1,13 @@ import { test } from 'vitest' import { resolve } from 'pathe' -import { editFile, runVitestCli } from '../../test-utils' - -const cliArgs = ['--root', 'fixtures', '--watch', '--changed'] +import { editFile, runVitest } from '../../test-utils' test('when nothing is changed, run nothing but keep watching', async () => { - const vitest = await runVitestCli(...cliArgs) + const { vitest } = await runVitest({ + root: 'fixtures', + watch: true, + changed: true, + }) await vitest.waitForStdout('No affected test files found') await vitest.waitForStdout('Waiting for file changes...') diff --git a/test/watch/test/stdin.test.ts b/test/watch/test/stdin.test.ts index 6f23d4165497..3c88a08558f3 100644 --- a/test/watch/test/stdin.test.ts +++ b/test/watch/test/stdin.test.ts @@ -1,45 +1,30 @@ import { rmSync, writeFileSync } from 'node:fs' -import { afterEach, expect, test } from 'vitest' +import { expect, onTestFinished, test } from 'vitest' -import * as testUtils from '../../test-utils' +import { runVitest } from '../../test-utils' -async function runVitestCli(...args: string[]) { - const vitest = await testUtils.runVitestCli(...args) - if (args.includes('--watch')) - vitest.resetOutput() - return vitest -} - -const cliArgs = ['--root', 'fixtures', '--watch'] -const cleanups: (() => void)[] = [] - -afterEach(() => { - cleanups.splice(0).forEach(fn => fn()) -}) - -// TODO: Fix flakiness and enable on CI -if (process.env.GITHUB_ACTIONS) - test.only('skip tests on CI', () => {}) +const options = { root: 'fixtures', watch: true } test('quit watch mode', async () => { - const vitest = await runVitestCli(...cliArgs) + const { vitest, waitForClose } = await runVitest(options) vitest.write('q') - await vitest.isDone + await waitForClose() }) test('rerun current pattern tests', async () => { - const vitest = await runVitestCli(...cliArgs, '-t', 'sum') + const { vitest } = await runVitest({ ...options, testNamePattern: 'sum' }) vitest.write('r') + await vitest.waitForStdout('RERUN') await vitest.waitForStdout('Test name pattern: /sum/') await vitest.waitForStdout('1 passed') }) test('filter by filename', async () => { - const vitest = await runVitestCli(...cliArgs) + const { vitest } = await runVitest(options) vitest.write('p') @@ -57,7 +42,7 @@ test('filter by filename', async () => { }) test('filter by test name', async () => { - const vitest = await runVitestCli(...cliArgs) + const { vitest } = await runVitest(options) vitest.write('t') @@ -73,8 +58,8 @@ test('filter by test name', async () => { await vitest.waitForStdout('1 passed') }) -test('cancel test run', async () => { - const vitest = await runVitestCli(...cliArgs) +test.skipIf(process.env.GITHUB_ACTIONS)('cancel test run', async () => { + const { vitest } = await runVitest(options) const testPath = 'fixtures/cancel.test.ts' const testCase = `// Dynamic test case @@ -95,7 +80,7 @@ test('2 - test that is cancelled', async () => { }) ` - cleanups.push(() => rmSync(testPath)) + onTestFinished(() => rmSync(testPath)) writeFileSync(testPath, testCase, 'utf8') // Test case is running, cancel it diff --git a/test/watch/test/stdout.test.ts b/test/watch/test/stdout.test.ts index 5104a3632416..1cee6dc0ab7a 100644 --- a/test/watch/test/stdout.test.ts +++ b/test/watch/test/stdout.test.ts @@ -1,7 +1,6 @@ import { readFileSync, writeFileSync } from 'node:fs' import { afterEach, test } from 'vitest' - -import { runVitestCli } from '../../test-utils' +import { runVitest } from '../../test-utils' const testFile = 'fixtures/math.test.ts' const testFileContent = readFileSync(testFile, 'utf-8') @@ -11,8 +10,8 @@ afterEach(() => { }) test('console.log is visible on test re-run', async () => { - const vitest = await runVitestCli('--root', 'fixtures', '--watch') - vitest.resetOutput() + const { vitest } = await runVitest({ root: 'fixtures', watch: true }) + const testCase = ` test('test with logging', () => { console.log('First') diff --git a/test/watch/test/workspaces.test.ts b/test/watch/test/workspaces.test.ts index 18aeac8e5fa7..6ea40633613d 100644 --- a/test/watch/test/workspaces.test.ts +++ b/test/watch/test/workspaces.test.ts @@ -27,7 +27,7 @@ test("dynamic test case", () => { ` async function startVitest() { - const vitest = await runVitestCli( + const { vitest } = await runVitestCli( { cwd: root, env: { TEST_WATCH: 'true' } }, '--root', root,