forked from vitest-dev/vitest
/
vi.ts
331 lines (289 loc) · 10.2 KB
/
vi.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
import type { FakeTimerInstallOpts } from '@sinonjs/fake-timers'
import { parseStacktrace } from '../utils/source-map'
import type { VitestMocker } from '../runtime/mocker'
import type { ResolvedConfig, RuntimeConfig } from '../types'
import { getWorkerState, resetModules, setTimeout } from '../utils'
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\nOne of the following is possible:'
+ '\n- "vitest" is imported outside of your tests (in that case, use "vitest/node" or import.meta.vitest)'
+ '\n- "vitest" is imported inside "globalSetup" (use "setupFiles", because "globalSetup" runs in a different context)'
+ '\n- Your dependency inside "node_modules" imports "vitest" directly (in that case, inline that dependency, using "deps.inline" config)'
+ '\n- Otherwise, 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 runAllTimers() {
this._timers.runAllTimers()
return this
}
public runAllTicks() {
this._timers.runAllTicks()
return this
}
public advanceTimersByTime(ms: number) {
this._timers.advanceTimersByTime(ms)
return this
}
public advanceTimersToNextTimer() {
this._timers.advanceTimersToNextTimer()
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 err = new Error('mock')
const [,, importer] = parseStacktrace(err, true)
return importer.file
}
/**
* Makes all `imports` to passed module to be mocked.
* - If there is a factory, will return it's result. The call to `vi.mock` is hoisted to the top of the file,
* so you don't have access to variables declared in the global file scope, if you didn't put them before imports!
* - If `__mocks__` folder with file of the same name exist, all imports will
* return it.
* - If there is no `__mocks__` folder or a file with the same name inside, will call original
* module and mock it.
* @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?: () => any) {
this._mocker.queueMock(path, this.getImporter(), factory)
}
/**
* 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)
}
public doUnmock(path: string) {
this._mocker.queueUnmock(path, this.getImporter())
}
/**
* Imports module, bypassing all checks if it should be mocked.
* Can be useful if you want to mock module partially.
* @example
* vi.mock('./example', async () => {
* const axios = await vi.importActual('./example')
*
* return { ...axios, get: vi.fn() }
* })
* @param path Path to the module. Can be aliased, if your config supports it
* @returns Actual module without spies
*/
public async importActual<T>(path: string): Promise<T> {
return this._mocker.importActual<T>(path, this.getImporter())
}
/**
* Imports a module with all of its properties and nested properties mocked.
* For the rules applied, see docs.
* @param path Path to the module. Can be aliased, if your config supports it
* @returns Fully mocked module
*/
public async importMock<T>(path: string): Promise<MaybeMockedDeep<T>> {
return this._mocker.importMock(path, this.getImporter())
}
/**
* Type helpers for TypeScript. In reality just returns the object that was passed.
*
* When `partial` is `true` it will expect a `Partial<T>` as a return value.
* @example
* import example from './example'
* vi.mock('./example')
*
* test('1+1 equals 2' async () => {
* vi.mocked(example.calc).mockRestore()
*
* const res = example.calc(1, '+', 1)
*
* expect(res).toBe(2)
* })
* @param item Anything that can be mocked
* @param deep If the object is deeply mocked
* @param options If the object is partially or deeply mocked
*/
public mocked<T>(item: T, deep?: false): MaybeMocked<T>
public mocked<T>(item: T, deep: true): MaybeMockedDeep<T>
public mocked<T>(item: T, options: { partial?: false; deep?: false }): MaybeMocked<T>
public mocked<T>(item: T, options: { partial?: false; deep: true }): MaybeMockedDeep<T>
public mocked<T>(item: T, options: { partial: true; deep?: false }): MaybePartiallyMocked<T>
public mocked<T>(item: T, options: { partial: true; deep: true }): MaybePartiallyMockedDeep<T>
public mocked<T>(item: T, _options = {}): MaybeMocked<T> {
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
}
public restoreAllMocks() {
spies.forEach(spy => spy.mockRestore())
return this
}
private _stubsGlobal = new Map<string | symbol | number, PropertyDescriptor | undefined>()
private _stubsEnv = new Map()
/**
* 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))
// @ts-expect-error we can do anything!
globalThis[name] = value
return 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
}
/**
* 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
}
/**
* Reset enviromental 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
}
public resetModules() {
const state = getWorkerState()
resetModules(state.moduleCache)
return this
}
/**
* Wait for all imports to load.
* Useful, if you have a synchronous call that starts
* importing a module that you cannot wait otherwise.
*/
public async dynamicImportSettled() {
const state = getWorkerState()
const promises: Promise<unknown>[] = []
for (const mod of state.moduleCache.values()) {
if (mod.promise && !mod.evaluated)
promises.push(mod.promise)
}
if (!promises.length)
return
await Promise.allSettled(promises)
// wait until the end of the loop, so `.then` on modules is called,
// like in import('./example').then(...)
// also call dynamicImportSettled again in case new imports were added
await new Promise(resolve => setTimeout(resolve, 1))
.then(() => Promise.resolve())
.then(() => this.dynamicImportSettled())
}
private _config: null | ResolvedConfig = null
/**
* 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)
}
/**
* If config was changed with `vi.setConfig`, this will reset it to the original state.
*/
public resetConfig() {
if (this._config) {
const state = getWorkerState()
state.config = { ...this._config }
}
}
}
export const vitest = new VitestUtils()
export const vi = vitest