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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658import type { VitestAPI } from './api'import type { VitestProcessAPI } from './apiProcess'import { basename, normalize, relative } from 'pathe'import * as vscode from 'vscode'import { version } from '../../../package.json'import { resolveVitestAPI } from './api'import { copyOutput, copyTestItemErrors } from './commands/copyOutput'import { getConfig, testControllerId } from './config'import { configGlob, workspaceGlob } from './constants'import { coverageContext } from './coverage'import { DebugManager, debugTests } from './debug'import { ExtensionDiagnostic } from './diagnostic'import { ImportsBreakdownProvider } from './importsBreakdownProvider'import { log } from './log'import { RunQueue } from './runQueue'import { TransformSchemaProvider } from './schemaProvider'import { resolveVitestPackages } from './spawn/pkg'import { ExtensionTerminalProcess } from './spawn/terminal'import { ExtensionState } from './state'import { TagsManager } from './tagsManager'import { TestTree } from './testTree'import { TraceViewManager } from './traceView'import { getTestData, TestFile } from './testTreeData'import { debounce, showVitestError } from './utils'import './polyfills'import { SnapshotEntryTool } from './snapshot/tools'import { SnapshotDocumentSymbolProvider } from './snapshot/documentSymbolProvider'import { SnapshotFoldingRangeProvider } from './snapshot/foldingRangeProvider'
export async function activate(context: vscode.ExtensionContext) { const extension = new VitestExtension(context) context.subscriptions.push(extension) await extension.activate()}
export function deactivate() {}
class VitestExtension { private testController: vscode.TestController private loadingTestItem: vscode.TestItem
private runProfiles = new Map<string, vscode.TestRunProfile>()
private testTree: TestTree private tagsManager: TagsManager private api: VitestAPI | undefined
private runQueues = new Map<string, RunQueue>() private state: ExtensionState
private disposables: vscode.Disposable[] = [] private diagnostic: ExtensionDiagnostic | undefined private debugManager: DebugManager private schemaProvider: TransformSchemaProvider private importsBreakdownProvider: ImportsBreakdownProvider private traceViewManager = new TraceViewManager()
/** @internal */ _debugDisposable: vscode.Disposable | undefined
constructor(context: vscode.ExtensionContext) { log.info( `[v${version}] Vitest extension is activated because Vitest is installed or there is a Vite/Vitest config file in the workspace.`, )
this.state = new ExtensionState(context) this.testController = vscode.tests.createTestController(testControllerId, 'Vitest') this.testController.refreshHandler = (cancelToken) => this.defineTestProfiles(true, cancelToken).catch((err) => { showVitestError('Failed to refresh Vitest', err) }) this.testController.resolveHandler = (item) => this.resolveTestFile(item) this.loadingTestItem = this.testController.createTestItem('_resolving', 'Resolving Vitest...') this.loadingTestItem.sortText = '.0' // show it first this.schemaProvider = new TransformSchemaProvider(async (apiId, project, environment, file) => { const api = this.api?.processes.find((a) => a.id === apiId) return api?.getTransformedModule(project, environment, file) ?? null }) this.tagsManager = new TagsManager() this.testTree = new TestTree( this.testController, this.loadingTestItem, this.tagsManager, this.schemaProvider, ) this.debugManager = new DebugManager() this.importsBreakdownProvider = new ImportsBreakdownProvider( async (moduleId: string) => this.api?.getSourceModuleDiagnostic(moduleId) || { modules: [], untrackedModules: [], }, ) }
private _defineTestProfilePromise: Promise<void> | undefined
private async defineTestProfiles(showWarning: boolean, cancelToken?: vscode.CancellationToken) { if (!this._defineTestProfilePromise) { this._defineTestProfilePromise = (() => this._defineTestProfiles(showWarning, cancelToken))().finally(() => { this._defineTestProfilePromise = undefined }) } return await this._defineTestProfilePromise }
private async _defineTestProfiles(showWarning: boolean, cancelToken?: vscode.CancellationToken) { this.importsBreakdownProvider.clear() this.testTree.reset([]) this.traceViewManager.clear() this.runQueues.forEach((q) => q.dispose()) this.runQueues.clear()
const { workspaces, configs } = await resolveVitestPackages(showWarning)
if (cancelToken?.isCancellationRequested) { return }
if (!workspaces.length && !configs.length) { log.error('[API]', 'Failed to start Vitest: No vitest config files found') this.testController.items.delete(this.loadingTestItem.id)
await this.api?.dispose() return }
const folders = new Set([...workspaces, ...configs].map((x) => x.folder)) this.testTree.reset([...folders])
const previousRunProfiles = this.runProfiles this.runProfiles = new Map()
try { await this.api?.dispose()
if (cancelToken?.isCancellationRequested) { return }
for (const [_, profile] of previousRunProfiles) { profile.dispose() }
this.api = await resolveVitestAPI( workspaces, configs, cancelToken, ({ api: vitest, files }) => { if (this.state.hasDisabledConfigs() && this.state.isConfigDisabled(vitest.id)) { return }
this.testTree.watchTestFilesInWorkspace(vitest, files) this.setupProcessAPI(vitest)
this.testController.items.forEach((item) => { if (item.children.size) { item.busy = false } }) }, ) } catch (err) { this.testTree.reset([]) showVitestError('Failed to start Vitest', err) return } finally { this.testController.items.delete(this.loadingTestItem.id) }
this.api.processes.forEach((process) => { const config = getConfig(process.workspaceFolder) if (config.watchOnStartup) { const profile = this.runProfiles.get(`${process.id}:run`) if (profile) { vscode.commands.executeCommand('testing.startContinuousRun', profile) } } })
// collect tests inside a test file vscode.window.visibleTextEditors.forEach(async (editor) => { const testItems = this.testTree.getFileTestItems(editor.document.uri.fsPath) const promises = [] for (const item of testItems) { const data = getTestData(item) as TestFile if (data instanceof TestFile) { promises.push(this.resolveTestFile(item)) } } await Promise.all(promises).catch((err) => { log.error('Failed to collect tests from visible text editors', err) }) }) }
private setupProcessAPI(vitest: VitestProcessAPI) { // Register collection listener so test tree gets notified when tests are collected vitest.onCollected((file) => { this.testTree.collectFile(vitest, file) })
const prefix = vitest.prefix
let runProfile = this.runProfiles.get(`${vitest.id}:run`) if (!runProfile) { runProfile = this.testController.createRunProfile( prefix, vscode.TestRunProfileKind.Run, () => { log.error('Run handler is not defined') }, true, undefined, true, ) }
const runQueue = new RunQueue( this.testController, runProfile, this.testTree, vitest, this.diagnostic, this.importsBreakdownProvider, this.traceViewManager, ) const runQueueId = `${vitest.id}:run` this.runQueues.set(runQueueId, runQueue)
runProfile.tag = vitest.tag runProfile.runHandler = (request, token) => runQueue.enqueue(request, token, false) this.runProfiles.set(runQueueId, runProfile)
let debugProfile = this.runProfiles.get(`${vitest.id}:debug`) if (!debugProfile) { debugProfile = this.testController.createRunProfile( prefix, vscode.TestRunProfileKind.Debug, () => { log.error('Run handler is not defined') }, true, undefined, false, // continues debugging is not supported ) } debugProfile.tag = vitest.tag debugProfile.runHandler = async (request, token) => { await this.registerDebugOptions()
await debugTests( this.testController, this.testTree, vitest.package, this.diagnostic, this.importsBreakdownProvider,
request, token, this.debugManager, ).catch((error) => { vscode.window.showErrorMessage(error.message) }) } this.runProfiles.set(`${vitest.id}:debug`, debugProfile)
let coverageProfile = this.runProfiles.get(`${vitest.id}:coverage`) if (!coverageProfile) { coverageProfile = this.testController.createRunProfile( prefix, vscode.TestRunProfileKind.Coverage, () => { log.error('Run handler is not defined') }, true, undefined, false, // continues run with coverage is not supported because we want to keep a single running process per API ) }
const coverageQueue = new RunQueue( this.testController, coverageProfile, this.testTree, vitest, this.diagnostic, this.importsBreakdownProvider, this.traceViewManager, ) const coverageQueueId = `${vitest.id}:coverage` this.runQueues.set(coverageQueueId, coverageQueue)
coverageProfile.tag = vitest.tag coverageProfile.runHandler = (request, token) => coverageQueue.enqueue(request, token, true) coverageProfile.loadDetailedCoverage = coverageContext.loadDetailedCoverage this.runProfiles.set(coverageQueueId, coverageProfile) }
private async resolveTestFile(item?: vscode.TestItem) { if (!item) return try { await this.testTree.discoverTestsInFile(item) } catch (err) { showVitestError('There was an error during test discovery', err) } }
async activate() { this.diagnostic = getConfig().applyDiagnostic ? new ExtensionDiagnostic() : undefined
this.loadingTestItem.busy = true this.testController.items.replace([this.loadingTestItem])
const reloadConfigNames = [ 'vitest.vitestPackagePath', 'vitest.nodeExecutable', 'vitest.nodeExecArgs', 'vitest.workspaceConfig', 'vitest.rootConfig', 'vitest.shellType', 'vitest.terminalShellArgs', 'vitest.terminalShellPath', 'vitest.filesWatcherInclude', 'vitest.cliArguments', 'vitest.runtime', 'deno.enabled', ] const snapshotEntryTool = new SnapshotEntryTool()
this.disposables = [ vscode.workspace.onDidChangeConfiguration((event) => { const configName = reloadConfigNames.find((x) => event.affectsConfiguration(x)) if (configName) { this.defineTestProfiles(false).catch((error) => { log.error('[API]', `Failed to reload Vitest after "${configName}" has changed`, error) }) } }), vscode.workspace.onDidChangeWorkspaceFolders(() => this.defineTestProfiles(false).catch((error) => { log.error('[API]', `Failed to reload Vitest after workspaces changed`, error) }), ), vscode.commands.registerCommand('vitest.openOutput', () => { log.openOutput() }), vscode.commands.registerCommand('vitest.openTraceView', (testItem: vscode.TestItem) => this.traceViewManager.open(testItem), ), vscode.commands.registerCommand('vitest.runRelatedTests', async (uri?: vscode.Uri) => { const currentUri = uri || vscode.window.activeTextEditor?.document.uri if (!currentUri) { return } const fsPath = normalize(currentUri.fsPath) if (this.testTree.getFileTestItems(fsPath).length) { vscode.window.showWarningMessage( `"${basename(fsPath)}" is a test file. Pick a source file to run related tests`, ) return } const promises = this.api?.processes.map(async (process) => { const runProfile = this.runProfiles.get(`${process.id}:run`) if (!runProfile) { return }
const request = new vscode.TestRunRequest(undefined, undefined, runProfile, false, false) const tokenSource = new vscode.CancellationTokenSource() Object.assign(request, { related: fsPath }) log.info( '[COMMAND] Running tests that import', relative(process.workspaceFolder.uri.fsPath, fsPath), ) await runProfile.runHandler(request, tokenSource.token) }) await Promise.all(promises || []) }), vscode.commands.registerCommand( 'vitest.toggleContinuousRun', async (testItem?: vscode.TestItem) => { if (!testItem) { return } this.api?.processes.forEach((process) => { const processId = `${process.id}:run` const runProfile = this.runProfiles.get(processId) const queue = this.runQueues.get(processId) if (runProfile && testItem.tags.includes(runProfile.tag!) && queue) { if (queue.isContinuousTestItem(testItem)) { vscode.commands.executeCommand('vscode.stopContinuousTestRun', [testItem]) } else { vscode.commands.executeCommand('vscode.startContinuousTestRun', runProfile, [ testItem, ]) } } }) }, ), vscode.commands.registerCommand( 'vitest.revealInTestExplorer', async (uri: vscode.Uri | undefined) => { if (uri === undefined) { uri = vscode.window.activeTextEditor?.document.uri } if (!(uri instanceof vscode.Uri)) { return } const testItems = this.testTree.getFileTestItems(uri.fsPath) if (testItems[0]) { vscode.commands.executeCommand('vscode.revealTestInExplorer', testItems[0]) } }, ), vscode.commands.registerCommand('vitest.showShellTerminal', async () => { const apis = this.api?.processes.filter( (api) => api.getPersistentProcessMeta()?.process instanceof ExtensionTerminalProcess, ) if (!apis?.length) { vscode.window.showInformationMessage( 'No shell terminals found. Did you change `vitest.shellType` to `terminal` in the configuration? Do you have any continuous runs active?', ) return } if (apis.length === 1) { log.info('Showing the only available shell terminal') ;(apis[0].getPersistentProcessMeta()?.process as ExtensionTerminalProcess).show() return } const pick = await vscode.window.showQuickPick( apis.map((api) => { return { label: api.prefix, process: api.getPersistentProcessMeta()?.process as ExtensionTerminalProcess, } }), ) if (pick) { log.info('Showing picked shell terminal:', pick.label) pick.process.show() } }), vscode.commands.registerCommand( 'vitest.updateSnapshot', async (testItem: vscode.TestItem | undefined) => { if (!testItem) return const api = this.testTree.getAPIFromTestItem(testItem) if (!api) return const profile = this.runProfiles.get(`${api.id}:run`) if (!profile) return const request = new vscode.TestRunRequest([testItem], undefined, profile, false) Object.assign(request, { updateSnapshots: true }) const tokenSource = new vscode.CancellationTokenSource() await profile.runHandler(request, tokenSource.token) }, ), vscode.commands.registerCommand( 'vitest.openTransformedModule', async (uri: vscode.Uri | undefined) => { const currentUri = uri || vscode.window.activeTextEditor?.document.uri if (!this.api || !currentUri || currentUri.scheme === 'vitest-transform') { return } const environments = await this.api.getModuleEnvironments(currentUri.fsPath) const options = environments .map(({ api, projects }) => { return projects.map((project) => { return project.environments.map((environment) => { let label = '' if (environments.length > 1) { label += `${api.prefix}: ` } if (project.name) { label += `[${project.name}] ` } label += environment return { label, uriParts: [ api.id, project.name, environment.name, environment.transformTimestamp, ], } }) }) }) .flat(2) if (options.length === 0) { vscode.window.showWarningMessage('All module graphs are empty, nothing to show.') return } const pick = options.length === 1 ? options[0] : await vscode.window.showQuickPick(options) if (!pick) { return } try { const [apiId, projectName, environment, t] = pick.uriParts const uri = vscode.Uri.parse( `vitest-transform://${currentUri.fsPath}.js?apiId=${apiId}&project=${projectName}&environment=${environment}&t=${t}`, ) const doc = await vscode.workspace.openTextDocument(uri) await vscode.window.showTextDocument(doc, { preview: false }) } catch (err) { log.error(err) vscode.window.showErrorMessage( `Vitest: The file was not processed by Vite yet. Try starting the continuous run first${options.length > 1 ? ' or select a different environment' : ''}.`, ) } }, ), vscode.commands.registerCommand('vitest.copyTestItemErrors', (testItem) => copyTestItemErrors(this.testController, testItem), ), vscode.commands.registerCommand('vitest.copyErrorOutput', copyOutput), vscode.commands.registerCommand('vitest.toggleConfigs', async () => { if (!this.api) { return }
const items: (vscode.QuickPickItem & { key: string })[] = [] for (const api of this.api.processes) { items.push({ label: relative(api.workspaceFolder.uri.fsPath, api.id), picked: !this.state.isConfigDisabled(api.id), key: api.id, }) }
const result = await vscode.window.showQuickPick(items, { canPickMany: true, title: 'Toggle Vitest Configs', })
if (!result) { return }
const enabledKeys = new Set(result.map((i) => i.key)) await this.state.setDisabledConfigs( new Set(items.filter((i) => !enabledKeys.has(i.key)).map((i) => i.key)), )
await this.defineTestProfiles(false) }), vscode.languages.registerDocumentSymbolProvider( { language: 'vitest-snapshot' }, new SnapshotDocumentSymbolProvider(snapshotEntryTool), ), vscode.languages.registerFoldingRangeProvider( { language: 'vitest-snapshot' }, new SnapshotFoldingRangeProvider(snapshotEntryTool), ), ]
// if the config changes, re-define all test profiles const configWatchers = [ vscode.workspace.createFileSystemWatcher(configGlob), vscode.workspace.createFileSystemWatcher(workspaceGlob), ] this.disposables.push(...configWatchers)
const redefineTestProfiles = debounce( (uri: vscode.Uri, event: 'create' | 'delete' | 'change') => { if (!this.api || uri.fsPath.includes('node_modules') || uri.fsPath.includes('.timestamp-')) return // if new config is created, always check if it should be respected if (event === 'create') { this.defineTestProfiles(false).catch((err) => { log.error('Failed to define test profiles after a new config file was created', err) }) return } // otherwise ignore changes to unrelated configs const filePath = normalize(uri.fsPath) for (const api of this.api.processes) { if (api.package.workspaceFile === filePath || api.configs.includes(filePath)) { this.defineTestProfiles(false).catch((err) => { log.error('Failed to define test profiles after a new config file was updated', err) }) return } } }, 300, )
configWatchers.forEach((watcher) => watcher.onDidChange((uri) => redefineTestProfiles(uri, 'change')), ) configWatchers.forEach((watcher) => watcher.onDidCreate((uri) => redefineTestProfiles(uri, 'create')), ) configWatchers.forEach((watcher) => watcher.onDidDelete((uri) => redefineTestProfiles(uri, 'delete')), )
try { await this.defineTestProfiles(true) } catch (err) { showVitestError('There was an error during Vitest startup', err) } }
async registerDebugOptions() { if (this._debugDisposable) { return } const config = getConfig() if (config.shellType !== 'terminal') { return } try { const jsDebugExt = vscode.extensions.getExtension('ms-vscode.js-debug-nightly') || vscode.extensions.getExtension('ms-vscode.js-debug') await jsDebugExt?.activate() const jsDebug: import('@vscode/js-debug').IExports = jsDebugExt?.exports
if (jsDebug) { this._debugDisposable = jsDebug.registerDebugTerminalOptionsProvider({ provideTerminalOptions(options) { options.shellArgs = getConfig().terminalShellArgs options.shellPath = getConfig().terminalShellPath return options }, }) this.disposables.push(this._debugDisposable) } else { log.error('Failed to connect to the debug extension. Debugger will open a terminal window.') } } catch (err) { log.error('Cannot create debug options provider.', err) } }
async dispose() { this.api?.dispose() this.testTree.dispose() this.testController.dispose() this.schemaProvider.dispose() this.importsBreakdownProvider.dispose() this.traceViewManager.dispose() this.runProfiles.forEach((p) => p.dispose()) this.runProfiles.clear() this.disposables.forEach((d) => d.dispose()) this.disposables = [] this.runQueues.forEach((q) => q.dispose()) this.runQueues.clear() }}