Skip to content

Commit

Permalink
fix(browser): print correct stack trace for unhandled errors (#6134)
Browse files Browse the repository at this point in the history
  • Loading branch information
sheremet-va authored Jul 31, 2024
1 parent c51c67a commit 1da6ceb
Show file tree
Hide file tree
Showing 6 changed files with 60 additions and 17 deletions.
26 changes: 10 additions & 16 deletions packages/browser/src/client/public/error-catcher.js
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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))
}

Expand Down
5 changes: 5 additions & 0 deletions packages/browser/src/node/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -67,6 +68,10 @@ export function setupBrowserRpc(
const rpc = createBirpc<WebSocketBrowserEvents, WebSocketBrowserHandlers>(
{
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) {
Expand Down
2 changes: 1 addition & 1 deletion packages/browser/src/node/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
11 changes: 11 additions & 0 deletions test/browser/fixtures/unhandled/throw-unhandled-error.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { test } from 'vitest';

interface _Unused {
_fake: never
}

test('unhandled exception', () => {
;(async () => {
throw new Error('custom_unhandled_error')
})()
})
18 changes: 18 additions & 0 deletions test/browser/fixtures/unhandled/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -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,
},
},
})
15 changes: 15 additions & 0 deletions test/browser/specs/unhandled.test.ts
Original file line number Diff line number Diff line change
@@ -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')
}
})

0 comments on commit 1da6ceb

Please sign in to comment.