Skip to content

Commit

Permalink
fix(browser): improve error handling and don't rely on Node.js builti…
Browse files Browse the repository at this point in the history
…n modules in browser mode (#4244)
  • Loading branch information
sheremet-va committed Oct 6, 2023
1 parent c05b11a commit e7e8c3c
Show file tree
Hide file tree
Showing 13 changed files with 106 additions and 78 deletions.
3 changes: 2 additions & 1 deletion package.json
Expand Up @@ -85,7 +85,8 @@
]
},
"patchedDependencies": {
"@types/chai@4.3.6": "patches/@types__chai@4.3.6.patch"
"@types/chai@4.3.6": "patches/@types__chai@4.3.6.patch",
"@sinonjs/fake-timers@11.1.0": "patches/@sinonjs__fake-timers@11.1.0.patch"
}
},
"simple-git-hooks": {
Expand Down
39 changes: 39 additions & 0 deletions packages/browser/src/client/main.ts
Expand Up @@ -57,12 +57,47 @@ async function loadConfig() {
throw new Error('cannot load configuration after 5 retries')
}

function on(event: string, listener: (...args: any[]) => void) {
window.addEventListener(event, listener)
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 = {
...unhandledError,
name: unhandledError.name,
message: unhandledError.message,
stack: unhandledError.stack,
}
await client.rpc.onUnhandledError(error, type)
await client.rpc.onDone(testId)
}

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

let runningTests = false

async function reportUnexpectedError(rpc: typeof client.rpc, type: string, error: any) {
const { processError } = await importId('vitest/browser') as typeof import('vitest/browser')
await rpc.onUnhandledError(processError(error), type)
if (!runningTests)
await rpc.onDone(testId)
}

ws.addEventListener('open', async () => {
await loadConfig()

const { getSafeTimers } = await importId('vitest/utils') as typeof import('vitest/utils')
const safeRpc = createSafeRpc(client, getSafeTimers)

stopErrorHandler()
stopRejectionHandler()

on('error', event => reportUnexpectedError(safeRpc, 'Error', event.error))
on('unhandledrejection', event => reportUnexpectedError(safeRpc, 'Unhandled Rejection', event.reason))

// @ts-expect-error untyped global for internal use
globalThis.__vitest_browser__ = true
// @ts-expect-error mocking vitest apis
Expand Down Expand Up @@ -134,10 +169,14 @@ async function runTests(paths: string[], config: ResolvedConfig) {
const now = `${new Date().getTime()}`
files.forEach(i => browserHashMap.set(i, [true, now]))

runningTests = true

for (const file of files)
await startTests([file], runner)
}
finally {
runningTests = false

await rpcDone()
await rpc().onDone(testId)
}
Expand Down
13 changes: 1 addition & 12 deletions packages/browser/src/client/rpc.ts
Expand Up @@ -6,24 +6,19 @@ import type { VitestClient } from '@vitest/ws-client'
const { get } = Reflect

function withSafeTimers(getTimers: typeof getSafeTimers, fn: () => void) {
const { setTimeout, clearTimeout, nextTick, setImmediate, clearImmediate } = getTimers()
const { setTimeout, clearTimeout, setImmediate, clearImmediate } = getTimers()

const currentSetTimeout = globalThis.setTimeout
const currentClearTimeout = globalThis.clearTimeout
const currentSetImmediate = globalThis.setImmediate
const currentClearImmediate = globalThis.clearImmediate

const currentNextTick = globalThis.process?.nextTick

try {
globalThis.setTimeout = setTimeout
globalThis.clearTimeout = clearTimeout
globalThis.setImmediate = setImmediate
globalThis.clearImmediate = clearImmediate

if (globalThis.process)
globalThis.process.nextTick = nextTick

const result = fn()
return result
}
Expand All @@ -32,12 +27,6 @@ function withSafeTimers(getTimers: typeof getSafeTimers, fn: () => void) {
globalThis.clearTimeout = currentClearTimeout
globalThis.setImmediate = currentSetImmediate
globalThis.clearImmediate = currentClearImmediate

if (globalThis.process) {
nextTick(() => {
globalThis.process.nextTick = currentNextTick
})
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/expect/package.json
Expand Up @@ -40,7 +40,7 @@
"chai": "^4.3.10"
},
"devDependencies": {
"@types/chai": "^4.3.6",
"@types/chai": "4.3.6",
"@vitest/runner": "workspace:*",
"picocolors": "^1.0.0",
"rollup-plugin-copy": "^3.5.0"
Expand Down
1 change: 1 addition & 0 deletions packages/runner/src/index.ts
Expand Up @@ -3,4 +3,5 @@ export { test, it, describe, suite, getCurrentSuite, createTaskCollector } from
export { beforeAll, beforeEach, afterAll, afterEach, onTestFailed } from './hooks'
export { setFn, getFn } from './map'
export { getCurrentTest } from './test-state'
export { processError } from '@vitest/utils/error'
export * from './types'
2 changes: 1 addition & 1 deletion packages/vitest/package.json
Expand Up @@ -172,7 +172,7 @@
"@ampproject/remapping": "^2.2.1",
"@antfu/install-pkg": "^0.1.1",
"@edge-runtime/vm": "3.0.3",
"@sinonjs/fake-timers": "^11.0.0",
"@sinonjs/fake-timers": "11.1.0",
"@types/diff": "^5.0.3",
"@types/estree": "^1.0.1",
"@types/istanbul-lib-coverage": "^2.0.4",
Expand Down
3 changes: 3 additions & 0 deletions packages/vitest/src/api/setup.ts
Expand Up @@ -40,6 +40,9 @@ export function setup(vitestOrWorkspace: Vitest | WorkspaceProject, server?: Vit
function setupClient(ws: WebSocket) {
const rpc = createBirpc<WebSocketEvents, WebSocketHandlers>(
{
async onUnhandledError(error, type) {
ctx.state.catchError(error, type)
},
async onDone(testId) {
return ctx.state.browserTestPromises.get(testId)?.resolve(true)
},
Expand Down
1 change: 1 addition & 0 deletions packages/vitest/src/api/types.ts
Expand Up @@ -7,6 +7,7 @@ export interface TransformResultWithSource extends TransformResult {
}

export interface WebSocketHandlers {
onUnhandledError(error: unknown, type: string): Promise<void>
onCollected(files?: File[]): Promise<void>
onTaskUpdate(packs: TaskResultPack[]): void
onAfterSuiteRun(meta: AfterSuiteRunMeta): void
Expand Down
2 changes: 1 addition & 1 deletion packages/vitest/src/browser.ts
@@ -1,3 +1,3 @@
export { startTests } from '@vitest/runner'
export { startTests, processError } from '@vitest/runner'
export { setupCommonEnv, loadDiffConfig } from './runtime/setup.common'
export { takeCoverageInsideWorker, stopCoverageInsideWorker, getCoverageProvider, startCoverageInsideWorker } from './integrations/coverage'
8 changes: 8 additions & 0 deletions packages/vitest/src/runtime/entry-vm.ts
@@ -1,5 +1,7 @@
import { isatty } from 'node:tty'
import { createRequire } from 'node:module'
import util from 'node:util'
import timers from 'node:timers'
import { performance } from 'node:perf_hooks'
import { startTests } from '@vitest/runner'
import { createColors, setupColors } from '@vitest/utils'
Expand Down Expand Up @@ -36,6 +38,12 @@ export async function run(files: string[], config: ResolvedConfig, executor: Vit
_require.extensions['.less'] = () => ({})
}

// @ts-expect-error not typed global for patched timers
globalThis.__vitest_required__ = {
util,
timers,
}

await startCoverageInsideWorker(config.coverage, executor)

if (config.chaiConfig)
Expand Down
8 changes: 8 additions & 0 deletions packages/vitest/src/runtime/setup.node.ts
@@ -1,4 +1,6 @@
import { createRequire } from 'node:module'
import util from 'node:util'
import timers from 'node:timers'
import { isatty } from 'node:tty'
import { installSourcemapsSupport } from 'vite-node/source-map'
import { createColors, setupColors } from '@vitest/utils'
Expand Down Expand Up @@ -43,6 +45,12 @@ export async function setupGlobalEnv(config: ResolvedConfig, { environment }: Re
process.env.SSR = '1'
}

// @ts-expect-error not typed global for patched timers
globalThis.__vitest_required__ = {
util,
timers,
}

installSourcemapsSupport({
getSourceMap: source => state.moduleCache.getSourceMap(source),
})
Expand Down
25 changes: 25 additions & 0 deletions patches/@sinonjs__fake-timers@11.1.0.patch
@@ -0,0 +1,25 @@
diff --git a/src/fake-timers-src.js b/src/fake-timers-src.js
index 607336d6a9c568a32b0cde4499c8fd56f06d424a..35187b0ee298df858118494b5a9b3e5efa8197b0 100644
--- a/src/fake-timers-src.js
+++ b/src/fake-timers-src.js
@@ -2,9 +2,9 @@

const globalObject = require("@sinonjs/commons").global;
let timersModule;
-if (typeof require === "function" && typeof module === "object") {
+if (typeof __vitest_required__ !== 'undefined') {
try {
- timersModule = require("timers");
+ timersModule = __vitest_required__.timers;
} catch (e) {
// ignored
}
@@ -159,7 +159,7 @@ function withGlobal(_global) {
hrtimePresent && typeof _global.process.hrtime.bigint === "function";
const nextTickPresent =
_global.process && typeof _global.process.nextTick === "function";
- const utilPromisify = _global.process && require("util").promisify;
+ const utilPromisify = _global.process && _global.__vitest_required__ && _global.__vitest_required__.util.promisify;
const performancePresent =
_global.performance && typeof _global.performance.now === "function";
const hasPerformancePrototype =
77 changes: 15 additions & 62 deletions pnpm-lock.yaml

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

0 comments on commit e7e8c3c

Please sign in to comment.