From 1da6cebe467e2338f5e669de23eec6f2503aef9f Mon Sep 17 00:00:00 2001 From: Vladimir Date: Wed, 31 Jul 2024 18:05:09 +0200 Subject: [PATCH] fix(browser): print correct stack trace for unhandled errors (#6134) --- .../src/client/public/error-catcher.js | 26 +++++++------------ packages/browser/src/node/rpc.ts | 5 ++++ packages/browser/src/node/server.ts | 2 +- .../unhandled/throw-unhandled-error.test.ts | 11 ++++++++ .../fixtures/unhandled/vitest.config.ts | 18 +++++++++++++ test/browser/specs/unhandled.test.ts | 15 +++++++++++ 6 files changed, 60 insertions(+), 17 deletions(-) create mode 100644 test/browser/fixtures/unhandled/throw-unhandled-error.test.ts create mode 100644 test/browser/fixtures/unhandled/vitest.config.ts create mode 100644 test/browser/specs/unhandled.test.ts diff --git a/packages/browser/src/client/public/error-catcher.js b/packages/browser/src/client/public/error-catcher.js index 7de41b15d3ed..a1326694e987 100644 --- a/packages/browser/src/client/public/error-catcher.js +++ b/packages/browser/src/client/public/error-catcher.js @@ -1,10 +1,5 @@ import { channel, client } from '@vitest/browser/client' -function on(event, listener) { - window.addEventListener(event, listener) - return () => window.removeEventListener(event, listener) -} - function serializeError(unhandledError) { if (typeof unhandledError !== 'object' || !unhandledError) { return { @@ -19,41 +14,40 @@ function serializeError(unhandledError) { } } -function catchWindowErrors(cb) { +function catchWindowErrors(errorEvent, prop, cb) { let userErrorListenerCount = 0 function throwUnhandlerError(e) { - if (userErrorListenerCount === 0 && e.error != null) { + if (userErrorListenerCount === 0 && e[prop] != null) { cb(e) } else { - console.error(e.error) + console.error(e[prop]) } } const addEventListener = window.addEventListener.bind(window) const removeEventListener = window.removeEventListener.bind(window) - window.addEventListener('error', throwUnhandlerError) + window.addEventListener(errorEvent, throwUnhandlerError) window.addEventListener = function (...args) { - if (args[0] === 'error') { + if (args[0] === errorEvent) { userErrorListenerCount++ } return addEventListener.apply(this, args) } window.removeEventListener = function (...args) { - if (args[0] === 'error' && userErrorListenerCount) { + if (args[0] === errorEvent && userErrorListenerCount) { userErrorListenerCount-- } return removeEventListener.apply(this, args) } return function clearErrorHandlers() { - window.removeEventListener('error', throwUnhandlerError) + window.removeEventListener(errorEvent, throwUnhandlerError) } } function registerUnexpectedErrors() { - catchWindowErrors(event => - reportUnexpectedError('Error', event.error), - ) - on('unhandledrejection', event => + catchWindowErrors('error', 'error', event => + reportUnexpectedError('Error', event.error)) + catchWindowErrors('unhandledrejection', 'reason', event => reportUnexpectedError('Unhandled Rejection', event.reason)) } diff --git a/packages/browser/src/node/rpc.ts b/packages/browser/src/node/rpc.ts index 17740981733d..5c780857b0a2 100644 --- a/packages/browser/src/node/rpc.ts +++ b/packages/browser/src/node/rpc.ts @@ -6,6 +6,7 @@ import type { WebSocket } from 'ws' import { WebSocketServer } from 'ws' import type { BrowserCommandContext } from 'vitest/node' import { createDebugger, isFileServingAllowed } from 'vitest/node' +import type { ErrorWithDiff } from 'vitest' import type { WebSocketBrowserEvents, WebSocketBrowserHandlers } from './types' import type { BrowserServer } from './server' import { cleanUrl, resolveMock } from './resolveMock' @@ -67,6 +68,10 @@ export function setupBrowserRpc( const rpc = createBirpc( { async onUnhandledError(error, type) { + if (error && typeof error === 'object') { + const _error = error as ErrorWithDiff + _error.stacks = server.parseErrorStacktrace(_error) + } ctx.state.catchError(error, type) }, async onCollected(files) { diff --git a/packages/browser/src/node/server.ts b/packages/browser/src/node/server.ts index b3ac794344a3..ef959fc78de3 100644 --- a/packages/browser/src/node/server.ts +++ b/packages/browser/src/node/server.ts @@ -87,7 +87,7 @@ export class BrowserServer implements IBrowserServer { resolve(distRoot, 'client/esm-client-injector.js'), 'utf8', ).then(js => (this.injectorJs = js)) - this.errorCatcherPath = resolve(distRoot, 'client/error-catcher.js') + this.errorCatcherPath = join('/@fs/', resolve(distRoot, 'client/error-catcher.js')) this.stateJs = readFile( resolve(distRoot, 'state.js'), 'utf-8', diff --git a/test/browser/fixtures/unhandled/throw-unhandled-error.test.ts b/test/browser/fixtures/unhandled/throw-unhandled-error.test.ts new file mode 100644 index 000000000000..02ace53eae48 --- /dev/null +++ b/test/browser/fixtures/unhandled/throw-unhandled-error.test.ts @@ -0,0 +1,11 @@ +import { test } from 'vitest'; + +interface _Unused { + _fake: never +} + +test('unhandled exception', () => { + ;(async () => { + throw new Error('custom_unhandled_error') + })() +}) diff --git a/test/browser/fixtures/unhandled/vitest.config.ts b/test/browser/fixtures/unhandled/vitest.config.ts new file mode 100644 index 000000000000..5f5d430812b3 --- /dev/null +++ b/test/browser/fixtures/unhandled/vitest.config.ts @@ -0,0 +1,18 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' + +const provider = process.env.PROVIDER || 'playwright' +const name = + process.env.BROWSER || (provider === 'playwright' ? 'chromium' : 'chrome') + +export default defineConfig({ + cacheDir: fileURLToPath(new URL("./node_modules/.vite", import.meta.url)), + test: { + browser: { + enabled: true, + provider, + name, + headless: true, + }, + }, +}) diff --git a/test/browser/specs/unhandled.test.ts b/test/browser/specs/unhandled.test.ts new file mode 100644 index 000000000000..12bb6748772f --- /dev/null +++ b/test/browser/specs/unhandled.test.ts @@ -0,0 +1,15 @@ +import { expect, test } from 'vitest' +import { runBrowserTests } from './utils' + +test('prints correct unhandled error stack', async () => { + const { stderr, browser } = await runBrowserTests({ + root: './fixtures/unhandled', + }) + + if (browser === 'webkit') { + expect(stderr).toContain('throw-unhandled-error.test.ts:9:20') + } + else { + expect(stderr).toContain('throw-unhandled-error.test.ts:9:10') + } +})