From 1531c4208d8a8c54cec9f4d48a17af28345792e2 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Mon, 20 Mar 2023 22:59:16 +0100 Subject: [PATCH] feat: allow accessing "vi" methods without context, don't fail when mocker is not available (#3047) --- packages/browser/package.json | 2 +- packages/browser/src/client/main.ts | 2 - packages/vitest/src/integrations/vi.ts | 524 ++++++++++++++----------- 3 files changed, 290 insertions(+), 238 deletions(-) diff --git a/packages/browser/package.json b/packages/browser/package.json index 18bfa8a9a666..a2ec4cf76535 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -46,8 +46,8 @@ }, "devDependencies": { "@types/ws": "^8.5.4", - "@vitest/ws-client": "workspace:*", "@vitest/ui": "workspace:*", + "@vitest/ws-client": "workspace:*", "rollup": "^2.79.1", "vitest": "workspace:*" } diff --git a/packages/browser/src/client/main.ts b/packages/browser/src/client/main.ts index ebf2ad4e67b0..3ff07184f5cd 100644 --- a/packages/browser/src/client/main.ts +++ b/packages/browser/src/client/main.ts @@ -59,8 +59,6 @@ ws.addEventListener('open', async () => { rpc: client.rpc, } - // @ts-expect-error mocking vitest apis - globalThis.__vitest_mocker__ = {} const paths = getQueryPaths() const iFrame = document.getElementById('vitest-ui') as HTMLIFrameElement diff --git a/packages/vitest/src/integrations/vi.ts b/packages/vitest/src/integrations/vi.ts index 592b5ad63907..1666909a934f 100644 --- a/packages/vitest/src/integrations/vi.ts +++ b/packages/vitest/src/integrations/vi.ts @@ -9,130 +9,26 @@ import { FakeTimers } from './mock/timers' import type { EnhancedSpy, MaybeMocked, MaybeMockedDeep, MaybePartiallyMocked, MaybePartiallyMockedDeep } from './spy' import { fn, isMockFunction, spies, spyOn } from './spy' -class VitestUtils { - private _timers: FakeTimers - private _mockedDate: string | number | Date | null - private _mocker: VitestMocker - - constructor() { - // @ts-expect-error injected by vite-nide - this._mocker = typeof __vitest_mocker__ !== 'undefined' ? __vitest_mocker__ : null - this._mockedDate = null - - if (!this._mocker) { - const errorMsg = 'Vitest was initialized with native Node instead of Vite Node.' - + '\n\nIt\'s possible that you are importing "vitest" directly inside "globalSetup". In that case, use "setupFiles" because "globalSetup" runs in a different context.' - + '\nOtherwise, it might be a Vitest bug. Please report it to https://github.com/vitest-dev/vitest/issues\n' - throw new Error(errorMsg) - } - - const workerState = getWorkerState() - this._timers = new FakeTimers({ - global: globalThis, - config: workerState.config.fakeTimers, - }) - } - - // timers - - public useFakeTimers(config?: FakeTimerInstallOpts) { - if (config) { - this._timers.configure(config) - } - else { - const workerState = getWorkerState() - this._timers.configure(workerState.config.fakeTimers) - } - this._timers.useFakeTimers() - return this - } - - public useRealTimers() { - this._timers.useRealTimers() - this._mockedDate = null - return this - } - - public runOnlyPendingTimers() { - this._timers.runOnlyPendingTimers() - return this - } - - public async runOnlyPendingTimersAsync() { - await this._timers.runOnlyPendingTimersAsync() - return this - } - - public runAllTimers() { - this._timers.runAllTimers() - return this - } - - public async runAllTimersAsync() { - await this._timers.runAllTimersAsync() - return this - } - - public runAllTicks() { - this._timers.runAllTicks() - return this - } - - public advanceTimersByTime(ms: number) { - this._timers.advanceTimersByTime(ms) - return this - } - - public async advanceTimersByTimeAsync(ms: number) { - await this._timers.advanceTimersByTimeAsync(ms) - return this - } - - public advanceTimersToNextTimer() { - this._timers.advanceTimersToNextTimer() - return this - } - - public async advanceTimersToNextTimerAsync() { - await this._timers.advanceTimersToNextTimerAsync() - return this - } - - public getTimerCount() { - return this._timers.getTimerCount() - } - - public setSystemTime(time: number | string | Date) { - const date = time instanceof Date ? time : new Date(time) - this._mockedDate = date - this._timers.setSystemTime(date) - return this - } - - public getMockedSystemTime() { - return this._mockedDate - } - - public getRealSystemTime() { - return this._timers.getRealSystemTime() - } - - public clearAllTimers() { - this._timers.clearAllTimers() - return this - } - - // mocks - - spyOn = spyOn - fn = fn - - private getImporter() { - const stackTrace = createSimpleStackTrace({ stackTraceLimit: 4 }) - const importerStack = stackTrace.split('\n')[4] - const stack = parseSingleStack(importerStack) - return stack?.file || '' - } +interface VitestUtils { + useFakeTimers(config?: FakeTimerInstallOpts): this + useRealTimers(): this + runOnlyPendingTimers(): this + runOnlyPendingTimersAsync(): Promise + runAllTimers(): this + runAllTimersAsync(): Promise + runAllTicks(): this + advanceTimersByTime(ms: number): this + advanceTimersByTimeAsync(ms: number): Promise + advanceTimersToNextTimer(): this + advanceTimersToNextTimerAsync(): Promise + getTimerCount(): number + setSystemTime(time: number | string | Date): this + getMockedSystemTime(): Date | null + getRealSystemTime(): number + clearAllTimers(): this + + spyOn: typeof spyOn + fn: typeof fn /** * Makes all `imports` to passed module to be mocked. @@ -145,31 +41,17 @@ class VitestUtils { * @param path Path to the module. Can be aliased, if your config supports it * @param factory Factory for the mocked module. Has the highest priority. */ - public mock(path: string, factory?: MockFactoryWithHelper) { - const importer = this.getImporter() - this._mocker.queueMock( - path, - importer, - factory ? () => factory(() => this._mocker.importActual(path, importer)) : undefined, - ) - } + mock(path: string, factory?: MockFactoryWithHelper): void /** * Removes module from mocked registry. All subsequent calls to import will * return original module even if it was mocked. * @param path Path to the module. Can be aliased, if your config supports it */ - public unmock(path: string) { - this._mocker.queueUnmock(path, this.getImporter()) - } - - public doMock(path: string, factory?: () => any) { - this._mocker.queueMock(path, this.getImporter(), factory) - } + unmock(path: string): void - public doUnmock(path: string) { - this._mocker.queueUnmock(path, this.getImporter()) - } + doMock(path: string, factory?: () => any): void + doUnmock(path: string): void /** * Imports module, bypassing all checks if it should be mocked. @@ -183,9 +65,7 @@ class VitestUtils { * @param path Path to the module. Can be aliased, if your config supports it * @returns Actual module without spies */ - public async importActual(path: string): Promise { - return this._mocker.importActual(path, this.getImporter()) - } + importActual(path: string): Promise /** * Imports a module with all of its properties and nested properties mocked. @@ -193,9 +73,7 @@ class VitestUtils { * @param path Path to the module. Can be aliased, if your config supports it * @returns Fully mocked module */ - public async importMock(path: string): Promise> { - return this._mocker.importMock(path, this.getImporter()) - } + importMock(path: string): Promise> /** * Type helpers for TypeScript. In reality just returns the object that was passed. @@ -216,131 +94,307 @@ class VitestUtils { * @param deep If the object is deeply mocked * @param options If the object is partially or deeply mocked */ - public mocked(item: T, deep?: false): MaybeMocked - public mocked(item: T, deep: true): MaybeMockedDeep - public mocked(item: T, options: { partial?: false; deep?: false }): MaybeMocked - public mocked(item: T, options: { partial?: false; deep: true }): MaybeMockedDeep - public mocked(item: T, options: { partial: true; deep?: false }): MaybePartiallyMocked - public mocked(item: T, options: { partial: true; deep: true }): MaybePartiallyMockedDeep - public mocked(item: T, _options = {}): MaybeMocked { - return item as any - } - - public isMockFunction(fn: any): fn is EnhancedSpy { - return isMockFunction(fn) - } - - public clearAllMocks() { - spies.forEach(spy => spy.mockClear()) - return this - } - - public resetAllMocks() { - spies.forEach(spy => spy.mockReset()) - return this - } + mocked(item: T, deep?: false): MaybeMocked + mocked(item: T, deep: true): MaybeMockedDeep + mocked(item: T, options: { partial?: false; deep?: false }): MaybeMocked + mocked(item: T, options: { partial?: false; deep: true }): MaybeMockedDeep + mocked(item: T, options: { partial: true; deep?: false }): MaybePartiallyMocked + mocked(item: T, options: { partial: true; deep: true }): MaybePartiallyMockedDeep + mocked(item: T): MaybeMocked - public restoreAllMocks() { - spies.forEach(spy => spy.mockRestore()) - return this - } + isMockFunction(fn: any): fn is EnhancedSpy - private _stubsGlobal = new Map() - private _stubsEnv = new Map() + clearAllMocks(): this + resetAllMocks(): this + restoreAllMocks(): this /** * Makes value available on global namespace. * Useful, if you want to have global variables available, like `IntersectionObserver`. * You can return it back to original value with `vi.unstubGlobals`, or by enabling `unstubGlobals` config option. */ - public stubGlobal(name: string | symbol | number, value: any) { - if (!this._stubsGlobal.has(name)) - this._stubsGlobal.set(name, Object.getOwnPropertyDescriptor(globalThis, name)) - Object.defineProperty(globalThis, name, { - value, - writable: true, - configurable: true, - enumerable: true, - }) - return this - } + stubGlobal(name: string | symbol | number, value: unknown): this /** * Changes the value of `import.meta.env` and `process.env`. * You can return it back to original value with `vi.unstubEnvs`, or by enabling `unstubEnvs` config option. */ - public stubEnv(name: string, value: string) { - if (!this._stubsEnv.has(name)) - this._stubsEnv.set(name, process.env[name]) - process.env[name] = value - return this - } + stubEnv(name: string, value: string): this /** * Reset the value to original value that was available before first `vi.stubGlobal` was called. */ - public unstubAllGlobals() { - this._stubsGlobal.forEach((original, name) => { - if (!original) - Reflect.deleteProperty(globalThis, name) - else - Object.defineProperty(globalThis, name, original) - }) - this._stubsGlobal.clear() - return this - } + unstubAllGlobals(): this /** * Reset environmental variables to the ones that were available before first `vi.stubEnv` was called. */ - public unstubAllEnvs() { - this._stubsEnv.forEach((original, name) => { - if (original === undefined) - delete process.env[name] - else - process.env[name] = original - }) - this._stubsEnv.clear() - return this - } + unstubAllEnvs(): this - public resetModules() { - const state = getWorkerState() - resetModules(state.moduleCache) - return this - } + resetModules(): this /** * Wait for all imports to load. Useful, if you have a synchronous call that starts * importing a module that you cannot await otherwise. * Will also wait for new imports, started during the wait. */ - public async dynamicImportSettled() { - return waitForImportsToResolve() - } - - private _config: null | ResolvedConfig = null + dynamicImportSettled(): Promise /** * Updates runtime config. You can only change values that are used when executing tests. */ - public setConfig(config: RuntimeConfig) { - const state = getWorkerState() - if (!this._config) - this._config = { ...state.config } - Object.assign(state.config, config) - } + setConfig(config: RuntimeConfig): void /** * If config was changed with `vi.setConfig`, this will reset it to the original state. */ - public resetConfig() { - if (this._config) { + resetConfig(): void +} + +function createVitest(): VitestUtils { + // @ts-expect-error injected by vite-nide + const _mocker: VitestMocker = typeof __vitest_mocker__ !== 'undefined' + // @ts-expect-error injected by vite-nide + ? __vitest_mocker__ + : new Proxy({}, { + get(name) { + throw new Error( + 'Vitest mocker was not initialized in this environment. ' + + `vi.${name}() is forbidden.`, + ) + }, + }) + let _mockedDate: Date | null = null + let _config: null | ResolvedConfig = null + + const workerState = getWorkerState() + const _timers = new FakeTimers({ + global: globalThis, + config: workerState.config.fakeTimers, + }) + + const _stubsGlobal = new Map() + const _stubsEnv = new Map() + + const getImporter = () => { + const stackTrace = createSimpleStackTrace({ stackTraceLimit: 4 }) + const importerStack = stackTrace.split('\n')[4] + const stack = parseSingleStack(importerStack) + return stack?.file || '' + } + + return { + useFakeTimers(config?: FakeTimerInstallOpts) { + if (config) { + _timers.configure(config) + } + else { + const workerState = getWorkerState() + _timers.configure(workerState.config.fakeTimers) + } + _timers.useFakeTimers() + return this + }, + + useRealTimers() { + _timers.useRealTimers() + _mockedDate = null + return this + }, + + runOnlyPendingTimers() { + _timers.runOnlyPendingTimers() + return this + }, + + async runOnlyPendingTimersAsync() { + await _timers.runOnlyPendingTimersAsync() + return this + }, + + runAllTimers() { + _timers.runAllTimers() + return this + }, + + async runAllTimersAsync() { + await _timers.runAllTimersAsync() + return this + }, + + runAllTicks() { + _timers.runAllTicks() + return this + }, + + advanceTimersByTime(ms: number) { + _timers.advanceTimersByTime(ms) + return this + }, + + async advanceTimersByTimeAsync(ms: number) { + await _timers.advanceTimersByTimeAsync(ms) + return this + }, + + advanceTimersToNextTimer() { + _timers.advanceTimersToNextTimer() + return this + }, + + async advanceTimersToNextTimerAsync() { + await _timers.advanceTimersToNextTimerAsync() + return this + }, + + getTimerCount() { + return _timers.getTimerCount() + }, + + setSystemTime(time: number | string | Date) { + const date = time instanceof Date ? time : new Date(time) + _mockedDate = date + _timers.setSystemTime(date) + return this + }, + + getMockedSystemTime() { + return _mockedDate + }, + + getRealSystemTime() { + return _timers.getRealSystemTime() + }, + + clearAllTimers() { + _timers.clearAllTimers() + return this + }, + + // mocks + + spyOn, + fn, + + mock(path: string, factory?: MockFactoryWithHelper) { + const importer = getImporter() + _mocker.queueMock( + path, + importer, + factory ? () => factory(() => _mocker.importActual(path, importer)) : undefined, + ) + }, + + unmock(path: string) { + _mocker.queueUnmock(path, getImporter()) + }, + + doMock(path: string, factory?: () => any) { + _mocker.queueMock(path, getImporter(), factory) + }, + + doUnmock(path: string) { + _mocker.queueUnmock(path, getImporter()) + }, + + async importActual(path: string): Promise { + return _mocker.importActual(path, getImporter()) + }, + + async importMock(path: string): Promise> { + return _mocker.importMock(path, getImporter()) + }, + + mocked(item: T, _options = {}): MaybeMocked { + return item as any + }, + + isMockFunction(fn: any): fn is EnhancedSpy { + return isMockFunction(fn) + }, + + clearAllMocks() { + spies.forEach(spy => spy.mockClear()) + return this + }, + + resetAllMocks() { + spies.forEach(spy => spy.mockReset()) + return this + }, + + restoreAllMocks() { + spies.forEach(spy => spy.mockRestore()) + return this + }, + + stubGlobal(name: string | symbol | number, value: any) { + if (!_stubsGlobal.has(name)) + _stubsGlobal.set(name, Object.getOwnPropertyDescriptor(globalThis, name)) + Object.defineProperty(globalThis, name, { + value, + writable: true, + configurable: true, + enumerable: true, + }) + return this + }, + + stubEnv(name: string, value: string) { + if (!_stubsEnv.has(name)) + _stubsEnv.set(name, process.env[name]) + process.env[name] = value + return this + }, + + unstubAllGlobals() { + _stubsGlobal.forEach((original, name) => { + if (!original) + Reflect.deleteProperty(globalThis, name) + else + Object.defineProperty(globalThis, name, original) + }) + _stubsGlobal.clear() + return this + }, + + unstubAllEnvs() { + _stubsEnv.forEach((original, name) => { + if (original === undefined) + delete process.env[name] + else + process.env[name] = original + }) + _stubsEnv.clear() + return this + }, + + resetModules() { const state = getWorkerState() - Object.assign(state.config, this._config) - } + resetModules(state.moduleCache) + return this + }, + + async dynamicImportSettled() { + return waitForImportsToResolve() + }, + + setConfig(config: RuntimeConfig) { + const state = getWorkerState() + if (!_config) + _config = { ...state.config } + Object.assign(state.config, config) + }, + + resetConfig() { + if (_config) { + const state = getWorkerState() + Object.assign(state.config, _config) + } + }, + } } -export const vitest = new VitestUtils() +export const vitest = createVitest() export const vi = vitest