fix(browser): improve error handling and don't rely on Node.js builtin modules in browser mode (#4244)

This commit is contained in:
Vladimir 2023-10-06 09:57:22 +02:00 committed by GitHub
parent c05b11a0fa
commit e7e8c3cc0d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 106 additions and 78 deletions

View File

@ -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": {

View File

@ -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
@ -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)
}

View File

@ -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
}
@ -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
})
}
}
}

View File

@ -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"

View File

@ -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'

View File

@ -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",

View File

@ -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)
},

View File

@ -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

View File

@ -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'

View File

@ -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'
@ -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)

View File

@ -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'
@ -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),
})

View File

@ -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
pnpm-lock.yaml generated
View File

@ -9,6 +9,9 @@ overrides:
vitest: workspace:*
patchedDependencies:
'@sinonjs/fake-timers@11.1.0':
hash: trok5obk3l5tdlygozv34fknii
path: patches/@sinonjs__fake-timers@11.1.0.patch
'@types/chai@4.3.6':
hash: s5kzatt2y2dzfxfynxzvzt5kbm
path: patches/@types__chai@4.3.6.patch
@ -1082,7 +1085,7 @@ importers:
version: 4.3.10
devDependencies:
'@types/chai':
specifier: ^4.3.6
specifier: 4.3.6
version: 4.3.6(patch_hash=s5kzatt2y2dzfxfynxzvzt5kbm)
'@vitest/runner':
specifier: workspace:*
@ -1372,8 +1375,8 @@ importers:
specifier: 3.0.3
version: 3.0.3
'@sinonjs/fake-timers':
specifier: ^11.0.0
version: 11.0.0
specifier: 11.1.0
version: 11.1.0(patch_hash=trok5obk3l5tdlygozv34fknii)
'@types/diff':
specifier: ^5.0.3
version: 5.0.3
@ -1397,7 +1400,7 @@ importers:
version: 2.4.4
'@types/sinonjs__fake-timers':
specifier: ^8.1.2
version: 8.1.2
version: 8.1.3
birpc:
specifier: 0.2.14
version: 0.2.14
@ -2775,24 +2778,6 @@ packages:
- supports-color
dev: true
/@babel/helper-create-class-features-plugin@7.22.15(@babel/core@7.22.9):
resolution: {integrity: sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0
dependencies:
'@babel/core': 7.22.9
'@babel/helper-annotate-as-pure': 7.22.5
'@babel/helper-environment-visitor': 7.22.20
'@babel/helper-function-name': 7.23.0
'@babel/helper-member-expression-to-functions': 7.22.15
'@babel/helper-optimise-call-expression': 7.22.5
'@babel/helper-replace-supers': 7.22.9(@babel/core@7.22.9)
'@babel/helper-skip-transparent-expression-wrappers': 7.22.5
'@babel/helper-split-export-declaration': 7.22.6
semver: 6.3.1
dev: true
/@babel/helper-create-class-features-plugin@7.22.15(@babel/core@7.23.0):
resolution: {integrity: sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg==}
engines: {node: '>=6.9.0'}
@ -3185,18 +3170,6 @@ packages:
- supports-color
dev: true
/@babel/helper-replace-supers@7.22.9(@babel/core@7.22.9):
resolution: {integrity: sha512-LJIKvvpgPOPUThdYqcX6IXRuIcTkcAub0IaDRGCZH0p5GPUp7PhRU9QVgFcDDd51BaPkk77ZjqFwh6DZTAEmGg==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0
dependencies:
'@babel/core': 7.22.9
'@babel/helper-environment-visitor': 7.22.20
'@babel/helper-member-expression-to-functions': 7.22.15
'@babel/helper-optimise-call-expression': 7.22.5
dev: true
/@babel/helper-replace-supers@7.22.9(@babel/core@7.23.0):
resolution: {integrity: sha512-LJIKvvpgPOPUThdYqcX6IXRuIcTkcAub0IaDRGCZH0p5GPUp7PhRU9QVgFcDDd51BaPkk77ZjqFwh6DZTAEmGg==}
engines: {node: '>=6.9.0'}
@ -4574,16 +4547,6 @@ packages:
'@babel/helper-plugin-utils': 7.22.5
dev: true
/@babel/plugin-syntax-typescript@7.22.5(@babel/core@7.22.9):
resolution: {integrity: sha512-1mS2o03i7t1c6VzH6fdQ3OA8tcEIxwG18zIPRp+UY1Ihv6W+XZzBCVxExF9upussPXJ0xE9XRHwMoNs1ep/nRQ==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.22.9
'@babel/helper-plugin-utils': 7.22.5
dev: true
/@babel/plugin-syntax-typescript@7.22.5(@babel/core@7.23.0):
resolution: {integrity: sha512-1mS2o03i7t1c6VzH6fdQ3OA8tcEIxwG18zIPRp+UY1Ihv6W+XZzBCVxExF9upussPXJ0xE9XRHwMoNs1ep/nRQ==}
engines: {node: '>=6.9.0'}
@ -5819,19 +5782,6 @@ packages:
- supports-color
dev: true
/@babel/plugin-transform-typescript@7.22.15(@babel/core@7.22.9):
resolution: {integrity: sha512-1uirS0TnijxvQLnlv5wQBwOX3E1wCFX7ITv+9pBV2wKEk4K+M5tqDaoNXnTH8tjEIYHLO98MwiTWO04Ggz4XuA==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.22.9
'@babel/helper-annotate-as-pure': 7.22.5
'@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.22.9)
'@babel/helper-plugin-utils': 7.22.5
'@babel/plugin-syntax-typescript': 7.22.5(@babel/core@7.22.9)
dev: true
/@babel/plugin-transform-typescript@7.22.15(@babel/core@7.23.0):
resolution: {integrity: sha512-1uirS0TnijxvQLnlv5wQBwOX3E1wCFX7ITv+9pBV2wKEk4K+M5tqDaoNXnTH8tjEIYHLO98MwiTWO04Ggz4XuA==}
engines: {node: '>=6.9.0'}
@ -6285,7 +6235,9 @@ packages:
'@babel/core': 7.22.9
'@babel/helper-plugin-utils': 7.22.5
'@babel/helper-validator-option': 7.22.5
'@babel/plugin-transform-typescript': 7.22.15(@babel/core@7.22.9)
'@babel/plugin-transform-typescript': 7.21.3(@babel/core@7.22.9)
transitivePeerDependencies:
- supports-color
dev: true
/@babel/register@7.18.9(@babel/core@7.22.9):
@ -8495,11 +8447,12 @@ packages:
type-detect: 4.0.8
dev: true
/@sinonjs/fake-timers@11.0.0:
resolution: {integrity: sha512-bqiI/5ur6ZOozG06BeJjbplIqHY/KftV1zaewbZHORH902GrHURKwl7H1G/4OC5EaxDYQJlrD0OLJ1XD6x01dQ==}
/@sinonjs/fake-timers@11.1.0(patch_hash=trok5obk3l5tdlygozv34fknii):
resolution: {integrity: sha512-pUBaWhXoa9N0R/LeYKLqkrN9mqN3jwKBeMfbvlRtHUzLmk55o+0swncIuZBcSH/PpXDttRf/AcPF22pknAzORQ==}
dependencies:
'@sinonjs/commons': 3.0.0
dev: true
patched: true
/@sinonjs/fake-timers@8.1.0:
resolution: {integrity: sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg==}
@ -10615,8 +10568,8 @@ packages:
resolution: {integrity: sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==}
dev: true
/@types/sinonjs__fake-timers@8.1.2:
resolution: {integrity: sha512-9GcLXF0/v3t80caGs5p2rRfkB+a8VBGLJZVih6CNFkx8IZ994wiKKLSRs9nuFwk1HevWs/1mnUmkApGrSGsShA==}
/@types/sinonjs__fake-timers@8.1.3:
resolution: {integrity: sha512-4g+2YyWe0Ve+LBh+WUm1697PD0Kdi6coG1eU0YjQbwx61AZ8XbEpL1zIT6WjuUKrCMCROpEaYQPDjBnDouBVAQ==}
dev: true
/@types/sizzle@2.3.3: