Skip to content

Commit

Permalink
feat: throw unhandled exception, if code throws "error" event (#2691)
Browse files Browse the repository at this point in the history
  • Loading branch information
sheremet-va committed Jan 19, 2023
1 parent b566912 commit 6a30cdd
Show file tree
Hide file tree
Showing 10 changed files with 63 additions and 7 deletions.
28 changes: 28 additions & 0 deletions packages/vitest/src/integrations/env/jsdom.ts
Expand Up @@ -2,6 +2,30 @@ import { importModule } from 'local-pkg'
import type { Environment } from '../../types'
import { populateGlobal } from './utils'

function catchWindowErrors(window: Window) {
let userErrorListenerCount = 0
function throwUnhandlerError(e: ErrorEvent) {
if (userErrorListenerCount === 0 && e.error != null)
process.emit('uncaughtException', e.error)
}
const addEventListener = window.addEventListener.bind(window)
const removeEventListener = window.removeEventListener.bind(window)
window.addEventListener('error', throwUnhandlerError)
window.addEventListener = function (...args: Parameters<typeof addEventListener>) {
if (args[0] === 'error')
userErrorListenerCount++
return addEventListener.apply(this, args)
}
window.removeEventListener = function (...args: Parameters<typeof removeEventListener>) {
if (args[0] === 'error' && userErrorListenerCount)
userErrorListenerCount--
return removeEventListener.apply(this, args)
}
return function clearErrorHandlers() {
window.removeEventListener('error', throwUnhandlerError)
}
}

export default <Environment>({
name: 'jsdom',
async setup(global, { jsdom = {} }) {
Expand Down Expand Up @@ -42,8 +66,12 @@ export default <Environment>({

const { keys, originals } = populateGlobal(global, dom.window, { bindFunctions: true })

const clearWindowErrors = catchWindowErrors(global)

return {
teardown(global) {
clearWindowErrors()
dom.window.close()
keys.forEach(key => delete global[key])
originals.forEach((v, k) => global[k] = v)
},
Expand Down
2 changes: 1 addition & 1 deletion packages/vitest/src/node/error.ts
Expand Up @@ -74,7 +74,7 @@ export async function printError(error: unknown, ctx: Vitest, options: PrintErro
if (testName) {
ctx.logger.error(c.red(`The latest test that might've caused the error is "${c.bold(testName)}". It might mean one of the following:`
+ '\n- The error was thrown, while Vitest was running this test.'
+ '\n- This was the last recorder test before the error was thrown, if error originated after test finished its execution.'))
+ '\n- This was the last recorded test before the error was thrown, if error originated after test finished its execution.'))
}

if (typeof e.cause === 'object' && e.cause && 'name' in e.cause) {
Expand Down
4 changes: 2 additions & 2 deletions packages/vitest/src/node/pool.ts
Expand Up @@ -197,8 +197,8 @@ function createChannel(ctx: Vitest) {
ctx.state.updateUserLog(log)
ctx.report('onUserConsoleLog', log)
},
onUnhandledRejection(err) {
ctx.state.catchError(err, 'Unhandled Rejection')
onUnhandledError(err, type) {
ctx.state.catchError(err, type)
},
onFinished(files) {
ctx.report('onFinished', files, ctx.state.getUnhandledErrors())
Expand Down
9 changes: 6 additions & 3 deletions packages/vitest/src/runtime/worker.ts
Expand Up @@ -32,15 +32,18 @@ async function startViteNode(ctx: WorkerContext) {
return processExit(code)
}

process.on('unhandledRejection', (err) => {
function catchError(err: unknown, type: string) {
const worker = getWorkerState()
const error = processError(err)
if (worker.filepath && !isPrimitive(error)) {
error.VITEST_TEST_NAME = worker.current?.name
error.VITEST_TEST_PATH = relative(config.root, worker.filepath)
}
rpc().onUnhandledRejection(error)
})
rpc().onUnhandledError(error, type)
}

process.on('uncaughtException', e => catchError(e, 'Uncaught Exception'))
process.on('unhandledRejection', e => catchError(e, 'Unhandled Rejection'))

const { run } = (await executeInViteNode({
files: [
Expand Down
2 changes: 1 addition & 1 deletion packages/vitest/src/types/worker.ts
Expand Up @@ -30,7 +30,7 @@ export interface WorkerRPC {
onWorkerExit: (error: unknown, code?: number) => void
onPathsCollected: (paths: string[]) => void
onUserConsoleLog: (log: UserConsoleLog) => void
onUnhandledRejection: (err: unknown) => void
onUnhandledError: (err: unknown, type: string) => void
onCollected: (files: File[]) => void
onAfterSuiteRun: (meta: AfterSuiteRunMeta) => void
onTaskUpdate: (pack: TaskResultPack[]) => void
Expand Down
2 changes: 2 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions test/core/test/dom.test.ts
Expand Up @@ -161,3 +161,13 @@ it('uses jsdom ArrayBuffer', async () => {
expect(arraybuffer instanceof ArrayBuffer).toBeTruthy()
expect(arraybuffer.constructor === ArrayBuffer).toBeTruthy()
})

it('doesn\'t throw, if listening for error', () => {
const spy = vi.fn((e: Event) => e.preventDefault())
window.addEventListener('error', spy)
addEventListener('custom', () => {
throw new Error('some error')
})
dispatchEvent(new Event('custom'))
expect(spy).toHaveBeenCalled()
})
10 changes: 10 additions & 0 deletions test/fails/fixtures/unhandled.test.ts
@@ -0,0 +1,10 @@
// @vitest-environment jsdom

import { test } from 'vitest'

test('unhandled exception', () => {
addEventListener('custom', () => {
throw new Error('some error')
})
dispatchEvent(new Event('custom'))
})
1 change: 1 addition & 0 deletions test/fails/package.json
Expand Up @@ -7,6 +7,7 @@
},
"devDependencies": {
"execa": "^6.1.0",
"jsdom": "^21.0.0",
"vitest": "workspace:*"
}
}
2 changes: 2 additions & 0 deletions test/fails/test/__snapshots__/runner.test.ts.snap
Expand Up @@ -17,3 +17,5 @@ exports[`should fails > nested-suite.test.ts > nested-suite.test.ts 1`] = `"Asse
exports[`should fails > stall.test.ts > stall.test.ts 1`] = `"TypeError: failure"`;
exports[`should fails > test-timeout.test.ts > test-timeout.test.ts 1`] = `"Error: Test timed out in 10ms."`;
exports[`should fails > unhandled.test.ts > unhandled.test.ts 1`] = `"Error: some error"`;

0 comments on commit 6a30cdd

Please sign in to comment.