From ec3d6949eb12b2b57ab6aeef920f6697d5e6a8a1 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Wed, 29 Nov 2023 14:30:59 +0100 Subject: [PATCH] fix(browser): don't go into an infinite reload loop, don't fail if "error" event is caught (#4618) --- packages/browser/src/client/main.ts | 59 ++++++++++++++++++++++++---- packages/browser/src/client/utils.ts | 20 +--------- 2 files changed, 52 insertions(+), 27 deletions(-) diff --git a/packages/browser/src/client/main.ts b/packages/browser/src/client/main.ts index 94a279c3829c..1e8420e0867b 100644 --- a/packages/browser/src/client/main.ts +++ b/packages/browser/src/client/main.ts @@ -22,6 +22,7 @@ const browserHashMap = new Map() const url = new URL(location.href) const testId = url.searchParams.get('id') || 'unknown' +const reloadTries = Number(url.searchParams.get('reloadTries') || '0') function getQueryPaths() { return url.searchParams.getAll('path') @@ -62,21 +63,51 @@ function on(event: string, listener: (...args: any[]) => void) { return () => window.removeEventListener(event, listener) } -// we can't import "processError" yet because error might've been thrown before the module was loaded -async function defaultErrorReport(type: string, unhandledError: any) { - const error = { +function serializeError(unhandledError: any) { + return { ...unhandledError, name: unhandledError.name, message: unhandledError.message, - stack: unhandledError.stack, + stack: String(unhandledError.stack), } +} + +// we can't import "processError" yet because error might've been thrown before the module was loaded +async function defaultErrorReport(type: string, unhandledError: any) { + const error = serializeError(unhandledError) if (testId !== 'no-isolate') error.VITEST_TEST_PATH = testId await client.rpc.onUnhandledError(error, type) await client.rpc.onDone(testId) } -const stopErrorHandler = on('error', e => defaultErrorReport('Error', e.error)) +function catchWindowErrors(cb: (e: ErrorEvent) => void) { + let userErrorListenerCount = 0 + function throwUnhandlerError(e: ErrorEvent) { + if (userErrorListenerCount === 0 && e.error != null) + cb(e) + else + console.error(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) + } +} + +const stopErrorHandler = catchWindowErrors(e => defaultErrorReport('Error', e.error)) const stopRejectionHandler = on('unhandledrejection', e => defaultErrorReport('Unhandled Rejection', e.reason)) let runningTests = false @@ -100,15 +131,27 @@ ws.addEventListener('open', async () => { const { getSafeTimers } = await importId('vitest/utils') as typeof import('vitest/utils') safeRpc = createSafeRpc(client, getSafeTimers) } - catch (err) { - location.reload() + catch (err: any) { + if (reloadTries >= 10) { + const error = serializeError(new Error('Vitest failed to load "vitest/utils" after 10 retries.')) + error.cause = serializeError(err) + + await client.rpc.onUnhandledError(error, 'Reload Error') + await client.rpc.onDone(testId) + return + } + + const tries = reloadTries + 1 + const newUrl = new URL(location.href) + newUrl.searchParams.set('reloadTries', String(tries)) + location.href = newUrl.href return } stopErrorHandler() stopRejectionHandler() - on('error', event => reportUnexpectedError(safeRpc, 'Error', event.error)) + catchWindowErrors(event => reportUnexpectedError(safeRpc, 'Error', event.error)) on('unhandledrejection', event => reportUnexpectedError(safeRpc, 'Unhandled Rejection', event.reason)) // @ts-expect-error untyped global for internal use diff --git a/packages/browser/src/client/utils.ts b/packages/browser/src/client/utils.ts index b79f353d319e..27a390af601d 100644 --- a/packages/browser/src/client/utils.ts +++ b/packages/browser/src/client/utils.ts @@ -1,23 +1,5 @@ -// it's possible that import was not optimized yet -async function tryImport(id: string, tries = 20): Promise { - try { - return await import(id) - } - catch (cause) { - if (tries <= 0) { - location.reload() - throw new Error(`Failed to import ${id}.`, { cause }) - } - - await new Promise(resolve => setTimeout(resolve, 0)) - return await tryImport(id, tries - 1) - } -} - export async function importId(id: string) { const name = `/@id/${id}` - // TODO: this import _should_ always work, but sometimes it doesn't - // this is a workaround until we can properly debug it - maybe server is not ready? // @ts-expect-error mocking vitest apis - return __vi_wrap_module__(tryImport(name)) + return __vi_wrap_module__(import(name)) }