diff --git a/package.json b/package.json index 0824c8875a54..5bbc0571e627 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/packages/browser/src/client/main.ts b/packages/browser/src/client/main.ts index 95abe1d22b3e..e3ff443e9b9c 100644 --- a/packages/browser/src/client/main.ts +++ b/packages/browser/src/client/main.ts @@ -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) } diff --git a/packages/browser/src/client/rpc.ts b/packages/browser/src/client/rpc.ts index 214f4e91ea82..7cf2153b5712 100644 --- a/packages/browser/src/client/rpc.ts +++ b/packages/browser/src/client/rpc.ts @@ -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 - }) - } } } diff --git a/packages/expect/package.json b/packages/expect/package.json index 34a3709450dc..f632149093f2 100644 --- a/packages/expect/package.json +++ b/packages/expect/package.json @@ -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" diff --git a/packages/runner/src/index.ts b/packages/runner/src/index.ts index 6bc4238ffc3f..89b9c0c284f3 100644 --- a/packages/runner/src/index.ts +++ b/packages/runner/src/index.ts @@ -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' diff --git a/packages/vitest/package.json b/packages/vitest/package.json index a4cae583af97..2878e61161d5 100644 --- a/packages/vitest/package.json +++ b/packages/vitest/package.json @@ -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", diff --git a/packages/vitest/src/api/setup.ts b/packages/vitest/src/api/setup.ts index 2b7a9078915a..d1ba82c42ed5 100644 --- a/packages/vitest/src/api/setup.ts +++ b/packages/vitest/src/api/setup.ts @@ -40,6 +40,9 @@ export function setup(vitestOrWorkspace: Vitest | WorkspaceProject, server?: Vit function setupClient(ws: WebSocket) { const rpc = createBirpc( { + async onUnhandledError(error, type) { + ctx.state.catchError(error, type) + }, async onDone(testId) { return ctx.state.browserTestPromises.get(testId)?.resolve(true) }, diff --git a/packages/vitest/src/api/types.ts b/packages/vitest/src/api/types.ts index cf7e7d1a744e..0933448d2b14 100644 --- a/packages/vitest/src/api/types.ts +++ b/packages/vitest/src/api/types.ts @@ -7,6 +7,7 @@ export interface TransformResultWithSource extends TransformResult { } export interface WebSocketHandlers { + onUnhandledError(error: unknown, type: string): Promise onCollected(files?: File[]): Promise onTaskUpdate(packs: TaskResultPack[]): void onAfterSuiteRun(meta: AfterSuiteRunMeta): void diff --git a/packages/vitest/src/browser.ts b/packages/vitest/src/browser.ts index 83d98341f387..c8802ceb4e07 100644 --- a/packages/vitest/src/browser.ts +++ b/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' diff --git a/packages/vitest/src/runtime/entry-vm.ts b/packages/vitest/src/runtime/entry-vm.ts index 78edc1a32e64..03330296693e 100644 --- a/packages/vitest/src/runtime/entry-vm.ts +++ b/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' @@ -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) diff --git a/packages/vitest/src/runtime/setup.node.ts b/packages/vitest/src/runtime/setup.node.ts index e1f1bc1f1d25..3621104eb07e 100644 --- a/packages/vitest/src/runtime/setup.node.ts +++ b/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' @@ -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), }) diff --git a/patches/@sinonjs__fake-timers@11.1.0.patch b/patches/@sinonjs__fake-timers@11.1.0.patch new file mode 100644 index 000000000000..316b82e959e0 --- /dev/null +++ b/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 = diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cec79dd6ba92..c71b6c95aa99 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: