Skip to content

Commit

Permalink
fix(browser): don't go into an infinite reload loop, don't fail if "e…
Browse files Browse the repository at this point in the history
…rror" event is caught (#4618)
  • Loading branch information
sheremet-va committed Nov 29, 2023
1 parent 2ff3533 commit ec3d694
Show file tree
Hide file tree
Showing 2 changed files with 52 additions and 27 deletions.
59 changes: 51 additions & 8 deletions packages/browser/src/client/main.ts
Expand Up @@ -22,6 +22,7 @@ const browserHashMap = new Map<string, [test: boolean, timestamp: string]>()

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')
Expand Down Expand Up @@ -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<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)
}
}

const stopErrorHandler = catchWindowErrors(e => defaultErrorReport('Error', e.error))
const stopRejectionHandler = on('unhandledrejection', e => defaultErrorReport('Unhandled Rejection', e.reason))

let runningTests = false
Expand All @@ -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
Expand Down
20 changes: 1 addition & 19 deletions 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<any> {
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))
}

0 comments on commit ec3d694

Please sign in to comment.