Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- **Run**, **debug**, and **watch** Vitest tests in Visual Studio Code.
- **Coverage** support (requires VS Code >= 1.88)
- An `@open` tag can be used when filtering tests, to only show the tests open in the editor.
- **Inline console.log display**: Show `console.log` output inline in the editor next to the line that produced it (enabled by default, requires `printConsoleTrace: true` in Vitest config)

## Requirements

Expand Down Expand Up @@ -99,6 +100,7 @@ These options are resolved relative to the [workspace file](https://code.visuals
- `vitest.logLevel`: How verbose should the logger be in the "Output" channel. Default: `info`
- `vitest.applyDiagnostic`: Show a squiggly line where the error was thrown. This also enables the error count in the File Tab. Default: `true`
- `vitest.experimentalStaticAstCollect`: uses AST parses to collect tests instead of running files and collecting them at runtime. Default: `true`
- `vitest.showConsoleLogInline`: Show `console.log` output inline in the editor next to the line that produced it. Note: This requires `printConsoleTrace: true` in your Vitest config to include stack trace information. Default: `true`

### Commands

Expand Down
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,12 @@
"description": "Show a squiggly line where the error was thrown. This also enables the error count in the File Tab.",
"type": "boolean",
"default": true
},
"vitest.showConsoleLogInline": {
"description": "Show console.log output inline in the editor next to the line that produced it.",
"type": "boolean",
"default": true,
"scope": "resource"
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions packages/extension/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export function getConfig(workspaceFolder?: WorkspaceFolder) {
const debugOutFiles = get<string[]>('debugOutFiles', [])
const applyDiagnostic = get<boolean>('applyDiagnostic', true)
const ignoreWorkspace = get<boolean>('ignoreWorkspace', false) ?? false
const showConsoleLogInline = get<boolean>('showConsoleLogInline', true)!

return {
env: get<null | Record<string, string>>('nodeEnv', null),
Expand All @@ -82,6 +83,7 @@ export function getConfig(workspaceFolder?: WorkspaceFolder) {
terminalShellPath,
shellType,
applyDiagnostic,
showConsoleLogInline,
cliArguments,
nodeExecArgs,
experimentalStaticAstCollect,
Expand Down
23 changes: 17 additions & 6 deletions packages/extension/src/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,18 +153,29 @@ export class TestRunner extends vscode.Disposable {
this.endTestRun()
})

api.onConsoleLog(({ content, taskId }) => {
const testItem = taskId ? tree.getTestItemByTaskId(taskId) : undefined
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
let location: vscode.Location | undefined
if (consoleLog.parsedLocation) {
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(content),
undefined,
formatTestOutput(consoleLog.content) + (consoleLog.browser ? '\r\n' : ''),
location,
testItem,
)
}
else {
log.info('[TEST]', content)
log.info('[TEST]', consoleLog.content)
}
})
}
Expand Down Expand Up @@ -765,7 +776,7 @@ function formatTestPattern(tests: readonly vscode.TestItem[]) {
}

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

function labelTestItems(items: readonly vscode.TestItem[] | undefined) {
Expand Down
11 changes: 10 additions & 1 deletion packages/shared/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ export type ExtensionTestFileSpecification = [
ExtensionTestFileMetadata,
]

export interface ExtensionUserConsoleLog extends UserConsoleLog {
// Parsed location from stack trace for inline display
parsedLocation?: {
file: string
line: number // 0-based line number
column: number
}
}

export interface ExtensionWorkerTransport {
getFiles: () => Promise<ExtensionTestFileSpecification[]>
collectTests: (testFile: ExtensionTestSpecification[]) => Promise<void>
Expand All @@ -54,7 +63,7 @@ export interface ExtensionWorkerTransport {
}

export interface ExtensionWorkerEvents {
onConsoleLog: (log: UserConsoleLog) => void
onConsoleLog: (log: ExtensionUserConsoleLog) => void
onTaskUpdate: (task: RunnerTaskResultPack[]) => void
onTestRunEnd: (files: RunnerTestFile[], unhandledError: string, collecting?: boolean) => void
onCollected: (file: RunnerTestFile, collecting?: boolean) => void
Expand Down
3 changes: 3 additions & 0 deletions packages/worker-legacy/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@ export async function initVitest(
}
},
configureVitest(context) {
// Enable printConsoleTrace for inline console log display
context.project.config.printConsoleTrace = true

const options = context.project.config.browser
if (options?.enabled && typeof data.debug === 'object') {
context.project.config.setupFiles.push(meta.setupFilePaths.browserDebug)
Expand Down
54 changes: 43 additions & 11 deletions packages/worker-legacy/src/reporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,47 @@ export class VSCodeReporter implements Reporter {
}

onUserConsoleLog(log: UserConsoleLog) {
this.rpc.onConsoleLog(log)
// Parse stack trace to extract file location for inline display
const extendedLog = log as any
if (log.origin) {
try {
const stacks = this.parseStackTrace({ stack: log.origin } as any, log.taskId)
if (stacks && stacks.length > 0) {
const firstStack = stacks[0]
if (firstStack.file && firstStack.line != null && firstStack.column != null) {
extendedLog.parsedLocation = {
file: firstStack.file,
line: firstStack.line - 1, // Convert to 0-based
column: firstStack.column,
}
}
}
}
catch {
// If parsing fails, continue without parsed location
}
}
this.rpc.onConsoleLog(extendedLog)
}

parseStackTrace(obj: unknown, taskId: string | undefined) {
const project = taskId
? this.vitest.getProjectByTaskId(taskId)
: this.vitest.getCoreWorkspaceProject()

// the new version uses browser.parseErrorStacktrace
if ('getBrowserSourceMapModuleById' in project) {
return parseErrorStacktrace(obj as Error, {
getSourceMap: file => (project as any).getBrowserSourceMapModuleById(file),
})
}

const task = taskId && this.vitest.state.idMap.get(taskId)
const isBrowser = task && task.file?.pool === 'browser'

return isBrowser
? project.browser?.parseErrorStacktrace(obj)
: parseErrorStacktrace(obj as Error)
}

onTaskUpdate(packs: TaskResultPack[]) {
Expand All @@ -111,26 +151,18 @@ export class VSCodeReporter implements Reporter {
if ('getBrowserSourceMapModuleById' in project) {
result?.errors?.forEach((error) => {
if (typeof error === 'object' && error) {
error.stacks = parseErrorStacktrace(error, {
getSourceMap: file => (project as any).getBrowserSourceMapModuleById(file),
})
error.stacks = this.parseStackTrace(error, taskId)
}
})
return
}

const task = this.vitest.state.idMap.get(taskId)
const isBrowser = task && task.file?.pool === 'browser'

result?.errors?.forEach((error) => {
if (isPrimitive(error)) {
return
}

const stacks = isBrowser
? project.browser?.parseErrorStacktrace(error)
: parseErrorStacktrace(error)
error.stacks = stacks
error.stacks = this.parseStackTrace(error, taskId)
})
})

Expand Down
1 change: 1 addition & 0 deletions packages/worker/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ export async function initVitest(
]
return {
test: {
printConsoleTrace: true,
coverage: {
reportOnFailure: true,
reportsDirectory: join(tmpdir(), `vitest-coverage-${randomUUID()}`),
Expand Down
30 changes: 29 additions & 1 deletion packages/worker/src/reporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {
Vite,
Vitest as VitestCore,
} from 'vitest/node'
import { parseErrorStacktrace } from '@vitest/utils/source-map'
import { ExtensionWorker } from './worker'

interface VSCodeReporterOptions {
Expand Down Expand Up @@ -59,7 +60,34 @@ export class VSCodeReporter implements Reporter {
}

onUserConsoleLog(log: UserConsoleLog) {
this.rpc.onConsoleLog(log)
// Parse stack trace to extract file location for inline display
const extendedLog = log as any
if (log.origin) {
try {
const task = log.taskId ? this.vitest.state.idMap.get(log.taskId) : null
const project = task
? this.vitest.state.getReportedEntity(task)!.project
: this.vitest.getRootProject()
const stacks = log.browser
? project.browser?.parseErrorStacktrace({ stack: log.origin } as any)
: parseErrorStacktrace({ stack: log.origin } as any)

if (stacks && stacks.length > 0) {
const firstStack = stacks[0]
if (firstStack.file && firstStack.line != null && firstStack.column != null) {
extendedLog.parsedLocation = {
file: firstStack.file,
line: firstStack.line - 1, // Convert to 0-based
column: firstStack.column,
}
}
}
}
catch {
// If parsing fails, continue without parsed location
}
}
this.rpc.onConsoleLog(extendedLog)
}

onTaskUpdate(packs: RunnerTaskResultPack[]) {
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.tsbuildinfo
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"root":["./debug-shims.d.ts","./packages/extension/src/api.ts","./packages/extension/src/config.ts","./packages/extension/src/constants.ts","./packages/extension/src/coverage.ts","./packages/extension/src/debug.ts","./packages/extension/src/diagnostic.ts","./packages/extension/src/extension.ts","./packages/extension/src/log.ts","./packages/extension/src/polyfills.ts","./packages/extension/src/runner.ts","./packages/extension/src/tagsmanager.ts","./packages/extension/src/testtree.ts","./packages/extension/src/testtreedata.ts","./packages/extension/src/utils.ts","./packages/extension/src/watcher.ts","./packages/extension/src/api/child_process.ts","./packages/extension/src/api/pkg.ts","./packages/extension/src/api/resolve.ts","./packages/extension/src/api/rpc.ts","./packages/extension/src/api/terminal.ts","./packages/extension/src/api/types.ts","./packages/extension/src/api/ws.ts","./packages/extension/src/worker/index.ts","./packages/extension/src/worker/setupfile.ts","./packages/shared/src/emitter.ts","./packages/shared/src/index.ts","./packages/shared/src/rpc.ts","./packages/shared/src/utils.ts","./packages/worker/src/coverage.ts","./packages/worker/src/index.ts","./packages/worker/src/reporter.ts","./packages/worker/src/runner.ts","./packages/worker/src/watcher.ts","./packages/worker/src/worker.ts","./packages/worker-legacy/src/collect.ts","./packages/worker-legacy/src/coverage.ts","./packages/worker-legacy/src/index.ts","./packages/worker-legacy/src/reporter.ts","./packages/worker-legacy/src/types.ts","./packages/worker-legacy/src/watcher.ts","./packages/worker-legacy/src/worker.ts"],"version":"5.8.3"}
{"root":["./debug-shims.d.ts","./packages/extension/src/api.ts","./packages/extension/src/config.ts","./packages/extension/src/constants.ts","./packages/extension/src/coverage.ts","./packages/extension/src/debug.ts","./packages/extension/src/diagnostic.ts","./packages/extension/src/extension.ts","./packages/extension/src/log.ts","./packages/extension/src/polyfills.ts","./packages/extension/src/runner.ts","./packages/extension/src/tagsManager.ts","./packages/extension/src/testTree.ts","./packages/extension/src/testTreeData.ts","./packages/extension/src/utils.ts","./packages/extension/src/watcher.ts","./packages/extension/src/api/child_process.ts","./packages/extension/src/api/pkg.ts","./packages/extension/src/api/resolve.ts","./packages/extension/src/api/rpc.ts","./packages/extension/src/api/terminal.ts","./packages/extension/src/api/types.ts","./packages/extension/src/api/ws.ts","./packages/extension/src/worker/browserSetupFile.ts","./packages/extension/src/worker/index.ts","./packages/shared/src/emitter.ts","./packages/shared/src/index.ts","./packages/shared/src/rpc.ts","./packages/shared/src/utils.ts","./packages/worker/src/coverage.ts","./packages/worker/src/index.ts","./packages/worker/src/reporter.ts","./packages/worker/src/runner.ts","./packages/worker/src/watcher.ts","./packages/worker/src/worker.ts","./packages/worker-legacy/src/collect.ts","./packages/worker-legacy/src/coverage.ts","./packages/worker-legacy/src/index.ts","./packages/worker-legacy/src/reporter.ts","./packages/worker-legacy/src/setupFile.ts","./packages/worker-legacy/src/types.ts","./packages/worker-legacy/src/watcher.ts","./packages/worker-legacy/src/worker.ts"],"version":"5.8.3"}
Loading