diff --git a/packages/extension/src/api/child_process.ts b/packages/extension/src/api/child_process.ts index fba302fb..dbd2a26f 100644 --- a/packages/extension/src/api/child_process.ts +++ b/packages/extension/src/api/child_process.ts @@ -95,11 +95,14 @@ export async function createVitestProcess(pkg: VitestPackage) { resolved.handlers.onStdout = (callback: (data: string) => void) => { stdoutCallbacks.add(callback) } - const clearListeners = resolved.handlers.clearListeners - resolved.handlers.clearListeners = () => { - clearListeners() - stdoutCallbacks.clear() - } + + // Forward RPC process logs to the same stdout callbacks + resolved.handlers.onProcessLog((type: 'stdout' | 'stderr', message: string) => { + if (type === 'stdout') { + stdoutCallbacks.forEach(cb => cb(message)) + } + }) + resolve({ ...resolved, process: new ExtensionChildProcess(vitest, server, resolved.ws), diff --git a/packages/extension/src/api/rpc.ts b/packages/extension/src/api/rpc.ts index b2a23b9d..e03ada68 100644 --- a/packages/extension/src/api/rpc.ts +++ b/packages/extension/src/api/rpc.ts @@ -33,6 +33,7 @@ export function createRpcOptions() { onCollected: createHandler(), onTestRunStart: createHandler(), onTestRunEnd: createHandler(), + onProcessLog: createHandler(), } const events: Omit = { @@ -42,6 +43,7 @@ export function createRpcOptions() { onCollected: handlers.onCollected.trigger, onTestRunStart: handlers.onTestRunStart.trigger, onProcessLog(type, message) { + handlers.onProcessLog.trigger(type, message) log.worker(type === 'stderr' ? 'error' : 'info', stripVTControlCharacters(message)) }, } @@ -54,12 +56,18 @@ export function createRpcOptions() { onTestRunEnd: handlers.onTestRunEnd.register, onCollected: handlers.onCollected.register, onTestRunStart: handlers.onTestRunStart.register, + onProcessLog: handlers.onProcessLog.register, removeListener(name: string, listener: any) { handlers[name as 'onCollected']?.remove(listener) }, clearListeners() { - for (const name in handlers) - handlers[name as 'onCollected']?.clear() + // Clear all handlers except onProcessLog, which needs to persist + // across test runs to forward stdout from Vitest to the extension + handlers.onConsoleLog.clear() + handlers.onTaskUpdate.clear() + handlers.onCollected.clear() + handlers.onTestRunStart.clear() + handlers.onTestRunEnd.clear() }, }, } diff --git a/packages/extension/src/runner.ts b/packages/extension/src/runner.ts index a2b8f350..abf7d6bc 100644 --- a/packages/extension/src/runner.ts +++ b/packages/extension/src/runner.ts @@ -160,37 +160,16 @@ export class TestRunner extends vscode.Disposable { if (unhandledError) testRun.appendOutput(formatTestOutput(unhandledError)) - if (!collecting) - this.endTestRun() - }) - - api.onConsoleLog((consoleLog) => { - const testItem = consoleLog.taskId ? tree.getTestItemByTaskId(consoleLog.taskId) : undefined - const testRun = this.testRun - if (testRun) { - // Create location from parsed console log for inline display - // Only set location if inline console logs are enabled - let location: vscode.Location | undefined - if (consoleLog.parsedLocation && this.showInlineConsoleLog) { - const uri = vscode.Uri.file(consoleLog.parsedLocation.file) - const position = new vscode.Position( - consoleLog.parsedLocation.line, - consoleLog.parsedLocation.column, - ) - location = new vscode.Location(uri, position) - } - - testRun.appendOutput( - formatTestOutput(consoleLog.content) + (consoleLog.browser ? '\r\n' : ''), - location, - testItem, - ) - } - else { - log.info('[TEST]', consoleLog.content) + // Signal that the test run is complete, but DON'T set this.testRun to undefined yet + // The testRun will be properly ended in the finally block of startTestRun + if (!collecting) { + this.testRunDefer?.resolve() } }) + // Skip individual console logs since DefaultReporter already includes them + // api.onConsoleLog((consoleLog) => { ... } + // Listen to configuration changes this.disposables.push( vscode.workspace.onDidChangeConfiguration((event) => { @@ -449,9 +428,26 @@ export class TestRunner extends vscode.Disposable { const run = this.testRun = this.controller.createTestRun(request, name) this.testRunRequest = request this.testRunDefer = Promise.withResolvers() + + // Show the equivalent CLI command for debugging/reproducibility + const fileList = files.map(f => this.relative(f)).join(' ') + const pattern = formatTestPattern(request.include || []) + let vitestCmd = 'vitest' + if (fileList) { + vitestCmd += ` ${fileList}` + } + if (pattern) { + vitestCmd += ` -t "${pattern}"` + } + run.appendOutput(`\x1B[36m\x1B[1m[command]\x1B[0m ${vitestCmd}\r\n\r\n`) + // run the next test when this one finished, or cancell or test runs if they were cancelled this.testRunDefer.promise = this.testRunDefer.promise.finally(() => { run.end() + this.testRun = undefined + this.testRunDefer = undefined + this.testRunRequest = undefined + if (this.cancelled) { log.verbose?.('Not starting a new test run because the previous one was cancelled manually.') this.scheduleTestRunsQueue.forEach(item => item.resolveWithoutRunning()) @@ -797,7 +793,7 @@ function formatTestPattern(tests: readonly vscode.TestItem[]) { } function formatTestOutput(output: string) { - return stripVTControlCharacters(output.replace(/(? r !== 'html') + const hasReporters = userReporters.length > 0 + return { test: { - printConsoleTrace: true, coverage: { reportOnFailure: true, reportsDirectory: join(tmpdir(), `vitest-coverage-${randomUUID()}`), }, + // If user already has reporters, we only return ours and let Vitest merge it. + // This prevents duplication since Vite merges arrays by appending. + reporters: hasReporters + ? [reporter] + : ['default', reporter], }, // TODO: type is not augmented } as any @@ -136,10 +145,6 @@ export async function initVitest( }, ], }, - { - stderr, - stdout, - }, ) await (vitest as any).report('onInit', vitest) const configs = [