Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(browser): don't go into an infinite reload loop, don't fail if "error" event is caught #4618

Merged
merged 1 commit into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 51 additions & 8 deletions packages/browser/src/client/main.ts
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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))
}
Loading