diff --git a/packages/vitest/src/integrations/env/jsdom.ts b/packages/vitest/src/integrations/env/jsdom.ts index 4e8bd92fabe8..92f8f6f80c50 100644 --- a/packages/vitest/src/integrations/env/jsdom.ts +++ b/packages/vitest/src/integrations/env/jsdom.ts @@ -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) { + if (args[0] === 'error') + userErrorListenerCount++ + return addEventListener.apply(this, args) + } + window.removeEventListener = function (...args: Parameters) { + if (args[0] === 'error' && userErrorListenerCount) + userErrorListenerCount-- + return removeEventListener.apply(this, args) + } + return function clearErrorHandlers() { + window.removeEventListener('error', throwUnhandlerError) + } +} + export default ({ name: 'jsdom', async setup(global, { jsdom = {} }) { @@ -42,8 +66,12 @@ export default ({ 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) }, diff --git a/packages/vitest/src/node/error.ts b/packages/vitest/src/node/error.ts index b009a79e945c..d7b27e53d094 100644 --- a/packages/vitest/src/node/error.ts +++ b/packages/vitest/src/node/error.ts @@ -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) { diff --git a/packages/vitest/src/node/pool.ts b/packages/vitest/src/node/pool.ts index d3c599815748..997cb80f1914 100644 --- a/packages/vitest/src/node/pool.ts +++ b/packages/vitest/src/node/pool.ts @@ -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()) diff --git a/packages/vitest/src/runtime/worker.ts b/packages/vitest/src/runtime/worker.ts index 245bcdebe7af..e769f5da1f40 100644 --- a/packages/vitest/src/runtime/worker.ts +++ b/packages/vitest/src/runtime/worker.ts @@ -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: [ diff --git a/packages/vitest/src/types/worker.ts b/packages/vitest/src/types/worker.ts index e08575a2c2b5..a4eb92dde048 100644 --- a/packages/vitest/src/types/worker.ts +++ b/packages/vitest/src/types/worker.ts @@ -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 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9d074c36b87e..a5baa3bb1faf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1084,9 +1084,11 @@ importers: test/fails: specifiers: execa: ^6.1.0 + jsdom: ^21.0.0 vitest: workspace:* devDependencies: execa: 6.1.0 + jsdom: 21.0.0 vitest: link:../../packages/vitest test/global-setup: diff --git a/test/core/test/dom.test.ts b/test/core/test/dom.test.ts index 27fcd7b977fa..1f6a04010072 100644 --- a/test/core/test/dom.test.ts +++ b/test/core/test/dom.test.ts @@ -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() +}) diff --git a/test/fails/fixtures/unhandled.test.ts b/test/fails/fixtures/unhandled.test.ts new file mode 100644 index 000000000000..00276381eceb --- /dev/null +++ b/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')) +}) diff --git a/test/fails/package.json b/test/fails/package.json index 7c17819c1ae6..7c39c13b224b 100644 --- a/test/fails/package.json +++ b/test/fails/package.json @@ -7,6 +7,7 @@ }, "devDependencies": { "execa": "^6.1.0", + "jsdom": "^21.0.0", "vitest": "workspace:*" } } diff --git a/test/fails/test/__snapshots__/runner.test.ts.snap b/test/fails/test/__snapshots__/runner.test.ts.snap index 8b0e88a42561..414dba9453e5 100644 --- a/test/fails/test/__snapshots__/runner.test.ts.snap +++ b/test/fails/test/__snapshots__/runner.test.ts.snap @@ -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"`;