Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/vitest-dev/vscode. VS Code extension for Vitest vitest.dev/vscode
testing vitest vscode
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389import type { SerializedProject, WorkerReadyMetadata } from 'vitest-vscode-shared'import type { VitestPackage } from './spawn/pkg'import { usesJestTestNamePattern } from './spawn/pkg'import type { ExtensionWorkerEvents, VitestExtensionRPC } from './spawn/rpc'import type { ExtensionWorkerProcess } from './spawn/types'import type { ProcessSpawnOptions } from './spawn/ws'import type { TestFileMetadata } from './testTreeData'import { readFileSync } from 'node:fs'import { normalize, relative } from 'pathe'import pm from 'picomatch'import { createQueuedHandler } from 'vitest-vscode-shared'import * as vscode from 'vscode'import { getConfig } from './config'import { log } from './log'import { createVitestProcess } from './spawn/child_process'import { createVitestTerminalProcess } from './spawn/terminal'
export class VitestProjectConfig { readonly id: string readonly tag: vscode.TestTag readonly workspaceFolder: vscode.WorkspaceFolder
constructor( readonly pkg: VitestPackage, readonly metadata: WorkerReadyMetadata, ) { this.id = normalize(pkg.id) this.workspaceFolder = pkg.folder this.tag = new vscode.TestTag(pkg.prefix) }
get prefix() { return this.pkg.prefix }
get cwd() { return this.pkg.cwd }
get configs() { return this.projects.map((p) => p.config).filter((n) => n != null) }
get projects(): SerializedProject[] { return this.metadata.projects }
get workspaceSource() { return this.metadata.workspaceSource }
get version() { return this.pkg.version }
get usesJestTestNamePattern() { return usesJestTestNamePattern(this.pkg) }
get htmlReportPath() { return this.metadata.htmlReportPath }
get package() { return this.pkg }
getPotentialTestFileMetadata(file: string): TestFileMetadata[] { const metadata: TestFileMetadata[] = [] let fileContent: string for (const project of this.projects) { if ( this.matchesTestGlob(project, file, () => (fileContent ??= readFileSync(file, 'utf-8'))) ) { metadata.push({ pool: project.pool, project: project.name, browser: project.browser, }) } } return metadata }
// TODO: if dir is set, this doesn't seem to work properly matchesTestGlob(project: SerializedProject, moduleId: string, source: () => string) { const relativeId = relative(project.dir || project.root, moduleId) if (pm.isMatch(relativeId, project.exclude)) { return false } if (pm.isMatch(relativeId, project.include)) { return true } if (project.includeSource?.length && pm.isMatch(relativeId, project.includeSource)) { const code = source() if (code.includes('import.meta.vitest')) { return true } } return false }}
export class VitestProcessAPI { readonly config: VitestProjectConfig
// Listeners for collection results (registered by testTree) private collectionListeners: ExtensionWorkerEvents['onCollected'][] = []
// Currently active process (for cancellation, continuous run) private currentMeta: ResolvedMeta | undefined private _spawningPersistentProcess = false private _pendingFileChanges: string[] = []
constructor(config: VitestProjectConfig) { this.config = config }
/** * Create a VitestFolderAPI for debug sessions where the process is * already spawned by the debug launcher. spawnForRun() will return * a handle wrapping the existing process (without closing it). */ static forDebug(pkg: VitestPackage, meta: ResolvedMeta): VitestProcessAPI { const config = new VitestProjectConfig(pkg, meta.metadata) const api = new VitestProcessAPI(config) api.currentMeta = meta return api }
// --- Delegated from config (local, no process needed) ---
get id() { return this.config.id }
get tag() { return this.config.tag }
get workspaceFolder() { return this.config.workspaceFolder }
get prefix() { return this.config.prefix }
get configs() { return this.config.configs }
get workspaceSource() { return this.config.workspaceSource }
get package() { return this.config.package }
get usesJestTestNamePattern() { return this.config.usesJestTestNamePattern }
getPersistentProcessMeta() { return this.currentMeta }
get isSpawningPersistentProcess() { return this._spawningPersistentProcess }
getPotentialTestFileMetadata(file: string): TestFileMetadata[] { return this.config.getPotentialTestFileMetadata(file) }
matchesTestGlob(project: SerializedProject, moduleId: string, source: () => string) { return this.config.matchesTestGlob(project, moduleId, source) }
// --- Collection (on-demand, batched ~300ms) ---
onCollected(callback: ExtensionWorkerEvents['onCollected']) { this.collectionListeners.push(callback) }
collectTests(projectName: string, testFile: string) { return this._collectTests(`${projectName}\0${normalize(testFile)}`) }
private _collectTests = createQueuedHandler(async (testsQueue: string[]) => { const tests = testsQueue.map((spec) => { const [projectName, filepath] = spec.split('\0', 2) return [projectName, filepath] as [string, string] }) const root = this.workspaceFolder.uri.fsPath log.info( '[API]', `Collecting tests: ${tests.map((t) => `${relative(root, t[1])}${t[0] ? ` [${t[0]}]` : ''}`).join(', ')}`, ) const projects = [...new Set(tests.map(([projectName]) => projectName))] try { // TODO make sure errors are reported during collection (throw error in the config, for example) await withProcess( this.config.pkg, async (meta) => { meta.handlers.onCollected((file, collecting) => { for (const listener of this.collectionListeners) { listener(file, collecting) } }) await meta.rpc.collectTests(tests) }, { projects }, ) } catch (err) { log.error('[API]', 'Collection failed:', err) } }, 300)
// --- Running (on-demand, spawned per run) ---
/** * Spawn a process for running tests. The caller (TestRunner) manages * event wiring and lifecycle. Returns a RunHandle. */ async spawnForRun(options?: ProcessSpawnOptions): Promise<RunHandle> { // For debug sessions, the process is already spawned — return a non-closing handle if (this.currentMeta && !this.currentMeta.process.closed) { const meta = this.currentMeta return { rpc: meta.rpc, process: meta.process, handlers: meta.handlers, async dispose() { // Debug process lifecycle is managed by the debug session, not by us }, } } this._spawningPersistentProcess = true const meta = await spawnVitestProcess(this.config.pkg, options).finally(() => { this._spawningPersistentProcess = false }) this.currentMeta = meta if (this._pendingFileChanges.length) { const pending = this._pendingFileChanges this._pendingFileChanges = [] meta.rpc.onFilesChanged(pending).catch((err) => { log.error('[API]', 'Failed to notify Vitest about pending file changes', err) }) } return { rpc: meta.rpc, process: meta.process, handlers: meta.handlers, dispose: async () => { this.currentMeta = undefined await meta.dispose().catch((err) => { log.error('[API]', 'Failed to close Vitest process', err) }) }, } }
async cancelRun() { if (!this.currentMeta || this.currentMeta.process.closed) return await this.currentMeta.rpc.cancelRun() }
// --- Module diagnostics (from run process, before closing) ---
async getSourceModuleDiagnostic(moduleId: string) { if (!this.currentMeta || this.currentMeta.process.closed) return { modules: [], untrackedModules: [] } return this.currentMeta.rpc.getSourceModuleDiagnostic(normalize(moduleId)) }
async getModuleEnvironments(moduleId: string) { if (!this.currentMeta || this.currentMeta.process.closed) return [] return this.currentMeta.rpc.getModuleEnvironments(normalize(moduleId)) }
async getTransformedModule(project: string, environment: string, moduleId: string) { if (!this.currentMeta || this.currentMeta.process.closed) { return null } return this.currentMeta.rpc.getTransformedModule(project, environment, normalize(moduleId)) }
onFileChanged = createQueuedHandler(async (files: string[]) => { if (this._spawningPersistentProcess) { this._pendingFileChanges.push(...files) return } if (!this.currentMeta || this.currentMeta.process.closed) { return } return this.currentMeta.rpc.onFilesChanged(files.map((f) => normalize(f))).catch((err) => { log.error('[API]', 'Failed to notify Vitest about file change', err) }) })
// --- Cleanup ---
async dispose() { delete require.cache[this.config.pkg.vitestPackageJsonPath] delete require.cache[this.config.pkg.vitestNodePath] if (this.currentMeta && !this.currentMeta.process.closed) { await this.currentMeta.dispose().catch((err) => { log.error('[API]', 'Failed to close Vitest process', err) }) } this.currentMeta = undefined this.collectionListeners = [] }}
export interface RunHandle { rpc: VitestExtensionRPC process: ExtensionWorkerProcess handlers: ResolvedMeta['handlers'] dispose: () => Promise<void>}
export interface RunHandlers { onCollected: ExtensionWorkerEvents['onCollected'] onTaskUpdate: ExtensionWorkerEvents['onTaskUpdate'] onTestRunStart: ExtensionWorkerEvents['onTestRunStart'] onTestRunEnd: ExtensionWorkerEvents['onTestRunEnd'] onConsoleLog: ExtensionWorkerEvents['onConsoleLog']}
export interface ResolvedMeta { rpc: VitestExtensionRPC metadata: WorkerReadyMetadata process: ExtensionWorkerProcess pkg: VitestPackage handlers: { onProcessLog: (listener: ExtensionWorkerEvents['onProcessLog']) => void onConsoleLog: (listener: ExtensionWorkerEvents['onConsoleLog']) => void onTaskUpdate: (listener: ExtensionWorkerEvents['onTaskUpdate']) => void onTestRunEnd: (listener: ExtensionWorkerEvents['onTestRunEnd']) => void onTestRunStart: (listener: ExtensionWorkerEvents['onTestRunStart']) => void onCollected: (listener: ExtensionWorkerEvents['onCollected']) => void clearListeners: () => void removeListener: (name: string, listener: any) => void } /** * Closes vitest process, will force exit with timeout, stops the WS server. */ dispose: () => Promise<void>}
export function spawnVitestProcess( pkg: VitestPackage, options?: ProcessSpawnOptions,): Promise<ResolvedMeta> { const config = getConfig(pkg.folder) if (config.cliArguments && !pkg.arguments) { pkg.arguments = `vitest ${config.cliArguments}` } const projects = options?.projects?.join(', ') if (projects) { log.verbose?.('[API]', `Filtering projects: ${projects}`) } return config.shellType === 'terminal' ? createVitestTerminalProcess(pkg, options) : createVitestProcess(pkg, options)}
export async function withProcess<T>( pkg: VitestPackage, fn: (meta: ResolvedMeta) => Promise<T>, options?: ProcessSpawnOptions,): Promise<T> { log.verbose?.('[API]', 'Spawning on-demand process...') const start = performance.now() const meta = await spawnVitestProcess(pkg, options) try { return await fn(meta) } finally { await meta.dispose().catch((err) => { log.error('[API]', 'Failed to close Vitest process', err) }) const duration = Math.round(performance.now() - start) log.verbose?.('[API]', `On-demand process finished in ${duration}ms`) }}