Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
Something went wrong. Try again.
30 kB · 959 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960import type { Options } from 'tinyexec'import type { FSWatcher, UserConfig as ViteUserConfig } from 'vite'import type { ParsedStack, SerializedConfig, TestContext, WorkerGlobalState } from 'vitest'import type { TestProjectConfiguration } from 'vitest/config'import type { TestCase, CliOptions as TestCliOptions, TestCollection, TestModule, TestSpecification, TestSuite, TestUserConfig, Vitest,} from 'vitest/node'import { webcrypto as crypto } from 'node:crypto'import fs from 'node:fs'import { Readable, Writable } from 'node:stream'import { fileURLToPath, pathToFileURL } from 'node:url'import { inspect, stripVTControlCharacters } from 'node:util'import { dirname, relative, resolve } from 'pathe'import { x } from 'tinyexec'import { afterEach, onTestFinished, TestRunner } from 'vitest'import { disableDefaultColors, Logger, PluginHarness, resolveConfig, startVitest,} from 'vitest/node'import { Cli } from './cli'
// Vitest bundles its own `tinyrainbow` instance (see rollup `manualChunks`), which// is a different object than the one imported above. Disable colors on it too so// reporter output captured in tests is deterministic regardless of `CI`/`FORCE_COLOR`.disableDefaultColors()
export interface VitestRunnerCLIOptions { std?: 'inherit' fails?: boolean printExitCode?: boolean preserveAnsi?: boolean tty?: boolean}
export interface RunVitestConfig extends TestUserConfig { $viteConfig?: Omit<ViteUserConfig, 'test'> $cliOptions?: TestCliOptions $cliFilters?: string[]}
const process_ = process
// Polling interval of the file watcher in spawned watch-mode instances. The// watcher-ready probe derives its timing from it: each probe state must// outlive a full poll cycle, or the poller can keep sampling the state that// matches its baseline and never observe a difference.const WATCH_POLL_INTERVAL = 100
export function createConsole({ tty, std }: { tty?: boolean; std?: 'inherit' } = {}) { const stdout = new Writable({ write(chunk, __, callback) { if (std === 'inherit') { process.stdout.write(chunk.toString()) } callback() }, })
if (tty) { ;(stdout as typeof process.stdout).isTTY = true }
const stderr = new Writable({ write(chunk, __, callback) { if (std === 'inherit') { process.stderr.write(chunk.toString()) } callback() }, })
// "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
return { stdin, stdout, stderr, }}
export async function resolveTestConfig( config: RunVitestConfig, runnerOptions: VitestRunnerCLIOptions = {},) { const { stdout, stderr, stdin } = createConsole(runnerOptions)
const cli = new Cli({ stdin, stdout, stderr, preserveAnsi: runnerOptions.preserveAnsi }) const { $cliOptions, $viteConfig, ...restConfig } = config const harness = new PluginHarness(new Logger(stdout, stderr)) const resolved = await resolveConfig( { config: false, ...$cliOptions }, { ...$viteConfig, test: restConfig }, harness, ) return { config: resolved, vitest: cli, stdout: cli.stdout, stderr: cli.stderr, }}
/** * The config is assumed to be the config on the file system, not CLI options * (Note that CLI only options like "standalone" are passed as CLI options, not config options) * - To pass options as CLI, provide `$cliOptions` in the config object. * - To pass other Vite config properties, provide `$viteConfig` in the config object. * * **WARNING** * If the fixture in `root` has a config file, its options **WILL TAKE PRIORITY** over the ones provided here, * except for the ones provided in `$cliOptions`. */export async function runVitest( config: RunVitestConfig, cliFilters: string[] = config.$cliFilters || [], runnerOptions: VitestRunnerCLIOptions = {},) { // Reset possible previous runs process.exitCode = 0 let exitCode = process.exitCode
if (runnerOptions.printExitCode) { globalThis.process = new Proxy(process_, { set(target, p, newValue, receiver) { if (p === 'exitCode') { // oxlint-disable-next-line no-console console.trace('exitCode was set to', newValue) } return Reflect.set(target, p, newValue, receiver) }, }) }
// Prevent possible process.exit() calls, e.g. from --browser const exit = process.exit process.exit = (() => {}) as never
const { stdout, stderr, stdin } = createConsole(runnerOptions)
const cli = new Cli({ stdin, stdout, stderr, preserveAnsi: runnerOptions.preserveAnsi }) // @ts-expect-error not typed global const currentConfig: SerializedConfig = __vitest_worker__.ctx.config
let ctx: Vitest | undefined let thrown = false
const { reporters, root, watch, maxWorkers, // #region cli-only options config: configFile, standalone, dom, related, mode, changed, shard, project, cliExclude, clearScreen, mergeReports, clearCache, // #endregion $cliOptions: cliOptions, $viteConfig: viteConfig = {}, ...rest } = config
if ((viteConfig as any).test) { throw new Error( `Don't pass down "viteConfig" with "test" property. Use the rest of the first argument.`, ) }
// Don't let unrelated package.json / config edits made by other tests running // in parallel force-rerun this test's watcher. Applied as a default here so a // fixture config or an explicit `forceRerunTriggers` still takes precedence. rest.forceRerunTriggers ??= []
;(viteConfig as any).test = rest
try { ctx = await startVitest( cliFilters, removeUndefinedValues({ root, config: configFile, standalone, dom, related, mode, changed, shard, project, cliExclude, clearScreen, mergeReports, clearCache, cache: 'cache' in config ? config.cache : false,
// Test cases are already run with multiple forks/threads maxWorkers: maxWorkers ?? 1,
watch: 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'] }), ...cliOptions, env: { NO_COLOR: 'true', FORCE_COLOR: undefined, AI_AGENT: '', ...rest.env, ...cliOptions?.env, }, // override cache config with the one that was used to run `vitest` from the CLI fsModuleCache: rest.fsModuleCache ?? currentConfig.fsModuleCache, experimental: { // keep performance hints out of captured test output unless a test opts in diagnostics: rest.experimental?.diagnostics ?? false, ...cliOptions?.experimental, }, }), { ...viteConfig, plugins: [ ...(viteConfig.plugins ?? []), // Spawning the worker is the dominant cost of these meta-tests (each `runVitest` // boots a fresh Vitest). Default the spawned run to `threads`, which is cheaper // to start than `forks`, especially on Windows. A `config` hook only fills the // gap when nothing else set a pool, so an explicit `pool` (from `config` or the // fixture's own config file) always wins. Browser runs are left alone since they // don't execute in a node pool. { name: 'vitest:test-utils:default-pool', config(config) { if (config.test?.pool == null && !config.test?.browser?.enabled) { return { test: { pool: 'threads' } } } }, }, ], server: { watch: { // During tests we edit the files too fast and sometimes chokidar // misses change events, so enforce polling for consistency // https://github.com/vitejs/vite/blob/b723a753ced0667470e72b4853ecda27b17f546a/playground/vitestSetup.ts#L211 usePolling: true, interval: WATCH_POLL_INTERVAL, ...viteConfig.server?.watch, }, ...viteConfig?.server, }, }, { stdin, stdout, stderr, }, ) } catch (e: any) { if (runnerOptions.fails !== true) { console.error(e) } thrown = true cli.stderr += inspect(e) } finally { if (runnerOptions.printExitCode) { globalThis.process = process_ } exitCode = process.exitCode process.exitCode = 0
// tests emulating CLI shortcuts (`q`, double CTRL+C) trigger `vitest.exit()`, // which arms an unref'd force-exit watchdog; it must be disarmed before the // real `process.exit` is restored, or it would kill this worker // `teardownTimeout` later, in the middle of a subsequent test file if (TestRunner.getCurrentTest()) { onTestFinished(async () => { clearTimeout(ctx?._exitTimeout) await ctx?.close() process.exit = exit }) } else { afterEach(async () => { clearTimeout(ctx?._exitTimeout) await ctx?.close() process.exit = exit }) } }
// In watch mode, `startVitest` can resolve before the file watcher finished // its initial scan (especially in standalone mode, which doesn't run any // tests on startup). A test file created right after would be folded into the // watcher's baseline and ignored, so newly added files wouldn't trigger a // rerun. Wait for the watcher to be ready before handing control back. if (watch && ctx) { await waitForWatcherReady(ctx) }
return { thrown, ctx, exitCode, vitest: cli, stdout: cli.stdout, stderr: cli.stderr, get results() { return ctx?.state.getTestModules() || [] }, errorTree(options?: BuildErrorTreeOptions & { project?: boolean }) { const modules = ctx?.state.getTestModules() || [] const tree = options?.project ? buildErrorProjectTree(modules, options) : buildErrorTree(modules, options) const errors = ctx?.state.getUnhandledErrors() const root = ctx?.config.root if (errors && errors.length > 0 && root) { tree.__unhandled_errors__ = errors.map((e: any) => replaceRoot(e.message, root)) } return tree }, testTree() { return buildTestTree(ctx?.state.getTestModules() || []) }, buildTree(onResult: (testResult: TestCase) => any) { return buildTestTree(ctx?.state.getTestModules() || [], onResult) }, waitForClose: async () => { await new Promise<void>((resolve) => ctx!.onClose(resolve)) return ctx?.closingPromise }, }}
// In watch mode `startVitest` can resolve before the file watcher has finished// establishing itself. chokidar's `ready`/`_readyEmitted` fires after the// initial scan, but with polling a file only gets a per-file stat poller some// time after it is discovered, and an edit made before the poller exists is// folded into its baseline stat and never emits an event. No amount of chokidar// bookkeeping (`_readyEmitted`, `getWatched()`) proves the pollers are live, so// verify end-to-end: keep cycling a probe file in the root until the watcher// reports it. The probe is recreated rather than rewritten because a creation// folded into the initial scan of the file AND its directory leaves nothing to// rescan; alternating present/absent guarantees the current state differs from// whichever state the poller's baseline captured, and any probe event (`add`,// `change` or `unlink`) proves delivery. Each state is held for multiple poll// intervals: on a loaded runner, a cycle as fast as the poll interval aliases// with it — every rescan can land when the probe is back in the state matching// the poller's baseline, and no event ever fires.//// The scan can also surface writes made by the PREVIOUS test (an `afterEach`// restoring a fixture right before this instance started) as fresh change// events, triggering a rerun the test never asked for. While waiting, neuter// every rerun attempt by clearing the watcher state (the rerun debounce// returns silently when `changedTests` is empty) and only hand control back// after the event backlog has been quiet for a couple of poll intervals.//// Only instances with an explicit small root (a fixture or an inline-test dir)// get the probe: tests that assert on watch reruns always use one. Instances// rooted at the whole test package (config-introspection tests passing// `watch: true` without a root) never wait for file events, and polling their// huge tree can take longer than any reasonable deadline, so for them only the// cheap bookkeeping check runs, as before.async function waitForWatcherReady(ctx: Vitest): Promise<void> { const watcher: FSWatcher = ctx.vite.watcher const root = ctx.config.root const slash = (p: string) => p.replace(/\\/g, '/') const parent = slash(dirname(root)) const bookkeepingDeadline = Date.now() + 2000
while (Date.now() < bookkeepingDeadline) { const isReady = (watcher as { _readyEmitted?: boolean })._readyEmitted const watchesParent = Object.keys(watcher.getWatched()).some((dir) => slash(dir) === parent) if (isReady && watchesParent) { break } await new Promise((resolve) => setTimeout(resolve, 10)) }
if (slash(root) === slash(process.cwd())) { return }
const deadline = Date.now() + 10_000 const probe = resolve(root, '.vitest-watcher-ready-probe') const suppressRerun = () => { ctx.watcher.changedTests.clear() ctx.watcher.invalidates.clear() } let probeSeen = false let lastEventAt = Date.now() const onWatcherEvent = (_event: string, file: string) => { suppressRerun() if (slash(file) === probe) { probeSeen = true } else { lastEventAt = Date.now() } } watcher.on('all', onWatcherEvent) try { const holdProbeState = async () => { const stateEnd = Date.now() + WATCH_POLL_INTERVAL * 2.5 while (Date.now() < stateEnd) { if (probeSeen) { return } await new Promise((resolve) => setTimeout(resolve, 25)) } } while (true) { if (probeSeen) { break } if (Date.now() > deadline) { throw new Error( `The watcher of ${root} did not report the probe file within 10s. Watched files:\n${JSON.stringify( watcher.getWatched(), null, 2, )}`, ) } fs.writeFileSync(probe, `${Date.now()}`, 'utf-8') await holdProbeState() if (probeSeen) { break } fs.rmSync(probe, { force: true }) await holdProbeState() }
while (Date.now() - lastEventAt < 200 && Date.now() < deadline) { await new Promise((resolve) => setTimeout(resolve, 50)) } } finally { watcher.off('all', onWatcherEvent) fs.rmSync(probe, { force: true }) suppressRerun() }}
interface CliOptions extends Partial<Options> { earlyReturn?: boolean preserveAnsi?: boolean}
async function runCli(command: 'vitest', _options?: CliOptions | string, ...args: string[]) { let options = _options
if (typeof _options === 'string') { args.unshift(_options) options = undefined }
if (command === 'vitest') { args.push('--maxWorkers=1') }
const subprocess = x(command, args, { ...(options as Options), nodeOptions: { ...(options as Options)?.nodeOptions, env: { ...process.env, AI_AGENT: '', ...(options as Options)?.nodeOptions?.env }, }, }).process! const cli = new Cli({ stdin: subprocess.stdin!, stdout: subprocess.stdout!, stderr: subprocess.stderr!, preserveAnsi: typeof _options !== 'string' ? _options?.preserveAnsi : false, })
let setDone: (value?: unknown) => void const isDone = new Promise((resolve) => (setDone = resolve)) subprocess.on('exit', () => setDone())
function output() { return { vitest: cli, exitCode: subprocess.exitCode, stdout: cli.stdout || '', stderr: cli.stderr || '', waitForClose: () => isDone, } }
// Manually stop the processes so that each test don't have to do this themselves onTestFinished(async () => { if (subprocess.exitCode === null) { subprocess.kill() }
await isDone })
if ( (options as CliOptions)?.earlyReturn || args.includes('--inspect') || args.includes('--inspect-brk') ) { return output() }
if (args[0] === 'init') { return output() }
if (args[0] !== 'list' && (args.includes('--watch') || args[0] === 'watch')) { if (command === 'vitest') { // Waiting for either success or failure await Promise.race([ cli.waitForStdout('Waiting for file changes'), cli.waitForStdout('Tests failed. Watching for file changes'), ]) } // make sure watcher is ready await cli.waitForStdout('[debug] watcher is ready') cli.stdout = cli.stdout.replace('[debug] watcher is ready\n', '') } else { await isDone }
return output()}
export async function runVitestCli(_options?: CliOptions | string, ...args: string[]) { process.env.VITE_TEST_WATCHER_DEBUG = 'true' return runCli('vitest', _options, ...args)}
export function getInternalState(): WorkerGlobalState { // @ts-expect-error untyped global return globalThis.__vitest_worker__}
const originalFiles = new Map<string, string>()const originalFileStats = new Map<string, fs.Stats>()
export function createFile(file: string, content: string) { fs.mkdirSync(dirname(file), { recursive: true }) fs.writeFileSync(file, content, 'utf-8') onTestFinished(() => { if (fs.existsSync(file)) { fs.unlinkSync(file) } })}
export function editFile(file: string, callback: (content: string) => string) { const content = fs.readFileSync(file, 'utf-8') if (!originalFiles.has(file)) { originalFiles.set(file, content) originalFileStats.set(file, fs.statSync(file)) } fs.writeFileSync(file, callback(content), 'utf-8') onTestFinished(() => { const original = originalFiles.get(file) if (original !== undefined) { restoreFile(file, original, originalFileStats.get(file)) originalFiles.delete(file) originalFileStats.delete(file) } })}
// Restore the original mtime along with the content: a restore with a fresh// mtime is indistinguishable from a real edit to any watcher whose stat// baseline predates it, and the phantom change event then reruns tests in the// NEXT test's instance. With content, size and mtime all matching the// pre-test state, no stat comparison can report the file as changed.export function restoreFile(file: string, content: string, stat?: fs.Stats) { fs.writeFileSync(file, content, 'utf-8') if (stat) { fs.utimesSync(file, stat.atime, stat.mtime) }}
export function resolvePath(baseUrl: string, path: string) { const filename = fileURLToPath(baseUrl) return resolve(dirname(filename), path)}
export type TestFsStructure = Record< string, | string | ViteUserConfig | TestProjectConfiguration[] | ((...args: any[]) => unknown) | [(...args: any[]) => unknown, { exports?: string[]; imports?: Record<string, string[]> }]>
export function stripIndent(str: string): string { const normalized = str.replace(/\t/g, ' ') const match = normalized.match(/^[ \t]*(?=\S)/gm) if (!match) { return normalized } const indent = match .filter((m) => !!m) .reduce((min, line) => Math.min(min, line.length), Infinity) if (indent === 0) { return normalized } return normalized.replace(new RegExp(`^[ ]{${indent}}`, 'gm'), '')}
function getGeneratedFileContent(content: TestFsStructure[string]) { if (typeof content === 'string') { return content } if (typeof content === 'function') { const code = `await (${stripIndent(String(content))})()` return code } if ( Array.isArray(content) && typeof content[1] === 'object' && ('exports' in content[1] || 'imports' in content[1]) ) { const imports = Object.entries(content[1].imports || []) const code = `${imports.map(([path, is]) => `import { ${is.join(', ')} } from '${path}'`)}const results = await (${stripIndent(String(content[0]))})({ ${imports.flatMap(([_, is]) => is).join(', ')} })${(content[1].exports || []).map((e) => `export const ${e} = results["${e}"]`)} ` return code } if ( 'test' in content && content.test?.browser?.enabled && content.test?.browser?.provider?.name ) { const name = content.test.browser.provider.name return `import { ${name} } from '@vitest/browser-${name}'const config = ${JSON.stringify(content)}config.test.browser.provider = ${name}(${JSON.stringify(content.test.browser.provider.options || {})})export default config ` } return `export default ${JSON.stringify(content)}`}
export function useTmpFS<T extends TestFsStructure>( structure: T, ensureConfig = true, task?: TestContext['task'],) { const root = resolve(process.cwd(), `vitest-test-${crypto.randomUUID()}`) return useFS(root, structure, ensureConfig, task)}
export function useFS<T extends TestFsStructure>( root: string, structure: T, ensureConfig = true, task?: TestContext['task'],) { const files = new Set<string>() const hasConfig = Object.keys(structure).some((file) => file.includes('.config.')) if (ensureConfig && !hasConfig) { ;(structure as any)['./vitest.config.js'] = {} } for (const file in structure) { const filepath = resolve(root, file) files.add(filepath) const content = getGeneratedFileContent(structure[file]) fs.mkdirSync(dirname(filepath), { recursive: true }) fs.writeFileSync(filepath, String(content), 'utf-8') } ;(task?.context.onTestFinished ?? onTestFinished)(() => { if (process.env.VITEST_FS_CLEANUP !== 'false') { fs.rmSync(root, { recursive: true, force: true }) } }) task?.context.signal.addEventListener('abort', () => { if (process.env.VITEST_FS_CLEANUP !== 'false') { fs.rmSync(root, { recursive: true, force: true }) } }) return { root, readdir: (file: string): string[] => { return fs.readdirSync(resolve(root, file)) }, readFile: (file: string): string => { const filepath = resolve(root, file) if (relative(root, filepath).startsWith('..')) { throw new Error(`file ${file} is outside of the test file system`) } return fs.readFileSync(filepath, 'utf-8') }, editFile: (file: string, callback: (content: string) => string) => { const filepath = resolve(root, file) if (!files.has(filepath)) { throw new Error(`file ${file} is outside of the test file system`) } const content = fs.readFileSync(filepath, 'utf-8') fs.writeFileSync(filepath, callback(content)) }, createFile: (file: string, content: string) => { if (file.startsWith('..')) { throw new Error(`file ${file} is outside of the test file system`) } const filepath = resolve(root, file) if (files.has(filepath)) { throw new Error(`file ${file} already exists in the test file system`) } files.add(filepath) createFile(filepath, content) }, statFile: (file: string): fs.Stats => { const filepath = resolve(root, file)
if (relative(root, filepath).startsWith('..')) { throw new Error(`file ${file} is outside of the test file system`) }
return fs.statSync(filepath) }, resolveFile: (file: string): string => { return resolve(root, file) }, renameFile: (oldFile: string, newFile: string) => { const oldFilepath = resolve(root, oldFile) const newFilepath = resolve(root, newFile) return fs.renameSync(oldFilepath, newFilepath) }, }}
export async function runInlineTests( structure: TestFsStructure, config?: RunVitestConfig, options?: VitestRunnerCLIOptions, task?: TestContext['task'],) { const fs = useTmpFS(structure, undefined, task ?? TestRunner.getCurrentTest()) const vitest = await runVitest( { root: fs.root, ...config, }, config?.$cliFilters ?? [], options, ) return { fs, root: fs.root, ...vitest, get results() { return vitest.ctx?.state.getTestModules() || [] }, testTree() { return buildTestTree(vitest.ctx?.state.getTestModules() || []) }, buildTree(onResult: (testResult: TestCase) => any) { return buildTestTree(vitest.ctx?.state.getTestModules() || [], onResult) }, }}
const isWindows = process.platform === 'win32'
export function replaceRoot(string: string, root: string) { const schemaRoot = root.startsWith('file://') ? root : pathToFileURL(root).toString() if (!root.endsWith('/') && !isWindows) { root += '?/' } if (!isWindows) { return string .replace(new RegExp(schemaRoot, 'g'), '<urlRoot>') .replace(new RegExp(root, 'g'), '<root>/') } let unixRoot = root.replace(/\\/g, '/') let win32Root = root.replaceAll('/', '\\\\') if (!root.endsWith('/') && !root.endsWith('\\')) { unixRoot += '?/' win32Root += '?\\\\' }
return string .replace(new RegExp(schemaRoot, 'gi'), '<urlRoot>') .replace(new RegExp(unixRoot, 'gi'), '<root>/') .replace(new RegExp(win32Root, 'gi'), '<root>/')}
export const ts = String.raw
export class StableTestFileOrderSorter { sort(files: TestSpecification[]) { return files.sort((a, b) => a.moduleId.localeCompare(b.moduleId)) }
shard(files: TestSpecification[]) { return files }}
export interface BuildErrorTreeOptions { stackTrace?: boolean diff?: boolean fileLabel?: boolean}
export function buildErrorTree(testModules: TestModule[], options?: BuildErrorTreeOptions) { const root = testModules[0]?.project.config.root
function mapError(e: { message: string; diff?: string; stacks?: ParsedStack[] }) { let message = stripVTControlCharacters(e.message) if (root) { message = replaceRoot(message, root) } if (options?.diff && e.diff) { message = [message, stripVTControlCharacters(e.diff)].join('\n') } if (options?.stackTrace) { const stacks = (e.stacks || []).map((s) => { const loc = `${relative(root, s.file)}:${s.line}:${s.column}` return s.method ? ` at ${s.method} (${loc})` : ` at ${loc}` }) message = [message, ...stacks].join('\n') } return message }
return buildTestTree( testModules, (testCase) => { const result = testCase.result() if (result.state === 'failed') { return result.errors.map((e) => mapError(e)) } return result.state }, (testSuite, suiteChildren) => { const errors = testSuite.errors() if (errors.length > 0) { return { ...suiteChildren, __suite_errors__: errors.map((e) => mapError(e)), } } return suiteChildren }, (testModule, moduleChildren) => { const errors = testModule.errors() if (errors.length > 0) { return { ...moduleChildren, __module_errors__: errors.map((e) => mapError(e)), } } return moduleChildren }, options?.fileLabel ? (module) => `${module.relativeModuleId} (${module.meta().__vitest_label__})` : undefined, )}
export function buildTestTree( testModules: TestModule[], onTestCase?: (result: TestCase) => unknown, onTestSuite?: (testSuite: TestSuite, suiteChildren: Record<string, any>) => unknown, onTestModule?: (testModule: TestModule, moduleChildren: Record<string, any>) => unknown, onTestModuleKey?: (testModule: TestModule) => string,) { type TestTree = Record<string, any>
function walkCollection(collection: TestCollection): TestTree { const node: TestTree = {}
for (const child of collection) { if (child.type === 'suite') { // Recursively walk suite children const suiteChildren = walkCollection(child.children) node[child.name] = onTestSuite ? onTestSuite(child, suiteChildren) : suiteChildren } else if (child.type === 'test') { const result = child.result() if (onTestCase) { node[child.name] = onTestCase(child) } else { node[child.name] = result.state } } }
return node }
const tree: TestTree = {}
for (const module of testModules) { // Use relative module ID for cleaner output const key = onTestModuleKey ? onTestModuleKey(module) : module.relativeModuleId const moduleChildren = walkCollection(module.children) tree[key] = onTestModule ? onTestModule(module, moduleChildren) : moduleChildren }
return tree}
export function buildTestProjectTree( testModules: TestModule[], onTestCase?: (result: TestCase) => unknown,) { const projectTree: Record<string, Record<string, any>> = {}
for (const testModule of testModules) { const projectName = testModule.project.name projectTree[projectName] = { ...projectTree[projectName], ...buildTestTree([testModule], onTestCase), } }
return projectTree}
export function buildErrorProjectTree(testModules: TestModule[], options?: BuildErrorTreeOptions) { const projectTree: Record<string, Record<string, any>> = {}
for (const testModule of testModules) { const projectName = testModule.project.name projectTree[projectName] = { ...projectTree[projectName], ...buildErrorTree([testModule], options), } }
return projectTree}
function removeUndefinedValues<T extends Record<string, any>>(obj: T): T { for (const key of Object.keys(obj)) { if (obj[key] === undefined) { delete obj[key] } } return obj}