Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions packages/extension/src/api/child_process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why?

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),
Expand Down
12 changes: 10 additions & 2 deletions packages/extension/src/api/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export function createRpcOptions() {
onCollected: createHandler<ExtensionWorkerEvents['onCollected']>(),
onTestRunStart: createHandler<ExtensionWorkerEvents['onTestRunStart']>(),
onTestRunEnd: createHandler<ExtensionWorkerEvents['onTestRunEnd']>(),
onProcessLog: createHandler<ExtensionWorkerEvents['onProcessLog']>(),
}

const events: Omit<ExtensionWorkerEvents, 'onReady' | 'onError'> = {
Expand All @@ -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))
},
}
Expand All @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do this? the handlers are not cleaned after the test run, they are cleaned when the fork is destroyed, onProcessLog needs to be cleaned

// 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()
},
},
}
Expand Down
54 changes: 25 additions & 29 deletions packages/extension/src/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Copy link
Member

@sheremet-va sheremet-va Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this change logs are no longer attributed to tests and are not shown inline in UI

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()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why replace this.endTestRun with an incomplete promise resolution?

}
})

// Skip individual console logs since DefaultReporter already includes them
// api.onConsoleLog((consoleLog) => { ... }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this?


// Listen to configuration changes
this.disposables.push(
vscode.workspace.onDidChangeConfiguration((event) => {
Expand Down Expand Up @@ -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`)

Comment on lines +431 to +443
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A nice to have in the first line:

Image

Inspired by this Jest extension

Copy link
Member

@sheremet-va sheremet-va Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not how the extension works, we don't create a new process for every test run. Even more, I am planning to change to the new project.createSpecification API that supports test ids, making this line useless

// 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why?

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())
Expand Down Expand Up @@ -797,7 +793,7 @@ function formatTestPattern(tests: readonly vscode.TestItem[]) {
}

function formatTestOutput(output: string) {
return stripVTControlCharacters(output.replace(/(?<!\r)\n/g, '\r\n'))
return output.replace(/(?<!\r)\n/g, '\r\n')
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test results panel in VSCode / Cursor supports colors. No need to remove them.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Colors are not supported in inline view, at least

}

function labelTestItems(items: readonly vscode.TestItem[] | undefined) {
Expand Down
17 changes: 11 additions & 6 deletions packages/worker/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ export async function initVitest(
api: false,
// @ts-expect-error private property
reporter: undefined,
reporters: [reporter],
ui: false,
includeTaskLocation: true,
execArgv: meta.pnpApi && meta.pnpLoader
Expand Down Expand Up @@ -105,13 +104,23 @@ export async function initVitest(
coverageOptions.reporter = [
['json', { ...jsonReporterOptions, file: meta.finalCoverageFileName }],
]

const rawReporters = testConfig.reporters
Copy link
Member

@sheremet-va sheremet-va Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We intentionally don't show the report since vscode already has the UI to show everything. It is a waste of resources to duplicate it

The extension also doesn't support the same hooks order, so custom reporters might break

If the intention is to help AI, then it is better to have a custom reporter designed for that (to reduce the amount of tokens, at least) or something like this: #676

const userReporters = (Array.isArray(rawReporters) ? rawReporters : (rawReporters ? [rawReporters] : []))
.filter((r: string) => 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
Expand All @@ -136,10 +145,6 @@ export async function initVitest(
},
],
},
{
stderr,
stdout,
},
)
Copy link
Member

@sheremet-va sheremet-va Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why? This makes it so unhandled errors outside of tests are not reporter (failed global setup, for example)

await (vitest as any).report('onInit', vitest)
const configs = [
Expand Down