From 24b5db07603d83a9009c0a5379e2f428ec96a73a Mon Sep 17 00:00:00 2001 From: hi-ogawa-agent <266689927+hi-ogawa-agent@users.noreply.github.com> Date: Mon, 7 Sep 2026 20:17:49 +0900 Subject: [PATCH] feat: prototype reusable trace webview Co-authored-by: OpenCode (gpt-6-astra) --- README.md | 4 +- packages/extension/src/extension.ts | 1 + packages/extension/src/traceView.ts | 115 +++++++++++++++++++++++++--- 3 files changed, 109 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index c6ad0b2..78bc580 100644 --- a/README.md +++ b/README.md @@ -153,7 +153,9 @@ export default defineConfig({ }) ``` -After running a browser test through the extension, right-click the test in the Testing view or its editor gutter icon and select **Open Trace View**. The extension opens the trace view of a selected test from the generated HTML report inside Visual Studio Code's Integrated Browser. +After running a browser test through the extension, right-click the test in the Testing view or its editor gutter icon and select **Open Trace View**. The extension opens the generated HTML report in a reusable editor webview beside your code. Opening another test updates the same tab. Changes to the report reload the view while preserving its trace selection. + +This webview prototype loads report resources through VS Code's local-resource API without a static server. Canvas replay and snapshots referencing external resources have not been verified. ![Vitest Trace View open in Visual Studio Code](./img/trace-view.png) diff --git a/packages/extension/src/extension.ts b/packages/extension/src/extension.ts index d7444f1..846071e 100644 --- a/packages/extension/src/extension.ts +++ b/packages/extension/src/extension.ts @@ -646,6 +646,7 @@ class VitestExtension { 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()) diff --git a/packages/extension/src/traceView.ts b/packages/extension/src/traceView.ts index 895e1bd..3731250 100644 --- a/packages/extension/src/traceView.ts +++ b/packages/extension/src/traceView.ts @@ -1,5 +1,6 @@ import type { RunnerTask, RunnerTestFile } from 'vitest' import type { TestTree } from './testTree' +import { randomBytes } from 'node:crypto' import * as vscode from 'vscode' interface TraceViewTarget { @@ -11,6 +12,19 @@ interface TraceViewTarget { export class TraceViewManager { private targets = new Map() + private panel?: vscode.WebviewPanel + private reportPath?: string + private selection = '' + private watcher?: vscode.FileSystemWatcher + private refreshTimer?: ReturnType + private revision = 0 + + dispose() { + this.panel?.dispose() + this.watcher?.dispose() + clearTimeout(this.refreshTimer) + this.revision++ + } clear() { this.targets.clear() @@ -43,7 +57,9 @@ export class TraceViewManager { } async open(testItem: vscode.TestItem) { - const target = this.targets.get(testItem)! + const target = this.targets.get(testItem) + if (!target) + return const reportUri = vscode.Uri.file(target.reportPath) try { @@ -55,15 +71,94 @@ export class TraceViewManager { return } - const commands = await vscode.commands.getCommands(true) - const url = createTraceViewUrl(target) - // The Integrated Browser command is internal; follow Simple Browser's feature detection before using it. - // https://github.com/microsoft/vscode/blob/008427a901bf4aa79b47f175ccc8da1731750f78/extensions/simple-browser/src/extension.ts#L15-L35 - if (commands.includes('workbench.action.browser.open')) { - await vscode.commands.executeCommand('workbench.action.browser.open', url) - } else { - // TODO: Support a single-file fallback because external browsers may block file:// metadata requests. - await vscode.env.openExternal(vscode.Uri.parse(url)) + const sameReport = this.reportPath === target.reportPath + this.reportPath = target.reportPath + this.selection = createTraceViewUrl(target).split('#')[1] + if (!this.panel) { + const panel = vscode.window.createWebviewPanel( + 'vitest.traceView', + 'Vitest Trace View', + { viewColumn: vscode.ViewColumn.Beside, preserveFocus: true }, + { enableScripts: true, retainContextWhenHidden: true }, + ) + this.panel = panel + panel.webview.onDidReceiveMessage((message) => { + if (message.type === 'selection' && typeof message.hash === 'string') + this.selection = message.hash.replace(/^#/, '') + }) + panel.onDidDispose(() => { + this.panel = undefined + this.watcher?.dispose() + clearTimeout(this.refreshTimer) + this.revision++ + }) + } + this.panel.reveal(undefined, true) + if (sameReport && this.panel.webview.html) { + await this.panel.webview.postMessage({ type: 'select', hash: this.selection }) + return + } + this.watcher?.dispose() + this.watcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern( + vscode.Uri.joinPath(reportUri, '..'), + '{index.html,ui/html.meta.json.gz}', + )) + const refresh = () => { + clearTimeout(this.refreshTimer) + this.refreshTimer = setTimeout(() => void this.refresh(), 150) + } + this.watcher.onDidChange(refresh) + this.watcher.onDidCreate(refresh) + await this.refresh() + } + + private async refresh() { + const panel = this.panel + const reportPath = this.reportPath + if (!panel || !reportPath) + return + const revision = ++this.revision + try { + const reportUri = vscode.Uri.file(reportPath) + const directory = vscode.Uri.joinPath(reportUri, '..') + const bytes = await vscode.workspace.fs.readFile(reportUri) + if (revision !== this.revision || panel !== this.panel) + return + panel.webview.options = { enableScripts: true, localResourceRoots: [directory] } + const base = `${panel.webview.asWebviewUri(directory).toString()}/` + const nonce = randomBytes(16).toString('hex') + const source = panel.webview.cspSource + // Adapt the reporter's generated bootstrap, which explicitly uses location + // rather than document.baseURI. Bust metadata cache after regeneration. + const metadata = panel.webview.asWebviewUri(vscode.Uri.joinPath(directory, 'ui', 'html.meta.json.gz')) + let html = Buffer.from(bytes).toString('utf8').replace( + /new URL\("\.\/ui\/html\.meta\.json\.gz", window\.location\.href\)/g, + JSON.stringify(`${metadata.toString()}?v=${Date.now()}`), + ) + html = html.replace(/]*>/i, `$& + + + `) + panel.webview.html = html + } catch (error) { + if (revision === this.revision) + void vscode.window.showWarningMessage(`Could not load Vitest trace report: ${String(error)}`) } } } -- 2.51.2