[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
12

Configure Feed

Select the types of activity you want to include in your feed.

feat(mocker): introduce @vitest/mocker package, allow `{ spy: true }` instead of a factory (#6289)

authored by

Vladimir and committed by
GitHub
(Aug 21, 2024, 5:17 PM +0200) 95f0203f 15ec2fba

+3383 -1931
+41 -8
docs/api/vi.md
··· 16 16 17 17 ### vi.mock 18 18 19 - - **Type**: `(path: string, factory?: (importOriginal: () => unknown) => unknown) => void` 20 - - **Type**: `<T>(path: Promise<T>, factory?: (importOriginal: () => T) => T | Promise<T>) => void` 19 + - **Type**: `(path: string, factory?: MockOptions | ((importOriginal: () => unknown) => unknown)) => void` 20 + - **Type**: `<T>(path: Promise<T>, factory?: MockOptions | ((importOriginal: () => T) => T | Promise<T>)) => void` 21 21 22 22 Substitutes all imported modules from provided `path` with another module. You can use configured Vite aliases inside a path. The call to `vi.mock` is hoisted, so it doesn't matter where you call it. It will always be executed before all imports. If you need to reference some variables outside of its scope, you can define them inside [`vi.hoisted`](#vi-hoisted) and reference them inside `vi.mock`. 23 23 ··· 29 29 Vitest will not mock modules that were imported inside a [setup file](/config/#setupfiles) because they are cached by the time a test file is running. You can call [`vi.resetModules()`](#vi-resetmodules) inside [`vi.hoisted`](#vi-hoisted) to clear all module caches before running a test file. 30 30 ::: 31 31 32 - If `factory` is defined, all imports will return its result. Vitest calls factory only once and caches results for all subsequent imports until [`vi.unmock`](#vi-unmock) or [`vi.doUnmock`](#vi-dounmock) is called. 32 + If the `factory` function is defined, all imports will return its result. Vitest calls factory only once and caches results for all subsequent imports until [`vi.unmock`](#vi-unmock) or [`vi.doUnmock`](#vi-dounmock) is called. 33 33 34 34 Unlike in `jest`, the factory can be asynchronous. You can use [`vi.importActual`](#vi-importactual) or a helper with the factory passed in as the first argument, and get the original module inside. 35 35 36 - Vitest also supports a module promise instead of a string in the `vi.mock` and `vi.doMock` methods for better IDE support. When the file is moved, the path will be updated, and `importOriginal` also inherits the type automatically. Using this signature will also enforce factory return type to be compatible with the original module (but every export is optional). 36 + Since Vitest 2.1, you can also provide an object with a `spy` property instead of a factory function. If `spy` is `true`, then Vitest will automock the module as usual, but it won't override the implementation of exports. This is useful if you just want to assert that the exported method was called correctly by another method. 37 + 38 + ```ts 39 + import { calculator } from './src/calculator.ts' 40 + 41 + vi.mock('./src/calculator.ts', { spy: true }) 42 + 43 + // calls the original implementation, 44 + // but allows asserting the behaviour later 45 + const result = calculator(1, 2) 46 + 47 + expect(result).toBe(3) 48 + expect(calculator).toHaveBeenCalledWith(1, 2) 49 + expect(calculator).toHaveReturned(3) 50 + ``` 51 + 52 + Vitest also supports a module promise instead of a string in the `vi.mock` and `vi.doMock` methods for better IDE support. When the file is moved, the path will be updated, and `importOriginal` inherits the type automatically. Using this signature will also enforce factory return type to be compatible with the original module (keeping exports optional). 37 53 38 54 ```ts twoslash 39 55 // @filename: ./path/to/module.js ··· 103 119 ``` 104 120 ::: 105 121 106 - If there is a `__mocks__` folder alongside a file that you are mocking, and the factory is not provided, Vitest will try to find a file with the same name in the `__mocks__` subfolder and use it as an actual module. If you are mocking a dependency, Vitest will try to find a `__mocks__` folder in the [root](/config/#root) of the project (default is `process.cwd()`). You can tell Vitest where the dependencies are located through the [deps.moduleDirectories](/config/#deps-moduledirectories) config option. 122 + If there is a `__mocks__` folder alongside a file that you are mocking, and the factory is not provided, Vitest will try to find a file with the same name in the `__mocks__` subfolder and use it as an actual module. If you are mocking a dependency, Vitest will try to find a `__mocks__` folder in the [root](/config/#root) of the project (default is `process.cwd()`). You can tell Vitest where the dependencies are located through the [`deps.moduleDirectories`](/config/#deps-moduledirectories) config option. 107 123 108 124 For example, you have this file structure: 109 125 ··· 118 134 - increment.test.js 119 135 ``` 120 136 121 - If you call `vi.mock` in a test file without a factory provided, it will find a file in the `__mocks__` folder to use as a module: 137 + If you call `vi.mock` in a test file without a factory or options provided, it will find a file in the `__mocks__` folder to use as a module: 122 138 123 139 ```ts 124 140 // increment.test.js ··· 144 160 145 161 ### vi.doMock 146 162 147 - - **Type**: `(path: string, factory?: (importOriginal: () => unknown) => unknown) => void` 148 - - **Type**: `<T>(path: Promise<T>, factory?: (importOriginal: () => T) => T | Promise<T>) => void` 163 + - **Type**: `(path: string, factory?: MockOptions | ((importOriginal: () => unknown) => unknown)) => void` 164 + - **Type**: `<T>(path: Promise<T>, factory?: MockOptions | ((importOriginal: () => T) => T | Promise<T>)) => void` 149 165 150 166 The same as [`vi.mock`](#vi-mock), but it's not hoisted to the top of the file, so you can reference variables in the global file scope. The next [dynamic import](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import) of the module will be mocked. 151 167 ··· 416 432 spy.mockReturnValue(10) 417 433 console.log(cart.getApples()) // still 42! 418 434 ``` 435 + ::: 436 + 437 + ::: tip 438 + It is not possible to spy on a specific exported method in [Browser Mode](/guide/browser/). Instead, you can spy on every exported method by calling `vi.mock("./file-path.js", { spy: true })`. This will mock every export but keep its implementation intact, allowing you to assert if the method was called correctly. 439 + 440 + ```ts 441 + import { calculator } from './src/calculator.ts' 442 + 443 + vi.mock('./src/calculator.ts', { spy: true }) 444 + 445 + calculator(1, 2) 446 + 447 + expect(calculator).toHaveBeenCalledWith(1, 2) 448 + expect(calculator).toHaveReturned(3) 449 + ``` 450 + 451 + And while it is possible to spy on exports in `jsdom` or other Node.js environments, this might change in the future. 419 452 ::: 420 453 421 454 ### vi.stubEnv {#vi-stubenv}
+1
eslint.config.js
··· 97 97 { 98 98 files: [ 99 99 `docs/${GLOB_SRC}`, 100 + `**/*.md`, 100 101 ], 101 102 rules: { 102 103 'style/max-statements-per-line': 'off',
+1
packages/browser/package.json
··· 95 95 "devDependencies": { 96 96 "@testing-library/jest-dom": "^6.4.8", 97 97 "@types/ws": "^8.5.12", 98 + "@vitest/mocker": "workspace:*", 98 99 "@vitest/runner": "workspace:*", 99 100 "@vitest/ui": "workspace:*", 100 101 "@vitest/ws-client": "workspace:*",
+3 -3
packages/browser/src/client/channel.ts
··· 1 1 import type { CancelReason } from '@vitest/runner' 2 + import type { MockedModuleSerialized } from '@vitest/mocker' 2 3 import { getBrowserState } from './utils' 3 4 4 5 export interface IframeDoneEvent { ··· 24 25 25 26 export interface IframeMockEvent { 26 27 type: 'mock' 27 - paths: string[] 28 - mock: string | undefined | null 28 + module: MockedModuleSerialized 29 29 } 30 30 31 31 export interface IframeUnmockEvent { 32 32 type: 'unmock' 33 - paths: string[] 33 + url: string 34 34 } 35 35 36 36 export interface IframeMockingDoneEvent {
+5 -5
packages/browser/src/client/orchestrator.ts
··· 5 5 import type { SerializedConfig } from 'vitest' 6 6 import { getBrowserState, getConfig } from './utils' 7 7 import { getUiAPI } from './ui' 8 - import { createModuleMocker } from './tester/msw' 8 + import { createModuleMockerInterceptor } from './tester/msw' 9 9 10 10 const url = new URL(location.href) 11 11 const ID_ALL = '__vitest_all__' ··· 13 13 class IframeOrchestrator { 14 14 private cancelled = false 15 15 private runningFiles = new Set<string>() 16 - private mocker = createModuleMocker() 16 + private interceptor = createModuleMockerInterceptor() 17 17 private iframes = new Map<string, HTMLIFrameElement>() 18 18 19 19 public async init() { ··· 187 187 break 188 188 } 189 189 case 'mock:invalidate': 190 - this.mocker.invalidate() 190 + this.interceptor.invalidate() 191 191 break 192 192 case 'unmock': 193 - await this.mocker.unmock(e.data) 193 + await this.interceptor.delete(e.data.url) 194 194 break 195 195 case 'mock': 196 - await this.mocker.mock(e.data) 196 + await this.interceptor.register(e.data.module) 197 197 break 198 198 case 'mock-factory:error': 199 199 case 'mock-factory:response':
+1
packages/browser/src/client/public/esm-client-injector.js
··· 17 17 18 18 window.__vitest_browser_runner__ = { 19 19 wrapModule, 20 + wrapDynamicImport: wrapModule, 20 21 moduleCache, 21 22 config: { __VITEST_CONFIG__ }, 22 23 viteConfig: { __VITEST_VITE_CONFIG__ },
+13 -437
packages/browser/src/client/tester/mocker.ts
··· 1 - import { getType } from '@vitest/utils' 2 - import { extname, join } from 'pathe' 3 1 import type { IframeChannelOutgoingEvent } from '@vitest/browser/client' 4 - import { channel, waitForChannel } from '@vitest/browser/client' 5 - import { getBrowserState, importId } from '../utils' 6 - import { rpc } from './rpc' 2 + import { channel } from '@vitest/browser/client' 3 + import { ModuleMocker } from '@vitest/mocker/browser' 4 + import { getBrowserState } from '../utils' 7 5 8 - const now = Date.now 9 - 10 - interface SpyModule { 11 - spyOn: typeof import('vitest').vi['spyOn'] 12 - } 13 - 14 - export class VitestBrowserClientMocker { 15 - private queue = new Set<Promise<void>>() 16 - private mocks: Record<string, undefined | null | string> = {} 17 - private mockObjects: Record<string, any> = {} 18 - private factories: Record<string, () => any> = {} 19 - private ids = new Set<string>() 20 - 21 - private spyModule!: SpyModule 22 - 6 + export class VitestBrowserClientMocker extends ModuleMocker { 23 7 setupWorker() { 24 8 channel.addEventListener( 25 9 'message', 26 10 async (e: MessageEvent<IframeChannelOutgoingEvent>) => { 27 11 if (e.data.type === 'mock-factory:request') { 28 12 try { 29 - const module = await this.resolve(e.data.id) 13 + const module = await this.resolveFactoryModule(e.data.id) 30 14 const exports = Object.keys(module) 31 15 channel.postMessage({ 32 16 type: 'mock-factory:response', ··· 34 18 }) 35 19 } 36 20 catch (err: any) { 37 - const { processError } = (await importId( 38 - 'vitest/browser', 39 - )) as typeof import('vitest/browser') 40 21 channel.postMessage({ 41 22 type: 'mock-factory:error', 42 - error: processError(err), 23 + error: { 24 + name: err.name, 25 + message: err.message, 26 + stack: err.stack, 27 + }, 43 28 }) 44 29 } 45 30 } ··· 47 32 ) 48 33 } 49 34 50 - public setSpyModule(mod: SpyModule) { 51 - this.spyModule = mod 52 - } 53 - 54 - public async importActual(id: string, importer: string) { 55 - const resolved = await rpc().resolveId(id, importer) 56 - if (resolved == null) { 57 - throw new Error( 58 - `[vitest] Cannot resolve ${id} imported from ${importer}`, 59 - ) 60 - } 61 - const ext = extname(resolved.id) 62 - const url = new URL(resolved.url, location.href) 63 - const query = `_vitest_original&ext${ext}` 64 - const actualUrl = `${url.pathname}${ 65 - url.search ? `${url.search}&${query}` : `?${query}` 66 - }${url.hash}` 67 - return getBrowserState().wrapModule(() => import(/* @vite-ignore */ actualUrl)).then((mod) => { 68 - if (!resolved.optimized || typeof mod.default === 'undefined') { 69 - return mod 70 - } 71 - // vite injects this helper for optimized modules, so we try to follow the same behavior 72 - const m = mod.default 73 - return m?.__esModule ? m : { ...((typeof m === 'object' && !Array.isArray(m)) || typeof m === 'function' ? m : {}), default: m } 74 - }) 75 - } 76 - 77 - public async importMock(rawId: string, importer: string) { 78 - await this.prepare() 79 - const { resolvedId, type, mockPath } = await rpc().resolveMock( 80 - rawId, 81 - importer, 82 - false, 83 - ) 84 - 85 - const factoryReturn = this.get(resolvedId) 86 - if (factoryReturn) { 87 - return factoryReturn 88 - } 89 - 90 - if (this.factories[resolvedId]) { 91 - return await this.resolve(resolvedId) 92 - } 93 - 94 - if (type === 'redirect') { 95 - const url = new URL(`/@id/${mockPath}`, location.href) 96 - return import(/* @vite-ignore */ url.toString()) 97 - } 98 - const url = new URL(`/@id/${resolvedId}`, location.href) 99 - const query = url.search ? `${url.search}&t=${now()}` : `?t=${now()}` 100 - const moduleObject = await import(/* @vite-ignore */ `${url.pathname}${query}${url.hash}`) 101 - return this.mockObject(moduleObject) 102 - } 103 - 35 + // default "vi" utility tries to access mock context to avoid circular dependencies 104 36 public getMockContext() { 105 37 return { callstack: null } 106 38 } 107 39 108 - public get(id: string) { 109 - return this.mockObjects[id] 110 - } 111 - 112 - public async invalidate() { 113 - const ids = Array.from(this.ids) 114 - if (!ids.length) { 115 - return 116 - } 117 - await rpc().invalidate(ids) 118 - channel.postMessage({ type: 'mock:invalidate' }) 119 - this.ids.clear() 120 - this.mocks = {} 121 - this.mockObjects = {} 122 - this.factories = {} 123 - } 124 - 125 - public async resolve(id: string) { 126 - const factory = this.factories[id] 127 - if (!factory) { 128 - throw new Error(`Cannot resolve ${id} mock: no factory provided`) 129 - } 130 - try { 131 - this.mockObjects[id] = await factory() 132 - return this.mockObjects[id] 133 - } 134 - catch (err) { 135 - const vitestError = new Error( 136 - '[vitest] There was an error when mocking a module. ' 137 - + 'If you are using "vi.mock" factory, make sure there are no top level variables inside, since this call is hoisted to top of the file. ' 138 - + 'Read more: https://vitest.dev/api/vi.html#vi-mock', 139 - ) 140 - vitestError.cause = err 141 - throw vitestError 142 - } 143 - } 144 - 145 - public queueMock(id: string, importer: string, factory?: () => any) { 146 - const promise = rpc() 147 - .resolveMock(id, importer, !!factory) 148 - .then(async ({ mockPath, resolvedId, needsInterop }) => { 149 - this.ids.add(resolvedId) 150 - const urlPaths = resolveMockPaths(cleanVersion(resolvedId)) 151 - const resolvedMock 152 - = typeof mockPath === 'string' 153 - ? new URL(resolvedMockedPath(cleanVersion(mockPath)), location.href).toString() 154 - : mockPath 155 - const _factory = factory && needsInterop 156 - ? async () => { 157 - const data = await factory() 158 - return { default: data } 159 - } 160 - : factory 161 - urlPaths.forEach((url) => { 162 - this.mocks[url] = resolvedMock 163 - if (_factory) { 164 - this.factories[url] = _factory 165 - } 166 - }) 167 - channel.postMessage({ 168 - type: 'mock', 169 - paths: urlPaths, 170 - mock: resolvedMock, 171 - }) 172 - await waitForChannel('mock:done') 173 - }) 174 - .finally(() => { 175 - this.queue.delete(promise) 176 - }) 177 - this.queue.add(promise) 178 - } 179 - 180 - public queueUnmock(id: string, importer: string) { 181 - const promise = rpc() 182 - .resolveId(id, importer) 183 - .then(async (resolved) => { 184 - if (!resolved) { 185 - return 186 - } 187 - this.ids.delete(resolved.id) 188 - const urlPaths = resolveMockPaths(cleanVersion(resolved.id)) 189 - urlPaths.forEach((url) => { 190 - delete this.mocks[url] 191 - delete this.factories[url] 192 - delete this.mockObjects[url] 193 - }) 194 - channel.postMessage({ 195 - type: 'unmock', 196 - paths: urlPaths, 197 - }) 198 - await waitForChannel('unmock:done') 199 - }) 200 - .finally(() => { 201 - this.queue.delete(promise) 202 - }) 203 - this.queue.add(promise) 204 - } 205 - 206 - public async prepare() { 207 - if (!this.queue.size) { 208 - return 209 - } 210 - await Promise.all([...this.queue.values()]) 211 - } 212 - 213 - // TODO: move this logic into a util(?) 214 - public mockObject( 215 - object: Record<Key, any>, 216 - mockExports: Record<Key, any> = {}, 217 - ) { 218 - const finalizers = new Array<() => void>() 219 - const refs = new RefTracker() 220 - 221 - const define = (container: Record<Key, any>, key: Key, value: any) => { 222 - try { 223 - container[key] = value 224 - return true 225 - } 226 - catch { 227 - return false 228 - } 229 - } 230 - 231 - const mockPropertiesOf = ( 232 - container: Record<Key, any>, 233 - newContainer: Record<Key, any>, 234 - ) => { 235 - const containerType = /* #__PURE__ */ getType(container) 236 - const isModule = containerType === 'Module' || !!container.__esModule 237 - for (const { key: property, descriptor } of getAllMockableProperties( 238 - container, 239 - isModule, 240 - )) { 241 - // Modules define their exports as getters. We want to process those. 242 - if (!isModule && descriptor.get) { 243 - try { 244 - Object.defineProperty(newContainer, property, descriptor) 245 - } 246 - catch { 247 - // Ignore errors, just move on to the next prop. 248 - } 249 - continue 250 - } 251 - 252 - // Skip special read-only props, we don't want to mess with those. 253 - if (isSpecialProp(property, containerType)) { 254 - continue 255 - } 256 - 257 - const value = container[property] 258 - 259 - // Special handling of references we've seen before to prevent infinite 260 - // recursion in circular objects. 261 - const refId = refs.getId(value) 262 - if (refId !== undefined) { 263 - finalizers.push(() => 264 - define(newContainer, property, refs.getMockedValue(refId)), 265 - ) 266 - continue 267 - } 268 - 269 - const type = /* #__PURE__ */ getType(value) 270 - 271 - if (Array.isArray(value)) { 272 - define(newContainer, property, []) 273 - continue 274 - } 275 - 276 - const isFunction 277 - = type.includes('Function') && typeof value === 'function' 278 - if ( 279 - (!isFunction || value.__isMockFunction) 280 - && type !== 'Object' 281 - && type !== 'Module' 282 - ) { 283 - define(newContainer, property, value) 284 - continue 285 - } 286 - 287 - // Sometimes this assignment fails for some unknown reason. If it does, 288 - // just move along. 289 - if (!define(newContainer, property, isFunction ? value : {})) { 290 - continue 291 - } 292 - 293 - if (isFunction) { 294 - const spyModule = this.spyModule 295 - if (!spyModule) { 296 - throw new Error( 297 - '[vitest] `spyModule` is not defined. This is Vitest error. Please open a new issue with reproduction.', 298 - ) 299 - } 300 - function mockFunction(this: any) { 301 - // detect constructor call and mock each instance's methods 302 - // so that mock states between prototype/instances don't affect each other 303 - // (jest reference https://github.com/jestjs/jest/blob/2c3d2409879952157433de215ae0eee5188a4384/packages/jest-mock/src/index.ts#L678-L691) 304 - if (this instanceof newContainer[property]) { 305 - for (const { key, descriptor } of getAllMockableProperties( 306 - this, 307 - false, 308 - )) { 309 - // skip getter since it's not mocked on prototype as well 310 - if (descriptor.get) { 311 - continue 312 - } 313 - 314 - const value = this[key] 315 - const type = /* #__PURE__ */ getType(value) 316 - const isFunction 317 - = type.includes('Function') && typeof value === 'function' 318 - if (isFunction) { 319 - // mock and delegate calls to original prototype method, which should be also mocked already 320 - const original = this[key] 321 - const mock = spyModule 322 - .spyOn(this, key as string) 323 - .mockImplementation(original) 324 - mock.mockRestore = () => { 325 - mock.mockReset() 326 - mock.mockImplementation(original) 327 - return mock 328 - } 329 - } 330 - } 331 - } 332 - } 333 - const mock = spyModule 334 - .spyOn(newContainer, property) 335 - .mockImplementation(mockFunction) 336 - mock.mockRestore = () => { 337 - mock.mockReset() 338 - mock.mockImplementation(mockFunction) 339 - return mock 340 - } 341 - // tinyspy retains length, but jest doesn't. 342 - Object.defineProperty(newContainer[property], 'length', { value: 0 }) 343 - } 344 - 345 - refs.track(value, newContainer[property]) 346 - mockPropertiesOf(value, newContainer[property]) 347 - } 348 - } 349 - 350 - const mockedObject: Record<Key, any> = mockExports 351 - mockPropertiesOf(object, mockedObject) 352 - 353 - // Plug together refs 354 - for (const finalizer of finalizers) { 355 - finalizer() 356 - } 357 - 358 - return mockedObject 40 + public override wrapDynamicImport<T>(moduleFactory: () => Promise<T>): Promise<T> { 41 + return getBrowserState().wrapModule(moduleFactory) 359 42 } 360 43 } 361 - 362 - function isSpecialProp(prop: Key, parentType: string) { 363 - return ( 364 - parentType.includes('Function') 365 - && typeof prop === 'string' 366 - && ['arguments', 'callee', 'caller', 'length', 'name'].includes(prop) 367 - ) 368 - } 369 - 370 - class RefTracker { 371 - private idMap = new Map<any, number>() 372 - private mockedValueMap = new Map<number, any>() 373 - 374 - public getId(value: any) { 375 - return this.idMap.get(value) 376 - } 377 - 378 - public getMockedValue(id: number) { 379 - return this.mockedValueMap.get(id) 380 - } 381 - 382 - public track(originalValue: any, mockedValue: any): number { 383 - const newId = this.idMap.size 384 - this.idMap.set(originalValue, newId) 385 - this.mockedValueMap.set(newId, mockedValue) 386 - return newId 387 - } 388 - } 389 - 390 - type Key = string | symbol 391 - 392 - function getAllMockableProperties(obj: any, isModule: boolean) { 393 - const allProps = new Map< 394 - string | symbol, 395 - { key: string | symbol; descriptor: PropertyDescriptor } 396 - >() 397 - let curr = obj 398 - do { 399 - // we don't need properties from these 400 - if ( 401 - curr === Object.prototype 402 - || curr === Function.prototype 403 - || curr === RegExp.prototype 404 - ) { 405 - break 406 - } 407 - 408 - collectOwnProperties(curr, (key) => { 409 - const descriptor = Object.getOwnPropertyDescriptor(curr, key) 410 - if (descriptor) { 411 - allProps.set(key, { key, descriptor }) 412 - } 413 - }) 414 - // eslint-disable-next-line no-cond-assign 415 - } while ((curr = Object.getPrototypeOf(curr))) 416 - // default is not specified in ownKeys, if module is interoped 417 - if (isModule && !allProps.has('default') && 'default' in obj) { 418 - const descriptor = Object.getOwnPropertyDescriptor(obj, 'default') 419 - if (descriptor) { 420 - allProps.set('default', { key: 'default', descriptor }) 421 - } 422 - } 423 - return Array.from(allProps.values()) 424 - } 425 - 426 - function collectOwnProperties( 427 - obj: any, 428 - collector: Set<string | symbol> | ((key: string | symbol) => void), 429 - ) { 430 - const collect 431 - = typeof collector === 'function' 432 - ? collector 433 - : (key: string | symbol) => collector.add(key) 434 - Object.getOwnPropertyNames(obj).forEach(collect) 435 - Object.getOwnPropertySymbols(obj).forEach(collect) 436 - } 437 - 438 - function resolvedMockedPath(path: string) { 439 - const config = getBrowserState().viteConfig 440 - if (path.startsWith(config.root)) { 441 - return path.slice(config.root.length) 442 - } 443 - return path 444 - } 445 - 446 - // TODO: check _base_ path 447 - function resolveMockPaths(path: string) { 448 - const config = getBrowserState().viteConfig 449 - const fsRoot = join('/@fs/', config.root) 450 - const paths = [path, join('/@fs/', path)] 451 - 452 - // URL can be /file/path.js, but path is resolved to /file/path 453 - if (path.startsWith(config.root)) { 454 - paths.push(path.slice(config.root.length)) 455 - } 456 - 457 - if (path.startsWith(fsRoot)) { 458 - paths.push(path.slice(fsRoot.length)) 459 - } 460 - 461 - return paths 462 - } 463 - 464 - const versionRegexp = /(\?|&)v=\w{8}/ 465 - function cleanVersion(url: string) { 466 - return url.replace(versionRegexp, '') 467 - }
+29 -132
packages/browser/src/client/tester/msw.ts
··· 1 1 import { channel } from '@vitest/browser/client' 2 2 import type { 3 3 IframeChannelEvent, 4 - IframeMockEvent, 5 4 IframeMockingDoneEvent, 6 - IframeUnmockEvent, 7 5 } from '@vitest/browser/client' 6 + import type { MockedModuleSerialized } from '@vitest/mocker' 7 + import { ManualMockedModule } from '@vitest/mocker' 8 + import { ModuleMockerMSWInterceptor } from '@vitest/mocker/browser' 8 9 9 - export function createModuleMocker() { 10 - const mocks: Map<string, string | null | undefined> = new Map() 11 - 12 - let started = false 13 - let startPromise: undefined | Promise<unknown> 14 - 15 - async function init() { 16 - if (started) { 17 - return 10 + export class VitestBrowserModuleMockerInterceptor extends ModuleMockerMSWInterceptor { 11 + override async register(event: MockedModuleSerialized): Promise<void> { 12 + if (event.type === 'manual') { 13 + const module = ManualMockedModule.fromJSON(event, async () => { 14 + const keys = await getFactoryExports(event.url) 15 + return Object.fromEntries(keys.map(key => [key, null])) 16 + }) 17 + await super.register(module) 18 18 } 19 - if (startPromise) { 20 - return startPromise 19 + else { 20 + await this.init() 21 + this.mocks.register(event) 21 22 } 22 - startPromise = Promise.all([ 23 - import('msw/browser'), 24 - import('msw/core/http'), 25 - ]).then(([{ setupWorker }, { http }]) => { 26 - const worker = setupWorker( 27 - http.get(/.+/, async ({ request }) => { 28 - const path = cleanQuery(request.url.slice(location.origin.length)) 29 - if (!mocks.has(path)) { 30 - if (path.includes('/deps/')) { 31 - return fetch(bypass(request)) 32 - } 23 + channel.postMessage(<IframeMockingDoneEvent>{ type: 'mock:done' }) 24 + } 33 25 34 - return passthrough() 35 - } 36 - 37 - const mock = mocks.get(path) 38 - 39 - // using a factory 40 - if (mock === undefined) { 41 - const exports = await getFactoryExports(path) 42 - const module = `const module = __vitest_mocker__.get('${path}');` 43 - const keys = exports 44 - .map((name) => { 45 - if (name === 'default') { 46 - return `export default module['default'];` 47 - } 48 - return `export const ${name} = module['${name}'];` 49 - }) 50 - .join('\n') 51 - const text = `${module}\n${keys}` 52 - return new Response(text, { 53 - headers: { 54 - 'Content-Type': 'application/javascript', 55 - }, 56 - }) 57 - } 58 - 59 - if (typeof mock === 'string') { 60 - return Response.redirect(mock) 61 - } 62 - 63 - return Response.redirect(injectQuery(path, 'mock=auto')) 64 - }), 65 - ) 66 - return worker.start({ 67 - serviceWorker: { 68 - url: '/__vitest_msw__', 69 - }, 70 - quiet: true, 71 - }) 72 - }) 73 - .finally(() => { 74 - started = true 75 - startPromise = undefined 76 - }) 77 - await startPromise 26 + override async delete(url: string): Promise<void> { 27 + await super.delete(url) 28 + channel.postMessage(<IframeMockingDoneEvent>{ type: 'unmock:done' }) 78 29 } 30 + } 79 31 80 - return { 81 - async mock(event: IframeMockEvent) { 82 - await init() 83 - event.paths.forEach(path => mocks.set(path, event.mock)) 84 - channel.postMessage(<IframeMockingDoneEvent>{ type: 'mock:done' }) 85 - }, 86 - async unmock(event: IframeUnmockEvent) { 87 - await init() 88 - event.paths.forEach(path => mocks.delete(path)) 89 - channel.postMessage(<IframeMockingDoneEvent>{ type: 'unmock:done' }) 90 - }, 91 - invalidate() { 92 - mocks.clear() 32 + export function createModuleMockerInterceptor() { 33 + return new VitestBrowserModuleMockerInterceptor({ 34 + globalThisAccessor: '"__vitest_mocker__"', 35 + mswOptions: { 36 + serviceWorker: { 37 + url: '/__vitest_msw__', 38 + }, 39 + quiet: true, 93 40 }, 94 - } 41 + }) 95 42 } 96 43 97 44 function getFactoryExports(id: string) { ··· 115 62 ) 116 63 }) 117 64 } 118 - 119 - const timestampRegexp = /(\?|&)t=\d{13}/ 120 - const versionRegexp = /(\?|&)v=\w{8}/ 121 - function cleanQuery(url: string) { 122 - return url.replace(timestampRegexp, '').replace(versionRegexp, '') 123 - } 124 - 125 - function passthrough() { 126 - return new Response(null, { 127 - status: 302, 128 - statusText: 'Passthrough', 129 - headers: { 130 - 'x-msw-intention': 'passthrough', 131 - }, 132 - }) 133 - } 134 - 135 - function bypass(request: Request) { 136 - const clonedRequest = request.clone() 137 - clonedRequest.headers.set('x-msw-intention', 'bypass') 138 - const cacheControl = clonedRequest.headers.get('cache-control') 139 - if (cacheControl) { 140 - clonedRequest.headers.set( 141 - 'cache-control', 142 - // allow reinvalidation of the cache so mocks can be updated 143 - cacheControl.replace(', immutable', ''), 144 - ) 145 - } 146 - return clonedRequest 147 - } 148 - 149 - const postfixRE = /[?#].*$/ 150 - function cleanUrl(url: string): string { 151 - return url.replace(postfixRE, '') 152 - } 153 - 154 - const replacePercentageRE = /%/g 155 - function injectQuery(url: string, queryToInject: string): string { 156 - // encode percents for consistent behavior with pathToFileURL 157 - // see #2614 for details 158 - const resolvedUrl = new URL( 159 - url.replace(replacePercentageRE, '%25'), 160 - location.href, 161 - ) 162 - const { search, hash } = resolvedUrl 163 - const pathname = cleanUrl(url) 164 - return `${pathname}?${queryToInject}${search ? `&${search.slice(1)}` : ''}${ 165 - hash ?? '' 166 - }` 167 - }
-1
packages/browser/src/client/tester/state.ts
··· 24 24 invalidates: [], 25 25 }, 26 26 onCancel: null as any, 27 - mockMap: new Map(), 28 27 config, 29 28 environment: { 30 29 name: 'browser',
+30 -3
packages/browser/src/client/tester/tester.ts
··· 1 1 import { SpyModule, collectTests, setupCommonEnv, startCoverageInsideWorker, startTests, stopCoverageInsideWorker } from 'vitest/browser' 2 2 import { page } from '@vitest/browser/context' 3 - import { channel, client, onCancel } from '@vitest/browser/client' 3 + import type { IframeMockEvent, IframeMockInvalidateEvent, IframeUnmockEvent } from '@vitest/browser/client' 4 + import { channel, client, onCancel, waitForChannel } from '@vitest/browser/client' 4 5 import { executor, getBrowserState, getConfig, getWorkerState } from '../utils' 5 6 import { setupDialogsSpy } from './dialog' 6 7 import { setupConsoleLogSpy } from './logger' ··· 33 34 state.onCancel = onCancel 34 35 state.rpc = rpc as any 35 36 36 - const mocker = new VitestBrowserClientMocker() 37 + const mocker = new VitestBrowserClientMocker( 38 + { 39 + async delete(url: string) { 40 + channel.postMessage({ 41 + type: 'unmock', 42 + url, 43 + } satisfies IframeUnmockEvent) 44 + await waitForChannel('unmock:done') 45 + }, 46 + async register(module) { 47 + channel.postMessage({ 48 + type: 'mock', 49 + module: module.toJSON(), 50 + } satisfies IframeMockEvent) 51 + await waitForChannel('mock:done') 52 + }, 53 + invalidate() { 54 + channel.postMessage({ 55 + type: 'mock:invalidate', 56 + } satisfies IframeMockInvalidateEvent) 57 + }, 58 + }, 59 + rpc, 60 + SpyModule.spyOn, 61 + { 62 + root: getBrowserState().viteConfig.root, 63 + }, 64 + ) 37 65 // @ts-expect-error mocking vitest apis 38 66 globalThis.__vitest_mocker__ = mocker 39 67 ··· 51 79 } 52 80 }) 53 81 54 - mocker.setSpyModule(SpyModule) 55 82 mocker.setupWorker() 56 83 57 84 onCancel.then((reason) => {
-46
packages/browser/src/node/esmInjector.ts
··· 1 - import MagicString from 'magic-string' 2 - import type { PluginContext } from 'rollup' 3 - import { esmWalker } from '@vitest/utils/ast' 4 - import type { Expression, Positioned } from '@vitest/utils/ast' 5 - 6 - export function injectDynamicImport( 7 - code: string, 8 - id: string, 9 - parse: PluginContext['parse'], 10 - ) { 11 - const s = new MagicString(code) 12 - 13 - let ast: any 14 - try { 15 - ast = parse(code) 16 - } 17 - catch (err) { 18 - console.error(`Cannot parse ${id}:\n${(err as any).message}`) 19 - return 20 - } 21 - 22 - // 3. convert references to import bindings & import.meta references 23 - esmWalker(ast, { 24 - // TODO: make env updatable 25 - onImportMeta() { 26 - // s.update(node.start, node.end, viImportMetaKey) 27 - }, 28 - onDynamicImport(node) { 29 - const replaceString = '__vitest_browser_runner__.wrapModule(() => import(' 30 - const importSubstring = code.substring(node.start, node.end) 31 - const hasIgnore = importSubstring.includes('/* @vite-ignore */') 32 - s.overwrite( 33 - node.start, 34 - (node.source as Positioned<Expression>).start, 35 - replaceString + (hasIgnore ? '/* @vite-ignore */ ' : ''), 36 - ) 37 - s.overwrite(node.end - 1, node.end, '))') 38 - }, 39 - }) 40 - 41 - return { 42 - ast, 43 - code: s.toString(), 44 - map: s.generateMap({ hires: 'boundary', source: id }), 45 - } 46 - }
+4 -2
packages/browser/src/node/plugin.ts
··· 9 9 import { type Plugin, coverageConfigDefaults } from 'vitest/config' 10 10 import { toArray } from '@vitest/utils' 11 11 import { defaultBrowserPort } from 'vitest/config' 12 + import { dynamicImportPlugin } from '@vitest/mocker/node' 12 13 import BrowserContext from './plugins/pluginContext' 13 - import DynamicImport from './plugins/pluginDynamicImport' 14 14 import type { BrowserServer } from './server' 15 15 import { resolveOrchestrator } from './serverOrchestrator' 16 16 import { resolveTester } from './serverTester' ··· 306 306 }, 307 307 }, 308 308 BrowserContext(browserServer), 309 - DynamicImport(), 309 + dynamicImportPlugin({ 310 + globalThisAccessor: '"__vitest_browser_runner__"', 311 + }), 310 312 { 311 313 name: 'vitest:browser:config', 312 314 enforce: 'post',
-18
packages/browser/src/node/plugins/pluginDynamicImport.ts
··· 1 - import type { Plugin } from 'vitest/config' 2 - import { injectDynamicImport } from '../esmInjector' 3 - 4 - const regexDynamicImport = /import\s*\(/ 5 - 6 - export default (): Plugin => { 7 - return { 8 - name: 'vitest:browser:esm-injector', 9 - enforce: 'post', 10 - transform(source, id) { 11 - // TODO: test is not called for static imports 12 - if (!regexDynamicImport.test(source)) { 13 - return 14 - } 15 - return injectDynamicImport(source, id, this.parse) 16 - }, 17 - } 18 - }
-156
packages/browser/src/node/resolveMock.ts
··· 1 - import { existsSync, readFileSync, readdirSync } from 'node:fs' 2 - import { builtinModules } from 'node:module' 3 - import { basename, dirname, extname, isAbsolute, join, resolve } from 'pathe' 4 - import type { PartialResolvedId } from 'rollup' 5 - import type { ResolvedConfig, WorkspaceProject } from 'vitest/node' 6 - import type { ResolvedConfig as ViteConfig } from 'vite' 7 - 8 - export async function resolveMock( 9 - project: WorkspaceProject, 10 - rawId: string, 11 - importer: string, 12 - hasFactory: boolean, 13 - ) { 14 - const { id, fsPath, external } = await resolveId(project, rawId, importer) 15 - 16 - if (hasFactory) { 17 - const needsInteropMap = viteDepsInteropMap(project.browser!.vite.config) 18 - const needsInterop = needsInteropMap?.get(fsPath) ?? false 19 - return { type: 'factory' as const, resolvedId: id, needsInterop } 20 - } 21 - 22 - const mockPath = resolveMockPath(project.config.root, fsPath, external) 23 - 24 - return { 25 - type: mockPath === null ? ('automock' as const) : ('redirect' as const), 26 - mockPath, 27 - resolvedId: id, 28 - } 29 - } 30 - 31 - async function resolveId(project: WorkspaceProject, rawId: string, importer: string) { 32 - const resolved = await project.browser!.vite.pluginContainer.resolveId( 33 - rawId, 34 - importer, 35 - { 36 - ssr: false, 37 - }, 38 - ) 39 - return resolveModule(project, rawId, resolved) 40 - } 41 - 42 - async function resolveModule(project: WorkspaceProject, rawId: string, resolved: PartialResolvedId | null) { 43 - const id = resolved?.id || rawId 44 - const external 45 - = !isAbsolute(id) || isModuleDirectory(project.config, id) ? rawId : null 46 - return { 47 - id, 48 - fsPath: cleanUrl(id), 49 - external, 50 - } 51 - } 52 - 53 - function isModuleDirectory(config: ResolvedConfig, path: string) { 54 - const moduleDirectories = config.server.deps?.moduleDirectories || [ 55 - '/node_modules/', 56 - ] 57 - return moduleDirectories.some((dir: string) => path.includes(dir)) 58 - } 59 - 60 - function resolveMockPath(root: string, mockPath: string, external: string | null) { 61 - const path = external || mockPath 62 - 63 - // it's a node_module alias 64 - // all mocks should be inside <root>/__mocks__ 65 - if (external || isNodeBuiltin(mockPath) || !existsSync(mockPath)) { 66 - const mockDirname = dirname(path) // for nested mocks: @vueuse/integration/useJwt 67 - const mockFolder = join( 68 - root, 69 - '__mocks__', 70 - mockDirname, 71 - ) 72 - 73 - if (!existsSync(mockFolder)) { 74 - return null 75 - } 76 - 77 - const files = readdirSync(mockFolder) 78 - const baseOriginal = basename(path) 79 - 80 - for (const file of files) { 81 - const baseFile = basename(file, extname(file)) 82 - if (baseFile === baseOriginal) { 83 - return resolve(mockFolder, file) 84 - } 85 - } 86 - 87 - return null 88 - } 89 - 90 - const dir = dirname(path) 91 - const baseId = basename(path) 92 - const fullPath = resolve(dir, '__mocks__', baseId) 93 - return existsSync(fullPath) ? fullPath : null 94 - } 95 - 96 - const prefixedBuiltins = new Set(['node:test']) 97 - 98 - const builtins = new Set([ 99 - ...builtinModules, 100 - 'assert/strict', 101 - 'diagnostics_channel', 102 - 'dns/promises', 103 - 'fs/promises', 104 - 'path/posix', 105 - 'path/win32', 106 - 'readline/promises', 107 - 'stream/consumers', 108 - 'stream/promises', 109 - 'stream/web', 110 - 'timers/promises', 111 - 'util/types', 112 - 'wasi', 113 - ]) 114 - 115 - const NODE_BUILTIN_NAMESPACE = 'node:' 116 - export function isNodeBuiltin(id: string): boolean { 117 - if (prefixedBuiltins.has(id)) { 118 - return true 119 - } 120 - return builtins.has( 121 - id.startsWith(NODE_BUILTIN_NAMESPACE) 122 - ? id.slice(NODE_BUILTIN_NAMESPACE.length) 123 - : id, 124 - ) 125 - } 126 - 127 - const postfixRE = /[?#].*$/ 128 - export function cleanUrl(url: string): string { 129 - return url.replace(postfixRE, '') 130 - } 131 - 132 - const metadata = new WeakMap<ViteConfig, Map<string, boolean>>() 133 - 134 - function viteDepsInteropMap(config: ViteConfig) { 135 - if (metadata.has(config)) { 136 - return metadata.get(config)! 137 - } 138 - const cacheDirPath = getDepsCacheDir(config) 139 - const metadataPath = resolve(cacheDirPath, '_metadata.json') 140 - if (!existsSync(metadataPath)) { 141 - return null 142 - } 143 - const { optimized } = JSON.parse(readFileSync(metadataPath, 'utf-8')) 144 - const needsInteropMap = new Map() 145 - for (const name in optimized) { 146 - const dep = optimized[name] 147 - const file = resolve(cacheDirPath, dep.file) 148 - needsInteropMap.set(file, dep.needsInterop) 149 - } 150 - metadata.set(config, needsInteropMap) 151 - return needsInteropMap 152 - } 153 - 154 - function getDepsCacheDir(config: ViteConfig): string { 155 - return resolve(config.cacheDir, 'deps') 156 - }
+11 -61
packages/browser/src/node/rpc.ts
··· 1 1 import { existsSync, promises as fs } from 'node:fs' 2 - import { dirname, isAbsolute, join } from 'pathe' 2 + import { dirname } from 'pathe' 3 3 import { createBirpc } from 'birpc' 4 4 import { parse, stringify } from 'flatted' 5 5 import type { WebSocket } from 'ws' ··· 7 7 import type { BrowserCommandContext } from 'vitest/node' 8 8 import { createDebugger, isFileServingAllowed } from 'vitest/node' 9 9 import type { ErrorWithDiff } from 'vitest' 10 + import { ServerMockResolver } from '@vitest/mocker/node' 10 11 import type { WebSocketBrowserEvents, WebSocketBrowserHandlers } from './types' 11 12 import type { BrowserServer } from './server' 12 - import { cleanUrl, resolveMock } from './resolveMock' 13 13 14 14 const debug = createDebugger('vitest:browser:api') 15 15 16 16 const BROWSER_API_PATH = '/__vitest_browser_api__' 17 - const VALID_ID_PREFIX = '/@id/' 18 17 19 - export function setupBrowserRpc( 20 - server: BrowserServer, 21 - ) { 18 + export function setupBrowserRpc(server: BrowserServer) { 22 19 const project = server.project 23 20 const vite = server.vite 24 21 const ctx = project.ctx ··· 65 62 } 66 63 67 64 function setupClient(sessionId: string, ws: WebSocket) { 65 + const mockResolver = new ServerMockResolver(server.vite, { 66 + moduleDirectories: project.config.server?.deps?.moduleDirectories, 67 + }) 68 + 68 69 const rpc = createBirpc<WebSocketBrowserEvents, WebSocketBrowserHandlers>( 69 70 { 70 71 async onUnhandledError(error, type) { ··· 124 125 ctx.cancelCurrentRun(reason) 125 126 }, 126 127 async resolveId(id, importer) { 127 - const resolved = await vite.pluginContainer.resolveId( 128 - id, 129 - importer, 130 - { 131 - ssr: false, 132 - }, 133 - ) 134 - if (!resolved) { 135 - return null 136 - } 137 - const isOptimized = resolved.id.startsWith(withTrailingSlash(vite.config.cacheDir)) 138 - let url: string 139 - // normalise the URL to be acceptible by the browser 140 - // https://github.com/vitejs/vite/blob/e833edf026d495609558fd4fb471cf46809dc369/packages/vite/src/node/plugins/importAnalysis.ts#L335 141 - const root = vite.config.root 142 - if (resolved.id.startsWith(withTrailingSlash(root))) { 143 - url = resolved.id.slice(root.length) 144 - } 145 - else if ( 146 - resolved.id !== '/@react-refresh' 147 - && isAbsolute(resolved.id) 148 - && existsSync(cleanUrl(resolved.id)) 149 - ) { 150 - url = join('/@fs/', resolved.id) 151 - } 152 - else { 153 - url = resolved.id 154 - } 155 - if (url[0] !== '.' && url[0] !== '/') { 156 - url = id.startsWith(VALID_ID_PREFIX) 157 - ? id 158 - : VALID_ID_PREFIX + id.replace('\0', '__x00__') 159 - } 160 - return { 161 - id: resolved.id, 162 - url, 163 - optimized: isOptimized, 164 - } 128 + return mockResolver.resolveId(id, importer) 165 129 }, 166 130 debug(...args) { 167 131 ctx.logger.console.debug(...args) ··· 206 170 debug?.('[%s] Finishing browser tests for context', contextId) 207 171 return server.state.getContext(contextId)?.resolve() 208 172 }, 209 - resolveMock(rawId, importer, hasFactory) { 210 - return resolveMock(project, rawId, importer, hasFactory) 173 + resolveMock(rawId, importer, options) { 174 + return mockResolver.resolveMock(rawId, importer, options) 211 175 }, 212 176 invalidate(ids) { 213 - ids.forEach((id) => { 214 - const moduleGraph = server.vite.moduleGraph 215 - const module = moduleGraph.getModuleById(id) 216 - if (module) { 217 - moduleGraph.invalidateModule(module, new Set(), Date.now(), true) 218 - } 219 - }) 177 + return mockResolver.invalidate(ids) 220 178 }, 221 179 222 180 // CDP ··· 278 236 return value 279 237 } 280 238 } 281 - 282 - function withTrailingSlash(path: string): string { 283 - if (path[path.length - 1] !== '/') { 284 - return `${path}/` 285 - } 286 - 287 - return path 288 - }
+6 -10
packages/browser/src/node/types.ts
··· 1 + import type { ServerIdResolution, ServerMockResolution } from '@vitest/mocker/node' 1 2 import type { BirpcReturn } from 'birpc' 2 - import type { AfterSuiteRunMeta, CancelReason, File, Reporter, SnapshotResult, TaskResultPack, UserConsoleLog } from 'vitest' 3 + import type { AfterSuiteRunMeta, CancelReason, Reporter, RunnerTestFile, SnapshotResult, TaskResultPack, UserConsoleLog } from 'vitest' 3 4 4 5 export interface WebSocketBrowserHandlers { 5 6 resolveSnapshotPath: (testPath: string) => string 6 7 resolveSnapshotRawPath: (testPath: string, rawPath: string) => string 7 8 onUnhandledError: (error: unknown, type: string) => Promise<void> 8 - onCollected: (files?: File[]) => Promise<void> 9 + onCollected: (files?: RunnerTestFile[]) => Promise<void> 9 10 onTaskUpdate: (packs: TaskResultPack[]) => void 10 11 onAfterSuiteRun: (meta: AfterSuiteRunMeta) => void 11 12 onCancel: (reason: CancelReason) => void ··· 20 21 resolveId: ( 21 22 id: string, 22 23 importer?: string 23 - ) => Promise<{ id: string; url: string; optimized: boolean } | null> 24 + ) => Promise<ServerIdResolution | null> 24 25 triggerCommand: <T>( 25 26 contextId: string, 26 27 command: string, ··· 30 31 resolveMock: ( 31 32 id: string, 32 33 importer: string, 33 - hasFactory: boolean 34 - ) => Promise<{ 35 - type: 'factory' | 'redirect' | 'automock' 36 - mockPath?: string | null 37 - resolvedId: string 38 - needsInterop?: boolean 39 - }> 34 + options: { mock: 'spy' | 'factory' | 'auto' }, 35 + ) => Promise<ServerMockResolution> 40 36 invalidate: (ids: string[]) => void 41 37 getBrowserFileSourceMap: ( 42 38 id: string
+289
packages/mocker/EXPORTS.md
··· 1 + # Using as a Vite plugin 2 + 3 + Make sure you have `vite` and `@vitest/spy` installed (and `msw` if you are planning to use `ModuleMockerMSWInterceptor`). 4 + 5 + ```ts 6 + import { mockerPlugin } from '@vitest/mocker/node' 7 + 8 + export default defineConfig({ 9 + plugins: [mockerPlugin()], 10 + }) 11 + ``` 12 + 13 + To use it in your code, register the runtime mocker. The naming of `vi` matters - it is used by the compiler. You can configure the name by changing the `hoistMocks.utilsObjectName` and `hoistMocks.regexpHoistable` options. 14 + 15 + ```ts 16 + import { 17 + ModuleMockerMSWInterceptor, 18 + ModuleMockerServerInterceptor, 19 + registerModuleMocker 20 + } from '@vitest/mocker/register' 21 + 22 + // you can use either a server interceptor (relies on Vite's websocket connection) 23 + const vi = registerModuleMocker(() => new ModuleMockerServerInterceptor()) 24 + // or you can use MSW to intercept requests directly in the browser 25 + const vi = registerModuleMocker(globalThisAccessor => new ModuleMockerMSWInterceptor({ globalThisAccessor })) 26 + ``` 27 + 28 + ```ts 29 + // you can also just import "auto-register" at the top of your entry point, 30 + // this will use the server interceptor by default 31 + import '@vitest/mocker/auto-register' 32 + // if you do this, you can create compiler hints with "createCompilerHints" 33 + // utility to use in your own code 34 + import { createCompilerHints } from '@vitest/mocker/browser' 35 + const vi = createCompilerHints() 36 + ``` 37 + 38 + `registerModuleMocker` returns compiler hints that Vite plugin will look for. 39 + 40 + By default, Vitest looks for `vi.mock`/`vi.doMock`/`vi.unmock`/`vi.doUnmock`/`vi.hoisted`. You can configure this with the `hoistMocks` option when initiating a plugin: 41 + 42 + ```ts 43 + import { mockerPlugin } from '@vitest/mocker/node' 44 + 45 + export default defineConfig({ 46 + plugins: [ 47 + mockerPlugin({ 48 + hoistMocks: { 49 + regexpHoistable: /myObj.mock/, 50 + // you will also need to update other options accordingly 51 + utilsObjectName: ['myObj'], 52 + }, 53 + }), 54 + ], 55 + }) 56 + ``` 57 + 58 + Now you can call `vi.mock` in your code and the mocker should kick in automatially: 59 + 60 + ```ts 61 + import { mocked } from './some-module.js' 62 + 63 + vi.mock('./some-module.js', () => { 64 + return { mocked: true } 65 + }) 66 + 67 + mocked === true 68 + ``` 69 + 70 + # Public Exports 71 + 72 + ## MockerRegistry 73 + 74 + Just a cache that holds mocked modules to be used by the actual mocker. 75 + 76 + ```ts 77 + import { ManualMockedModule, MockerRegistry } from '@vitest/mocker' 78 + const registry = new MockerRegistry() 79 + 80 + // Vitest requites the original ID for better error messages, 81 + // You can pass down anything related to the module there 82 + registry.register('manual', './id.js', '/users/custom/id.js', factory) 83 + registry.get('/users/custom/id.js') instanceof ManualMockedModule 84 + ``` 85 + 86 + ## mockObject 87 + 88 + Deeply mock an object. This is the function that automocks modules in Vitest. 89 + 90 + ```ts 91 + import { mockObject } from '@vitest/mocker' 92 + import { spyOn } from '@vitest/spy' 93 + 94 + mockObject( 95 + { 96 + // this is needed because it can be used in vm context 97 + globalContructors: { 98 + Object, 99 + // ... 100 + }, 101 + // you can provide your own spyOn implementation 102 + spyOn, 103 + mockType: 'automock' // or 'autospy' 104 + }, 105 + { 106 + myDeep: { 107 + object() { 108 + return { 109 + willAlso: { 110 + beMocked() { 111 + return true 112 + }, 113 + }, 114 + } 115 + }, 116 + }, 117 + } 118 + ) 119 + ``` 120 + 121 + ## automockPlugin 122 + 123 + The Vite plugin that can mock any module in the browser. 124 + 125 + ```ts 126 + import { automockPlugin } from '@vitest/mocker/node' 127 + import { createServer } from 'vite' 128 + 129 + await createServer({ 130 + plugins: [ 131 + automockPlugin(), 132 + ], 133 + }) 134 + ``` 135 + 136 + Any module that has `mock=automock` or `mock=autospy` query will be mocked: 137 + 138 + ```ts 139 + import { calculator } from './src/calculator.js?mock=automock' 140 + 141 + calculator(1, 2) 142 + calculator.mock.calls[0] === [1, 2] 143 + ``` 144 + 145 + Ideally, you would inject those queries somehow, not write them manually. In the future, this package will support `with { mock: 'auto' }` syntax. 146 + 147 + > [!WARNING] 148 + > The plugin expects a global `__vitest_mocker__` variable with a `mockObject` method. Make sure it is injected _before_ the mocked file is imported. You can also configure the accessor by changing the `globalThisAccessor` option. 149 + 150 + > [!NOTE] 151 + > This plugin is included in `mockerPlugin`. 152 + 153 + ## automockModule 154 + 155 + Replace every export with a mock in the code. 156 + 157 + ```ts 158 + import { automockModule } from '@vitest/mocker/node' 159 + import { parseAst } from 'vite' 160 + 161 + const ms = await automockModule( 162 + `export function test() {}`, 163 + 'automock', 164 + parseAst, 165 + ) 166 + console.log( 167 + ms.toString(), 168 + ms.generateMap({ hires: 'boundary' }) 169 + ) 170 + ``` 171 + 172 + Produces this: 173 + 174 + ```ts 175 + function test() {} 176 + 177 + const __vitest_es_current_module__ = { 178 + __esModule: true, 179 + test, 180 + } 181 + const __vitest_mocked_module__ = __vitest_mocker__.mockObject(__vitest_es_current_module__, 'automock') 182 + const __vitest_mocked_0__ = __vitest_mocked_module__.test 183 + export { 184 + __vitest_mocked_0__ as test, 185 + } 186 + ``` 187 + 188 + ## hoistMocksPlugin 189 + 190 + The plugin that hoists every compiler hint, replaces every static import with dynamic one and updates exports access to make sure live-binding is not broken. 191 + 192 + ```ts 193 + import { hoistMocksPlugin } from '@vitest/mocker/node' 194 + import { createServer } from 'vite' 195 + 196 + await createServer({ 197 + plugins: [ 198 + hoistMocksPlugin({ 199 + hoistedModules: ['virtual:my-module'], 200 + regexpHoistable: /myObj.(mock|hoist)/, 201 + utilsObjectName: ['myObj'], 202 + hoistableMockMethodNames: ['mock'], 203 + // disable support for vi.mock(import('./path')) 204 + dynamicImportMockMethodNames: [], 205 + hoistedMethodNames: ['hoist'], 206 + }), 207 + ], 208 + }) 209 + ``` 210 + 211 + > [!NOTE] 212 + > This plugin is included in `mockerPlugin`. 213 + 214 + ## hoistMocks 215 + 216 + Hoist compiler hints, replace static imports with dynamic ones and update exports access to make sure live-binding is not broken. 217 + 218 + This is required to ensure mocks are resolved before we import the user module. 219 + 220 + ```ts 221 + import { parseAst } from 'vite' 222 + 223 + hoistMocks( 224 + ` 225 + import { mocked } from './some-module.js' 226 + 227 + vi.mock('./some-module.js', () => { 228 + return { mocked: true } 229 + }) 230 + 231 + mocked === true 232 + `, 233 + '/my-module.js', 234 + parseAst 235 + ) 236 + ``` 237 + 238 + Produces this code: 239 + 240 + ```js 241 + vi.mock('./some-module.js', () => { 242 + return { mocked: true } 243 + }) 244 + 245 + const __vi_import_0__ = await import('./some-module.js') 246 + __vi_import_0__.mocked === true 247 + ``` 248 + 249 + ## dynamicImportPlugin 250 + 251 + Wrap every dynamic import with `mocker.wrapDynamicImport`. This is required to ensure mocks are resolved before we import the user module. You can configure the `globalThis` accessor with `globalThisAccessor` option. 252 + 253 + It doesn't make sense to use this plugin in isolation from other plugins. 254 + 255 + ```ts 256 + import { dynamicImportPlugin } from '@vitest/mocker/node' 257 + import { createServer } from 'vite' 258 + 259 + await createServer({ 260 + plugins: [ 261 + dynamicImportPlugin({ 262 + globalThisAccessor: 'Symbol.for("my-mocker")' 263 + }), 264 + ], 265 + }) 266 + ``` 267 + 268 + ```ts 269 + await import('./my-module.js') 270 + 271 + // produces this: 272 + await globalThis[`Symbol.for('my-mocker')`].wrapDynamicImport(() => import('./my-module.js')) 273 + ``` 274 + 275 + ## findMockRedirect 276 + 277 + This method will try to find a file inside `__mocks__` folder that corresponds to the current file. 278 + 279 + ```ts 280 + import { findMockRedirect } from '@vitest/mocker/node' 281 + 282 + // uses sync fs APIs 283 + const mockRedirect = findMockRedirect( 284 + root, 285 + 'vscode', 286 + 'vscode', // if defined, will assume the file is a library name 287 + ) 288 + // mockRedirect == ${root}/__mocks__/vscode.js 289 + ```
+5
packages/mocker/README.md
··· 1 + # @vitest/mocker 2 + 3 + Vitest's module mocker implementation. 4 + 5 + [GitHub](https://github.com/vitest-dev/vitest/packages/mocker) | [Documentation](https://github.com/vitest-dev/vitest/packages/mocker/EXPORTS.md)
+83
packages/mocker/package.json
··· 1 + { 2 + "name": "@vitest/mocker", 3 + "type": "module", 4 + "version": "2.1.0-beta.5", 5 + "description": "Vitest module mocker implementation", 6 + "license": "MIT", 7 + "funding": "https://opencollective.com/vitest", 8 + "homepage": "https://github.com/vitest-dev/vitest/tree/main/packages/mocker#readme", 9 + "repository": { 10 + "type": "git", 11 + "url": "git+https://github.com/vitest-dev/vitest.git", 12 + "directory": "packages/mocker" 13 + }, 14 + "bugs": { 15 + "url": "https://github.com/vitest-dev/vitest/issues" 16 + }, 17 + "sideEffects": false, 18 + "exports": { 19 + ".": { 20 + "types": "./dist/index.d.ts", 21 + "default": "./dist/index.js" 22 + }, 23 + "./node": { 24 + "types": "./dist/node.d.ts", 25 + "default": "./dist/node.js" 26 + }, 27 + "./browser": { 28 + "types": "./dist/browser.d.ts", 29 + "default": "./dist/browser.js" 30 + }, 31 + "./redirect": { 32 + "types": "./dist/redirect.d.ts", 33 + "default": "./dist/redirect.js" 34 + }, 35 + "./register": { 36 + "types": "./dist/register.d.ts", 37 + "default": "./dist/register.js" 38 + }, 39 + "./auto-register": { 40 + "types": "./dist/register.d.ts", 41 + "default": "./dist/register.js" 42 + }, 43 + "./*": "./*" 44 + }, 45 + "main": "./dist/index.js", 46 + "module": "./dist/index.js", 47 + "types": "./dist/index.d.ts", 48 + "files": [ 49 + "*.d.ts", 50 + "dist" 51 + ], 52 + "scripts": { 53 + "build": "rimraf dist && rollup -c", 54 + "dev": "rollup -c --watch" 55 + }, 56 + "peerDependencies": { 57 + "@vitest/spy": "workspace:*", 58 + "msw": "^2.3.5", 59 + "vite": "^5.0.0" 60 + }, 61 + "peerDependenciesMeta": { 62 + "msw": { 63 + "optional": true 64 + }, 65 + "vite": { 66 + "optional": true 67 + } 68 + }, 69 + "dependencies": { 70 + "@vitest/spy": "workspace:^2.1.0-beta.1", 71 + "estree-walker": "^3.0.3", 72 + "magic-string": "^0.30.11" 73 + }, 74 + "devDependencies": { 75 + "@types/estree": "^1.0.5", 76 + "@vitest/spy": "workspace:*", 77 + "@vitest/utils": "workspace:*", 78 + "acorn-walk": "^8.3.3", 79 + "msw": "^2.3.5", 80 + "pathe": "^1.1.2", 81 + "vite": "^5.4.0" 82 + } 83 + }
+70
packages/mocker/rollup.config.js
··· 1 + import { builtinModules, createRequire } from 'node:module' 2 + import { defineConfig } from 'rollup' 3 + import esbuild from 'rollup-plugin-esbuild' 4 + import dts from 'rollup-plugin-dts' 5 + import resolve from '@rollup/plugin-node-resolve' 6 + import json from '@rollup/plugin-json' 7 + import commonjs from '@rollup/plugin-commonjs' 8 + 9 + const require = createRequire(import.meta.url) 10 + const pkg = require('./package.json') 11 + 12 + const entries = { 13 + 'index': 'src/index.ts', 14 + 'node': 'src/node/index.ts', 15 + 'redirect': 'src/node/redirect.ts', 16 + 'browser': 'src/browser/index.ts', 17 + 'register': 'src/browser/register.ts', 18 + 'auto-register': 'src/browser/auto-register.ts', 19 + } 20 + 21 + const external = [ 22 + ...builtinModules, 23 + ...Object.keys(pkg.dependencies || {}), 24 + ...Object.keys(pkg.peerDependencies || {}), 25 + /^msw/, 26 + ] 27 + 28 + const plugins = [ 29 + resolve({ 30 + preferBuiltins: true, 31 + }), 32 + json(), 33 + esbuild({ 34 + target: 'node14', 35 + }), 36 + commonjs(), 37 + ] 38 + 39 + export default defineConfig([ 40 + { 41 + input: entries, 42 + output: { 43 + dir: 'dist', 44 + format: 'esm', 45 + entryFileNames: '[name].js', 46 + chunkFileNames: 'chunk-[name].js', 47 + }, 48 + external, 49 + plugins, 50 + onwarn, 51 + }, 52 + { 53 + input: entries, 54 + output: { 55 + dir: 'dist', 56 + entryFileNames: '[name].d.ts', 57 + format: 'esm', 58 + }, 59 + external, 60 + plugins: [dts({ respectExternal: true })], 61 + onwarn, 62 + }, 63 + ]) 64 + 65 + function onwarn(message) { 66 + if (['EMPTY_BUNDLE', 'CIRCULAR_DEPENDENCY'].includes(message.code)) { 67 + return 68 + } 69 + console.error(message) 70 + }
+251
packages/mocker/src/automocker.ts
··· 1 + import type { MockedModuleType } from './registry' 2 + 3 + type Key = string | symbol 4 + 5 + export interface MockObjectOptions { 6 + type: MockedModuleType 7 + globalConstructors: GlobalConstructors 8 + spyOn: (obj: any, prop: Key) => any 9 + } 10 + 11 + export function mockObject( 12 + options: MockObjectOptions, 13 + object: Record<Key, any>, 14 + mockExports: Record<Key, any> = {}, 15 + ): Record<Key, any> { 16 + const finalizers = new Array<() => void>() 17 + const refs = new RefTracker() 18 + 19 + const define = (container: Record<Key, any>, key: Key, value: any) => { 20 + try { 21 + container[key] = value 22 + return true 23 + } 24 + catch { 25 + return false 26 + } 27 + } 28 + 29 + const mockPropertiesOf = ( 30 + container: Record<Key, any>, 31 + newContainer: Record<Key, any>, 32 + ) => { 33 + const containerType = getType(container) 34 + const isModule = containerType === 'Module' || !!container.__esModule 35 + for (const { key: property, descriptor } of getAllMockableProperties( 36 + container, 37 + isModule, 38 + options.globalConstructors, 39 + )) { 40 + // Modules define their exports as getters. We want to process those. 41 + if (!isModule && descriptor.get) { 42 + try { 43 + Object.defineProperty(newContainer, property, descriptor) 44 + } 45 + catch { 46 + // Ignore errors, just move on to the next prop. 47 + } 48 + continue 49 + } 50 + 51 + // Skip special read-only props, we don't want to mess with those. 52 + if (isSpecialProp(property, containerType)) { 53 + continue 54 + } 55 + 56 + const value = container[property] 57 + 58 + // Special handling of references we've seen before to prevent infinite 59 + // recursion in circular objects. 60 + const refId = refs.getId(value) 61 + if (refId !== undefined) { 62 + finalizers.push(() => 63 + define(newContainer, property, refs.getMockedValue(refId)), 64 + ) 65 + continue 66 + } 67 + 68 + const type = getType(value) 69 + 70 + if (Array.isArray(value)) { 71 + define(newContainer, property, []) 72 + continue 73 + } 74 + 75 + const isFunction 76 + = type.includes('Function') && typeof value === 'function' 77 + if ( 78 + (!isFunction || value.__isMockFunction) 79 + && type !== 'Object' 80 + && type !== 'Module' 81 + ) { 82 + define(newContainer, property, value) 83 + continue 84 + } 85 + 86 + // Sometimes this assignment fails for some unknown reason. If it does, 87 + // just move along. 88 + if (!define(newContainer, property, isFunction ? value : {})) { 89 + continue 90 + } 91 + 92 + if (isFunction) { 93 + if (!options.spyOn) { 94 + throw new Error( 95 + '[@vitest/mocker] `spyOn` is not defined. This is a Vitest error. Please open a new issue with reproduction.', 96 + ) 97 + } 98 + const spyOn = options.spyOn 99 + function mockFunction(this: any) { 100 + // detect constructor call and mock each instance's methods 101 + // so that mock states between prototype/instances don't affect each other 102 + // (jest reference https://github.com/jestjs/jest/blob/2c3d2409879952157433de215ae0eee5188a4384/packages/jest-mock/src/index.ts#L678-L691) 103 + if (this instanceof newContainer[property]) { 104 + for (const { key, descriptor } of getAllMockableProperties( 105 + this, 106 + false, 107 + options.globalConstructors, 108 + )) { 109 + // skip getter since it's not mocked on prototype as well 110 + if (descriptor.get) { 111 + continue 112 + } 113 + 114 + const value = this[key] 115 + const type = getType(value) 116 + const isFunction 117 + = type.includes('Function') && typeof value === 'function' 118 + if (isFunction) { 119 + // mock and delegate calls to original prototype method, which should be also mocked already 120 + const original = this[key] 121 + const mock = spyOn(this, key as string) 122 + .mockImplementation(original) 123 + mock.mockRestore = () => { 124 + mock.mockReset() 125 + mock.mockImplementation(original) 126 + return mock 127 + } 128 + } 129 + } 130 + } 131 + } 132 + const mock = spyOn(newContainer, property) 133 + if (options.type === 'automock') { 134 + mock.mockImplementation(mockFunction) 135 + mock.mockRestore = () => { 136 + mock.mockReset() 137 + mock.mockImplementation(mockFunction) 138 + return mock 139 + } 140 + } 141 + // tinyspy retains length, but jest doesn't. 142 + Object.defineProperty(newContainer[property], 'length', { value: 0 }) 143 + } 144 + 145 + refs.track(value, newContainer[property]) 146 + mockPropertiesOf(value, newContainer[property]) 147 + } 148 + } 149 + 150 + const mockedObject: Record<Key, any> = mockExports 151 + mockPropertiesOf(object, mockedObject) 152 + 153 + // Plug together refs 154 + for (const finalizer of finalizers) { 155 + finalizer() 156 + } 157 + 158 + return mockedObject 159 + } 160 + 161 + class RefTracker { 162 + private idMap = new Map<any, number>() 163 + private mockedValueMap = new Map<number, any>() 164 + 165 + public getId(value: any) { 166 + return this.idMap.get(value) 167 + } 168 + 169 + public getMockedValue(id: number) { 170 + return this.mockedValueMap.get(id) 171 + } 172 + 173 + public track(originalValue: any, mockedValue: any): number { 174 + const newId = this.idMap.size 175 + this.idMap.set(originalValue, newId) 176 + this.mockedValueMap.set(newId, mockedValue) 177 + return newId 178 + } 179 + } 180 + 181 + function getType(value: unknown): string { 182 + return Object.prototype.toString.apply(value).slice(8, -1) 183 + } 184 + 185 + function isSpecialProp(prop: Key, parentType: string) { 186 + return ( 187 + parentType.includes('Function') 188 + && typeof prop === 'string' 189 + && ['arguments', 'callee', 'caller', 'length', 'name'].includes(prop) 190 + ) 191 + } 192 + 193 + export interface GlobalConstructors { 194 + Object: ObjectConstructor 195 + Function: FunctionConstructor 196 + RegExp: RegExpConstructor 197 + Array: ArrayConstructor 198 + Map: MapConstructor 199 + } 200 + 201 + function getAllMockableProperties( 202 + obj: any, 203 + isModule: boolean, 204 + constructors: GlobalConstructors, 205 + ) { 206 + const { Map, Object, Function, RegExp, Array } = constructors 207 + 208 + const allProps = new Map< 209 + string | symbol, 210 + { key: string | symbol; descriptor: PropertyDescriptor } 211 + >() 212 + let curr = obj 213 + do { 214 + // we don't need properties from these 215 + if ( 216 + curr === Object.prototype 217 + || curr === Function.prototype 218 + || curr === RegExp.prototype 219 + ) { 220 + break 221 + } 222 + 223 + collectOwnProperties(curr, (key) => { 224 + const descriptor = Object.getOwnPropertyDescriptor(curr, key) 225 + if (descriptor) { 226 + allProps.set(key, { key, descriptor }) 227 + } 228 + }) 229 + // eslint-disable-next-line no-cond-assign 230 + } while ((curr = Object.getPrototypeOf(curr))) 231 + // default is not specified in ownKeys, if module is interoped 232 + if (isModule && !allProps.has('default') && 'default' in obj) { 233 + const descriptor = Object.getOwnPropertyDescriptor(obj, 'default') 234 + if (descriptor) { 235 + allProps.set('default', { key: 'default', descriptor }) 236 + } 237 + } 238 + return Array.from(allProps.values()) 239 + } 240 + 241 + function collectOwnProperties( 242 + obj: any, 243 + collector: Set<string | symbol> | ((key: string | symbol) => void), 244 + ) { 245 + const collect 246 + = typeof collector === 'function' 247 + ? collector 248 + : (key: string | symbol) => collector.add(key) 249 + Object.getOwnPropertyNames(obj).forEach(collect) 250 + Object.getOwnPropertySymbols(obj).forEach(collect) 251 + }
+6
packages/mocker/src/browser/auto-register.ts
··· 1 + import { ModuleMockerServerInterceptor } from './interceptor-native' 2 + import { registerModuleMocker } from './register' 3 + 4 + registerModuleMocker( 5 + () => new ModuleMockerServerInterceptor(), 6 + )
+146
packages/mocker/src/browser/hints.ts
··· 1 + import type { MaybeMockedDeep } from '@vitest/spy' 2 + import { createSimpleStackTrace } from '@vitest/utils' 3 + import { parseSingleStack } from '@vitest/utils/source-map' 4 + import type { ModuleMockFactoryWithHelper, ModuleMockOptions } from '../types' 5 + import type { ModuleMocker } from './mocker' 6 + 7 + export interface CompilerHintsOptions { 8 + /** 9 + * This is the key used to access the globalThis object in the worker. 10 + * Unlike `globalThisAccessor` in other APIs, this is not injected into the script. 11 + * ```ts 12 + * // globalThisKey: '__my_variable__' produces: 13 + * globalThis['__my_variable__'] 14 + * // globalThisKey: '"__my_variable__"' produces: 15 + * globalThis['"__my_variable__"'] // notice double quotes 16 + * ``` 17 + * @default '__vitest_mocker__' 18 + */ 19 + globalThisKey?: string 20 + } 21 + 22 + export interface ModuleMockerCompilerHints { 23 + hoisted: <T>(factory: () => T) => T 24 + mock: (path: string | Promise<unknown>, factory?: ModuleMockOptions | ModuleMockFactoryWithHelper) => void 25 + unmock: (path: string | Promise<unknown>) => void 26 + doMock: (path: string | Promise<unknown>, factory?: ModuleMockOptions | ModuleMockFactoryWithHelper) => void 27 + doUnmock: (path: string | Promise<unknown>) => void 28 + importActual: <T>(path: string) => Promise<T> 29 + importMock: <T>(path: string) => Promise<MaybeMockedDeep<T>> 30 + } 31 + 32 + export function createCompilerHints(options?: CompilerHintsOptions): ModuleMockerCompilerHints { 33 + const globalThisAccessor = options?.globalThisKey || '__vitest_mocker__' 34 + function _mocker(): ModuleMocker { 35 + // @ts-expect-error injected by the plugin 36 + return typeof globalThis[globalThisAccessor] !== 'undefined' 37 + // @ts-expect-error injected by the plugin 38 + ? globalThis[globalThisAccessor] 39 + : new Proxy( 40 + {}, 41 + { 42 + get(_, name) { 43 + throw new Error( 44 + 'Vitest mocker was not initialized in this environment. ' 45 + + `vi.${String(name)}() is forbidden.`, 46 + ) 47 + }, 48 + }, 49 + ) 50 + } 51 + 52 + const getImporter = (name: string) => { 53 + const stackTrace = /* @__PURE__ */ createSimpleStackTrace({ stackTraceLimit: 5 }) 54 + const stackArray = stackTrace.split('\n') 55 + // if there is no message in a stack trace, use the item - 1 56 + const importerStackIndex = stackArray.findIndex((stack) => { 57 + return stack.includes(` at Object.${name}`) || stack.includes(`${name}@`) 58 + }) 59 + const stack = /* @__PURE__ */ parseSingleStack(stackArray[importerStackIndex + 1]) 60 + return stack?.file || '' 61 + } 62 + 63 + return { 64 + hoisted<T>(factory: () => T): T { 65 + if (typeof factory !== 'function') { 66 + throw new TypeError( 67 + `vi.hoisted() expects a function, but received a ${typeof factory}`, 68 + ) 69 + } 70 + return factory() 71 + }, 72 + 73 + mock(path: string | Promise<unknown>, factory?: ModuleMockOptions | ModuleMockFactoryWithHelper): void { 74 + if (typeof path !== 'string') { 75 + throw new TypeError( 76 + `vi.mock() expects a string path, but received a ${typeof path}`, 77 + ) 78 + } 79 + const importer = getImporter('mock') 80 + _mocker().queueMock( 81 + path, 82 + importer, 83 + typeof factory === 'function' 84 + ? () => 85 + factory(() => 86 + _mocker().importActual( 87 + path, 88 + importer, 89 + ), 90 + ) 91 + : factory, 92 + ) 93 + }, 94 + 95 + unmock(path: string | Promise<unknown>): void { 96 + if (typeof path !== 'string') { 97 + throw new TypeError( 98 + `vi.unmock() expects a string path, but received a ${typeof path}`, 99 + ) 100 + } 101 + _mocker().queueUnmock(path, getImporter('unmock')) 102 + }, 103 + 104 + doMock(path: string | Promise<unknown>, factory?: ModuleMockOptions | ModuleMockFactoryWithHelper): void { 105 + if (typeof path !== 'string') { 106 + throw new TypeError( 107 + `vi.doMock() expects a string path, but received a ${typeof path}`, 108 + ) 109 + } 110 + const importer = getImporter('doMock') 111 + _mocker().queueMock( 112 + path, 113 + importer, 114 + typeof factory === 'function' 115 + ? () => 116 + factory(() => 117 + _mocker().importActual( 118 + path, 119 + importer, 120 + ), 121 + ) 122 + : factory, 123 + ) 124 + }, 125 + 126 + doUnmock(path: string | Promise<unknown>): void { 127 + if (typeof path !== 'string') { 128 + throw new TypeError( 129 + `vi.doUnmock() expects a string path, but received a ${typeof path}`, 130 + ) 131 + } 132 + _mocker().queueUnmock(path, getImporter('doUnmock')) 133 + }, 134 + 135 + async importActual<T = unknown>(path: string): Promise<T> { 136 + return _mocker().importActual<T>( 137 + path, 138 + getImporter('importActual'), 139 + ) 140 + }, 141 + 142 + async importMock<T>(path: string): Promise<MaybeMockedDeep<T>> { 143 + return _mocker().importMock(path, getImporter('importMock')) 144 + }, 145 + } 146 + }
+13
packages/mocker/src/browser/index.ts
··· 1 + export type { ModuleMockerInterceptor } from './interceptor' 2 + export { ModuleMocker } from './mocker' 3 + export { ModuleMockerMSWInterceptor, type ModuleMockerMSWInterceptorOptions } from './interceptor-msw' 4 + export { ModuleMockerServerInterceptor } from './interceptor-native' 5 + 6 + export type { 7 + ModuleMockerRPC, 8 + ModuleMockerConfig, 9 + ResolveIdResult, 10 + ResolveMockResul, 11 + } from './mocker' 12 + export { createCompilerHints } from './hints' 13 + export type { CompilerHintsOptions, ModuleMockerCompilerHints } from './hints'
+182
packages/mocker/src/browser/interceptor-msw.ts
··· 1 + import type { SetupWorker, StartOptions } from 'msw/browser' 2 + import type { HttpHandler } from 'msw' 3 + import type { ManualMockedModule, MockedModule } from '../registry' 4 + import { MockerRegistry } from '../registry' 5 + import { cleanUrl } from '../utils' 6 + import type { ModuleMockerInterceptor } from './interceptor' 7 + 8 + export interface ModuleMockerMSWInterceptorOptions { 9 + /** 10 + * The identifier to access the globalThis object in the worker. 11 + * This will be injected into the script as is, so make sure it's a valid JS expression. 12 + * @example 13 + * ```js 14 + * // globalThisAccessor: '__my_variable__' produces: 15 + * globalThis[__my_variable__] 16 + * // globalThisAccessor: 'Symbol.for('secret:mocks')' produces: 17 + * globalThis[Symbol.for('secret:mocks')] 18 + * // globalThisAccessor: '"__vitest_mocker__"' (notice quotes) produces: 19 + * globalThis["__vitest_mocker__"] 20 + * ``` 21 + * @default `"__vitest_mocker__"` 22 + */ 23 + globalThisAccessor?: string 24 + /** 25 + * Options passed down to `msw.setupWorker().start(options)` 26 + */ 27 + mswOptions?: StartOptions 28 + /** 29 + * A pre-configured `msw.setupWorker` instance. 30 + */ 31 + mswWorker?: SetupWorker 32 + } 33 + 34 + export class ModuleMockerMSWInterceptor implements ModuleMockerInterceptor { 35 + protected readonly mocks: MockerRegistry = new MockerRegistry() 36 + 37 + private started = false 38 + private startPromise: undefined | Promise<unknown> 39 + 40 + constructor( 41 + private readonly options: ModuleMockerMSWInterceptorOptions = {}, 42 + ) { 43 + if (!options.globalThisAccessor) { 44 + options.globalThisAccessor = '"__vitest_mocker__"' 45 + } 46 + } 47 + 48 + async register(module: MockedModule): Promise<void> { 49 + await this.init() 50 + this.mocks.add(module) 51 + } 52 + 53 + async delete(url: string): Promise<void> { 54 + await this.init() 55 + this.mocks.delete(url) 56 + } 57 + 58 + invalidate(): void { 59 + this.mocks.clear() 60 + } 61 + 62 + private async resolveManualMock(mock: ManualMockedModule) { 63 + const exports = Object.keys(await mock.resolve()) 64 + const module = `const module = globalThis[${this.options.globalThisAccessor!}].getFactoryModule("${mock.url}");` 65 + const keys = exports 66 + .map((name) => { 67 + if (name === 'default') { 68 + return `export default module["default"];` 69 + } 70 + return `export const ${name} = module["${name}"];` 71 + }) 72 + .join('\n') 73 + const text = `${module}\n${keys}` 74 + return new Response(text, { 75 + headers: { 76 + 'Content-Type': 'application/javascript', 77 + }, 78 + }) 79 + } 80 + 81 + protected async init(): Promise<unknown> { 82 + if (this.started) { 83 + return 84 + } 85 + if (this.startPromise) { 86 + return this.startPromise 87 + } 88 + const worker = this.options.mswWorker 89 + this.startPromise = Promise.all([ 90 + worker 91 + ? { 92 + setupWorker(handler: HttpHandler) { 93 + worker.use(handler) 94 + return worker 95 + }, 96 + } 97 + : import('msw/browser'), 98 + import('msw/core/http'), 99 + ]).then(([{ setupWorker }, { http }]) => { 100 + const worker = setupWorker( 101 + http.get(/.+/, async ({ request }) => { 102 + const path = cleanQuery(request.url.slice(location.origin.length)) 103 + if (!this.mocks.has(path)) { 104 + // do not cache deps like Vite does for performance 105 + // because we want to be able to update mocks without restarting the server 106 + // TODO: check if it's still neded - we invalidate modules after each test 107 + if (path.includes('/deps/')) { 108 + return fetch(bypass(request)) 109 + } 110 + 111 + return passthrough() 112 + } 113 + 114 + const mock = this.mocks.get(path)! 115 + 116 + switch (mock.type) { 117 + case 'manual': 118 + return this.resolveManualMock(mock) 119 + case 'automock': 120 + case 'autospy': 121 + return Response.redirect(injectQuery(path, `mock=${mock.type}`)) 122 + case 'redirect': 123 + return Response.redirect(mock.redirect) 124 + default: 125 + throw new Error(`Unknown mock type: ${(mock as any).type}`) 126 + } 127 + }), 128 + ) 129 + return worker.start(this.options.mswOptions) 130 + }) 131 + .finally(() => { 132 + this.started = true 133 + this.startPromise = undefined 134 + }) 135 + await this.startPromise 136 + } 137 + } 138 + 139 + const timestampRegexp = /(\?|&)t=\d{13}/ 140 + const versionRegexp = /(\?|&)v=\w{8}/ 141 + function cleanQuery(url: string) { 142 + return url.replace(timestampRegexp, '').replace(versionRegexp, '') 143 + } 144 + 145 + function passthrough() { 146 + return new Response(null, { 147 + status: 302, 148 + statusText: 'Passthrough', 149 + headers: { 150 + 'x-msw-intention': 'passthrough', 151 + }, 152 + }) 153 + } 154 + 155 + function bypass(request: Request) { 156 + const clonedRequest = request.clone() 157 + clonedRequest.headers.set('x-msw-intention', 'bypass') 158 + const cacheControl = clonedRequest.headers.get('cache-control') 159 + if (cacheControl) { 160 + clonedRequest.headers.set( 161 + 'cache-control', 162 + // allow reinvalidation of the cache so mocks can be updated 163 + cacheControl.replace(', immutable', ''), 164 + ) 165 + } 166 + return clonedRequest 167 + } 168 + 169 + const replacePercentageRE = /%/g 170 + function injectQuery(url: string, queryToInject: string): string { 171 + // encode percents for consistent behavior with pathToFileURL 172 + // see #2614 for details 173 + const resolvedUrl = new URL( 174 + url.replace(replacePercentageRE, '%25'), 175 + location.href, 176 + ) 177 + const { search, hash } = resolvedUrl 178 + const pathname = cleanUrl(url) 179 + return `${pathname}?${queryToInject}${search ? `&${search.slice(1)}` : ''}${ 180 + hash ?? '' 181 + }` 182 + }
+17
packages/mocker/src/browser/interceptor-native.ts
··· 1 + import type { MockedModule } from '../registry' 2 + import type { ModuleMockerInterceptor } from './interceptor' 3 + import { rpc } from './utils' 4 + 5 + export class ModuleMockerServerInterceptor implements ModuleMockerInterceptor { 6 + async register(module: MockedModule): Promise<void> { 7 + await rpc('vitest:interceptor:register', module.toJSON()) 8 + } 9 + 10 + async delete(id: string): Promise<void> { 11 + await rpc('vitest:interceptor:delete', id) 12 + } 13 + 14 + invalidate(): void { 15 + rpc('vitest:interceptor:invalidate') 16 + } 17 + }
+7
packages/mocker/src/browser/interceptor.ts
··· 1 + import type { MockedModule } from '../registry' 2 + 3 + export interface ModuleMockerInterceptor { 4 + register: (module: MockedModule) => Promise<void> 5 + delete: (url: string) => Promise<void> 6 + invalidate: () => void 7 + }
+261
packages/mocker/src/browser/mocker.ts
··· 1 + import { extname, join } from 'pathe' 2 + import type { MockedModule, MockedModuleType } from '../registry' 3 + import { AutomockedModule, MockerRegistry, RedirectedModule } from '../registry' 4 + import type { ModuleMockOptions } from '../types' 5 + import { mockObject } from '../automocker' 6 + import type { ModuleMockerInterceptor } from './interceptor' 7 + 8 + const { now } = Date 9 + 10 + // TODO: define an interface thath both node.js and browser mocker can implement 11 + export class ModuleMocker { 12 + protected registry: MockerRegistry = new MockerRegistry() 13 + 14 + private queue = new Set<Promise<void>>() 15 + private mockedIds = new Set<string>() 16 + 17 + constructor( 18 + private interceptor: ModuleMockerInterceptor, 19 + private rpc: ModuleMockerRPC, 20 + private spyOn: (obj: any, method: string | symbol) => any, 21 + private config: ModuleMockerConfig, 22 + ) {} 23 + 24 + public async prepare(): Promise<void> { 25 + if (!this.queue.size) { 26 + return 27 + } 28 + await Promise.all([...this.queue.values()]) 29 + } 30 + 31 + public async resolveFactoryModule(id: string): Promise<Record<string | symbol, any>> { 32 + const mock = this.registry.get(id) 33 + if (!mock || mock.type !== 'manual') { 34 + throw new Error(`Mock ${id} wasn't registered. This is probably a Vitest error. Please, open a new issue with reproduction.`) 35 + } 36 + const result = await mock.resolve() 37 + return result 38 + } 39 + 40 + public getFactoryModule(id: string): any { 41 + const mock = this.registry.get(id) 42 + if (!mock || mock.type !== 'manual') { 43 + throw new Error(`Mock ${id} wasn't registered. This is probably a Vitest error. Please, open a new issue with reproduction.`) 44 + } 45 + if (!mock.cache) { 46 + throw new Error(`Mock ${id} wasn't resolved. This is probably a Vitest error. Please, open a new issue with reproduction.`) 47 + } 48 + return mock.cache 49 + } 50 + 51 + public async invalidate(): Promise<void> { 52 + const ids = Array.from(this.mockedIds) 53 + if (!ids.length) { 54 + return 55 + } 56 + await this.rpc.invalidate(ids) 57 + this.interceptor.invalidate() 58 + this.registry.clear() 59 + } 60 + 61 + public async importActual<T>(id: string, importer: string): Promise<T> { 62 + const resolved = await this.rpc.resolveId(id, importer) 63 + if (resolved == null) { 64 + throw new Error( 65 + `[vitest] Cannot resolve ${id} imported from ${importer}`, 66 + ) 67 + } 68 + const ext = extname(resolved.id) 69 + const url = new URL(resolved.url, location.href) 70 + const query = `_vitest_original&ext${ext}` 71 + const actualUrl = `${url.pathname}${ 72 + url.search ? `${url.search}&${query}` : `?${query}` 73 + }${url.hash}` 74 + return this.wrapDynamicImport(() => import(/* @vite-ignore */ actualUrl)).then((mod) => { 75 + if (!resolved.optimized || typeof mod.default === 'undefined') { 76 + return mod 77 + } 78 + // vite injects this helper for optimized modules, so we try to follow the same behavior 79 + const m = mod.default 80 + return m?.__esModule ? m : { ...((typeof m === 'object' && !Array.isArray(m)) || typeof m === 'function' ? m : {}), default: m } 81 + }) 82 + } 83 + 84 + public async importMock<T>(rawId: string, importer: string): Promise<T> { 85 + await this.prepare() 86 + const { resolvedId, redirectUrl } = await this.rpc.resolveMock( 87 + rawId, 88 + importer, 89 + { mock: 'auto' }, 90 + ) 91 + 92 + const mockUrl = this.resolveMockPath(cleanVersion(resolvedId)) 93 + let mock = this.registry.get(mockUrl) 94 + 95 + if (!mock) { 96 + if (redirectUrl) { 97 + const resolvedRedirect = new URL(this.resolveMockPath(cleanVersion(redirectUrl)), location.href).toString() 98 + mock = new RedirectedModule(rawId, mockUrl, resolvedRedirect) 99 + } 100 + else { 101 + mock = new AutomockedModule(rawId, mockUrl) 102 + } 103 + } 104 + 105 + if (mock.type === 'manual') { 106 + return await mock.resolve() as T 107 + } 108 + 109 + if (mock.type === 'automock' || mock.type === 'autospy') { 110 + const url = new URL(`/@id/${resolvedId}`, location.href) 111 + const query = url.search ? `${url.search}&t=${now()}` : `?t=${now()}` 112 + const moduleObject = await import(/* @vite-ignore */ `${url.pathname}${query}&mock=${mock.type}${url.hash}`) 113 + return this.mockObject(moduleObject, mock.type) as T 114 + } 115 + 116 + return import(/* @vite-ignore */ mock.redirect) 117 + } 118 + 119 + public mockObject( 120 + object: Record<string | symbol, any>, 121 + moduleType: MockedModuleType = 'automock', 122 + ): Record<string | symbol, any> { 123 + return mockObject({ 124 + globalConstructors: { 125 + Object, 126 + Function, 127 + Array, 128 + Map, 129 + RegExp, 130 + }, 131 + spyOn: this.spyOn, 132 + type: moduleType, 133 + }, object) 134 + } 135 + 136 + public queueMock(rawId: string, importer: string, factoryOrOptions?: ModuleMockOptions | (() => any)): void { 137 + const promise = this.rpc 138 + .resolveMock(rawId, importer, { 139 + mock: typeof factoryOrOptions === 'function' 140 + ? 'factory' 141 + : factoryOrOptions?.spy ? 'spy' : 'auto', 142 + }) 143 + .then(async ({ redirectUrl, resolvedId, needsInterop, mockType }) => { 144 + const mockUrl = this.resolveMockPath(cleanVersion(resolvedId)) 145 + this.mockedIds.add(resolvedId) 146 + const factory = typeof factoryOrOptions === 'function' 147 + ? async () => { 148 + const data = await factoryOrOptions() 149 + // vite wraps all external modules that have "needsInterop" in a function that 150 + // merges all exports from default into the module object 151 + return needsInterop ? { default: data } : data 152 + } 153 + : undefined 154 + 155 + const mockRedirect = typeof redirectUrl === 'string' 156 + ? new URL(this.resolveMockPath(cleanVersion(redirectUrl)), location.href).toString() 157 + : null 158 + 159 + let module: MockedModule 160 + if (mockType === 'manual') { 161 + module = this.registry.register('manual', rawId, mockUrl, factory!) 162 + } 163 + // autospy takes higher priority over redirect, so it needs to be checked first 164 + else if (mockType === 'autospy') { 165 + module = this.registry.register('autospy', rawId, mockUrl) 166 + } 167 + else if (mockType === 'redirect') { 168 + module = this.registry.register('redirect', rawId, mockUrl, mockRedirect!) 169 + } 170 + else { 171 + module = this.registry.register('automock', rawId, mockUrl) 172 + } 173 + 174 + await this.interceptor.register(module) 175 + }) 176 + .finally(() => { 177 + this.queue.delete(promise) 178 + }) 179 + this.queue.add(promise) 180 + } 181 + 182 + public queueUnmock(id: string, importer: string): void { 183 + const promise = this.rpc 184 + .resolveId(id, importer) 185 + .then(async (resolved) => { 186 + if (!resolved) { 187 + return 188 + } 189 + const mockUrl = this.resolveMockPath(cleanVersion(resolved.id)) 190 + this.mockedIds.add(resolved.id) 191 + this.registry.delete(mockUrl) 192 + await this.interceptor.delete(mockUrl) 193 + }) 194 + .finally(() => { 195 + this.queue.delete(promise) 196 + }) 197 + this.queue.add(promise) 198 + } 199 + 200 + // We need to await mock registration before importing the actual module 201 + // In case there is a mocked module in the import chain 202 + public wrapDynamicImport<T>(moduleFactory: () => Promise<T>): Promise<T> { 203 + if (typeof moduleFactory === 'function') { 204 + const promise = new Promise<T>((resolve, reject) => { 205 + this.prepare().finally(() => { 206 + moduleFactory().then(resolve, reject) 207 + }) 208 + }) 209 + return promise 210 + } 211 + return moduleFactory 212 + } 213 + 214 + private resolveMockPath(path: string) { 215 + const config = this.config 216 + const fsRoot = join('/@fs/', config.root) 217 + 218 + // URL can be /file/path.js, but path is resolved to /file/path 219 + if (path.startsWith(config.root)) { 220 + return path.slice(config.root.length) 221 + } 222 + 223 + if (path.startsWith(fsRoot)) { 224 + return path.slice(fsRoot.length) 225 + } 226 + 227 + return path 228 + } 229 + } 230 + 231 + const versionRegexp = /(\?|&)v=\w{8}/ 232 + function cleanVersion(url: string) { 233 + return url.replace(versionRegexp, '') 234 + } 235 + 236 + export interface ResolveIdResult { 237 + id: string 238 + url: string 239 + optimized: boolean 240 + } 241 + 242 + export interface ResolveMockResul { 243 + mockType: MockedModuleType 244 + resolvedId: string 245 + redirectUrl?: string | null 246 + needsInterop?: boolean 247 + } 248 + 249 + export interface ModuleMockerRPC { 250 + invalidate: (ids: string[]) => Promise<void> 251 + resolveId: (id: string, importer: string) => Promise<ResolveIdResult | null> 252 + resolveMock: ( 253 + id: string, 254 + importer: string, 255 + options: { mock: 'spy' | 'factory' | 'auto' } 256 + ) => Promise<ResolveMockResul> 257 + } 258 + 259 + export interface ModuleMockerConfig { 260 + root: string 261 + }
+48
packages/mocker/src/browser/register.ts
··· 1 + import { spyOn } from '@vitest/spy' 2 + import type { ModuleMockerCompilerHints } from './hints' 3 + import { createCompilerHints } from './hints' 4 + import { hot, rpc } from './utils' 5 + import type { ModuleMockerInterceptor } from './index' 6 + import { ModuleMocker } from './index' 7 + 8 + declare const __VITEST_GLOBAL_THIS_ACCESSOR__: string 9 + declare const __VITEST_MOCKER_ROOT__: string 10 + 11 + export function registerModuleMocker( 12 + interceptor: (accessor: string) => ModuleMockerInterceptor, 13 + ): ModuleMockerCompilerHints { 14 + const mocker = new ModuleMocker( 15 + interceptor(__VITEST_GLOBAL_THIS_ACCESSOR__), 16 + { 17 + resolveId(id, importer) { 18 + return rpc('vitest:mocks:resolveId', { id, importer }) 19 + }, 20 + resolveMock(id, importer, options) { 21 + return rpc('vitest:mocks:resolveMock', { id, importer, options }) 22 + }, 23 + async invalidate(ids) { 24 + return rpc('vitest:mocks:invalidate', { ids }) 25 + }, 26 + }, 27 + spyOn, 28 + { 29 + root: __VITEST_MOCKER_ROOT__, 30 + }, 31 + ) 32 + 33 + ;(globalThis as any)[__VITEST_GLOBAL_THIS_ACCESSOR__] = mocker 34 + 35 + registerNativeFactoryResolver(mocker) 36 + 37 + return createCompilerHints({ 38 + globalThisKey: __VITEST_GLOBAL_THIS_ACCESSOR__, 39 + }) 40 + } 41 + 42 + export function registerNativeFactoryResolver(mocker: ModuleMocker): void { 43 + hot.on('vitest:interceptor:resolve', async (url: string) => { 44 + const exports = await mocker.resolveFactoryModule(url) 45 + const keys = Object.keys(exports) 46 + hot.send('vitest:interceptor:resolved', { url, keys }) 47 + }) 48 + }
+27
packages/mocker/src/browser/utils.ts
··· 1 + import type { ViteHotContext } from 'vite/types/hot.js' 2 + 3 + const hot: ViteHotContext = import.meta.hot! || { 4 + on: warn, 5 + off: warn, 6 + send: warn, 7 + } 8 + 9 + function warn() { 10 + console.warn('Vitest mocker cannot work if Vite didn\'t establish WS connection.') 11 + } 12 + 13 + export { hot } 14 + 15 + export function rpc<T>(event: string, data?: any): Promise<T> { 16 + hot.send(event, data) 17 + return new Promise<T>((resolve, reject) => { 18 + const timeout = setTimeout(() => { 19 + reject(new Error(`Failed to resolve ${event} in time`)) 20 + }, 5_000) 21 + hot.on(`${event}:result`, function r(data) { 22 + resolve(data) 23 + clearTimeout(timeout) 24 + hot.off('vitest:mocks:resolvedId:result', r) 25 + }) 26 + }) 27 + }
+25
packages/mocker/src/index.ts
··· 1 + export { 2 + MockerRegistry, 3 + ManualMockedModule, 4 + RedirectedModule, 5 + AutomockedModule, 6 + AutospiedModule, 7 + } from './registry' 8 + export { mockObject } from './automocker' 9 + 10 + export type { GlobalConstructors, MockObjectOptions } from './automocker' 11 + export type { 12 + MockedModule, 13 + MockedModuleType, 14 + MockedModuleSerialized, 15 + AutomockedModuleSerialized, 16 + AutospiedModuleSerialized, 17 + RedirectedModuleSerialized, 18 + ManualMockedModuleSerialized, 19 + } from './registry' 20 + 21 + export type { 22 + ModuleMockFactory, 23 + ModuleMockFactoryWithHelper, 24 + ModuleMockOptions, 25 + } from './types'
+78
packages/mocker/src/node/dynamicImportPlugin.ts
··· 1 + import type { SourceMap } from 'magic-string' 2 + import MagicString from 'magic-string' 3 + import type { Plugin, Rollup } from 'vite' 4 + import type { Expression, Positioned } from './esmWalker' 5 + import { esmWalker } from './esmWalker' 6 + 7 + const regexDynamicImport = /import\s*\(/ 8 + 9 + export interface DynamicImportPluginOptions { 10 + /** 11 + * @default `"__vitest_mocker__"` 12 + */ 13 + globalThisAccessor?: string 14 + } 15 + 16 + export function dynamicImportPlugin(options: DynamicImportPluginOptions = {}): Plugin { 17 + return { 18 + name: 'vitest:browser:esm-injector', 19 + enforce: 'post', 20 + transform(source, id) { 21 + // TODO: test is not called for static imports 22 + if (!regexDynamicImport.test(source)) { 23 + return 24 + } 25 + return injectDynamicImport(source, id, this.parse, options) 26 + }, 27 + } 28 + } 29 + 30 + export interface DynamicImportInjectorResult { 31 + ast: Rollup.ProgramNode 32 + code: string 33 + map: SourceMap 34 + } 35 + 36 + export function injectDynamicImport( 37 + code: string, 38 + id: string, 39 + parse: Rollup.PluginContext['parse'], 40 + options: DynamicImportPluginOptions = {}, 41 + ): DynamicImportInjectorResult | undefined { 42 + const s = new MagicString(code) 43 + 44 + let ast: any 45 + try { 46 + ast = parse(code) 47 + } 48 + catch (err) { 49 + console.error(`Cannot parse ${id}:\n${(err as any).message}`) 50 + return 51 + } 52 + 53 + // 3. convert references to import bindings & import.meta references 54 + esmWalker(ast, { 55 + // TODO: make env updatable 56 + onImportMeta() { 57 + // s.update(node.start, node.end, viImportMetaKey) 58 + }, 59 + onDynamicImport(node) { 60 + const globalThisAccessor = options.globalThisAccessor || '"__vitest_mocker__"' 61 + const replaceString = `globalThis[${globalThisAccessor}].wrapDynamicImport(() => import(` 62 + const importSubstring = code.substring(node.start, node.end) 63 + const hasIgnore = importSubstring.includes('/* @vite-ignore */') 64 + s.overwrite( 65 + node.start, 66 + (node.source as Positioned<Expression>).start, 67 + replaceString + (hasIgnore ? '/* @vite-ignore */ ' : ''), 68 + ) 69 + s.overwrite(node.end - 1, node.end, '))') 70 + }, 71 + }) 72 + 73 + return { 74 + ast, 75 + code: s.toString(), 76 + map: s.generateMap({ hires: 'boundary', source: id }), 77 + } 78 + }
+16
packages/mocker/src/node/index.ts
··· 1 + export { automockModule, automockPlugin } from './automockPlugin' 2 + export { findMockRedirect } from './redirect' 3 + export { hoistMocksPlugin, hoistMocks } from './hoistMocksPlugin' 4 + export { ServerMockResolver } from './resolver' 5 + export { dynamicImportPlugin } from './dynamicImportPlugin' 6 + export { interceptorPlugin } from './interceptorPlugin' 7 + export { mockerPlugin } from './mockerPlugin' 8 + 9 + export type { 10 + ServerMockResolution, 11 + ServerIdResolution, 12 + ServerResolverOptions, 13 + } from './resolver' 14 + export type { AutomockPluginOptions } from './automockPlugin' 15 + export type { HoistMocksPluginOptions, HoistMocksResult } from './hoistMocksPlugin' 16 + export type { InterceptorPluginOptions } from './interceptorPlugin'
+115
packages/mocker/src/node/interceptorPlugin.ts
··· 1 + import { join } from 'node:path/posix' 2 + import { readFile } from 'node:fs/promises' 3 + import type { Plugin } from 'vite' 4 + import type { MockedModuleSerialized } from '../registry' 5 + import { ManualMockedModule, MockerRegistry } from '../registry' 6 + import { cleanUrl } from '../utils' 7 + import { automockModule } from './automockPlugin' 8 + 9 + export interface InterceptorPluginOptions { 10 + /** 11 + * @default "__vitest_mocker__" 12 + */ 13 + globalThisAccessor?: string 14 + } 15 + 16 + export function interceptorPlugin(options: InterceptorPluginOptions): Plugin { 17 + const registry = new MockerRegistry() 18 + return { 19 + name: 'vitest:mocks:interceptor', 20 + enforce: 'pre', 21 + async load(id) { 22 + const mock = registry.get(id) 23 + if (!mock) { 24 + return 25 + } 26 + if (mock.type === 'manual') { 27 + const exports = Object.keys(await mock.resolve()) 28 + const accessor = options.globalThisAccessor || '"__vitest_mocker__"' 29 + const serverUrl = (mock as any).serverUrl as string 30 + const module = `const module = globalThis[${accessor}].getFactoryModule("${serverUrl}");` 31 + const keys = exports 32 + .map((name) => { 33 + if (name === 'default') { 34 + return `export default module["default"];` 35 + } 36 + return `export const ${name} = module["${name}"];` 37 + }) 38 + .join('\n') 39 + return `${module}\n${keys}` 40 + } 41 + if (mock.type === 'redirect') { 42 + return readFile(mock.redirect, 'utf-8') 43 + } 44 + }, 45 + transform: { 46 + order: 'post', 47 + handler(code, id) { 48 + const mock = registry.get(id) 49 + if (!mock) { 50 + return 51 + } 52 + if (mock.type === 'automock' || mock.type === 'autospy') { 53 + const m = automockModule(code, mock.type, this.parse, { 54 + globalThisAccessor: options.globalThisAccessor, 55 + }) 56 + 57 + return { 58 + code: m.toString(), 59 + map: m.generateMap({ hires: 'boundary', source: cleanUrl(id) }), 60 + } 61 + } 62 + }, 63 + }, 64 + configureServer(server) { 65 + server.ws.on('vitest:interceptor:register', (event: MockedModuleSerialized) => { 66 + const serverUrl = event.url 67 + // the browsers stores the url relative to the root 68 + // but on the server "id" operates on the file paths 69 + event.url = join(server.config.root, event.url) 70 + if (event.type === 'manual') { 71 + const module = ManualMockedModule.fromJSON(event, async () => { 72 + const keys = await getFactoryExports(serverUrl) 73 + return Object.fromEntries(keys.map(key => [key, null])) 74 + }) 75 + Object.assign(module, { 76 + serverUrl, 77 + }) 78 + registry.add(module) 79 + } 80 + else { 81 + if (event.type === 'redirect') { 82 + const redirectUrl = new URL(event.redirect) 83 + event.redirect = join(server.config.root, redirectUrl.pathname) 84 + } 85 + registry.register(event) 86 + } 87 + server.ws.send('vitest:interceptor:register:result') 88 + }) 89 + server.ws.on('vitest:interceptor:delete', (id: string) => { 90 + registry.delete(id) 91 + server.ws.send('vitest:interceptor:register:delete') 92 + }) 93 + server.ws.on('vitest:interceptor:invalidate', () => { 94 + registry.clear() 95 + server.ws.send('vitest:interceptor:register:invalidate') 96 + }) 97 + 98 + function getFactoryExports(url: string) { 99 + server.ws.send('vitest:interceptor:resolve', url) 100 + let timeout: NodeJS.Timeout 101 + return new Promise<string[]>((resolve, reject) => { 102 + timeout = setTimeout(() => { 103 + reject(new Error(`Timeout while waiting for factory exports of ${url}`)) 104 + }, 10_000) 105 + server.ws.on('vitest:interceptor:resolved', ({ url: resolvedUrl, keys }: { url: string; keys: string[] }) => { 106 + if (resolvedUrl === url) { 107 + clearTimeout(timeout) 108 + resolve(keys) 109 + } 110 + }) 111 + }) 112 + } 113 + }, 114 + } 115 + }
+83
packages/mocker/src/node/mockerPlugin.ts
··· 1 + import { fileURLToPath } from 'node:url' 2 + import { readFile } from 'node:fs/promises' 3 + import type { Plugin, ViteDevServer } from 'vite' 4 + 5 + import { resolve } from 'pathe' 6 + import { type AutomockPluginOptions, automockPlugin } from './automockPlugin' 7 + import { type HoistMocksPluginOptions, hoistMocksPlugin } from './hoistMocksPlugin' 8 + import { dynamicImportPlugin } from './dynamicImportPlugin' 9 + import { ServerMockResolver } from './resolver' 10 + import { interceptorPlugin } from './interceptorPlugin' 11 + 12 + interface MockerPluginOptions extends AutomockPluginOptions { 13 + hoistMocks?: HoistMocksPluginOptions 14 + } 15 + 16 + // this is an implementation for public usage 17 + // vitest doesn't use this plugin directly 18 + 19 + export function mockerPlugin(options: MockerPluginOptions = {}): Plugin[] { 20 + let server: ViteDevServer 21 + const registerPath = resolve(fileURLToPath(new URL('./register.js', import.meta.url))) 22 + return [ 23 + { 24 + name: 'vitest:mocker:ws-rpc', 25 + config(_, { command }) { 26 + if (command !== 'serve') { 27 + return 28 + } 29 + return { 30 + server: { 31 + // don't pre-transform request because they might be mocked at runtime 32 + preTransformRequests: false, 33 + }, 34 + optimizeDeps: { 35 + exclude: ['@vitest/mocker/register', '@vitest/mocker/browser'], 36 + }, 37 + } 38 + }, 39 + configureServer(server_) { 40 + server = server_ 41 + const mockResolver = new ServerMockResolver(server) 42 + server.ws.on('vitest:mocks:resolveId', async ({ id, importer }: { id: string; importer: string }) => { 43 + const resolved = await mockResolver.resolveId(id, importer) 44 + server.ws.send('vitest:mocks:resolvedId:result', resolved) 45 + }) 46 + server.ws.on('vitest:mocks:resolveMock', async ({ id, importer, options }: { id: string; importer: string; options: any }) => { 47 + const resolved = await mockResolver.resolveMock(id, importer, options) 48 + server.ws.send('vitest:mocks:resolveMock:result', resolved) 49 + }) 50 + server.ws.on('vitest:mocks:invalidate', async ({ ids }: { ids: string[] }) => { 51 + mockResolver.invalidate(ids) 52 + server.ws.send('vitest:mocks:invalidate:result') 53 + }) 54 + }, 55 + async load(id) { 56 + if (id !== registerPath) { 57 + return 58 + } 59 + 60 + if (!server) { 61 + // mocker doesn't work during build 62 + return 'export {}' 63 + } 64 + 65 + const content = await readFile(registerPath, 'utf-8') 66 + const result = content 67 + .replace( 68 + /__VITEST_GLOBAL_THIS_ACCESSOR__/g, 69 + options.globalThisAccessor ?? '"__vitest_mocker__"', 70 + ) 71 + .replace( 72 + '__VITEST_MOCKER_ROOT__', 73 + JSON.stringify(server.config.root), 74 + ) 75 + return result 76 + }, 77 + }, 78 + hoistMocksPlugin(options.hoistMocks), 79 + interceptorPlugin(options), 80 + automockPlugin(options), 81 + dynamicImportPlugin(options), 82 + ] 83 + }
+85
packages/mocker/src/node/redirect.ts
··· 1 + import fs from 'node:fs' 2 + import { builtinModules } from 'node:module' 3 + import { basename, dirname, extname, join, resolve } from 'pathe' 4 + 5 + const { existsSync, readdirSync, statSync } = fs 6 + 7 + export function findMockRedirect( 8 + root: string, 9 + mockPath: string, 10 + external: string | null, 11 + ): string | null { 12 + const path = external || mockPath 13 + 14 + // it's a node_module alias 15 + // all mocks should be inside <root>/__mocks__ 16 + if (external || isNodeBuiltin(mockPath) || !existsSync(mockPath)) { 17 + const mockDirname = dirname(path) // for nested mocks: @vueuse/integration/useJwt 18 + const mockFolder = join(root, '__mocks__', mockDirname) 19 + 20 + if (!existsSync(mockFolder)) { 21 + return null 22 + } 23 + 24 + const baseOriginal = basename(path) 25 + 26 + function findFile(mockFolder: string, baseOriginal: string): string | null { 27 + const files = readdirSync(mockFolder) 28 + for (const file of files) { 29 + const baseFile = basename(file, extname(file)) 30 + if (baseFile === baseOriginal) { 31 + const path = resolve(mockFolder, file) 32 + // if the same name, return the file 33 + if (statSync(path).isFile()) { 34 + return path 35 + } 36 + else { 37 + // find folder/index.{js,ts} 38 + const indexFile = findFile(path, 'index') 39 + if (indexFile) { 40 + return indexFile 41 + } 42 + } 43 + } 44 + } 45 + return null 46 + } 47 + 48 + return findFile(mockFolder, baseOriginal) 49 + } 50 + 51 + const dir = dirname(path) 52 + const baseId = basename(path) 53 + const fullPath = resolve(dir, '__mocks__', baseId) 54 + return existsSync(fullPath) ? fullPath : null 55 + } 56 + 57 + const builtins = new Set([ 58 + ...builtinModules, 59 + 'assert/strict', 60 + 'diagnostics_channel', 61 + 'dns/promises', 62 + 'fs/promises', 63 + 'path/posix', 64 + 'path/win32', 65 + 'readline/promises', 66 + 'stream/consumers', 67 + 'stream/promises', 68 + 'stream/web', 69 + 'timers/promises', 70 + 'util/types', 71 + 'wasi', 72 + ]) 73 + 74 + const prefixedBuiltins = new Set(['node:test', 'node:sqlite']) 75 + const NODE_BUILTIN_NAMESPACE = 'node:' 76 + function isNodeBuiltin(id: string): boolean { 77 + if (prefixedBuiltins.has(id)) { 78 + return true 79 + } 80 + return builtins.has( 81 + id.startsWith(NODE_BUILTIN_NAMESPACE) 82 + ? id.slice(NODE_BUILTIN_NAMESPACE.length) 83 + : id, 84 + ) 85 + }
+188
packages/mocker/src/node/resolver.ts
··· 1 + import { existsSync, readFileSync } from 'node:fs' 2 + import { isAbsolute, join, resolve } from 'pathe' 3 + import type { PartialResolvedId } from 'rollup' 4 + import type { ResolvedConfig as ViteConfig, ViteDevServer } from 'vite' 5 + import { cleanUrl } from '../utils' 6 + import { findMockRedirect } from './redirect' 7 + 8 + export interface ServerResolverOptions { 9 + /** 10 + * @default ['/node_modules/'] 11 + */ 12 + moduleDirectories?: string[] 13 + } 14 + 15 + const VALID_ID_PREFIX = '/@id/' 16 + 17 + export class ServerMockResolver { 18 + constructor( 19 + private server: ViteDevServer, 20 + private options: ServerResolverOptions = {}, 21 + ) {} 22 + 23 + async resolveMock( 24 + rawId: string, 25 + importer: string, 26 + options: { mock: 'spy' | 'factory' | 'auto' }, 27 + ): Promise<ServerMockResolution> { 28 + const { id, fsPath, external } = await this.resolveMockId(rawId, importer) 29 + 30 + if (options.mock === 'factory') { 31 + const manifest = getViteDepsManifest(this.server.config) 32 + const needsInterop = manifest?.[fsPath]?.needsInterop ?? false 33 + return { mockType: 'manual', resolvedId: id, needsInterop } 34 + } 35 + 36 + if (options.mock === 'spy') { 37 + return { mockType: 'autospy', resolvedId: id } 38 + } 39 + 40 + const redirectUrl = findMockRedirect(this.server.config.root, fsPath, external) 41 + 42 + return { 43 + mockType: redirectUrl === null ? 'automock' : 'redirect', 44 + redirectUrl, 45 + resolvedId: id, 46 + } 47 + } 48 + 49 + public invalidate(ids: string[]): void { 50 + ids.forEach((id) => { 51 + const moduleGraph = this.server.moduleGraph 52 + const module = moduleGraph.getModuleById(id) 53 + if (module) { 54 + moduleGraph.invalidateModule(module, new Set(), Date.now(), true) 55 + } 56 + }) 57 + } 58 + 59 + public async resolveId(id: string, importer?: string): Promise<ServerIdResolution | null> { 60 + const resolved = await this.server.pluginContainer.resolveId( 61 + id, 62 + importer, 63 + { 64 + ssr: false, 65 + }, 66 + ) 67 + if (!resolved) { 68 + return null 69 + } 70 + const isOptimized = resolved.id.startsWith(withTrailingSlash(this.server.config.cacheDir)) 71 + let url: string 72 + // normalise the URL to be acceptible by the browser 73 + // https://github.com/vitejs/vite/blob/e833edf026d495609558fd4fb471cf46809dc369/packages/vite/src/node/plugins/importAnalysis.ts#L335 74 + const root = this.server.config.root 75 + if (resolved.id.startsWith(withTrailingSlash(root))) { 76 + url = resolved.id.slice(root.length) 77 + } 78 + else if ( 79 + resolved.id !== '/@react-refresh' 80 + && isAbsolute(resolved.id) 81 + && existsSync(cleanUrl(resolved.id)) 82 + ) { 83 + url = join('/@fs/', resolved.id) 84 + } 85 + else { 86 + url = resolved.id 87 + } 88 + if (url[0] !== '.' && url[0] !== '/') { 89 + url = id.startsWith(VALID_ID_PREFIX) 90 + ? id 91 + : VALID_ID_PREFIX + id.replace('\0', '__x00__') 92 + } 93 + return { 94 + id: resolved.id, 95 + url, 96 + optimized: isOptimized, 97 + } 98 + } 99 + 100 + private async resolveMockId(rawId: string, importer: string) { 101 + if (!importer.startsWith(this.server.config.root)) { 102 + importer = join(this.server.config.root, importer) 103 + } 104 + const resolved = await this.server.pluginContainer.resolveId( 105 + rawId, 106 + importer, 107 + { 108 + ssr: false, 109 + }, 110 + ) 111 + return this.resolveModule(rawId, resolved) 112 + } 113 + 114 + private resolveModule(rawId: string, resolved: PartialResolvedId | null) { 115 + const id = resolved?.id || rawId 116 + const external 117 + = !isAbsolute(id) || isModuleDirectory(this.options, id) ? rawId : null 118 + return { 119 + id, 120 + fsPath: cleanUrl(id), 121 + external, 122 + } 123 + } 124 + } 125 + 126 + function isModuleDirectory(config: ServerResolverOptions, path: string) { 127 + const moduleDirectories = config.moduleDirectories || [ 128 + '/node_modules/', 129 + ] 130 + return moduleDirectories.some((dir: string) => path.includes(dir)) 131 + } 132 + 133 + interface PartialManifest { 134 + [name: string]: { 135 + hash: string 136 + needsInterop: boolean 137 + } 138 + } 139 + 140 + const metadata = new WeakMap<ViteConfig, PartialManifest>() 141 + 142 + function getViteDepsManifest(config: ViteConfig) { 143 + if (metadata.has(config)) { 144 + return metadata.get(config)! 145 + } 146 + const cacheDirPath = getDepsCacheDir(config) 147 + const metadataPath = resolve(cacheDirPath, '_metadata.json') 148 + if (!existsSync(metadataPath)) { 149 + return null 150 + } 151 + const { optimized } = JSON.parse(readFileSync(metadataPath, 'utf-8')) 152 + const newManifest: PartialManifest = {} 153 + for (const name in optimized) { 154 + const dep = optimized[name] 155 + const file = resolve(cacheDirPath, dep.file) 156 + newManifest[file] = { 157 + hash: dep.fileHash, 158 + needsInterop: dep.needsInterop, 159 + } 160 + } 161 + metadata.set(config, newManifest) 162 + return newManifest 163 + } 164 + 165 + function getDepsCacheDir(config: ViteConfig): string { 166 + return resolve(config.cacheDir, 'deps') 167 + } 168 + 169 + function withTrailingSlash(path: string): string { 170 + if (path[path.length - 1] !== '/') { 171 + return `${path}/` 172 + } 173 + 174 + return path 175 + } 176 + 177 + export interface ServerMockResolution { 178 + mockType: 'manual' | 'redirect' | 'automock' | 'autospy' 179 + resolvedId: string 180 + needsInterop?: boolean 181 + redirectUrl?: string | null 182 + } 183 + 184 + export interface ServerIdResolution { 185 + id: string 186 + url: string 187 + optimized: boolean 188 + }
+286
packages/mocker/src/registry.ts
··· 1 + export class MockerRegistry { 2 + private readonly registry: Map<string, MockedModule> = new Map() 3 + 4 + clear(): void { 5 + this.registry.clear() 6 + } 7 + 8 + keys(): IterableIterator<string> { 9 + return this.registry.keys() 10 + } 11 + 12 + add(mock: MockedModule): void { 13 + this.registry.set(mock.url, mock) 14 + } 15 + 16 + public register( 17 + json: MockedModuleSerialized, 18 + ): MockedModule 19 + public register( 20 + type: 'redirect', 21 + raw: string, 22 + url: string, 23 + redirect: string, 24 + ): RedirectedModule 25 + public register( 26 + type: 'manual', 27 + raw: string, 28 + url: string, 29 + factory: () => any, 30 + ): ManualMockedModule 31 + public register( 32 + type: 'automock', 33 + raw: string, 34 + url: string, 35 + ): AutomockedModule 36 + public register( 37 + type: 'autospy', 38 + raw: string, 39 + url: string, 40 + ): AutospiedModule 41 + public register( 42 + typeOrEvent: MockedModuleType | MockedModuleSerialized, 43 + raw?: string, 44 + url?: string, 45 + factoryOrRedirect?: string | (() => any), 46 + ): MockedModule { 47 + const type = typeof typeOrEvent === 'object' ? typeOrEvent.type : typeOrEvent 48 + 49 + if (typeof typeOrEvent === 'object') { 50 + const event = typeOrEvent 51 + if ( 52 + event instanceof AutomockedModule 53 + || event instanceof AutospiedModule 54 + || event instanceof ManualMockedModule 55 + || event instanceof RedirectedModule 56 + ) { 57 + throw new TypeError( 58 + `[vitest] Cannot register a mock that is already defined. ` 59 + + `Expected a JSON representation from \`MockedModule.toJSON\`, instead got "${event.type}". ` 60 + + `Use "registry.add()" to update a mock instead.`, 61 + ) 62 + } 63 + if (event.type === 'automock') { 64 + const module = AutomockedModule.fromJSON(event) 65 + this.add(module) 66 + return module 67 + } 68 + else if (event.type === 'autospy') { 69 + const module = AutospiedModule.fromJSON(event) 70 + this.add(module) 71 + return module 72 + } 73 + else if (event.type === 'redirect') { 74 + const module = RedirectedModule.fromJSON(event) 75 + this.add(module) 76 + return module 77 + } 78 + else if (event.type === 'manual') { 79 + throw new Error(`Cannot set serialized manual mock. Define a factory function manually with \`ManualMockedModule.fromJSON()\`.`) 80 + } 81 + else { 82 + throw new Error(`Unknown mock type: ${(event as any).type}`) 83 + } 84 + } 85 + 86 + if (typeof raw !== 'string') { 87 + throw new TypeError('[vitest] Mocks require a raw string.') 88 + } 89 + 90 + if (typeof url !== 'string') { 91 + throw new TypeError('[vitest] Mocks require a url string.') 92 + } 93 + 94 + if (type === 'manual') { 95 + if (typeof factoryOrRedirect !== 'function') { 96 + throw new TypeError('[vitest] Manual mocks require a factory function.') 97 + } 98 + const mock = new ManualMockedModule(raw, url, factoryOrRedirect) 99 + this.add(mock) 100 + return mock 101 + } 102 + else if (type === 'automock' || type === 'autospy') { 103 + const mock = type === 'automock' 104 + ? new AutomockedModule(raw, url) 105 + : new AutospiedModule(raw, url) 106 + this.add(mock) 107 + return mock 108 + } 109 + else if (type === 'redirect') { 110 + if (typeof factoryOrRedirect !== 'string') { 111 + throw new TypeError('[vitest] Redirect mocks require a redirect string.') 112 + } 113 + const mock = new RedirectedModule(raw, url, factoryOrRedirect) 114 + this.add(mock) 115 + return mock 116 + } 117 + else { 118 + throw new Error(`[vitest] Unknown mock type: ${type}`) 119 + } 120 + } 121 + 122 + public delete(id: string): void { 123 + this.registry.delete(id) 124 + } 125 + 126 + public get(id: string): MockedModule | undefined { 127 + return this.registry.get(id) 128 + } 129 + 130 + public has(id: string): boolean { 131 + return this.registry.has(id) 132 + } 133 + } 134 + 135 + export type MockedModule = 136 + | AutomockedModule 137 + | AutospiedModule 138 + | ManualMockedModule 139 + | RedirectedModule 140 + export type MockedModuleType = 'automock' | 'autospy' | 'manual' | 'redirect' 141 + 142 + export type MockedModuleSerialized = 143 + | AutomockedModuleSerialized 144 + | AutospiedModuleSerialized 145 + | ManualMockedModuleSerialized 146 + | RedirectedModuleSerialized 147 + 148 + export class AutomockedModule { 149 + public readonly type = 'automock' 150 + 151 + constructor( 152 + public raw: string, 153 + public url: string, 154 + ) {} 155 + 156 + static fromJSON(data: AutomockedModuleSerialized): AutospiedModule { 157 + return new AutospiedModule(data.raw, data.url) 158 + } 159 + 160 + toJSON(): AutomockedModuleSerialized { 161 + return { 162 + type: this.type, 163 + url: this.url, 164 + raw: this.raw, 165 + } 166 + } 167 + } 168 + 169 + export interface AutomockedModuleSerialized { 170 + type: 'automock' 171 + url: string 172 + raw: string 173 + } 174 + 175 + export class AutospiedModule { 176 + public readonly type = 'autospy' 177 + 178 + constructor( 179 + public raw: string, 180 + public url: string, 181 + ) {} 182 + 183 + static fromJSON(data: AutospiedModuleSerialized): AutospiedModule { 184 + return new AutospiedModule(data.raw, data.url) 185 + } 186 + 187 + toJSON(): AutospiedModuleSerialized { 188 + return { 189 + type: this.type, 190 + url: this.url, 191 + raw: this.raw, 192 + } 193 + } 194 + } 195 + 196 + export interface AutospiedModuleSerialized { 197 + type: 'autospy' 198 + url: string 199 + raw: string 200 + } 201 + 202 + export class RedirectedModule { 203 + public readonly type = 'redirect' 204 + 205 + constructor( 206 + public raw: string, 207 + public url: string, 208 + public redirect: string, 209 + ) {} 210 + 211 + static fromJSON(data: RedirectedModuleSerialized): RedirectedModule { 212 + return new RedirectedModule(data.raw, data.url, data.redirect) 213 + } 214 + 215 + toJSON(): RedirectedModuleSerialized { 216 + return { 217 + type: this.type, 218 + url: this.url, 219 + raw: this.raw, 220 + redirect: this.redirect, 221 + } 222 + } 223 + } 224 + 225 + export interface RedirectedModuleSerialized { 226 + type: 'redirect' 227 + url: string 228 + raw: string 229 + redirect: string 230 + } 231 + 232 + export class ManualMockedModule { 233 + public cache: Record<string | symbol, any> | undefined 234 + public readonly type = 'manual' 235 + 236 + constructor( 237 + public raw: string, 238 + public url: string, 239 + public factory: () => any, 240 + ) {} 241 + 242 + async resolve(): Promise<Record<string | symbol, any>> { 243 + if (this.cache) { 244 + return this.cache 245 + } 246 + let exports: any 247 + try { 248 + exports = await this.factory() 249 + } 250 + catch (err) { 251 + const vitestError = new Error( 252 + '[vitest] There was an error when mocking a module. ' 253 + + 'If you are using "vi.mock" factory, make sure there are no top level variables inside, since this call is hoisted to top of the file. ' 254 + + 'Read more: https://vitest.dev/api/vi.html#vi-mock', 255 + ) 256 + vitestError.cause = err 257 + throw vitestError 258 + } 259 + 260 + if (exports === null || typeof exports !== 'object' || Array.isArray(exports)) { 261 + throw new TypeError( 262 + `[vitest] vi.mock("${this.raw}", factory?: () => unknown) is not returning an object. Did you mean to return an object with a "default" key?`, 263 + ) 264 + } 265 + 266 + return (this.cache = exports) 267 + } 268 + 269 + static fromJSON(data: ManualMockedModuleSerialized, factory: () => any): ManualMockedModule { 270 + return new ManualMockedModule(data.raw, data.url, factory) 271 + } 272 + 273 + toJSON(): ManualMockedModuleSerialized { 274 + return { 275 + type: this.type, 276 + url: this.url, 277 + raw: this.raw, 278 + } 279 + } 280 + } 281 + 282 + export interface ManualMockedModuleSerialized { 283 + type: 'manual' 284 + url: string 285 + raw: string 286 + }
+9
packages/mocker/src/types.ts
··· 1 + type Awaitable<T> = T | PromiseLike<T> 2 + 3 + export type ModuleMockFactoryWithHelper<M = unknown> = ( 4 + importOriginal: <T extends M = M>() => Promise<T> 5 + ) => Awaitable<Partial<M>> 6 + export type ModuleMockFactory = () => any 7 + export interface ModuleMockOptions { 8 + spy?: boolean 9 + }
+4
packages/mocker/src/utils.ts
··· 1 + const postfixRE = /[?#].*$/ 2 + export function cleanUrl(url: string): string { 3 + return url.replace(postfixRE, '') 4 + }
+11
packages/mocker/tsconfig.json
··· 1 + { 2 + "extends": "../../tsconfig.base.json", 3 + "compilerOptions": { 4 + "lib": ["ESNext", "DOM"], 5 + "moduleResolution": "Bundler", 6 + "types": ["vite/client"], 7 + "isolatedDeclarations": true 8 + }, 9 + "include": ["src/**/*"], 10 + "exclude": ["**/dist/**"] 11 + }
-1
packages/utils/package.json
··· 65 65 }, 66 66 "dependencies": { 67 67 "@vitest/pretty-format": "workspace:*", 68 - "estree-walker": "^3.0.3", 69 68 "loupe": "^3.1.1", 70 69 "tinyrainbow": "^1.2.0" 71 70 },
-1
packages/utils/rollup.config.js
··· 13 13 'index': 'src/index.ts', 14 14 'helpers': 'src/helpers.ts', 15 15 'diff': 'src/diff/index.ts', 16 - 'ast': 'src/ast/index.ts', 17 16 'error': 'src/error.ts', 18 17 'source-map': 'src/source-map.ts', 19 18 'types': 'src/types.ts',
-328
packages/utils/src/ast/esmWalker.ts
··· 1 - import type { 2 - Function as FunctionNode, 3 - Identifier, 4 - ImportExpression, 5 - Pattern, 6 - Property, 7 - VariableDeclaration, 8 - Node as _Node, 9 - } from 'estree' 10 - import { walk as eswalk } from 'estree-walker' 11 - 12 - export type Positioned<T> = T & { 13 - start: number 14 - end: number 15 - } 16 - 17 - export type Node = Positioned<_Node> 18 - 19 - interface Visitors { 20 - onIdentifier: ( 21 - node: Positioned<Identifier>, 22 - parent: Node, 23 - parentStack: Node[] 24 - ) => void 25 - onImportMeta: (node: Node) => void 26 - onDynamicImport: (node: Positioned<ImportExpression>) => void 27 - } 28 - 29 - const isNodeInPatternWeakSet = new WeakSet<_Node>() 30 - export function setIsNodeInPattern(node: Property): WeakSet<_Node> { 31 - return isNodeInPatternWeakSet.add(node) 32 - } 33 - export function isNodeInPattern(node: _Node): node is Property { 34 - return isNodeInPatternWeakSet.has(node) 35 - } 36 - 37 - /** 38 - * Same logic from \@vue/compiler-core & \@vue/compiler-sfc 39 - * Except this is using acorn AST 40 - */ 41 - export function esmWalker( 42 - root: Node, 43 - { onIdentifier, onImportMeta, onDynamicImport }: Visitors, 44 - ): void { 45 - const parentStack: Node[] = [] 46 - const varKindStack: VariableDeclaration['kind'][] = [] 47 - const scopeMap = new WeakMap<_Node, Set<string>>() 48 - const identifiers: [id: any, stack: Node[]][] = [] 49 - 50 - const setScope = (node: _Node, name: string) => { 51 - let scopeIds = scopeMap.get(node) 52 - if (scopeIds && scopeIds.has(name)) { 53 - return 54 - } 55 - 56 - if (!scopeIds) { 57 - scopeIds = new Set() 58 - scopeMap.set(node, scopeIds) 59 - } 60 - scopeIds.add(name) 61 - } 62 - 63 - function isInScope(name: string, parents: Node[]) { 64 - return parents.some(node => node && scopeMap.get(node)?.has(name)) 65 - } 66 - function handlePattern(p: Pattern, parentScope: _Node) { 67 - if (p.type === 'Identifier') { 68 - setScope(parentScope, p.name) 69 - } 70 - else if (p.type === 'RestElement') { 71 - handlePattern(p.argument, parentScope) 72 - } 73 - else if (p.type === 'ObjectPattern') { 74 - p.properties.forEach((property) => { 75 - if (property.type === 'RestElement') { 76 - setScope(parentScope, (property.argument as Identifier).name) 77 - } 78 - else { 79 - handlePattern(property.value, parentScope) 80 - } 81 - }) 82 - } 83 - else if (p.type === 'ArrayPattern') { 84 - p.elements.forEach((element) => { 85 - if (element) { 86 - handlePattern(element, parentScope) 87 - } 88 - }) 89 - } 90 - else if (p.type === 'AssignmentPattern') { 91 - handlePattern(p.left, parentScope) 92 - } 93 - else { 94 - setScope(parentScope, (p as any).name) 95 - } 96 - } 97 - 98 - (eswalk as any)(root, { 99 - enter(node: Node, parent: Node | null) { 100 - if (node.type === 'ImportDeclaration') { 101 - return this.skip() 102 - } 103 - 104 - // track parent stack, skip for "else-if"/"else" branches as acorn nests 105 - // the ast within "if" nodes instead of flattening them 106 - if ( 107 - parent 108 - && !(parent.type === 'IfStatement' && node === parent.alternate) 109 - ) { 110 - parentStack.unshift(parent) 111 - } 112 - 113 - // track variable declaration kind stack used by VariableDeclarator 114 - if (node.type === 'VariableDeclaration') { 115 - varKindStack.unshift(node.kind) 116 - } 117 - 118 - if (node.type === 'MetaProperty' && node.meta.name === 'import') { 119 - onImportMeta(node) 120 - } 121 - else if (node.type === 'ImportExpression') { 122 - onDynamicImport(node) 123 - } 124 - 125 - if (node.type === 'Identifier') { 126 - if ( 127 - !isInScope(node.name, parentStack) 128 - && isRefIdentifier(node, parent!, parentStack) 129 - ) { 130 - // record the identifier, for DFS -> BFS 131 - identifiers.push([node, parentStack.slice(0)]) 132 - } 133 - } 134 - else if (isFunctionNode(node)) { 135 - // If it is a function declaration, it could be shadowing an import 136 - // Add its name to the scope so it won't get replaced 137 - if (node.type === 'FunctionDeclaration') { 138 - const parentScope = findParentScope(parentStack) 139 - if (parentScope) { 140 - setScope(parentScope, node.id!.name) 141 - } 142 - } 143 - // walk function expressions and add its arguments to known identifiers 144 - // so that we don't prefix them 145 - node.params.forEach((p) => { 146 - if (p.type === 'ObjectPattern' || p.type === 'ArrayPattern') { 147 - handlePattern(p, node) 148 - return 149 - } 150 - (eswalk as any)(p.type === 'AssignmentPattern' ? p.left : p, { 151 - enter(child: Node, parent: Node) { 152 - // skip params default value of destructure 153 - if ( 154 - parent?.type === 'AssignmentPattern' 155 - && parent?.right === child 156 - ) { 157 - return this.skip() 158 - } 159 - 160 - if (child.type !== 'Identifier') { 161 - return 162 - } 163 - // do not record as scope variable if is a destructuring keyword 164 - if (isStaticPropertyKey(child, parent)) { 165 - return 166 - } 167 - // do not record if this is a default value 168 - // assignment of a destructuring variable 169 - if ( 170 - (parent?.type === 'TemplateLiteral' 171 - && parent?.expressions.includes(child)) 172 - || (parent?.type === 'CallExpression' && parent?.callee === child) 173 - ) { 174 - return 175 - } 176 - 177 - setScope(node, child.name) 178 - }, 179 - }) 180 - }) 181 - } 182 - else if (node.type === 'Property' && parent!.type === 'ObjectPattern') { 183 - // mark property in destructuring pattern 184 - setIsNodeInPattern(node) 185 - } 186 - else if (node.type === 'VariableDeclarator') { 187 - const parentFunction = findParentScope( 188 - parentStack, 189 - varKindStack[0] === 'var', 190 - ) 191 - if (parentFunction) { 192 - handlePattern(node.id, parentFunction) 193 - } 194 - } 195 - else if (node.type === 'CatchClause' && node.param) { 196 - handlePattern(node.param, node) 197 - } 198 - }, 199 - 200 - leave(node: Node, parent: Node | null) { 201 - // untrack parent stack from above 202 - if ( 203 - parent 204 - && !(parent.type === 'IfStatement' && node === parent.alternate) 205 - ) { 206 - parentStack.shift() 207 - } 208 - 209 - if (node.type === 'VariableDeclaration') { 210 - varKindStack.shift() 211 - } 212 - }, 213 - }) 214 - 215 - // emit the identifier events in BFS so the hoisted declarations 216 - // can be captured correctly 217 - identifiers.forEach(([node, stack]) => { 218 - if (!isInScope(node.name, stack)) { 219 - onIdentifier(node, stack[0], stack) 220 - } 221 - }) 222 - } 223 - 224 - function isRefIdentifier(id: Identifier, parent: _Node, parentStack: _Node[]) { 225 - // declaration id 226 - if ( 227 - parent.type === 'CatchClause' 228 - || ((parent.type === 'VariableDeclarator' 229 - || parent.type === 'ClassDeclaration') 230 - && parent.id === id) 231 - ) { 232 - return false 233 - } 234 - 235 - if (isFunctionNode(parent)) { 236 - // function declaration/expression id 237 - if ((parent as any).id === id) { 238 - return false 239 - } 240 - 241 - // params list 242 - if (parent.params.includes(id)) { 243 - return false 244 - } 245 - } 246 - 247 - // class method name 248 - if (parent.type === 'MethodDefinition' && !parent.computed) { 249 - return false 250 - } 251 - 252 - // property key 253 - if (isStaticPropertyKey(id, parent)) { 254 - return false 255 - } 256 - 257 - // object destructuring pattern 258 - if (isNodeInPattern(parent) && parent.value === id) { 259 - return false 260 - } 261 - 262 - // non-assignment array destructuring pattern 263 - if ( 264 - parent.type === 'ArrayPattern' 265 - && !isInDestructuringAssignment(parent, parentStack) 266 - ) { 267 - return false 268 - } 269 - 270 - // member expression property 271 - if ( 272 - parent.type === 'MemberExpression' 273 - && parent.property === id 274 - && !parent.computed 275 - ) { 276 - return false 277 - } 278 - 279 - if (parent.type === 'ExportSpecifier') { 280 - return false 281 - } 282 - 283 - // is a special keyword but parsed as identifier 284 - if (id.name === 'arguments') { 285 - return false 286 - } 287 - 288 - return true 289 - } 290 - 291 - export function isStaticProperty(node: _Node): node is Property { 292 - return node && node.type === 'Property' && !node.computed 293 - } 294 - 295 - export function isStaticPropertyKey(node: _Node, parent: _Node): boolean { 296 - return isStaticProperty(parent) && parent.key === node 297 - } 298 - 299 - const functionNodeTypeRE = /Function(?:Expression|Declaration)$|Method$/ 300 - export function isFunctionNode(node: _Node): node is FunctionNode { 301 - return functionNodeTypeRE.test(node.type) 302 - } 303 - 304 - const blockNodeTypeRE = /^BlockStatement$|^For(?:In|Of)?Statement$/ 305 - function isBlock(node: _Node) { 306 - return blockNodeTypeRE.test(node.type) 307 - } 308 - 309 - function findParentScope( 310 - parentStack: _Node[], 311 - isVar = false, 312 - ): _Node | undefined { 313 - return parentStack.find(isVar ? isFunctionNode : isBlock) 314 - } 315 - 316 - export function isInDestructuringAssignment( 317 - parent: _Node, 318 - parentStack: _Node[], 319 - ): boolean { 320 - if ( 321 - parent 322 - && (parent.type === 'Property' || parent.type === 'ArrayPattern') 323 - ) { 324 - return parentStack.some(i => i.type === 'AssignmentExpression') 325 - } 326 - 327 - return false 328 - }
+3 -2
packages/utils/src/ast/index.ts packages/mocker/src/node/esmWalker.ts
··· 9 9 Node as _Node, 10 10 } from 'estree' 11 11 import { walk as eswalk } from 'estree-walker' 12 + import type { Rollup } from 'vite' 12 13 13 14 export type * from 'estree' 14 15 ··· 59 60 * Except this is using acorn AST 60 61 */ 61 62 export function esmWalker( 62 - root: Node, 63 + root: Rollup.ProgramNode, 63 64 { onIdentifier, onImportMeta, onDynamicImport, onCallExpression }: Visitors, 64 65 ): void { 65 66 const parentStack: Node[] = [] ··· 115 116 } 116 117 } 117 118 118 - eswalk(root, { 119 + eswalk(root as Node, { 119 120 enter(node, parent) { 120 121 if (node.type === 'ImportDeclaration') { 121 122 return this.skip()
+29
packages/vitest/LICENSE.md
··· 1557 1557 1558 1558 --------------------------------------- 1559 1559 1560 + ## tinyexec 1561 + License: MIT 1562 + By: James Garbutt 1563 + Repository: git+https://github.com/tinylibs/tinyexec.git 1564 + 1565 + > MIT License 1566 + > 1567 + > Copyright (c) 2024 Tinylibs 1568 + > 1569 + > Permission is hereby granted, free of charge, to any person obtaining a copy 1570 + > of this software and associated documentation files (the "Software"), to deal 1571 + > in the Software without restriction, including without limitation the rights 1572 + > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 1573 + > copies of the Software, and to permit persons to whom the Software is 1574 + > furnished to do so, subject to the following conditions: 1575 + > 1576 + > The above copyright notice and this permission notice shall be included in all 1577 + > copies or substantial portions of the Software. 1578 + > 1579 + > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 1580 + > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 1581 + > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 1582 + > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 1583 + > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 1584 + > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 1585 + > SOFTWARE. 1586 + 1587 + --------------------------------------- 1588 + 1560 1589 ## to-regex-range 1561 1590 License: MIT 1562 1591 By: Jon Schlinkert, Rouven Weßling
+1
packages/vitest/mocker.d.ts
··· 1 + export * from './dist/mocker.js'
+5
packages/vitest/package.json
··· 94 94 "./snapshot": { 95 95 "types": "./dist/snapshot.d.ts", 96 96 "default": "./dist/snapshot.js" 97 + }, 98 + "./mocker": { 99 + "types": "./dist/mocker.d.ts", 100 + "default": "./dist/mocker.js" 97 101 } 98 102 }, 99 103 "main": "./dist/index.js", ··· 148 152 "dependencies": { 149 153 "@ampproject/remapping": "^2.3.0", 150 154 "@vitest/expect": "workspace:*", 155 + "@vitest/mocker": "workspace:*", 151 156 "@vitest/pretty-format": "workspace:^", 152 157 "@vitest/runner": "workspace:*", 153 158 "@vitest/snapshot": "workspace:*",
+4
packages/vitest/rollup.config.js
··· 24 24 'browser': 'src/public/browser.ts', 25 25 'runners': 'src/public/runners.ts', 26 26 'environments': 'src/public/environments.ts', 27 + 'mocker': 'src/public/mocker.ts', 27 28 'spy': 'src/integrations/spy.ts', 28 29 'coverage': 'src/public/coverage.ts', 29 30 'utils': 'src/public/utils.ts', ··· 56 57 utils: 'src/public/utils.ts', 57 58 execute: 'src/public/execute.ts', 58 59 reporters: 'src/public/reporters.ts', 60 + mocker: 'src/public/mocker.ts', 59 61 workers: 'src/public/workers.ts', 60 62 snapshot: 'src/public/snapshot.ts', 61 63 } ··· 76 78 'vite-node/server', 77 79 'vite-node/constants', 78 80 'vite-node/utils', 81 + '@vitest/mocker', 82 + '@vitest/mocker/node', 79 83 '@vitest/utils/diff', 80 84 '@vitest/utils/ast', 81 85 '@vitest/utils/error',
+11 -13
packages/vitest/src/integrations/vi.ts
··· 3 3 import { parseSingleStack } from '../utils/source-map' 4 4 import type { VitestMocker } from '../runtime/mocker' 5 5 import type { RuntimeOptions, SerializedConfig } from '../runtime/config' 6 - import type { MockFactoryWithHelper } from '../types/mocker' 6 + import type { MockFactoryWithHelper, MockOptions } from '../types/mocker' 7 7 import { getWorkerState } from '../runtime/utils' 8 8 import { resetModules, waitForImportsToResolve } from '../utils/modules' 9 9 import { isChildProcess } from '../utils/base' ··· 199 199 * @param factory Mocked module factory. The result of this function will be an exports object 200 200 */ 201 201 // eslint-disable-next-line ts/method-signature-style 202 - mock(path: string, factory?: MockFactoryWithHelper): void 202 + mock(path: string, factory?: MockFactoryWithHelper | MockOptions): void 203 203 // eslint-disable-next-line ts/method-signature-style 204 - mock<T>(module: Promise<T>, factory?: MockFactoryWithHelper<T>): void 204 + mock<T>(module: Promise<T>, factory?: MockFactoryWithHelper<T> | MockOptions): void 205 205 206 206 /** 207 207 * Removes module from mocked registry. All calls to import will return the original module even if it was mocked before. ··· 224 224 * @param factory Mocked module factory. The result of this function will be an exports object 225 225 */ 226 226 // eslint-disable-next-line ts/method-signature-style 227 - doMock(path: string, factory?: MockFactoryWithHelper): void 227 + doMock(path: string, factory?: MockFactoryWithHelper | MockOptions): void 228 228 // eslint-disable-next-line ts/method-signature-style 229 - doMock<T>(module: Promise<T>, factory?: MockFactoryWithHelper<T>): void 229 + doMock<T>(module: Promise<T>, factory?: MockFactoryWithHelper<T> | MockOptions): void 230 230 /** 231 231 * Removes module from mocked registry. All subsequent calls to import will return original module. 232 232 * ··· 555 555 return factory() 556 556 }, 557 557 558 - mock(path: string | Promise<unknown>, factory?: MockFactoryWithHelper) { 558 + mock(path: string | Promise<unknown>, factory?: MockOptions | MockFactoryWithHelper) { 559 559 if (typeof path !== 'string') { 560 560 throw new TypeError( 561 561 `vi.mock() expects a string path, but received a ${typeof path}`, ··· 565 565 _mocker().queueMock( 566 566 path, 567 567 importer, 568 - factory 568 + typeof factory === 'function' 569 569 ? () => 570 570 factory(() => 571 571 _mocker().importActual( ··· 574 574 _mocker().getMockContext().callstack, 575 575 ), 576 576 ) 577 - : undefined, 578 - true, 577 + : factory, 579 578 ) 580 579 }, 581 580 ··· 588 587 _mocker().queueUnmock(path, getImporter('unmock')) 589 588 }, 590 589 591 - doMock(path: string | Promise<unknown>, factory?: MockFactoryWithHelper) { 590 + doMock(path: string | Promise<unknown>, factory?: MockOptions | MockFactoryWithHelper) { 592 591 if (typeof path !== 'string') { 593 592 throw new TypeError( 594 593 `vi.doMock() expects a string path, but received a ${typeof path}`, ··· 598 597 _mocker().queueMock( 599 598 path, 600 599 importer, 601 - factory 600 + typeof factory === 'function' 602 601 ? () => 603 602 factory(() => 604 603 _mocker().importActual( ··· 607 606 _mocker().getMockContext().callstack, 608 607 ), 609 608 ) 610 - : undefined, 611 - false, 609 + : factory, 612 610 ) 613 611 }, 614 612
+38 -6
packages/vitest/src/node/automock.ts packages/mocker/src/node/automockPlugin.ts
··· 1 + import MagicString from 'magic-string' 2 + import type { Plugin } from 'vite' 3 + import { cleanUrl } from '../utils' 1 4 import type { 2 5 Declaration, 3 6 ExportDefaultDeclaration, ··· 8 11 Pattern, 9 12 Positioned, 10 13 Program, 11 - } from '@vitest/utils/ast' 12 - import MagicString from 'magic-string' 14 + } from './esmWalker' 15 + 16 + export interface AutomockPluginOptions { 17 + /** 18 + * @default "__vitest_mocker__" 19 + */ 20 + globalThisAccessor?: string 21 + } 22 + 23 + export function automockPlugin(options: AutomockPluginOptions = {}): Plugin { 24 + return { 25 + name: 'vitest:automock', 26 + enforce: 'post', 27 + transform(code, id) { 28 + if (id.includes('mock=automock') || id.includes('mock=autospy')) { 29 + const mockType = id.includes('mock=automock') ? 'automock' : 'autospy' 30 + const ms = automockModule(code, mockType, this.parse, options) 31 + return { 32 + code: ms.toString(), 33 + map: ms.generateMap({ hires: 'boundary', source: cleanUrl(id) }), 34 + } 35 + } 36 + }, 37 + } 38 + } 13 39 14 40 // TODO: better source map replacement 15 - export function automockModule(code: string, parse: (code: string) => Program) { 16 - const ast = parse(code) 41 + export function automockModule( 42 + code: string, 43 + mockType: 'automock' | 'autospy', 44 + parse: (code: string) => any, 45 + options: AutomockPluginOptions = {}, 46 + ): MagicString { 47 + const globalThisAccessor = options.globalThisAccessor || '"__vitest_mocker__"' 48 + const ast = parse(code) as Program 17 49 18 50 const m = new MagicString(code) 19 51 ··· 141 173 } 142 174 } 143 175 const moduleObject = ` 144 - const __vitest_es_current_module__ = { 176 + const __vitest_current_es_module__ = { 145 177 __esModule: true, 146 178 ${allSpecifiers.map(({ name }) => `["${name}"]: ${name},`).join('\n ')} 147 179 } 148 - const __vitest_mocked_module__ = __vitest_mocker__.mockObject(__vitest_es_current_module__) 180 + const __vitest_mocked_module__ = globalThis[${globalThisAccessor}].mockObject(__vitest_current_es_module__, "${mockType}") 149 181 ` 150 182 const assigning = allSpecifiers 151 183 .map(({ name }, index) => {
+121 -72
packages/vitest/src/node/hoistMocks.ts packages/mocker/src/node/hoistMocksPlugin.ts
··· 1 + import type { SourceMap } from 'magic-string' 1 2 import MagicString from 'magic-string' 2 3 import type { 3 4 AwaitExpression, ··· 9 10 ImportDeclaration, 10 11 ImportExpression, 11 12 VariableDeclaration, 12 - Node as _Node, 13 13 } from 'estree' 14 14 import { findNodeAround } from 'acorn-walk' 15 - import type { PluginContext, ProgramNode } from 'rollup' 16 - import { esmWalker } from '@vitest/utils/ast' 17 - import type { Colors } from 'tinyrainbow' 18 - import { highlightCode } from '../utils/colors' 19 - import { generateCodeFrame } from './error' 15 + import type { Plugin, Rollup } from 'vite' 16 + import { createFilter } from 'vite' 17 + import type { Node, Positioned } from './esmWalker' 18 + import { esmWalker } from './esmWalker' 19 + 20 + interface HoistMocksOptions { 21 + /** 22 + * List of modules that should always be imported before compiler hints. 23 + * @default ['vitest'] 24 + */ 25 + hoistedModules?: string[] 26 + /** 27 + * @default ["vi", "vitest"] 28 + */ 29 + utilsObjectNames?: string[] 30 + /** 31 + * @default ["mock", "unmock"] 32 + */ 33 + hoistableMockMethodNames?: string[] 34 + /** 35 + * @default ["mock", "unmock", "doMock", "doUnmock"] 36 + */ 37 + dynamicImportMockMethodNames?: string[] 38 + /** 39 + * @default ["hoisted"] 40 + */ 41 + hoistedMethodNames?: string[] 42 + regexpHoistable?: RegExp 43 + codeFrameGenerator?: CodeFrameGenerator 44 + } 20 45 21 - export type Positioned<T> = T & { 22 - start: number 23 - end: number 46 + export interface HoistMocksPluginOptions extends Omit<HoistMocksOptions, 'regexpHoistable'> { 47 + include?: string | RegExp | (string | RegExp)[] 48 + exclude?: string | RegExp | (string | RegExp)[] 49 + /** 50 + * overrides include/exclude options 51 + */ 52 + filter?: (id: string) => boolean 24 53 } 25 54 26 - export type Node = Positioned<_Node> 55 + export function hoistMocksPlugin(options: HoistMocksPluginOptions = {}): Plugin { 56 + const filter = options.filter || createFilter(options.include, options.exclude) 57 + 58 + const { 59 + hoistableMockMethodNames = ['mock', 'unmock'], 60 + dynamicImportMockMethodNames = ['mock', 'unmock', 'doMock', 'doUnmock'], 61 + hoistedMethodNames = ['hoisted'], 62 + utilsObjectNames = ['vi', 'vitest'], 63 + } = options 64 + 65 + const methods = new Set([ 66 + ...hoistableMockMethodNames, 67 + ...hoistedMethodNames, 68 + ...dynamicImportMockMethodNames, 69 + ]) 70 + 71 + const regexpHoistable = new RegExp( 72 + `\\b(?:${utilsObjectNames.join('|')})\\s*\.\\s*(?:${Array.from(methods).join('|')})\\(`, 73 + ) 74 + 75 + return { 76 + name: 'vitest:mocks', 77 + enforce: 'post', 78 + transform(code, id) { 79 + if (!filter(id)) { 80 + return 81 + } 82 + return hoistMocks(code, id, this.parse, { 83 + regexpHoistable, 84 + hoistableMockMethodNames, 85 + hoistedMethodNames, 86 + utilsObjectNames, 87 + dynamicImportMockMethodNames, 88 + ...options, 89 + }) 90 + }, 91 + } 92 + } 27 93 28 94 const API_NOT_FOUND_ERROR = `There are some problems in resolving the mocks API. 29 95 You may encounter this issue when importing the mocks API from another module other than 'vitest'. ··· 31 97 - import the mocks API directly from 'vitest' 32 98 - enable the 'globals' options` 33 99 34 - const API_NOT_FOUND_CHECK 35 - = '\nif (typeof globalThis.vi === "undefined" && typeof globalThis.vitest === "undefined") ' 36 - + `{ throw new Error(${JSON.stringify(API_NOT_FOUND_ERROR)}) }\n` 100 + function API_NOT_FOUND_CHECK(names: string[]) { 101 + return `\nif (${names.map(name => `typeof globalThis["${name}"] === "undefined"`).join(' && ')}) ` 102 + + `{ throw new Error(${JSON.stringify(API_NOT_FOUND_ERROR)}) }\n` 103 + } 37 104 38 105 function isIdentifier(node: any): node is Positioned<Identifier> { 39 106 return node.type === 'Identifier' 40 107 } 41 108 42 - function transformImportSpecifiers(node: ImportDeclaration) { 43 - const dynamicImports = node.specifiers 44 - .map((specifier) => { 45 - if (specifier.type === 'ImportDefaultSpecifier') { 46 - return `default: ${specifier.local.name}` 47 - } 48 - 49 - if (specifier.type === 'ImportSpecifier') { 50 - const local = specifier.local.name 51 - const imported = specifier.imported.name 52 - if (local === imported) { 53 - return local 54 - } 55 - return `${imported}: ${local}` 56 - } 57 - 58 - return null 59 - }) 60 - .filter(Boolean) 61 - .join(', ') 62 - 63 - if (!dynamicImports.length) { 64 - return '' 65 - } 66 - 67 - return `{ ${dynamicImports} }` 68 - } 69 - 70 - export function getBetterEnd(code: string, node: Node) { 109 + function getBetterEnd(code: string, node: Node) { 71 110 let end = node.end 72 111 if (code[node.end] === ';') { 73 112 end += 1 ··· 82 121 = /\b(?:vi|vitest)\s*\.\s*(?:mock|unmock|hoisted|doMock|doUnmock)\(/ 83 122 const hashbangRE = /^#!.*\n/ 84 123 124 + export interface HoistMocksResult { 125 + ast: Rollup.ProgramNode 126 + code: string 127 + map: SourceMap 128 + } 129 + 130 + interface CodeFrameGenerator { 131 + (node: Positioned<Node>, id: string, code: string): string 132 + } 133 + 134 + // this is a fork of Vite SSR trandform 85 135 export function hoistMocks( 86 136 code: string, 87 137 id: string, 88 - parse: PluginContext['parse'], 89 - colors?: Colors, 90 - ) { 91 - const needHoisting = regexpHoistable.test(code) 138 + parse: Rollup.PluginContext['parse'], 139 + options: HoistMocksOptions = {}, 140 + ): HoistMocksResult | undefined { 141 + const needHoisting = (options.regexpHoistable || regexpHoistable).test(code) 92 142 93 143 if (!needHoisting) { 94 144 return ··· 96 146 97 147 const s = new MagicString(code) 98 148 99 - let ast: ProgramNode 149 + let ast: Rollup.ProgramNode 100 150 try { 101 151 ast = parse(code) 102 152 } 103 153 catch (err) { 104 - console.error(`Cannot parse ${id}:\n${(err as any).message}`) 154 + console.error(`Cannot parse ${id}:\n${(err as any).message}.`) 105 155 return 106 156 } 107 157 158 + const { 159 + hoistableMockMethodNames = ['mock', 'unmock'], 160 + dynamicImportMockMethodNames = ['mock', 'unmock', 'doMock', 'doUnmock'], 161 + hoistedMethodNames = ['hoisted'], 162 + utilsObjectNames = ['vi', 'vitest'], 163 + hoistedModules = ['vitest'], 164 + } = options 165 + 108 166 const hoistIndex = code.match(hashbangRE)?.[0].length ?? 0 109 167 110 - let hoistedVitestImports = '' 168 + let hoistedModuleImported = false 111 169 112 170 let uid = 0 113 171 const idToImportMap = new Map<string, string>() ··· 133 191 function defineImport(node: Positioned<ImportDeclaration>) { 134 192 // always hoist vitest import to top of the file, so 135 193 // "vi" helpers can access it 136 - if (node.source.value === 'vitest') { 137 - const code = `const ${transformImportSpecifiers( 138 - node, 139 - )} = await import('vitest')\n` 140 - hoistedVitestImports += code 141 - s.remove(node.start, getBetterEnd(code, node)) 194 + if (hoistedModules.includes(node.source.value as string)) { 195 + hoistedModuleImported = true 142 196 return 143 197 } 144 198 ··· 187 241 function createSyntaxError(node: Positioned<Node>, message: string) { 188 242 const _error = new SyntaxError(message) 189 243 Error.captureStackTrace(_error, createSyntaxError) 190 - return { 244 + const serializedError: any = { 191 245 name: 'SyntaxError', 192 246 message: _error.message, 193 247 stack: _error.stack, 194 - frame: generateCodeFrame( 195 - highlightCode(id, code, colors), 196 - 4, 197 - node.start + 1, 198 - ), 199 248 } 249 + if (options.codeFrameGenerator) { 250 + serializedError.frame = options.codeFrameGenerator(node, id, code) 251 + } 252 + return serializedError 200 253 } 201 254 202 255 function assertNotDefaultExport( ··· 276 329 if ( 277 330 node.callee.type === 'MemberExpression' 278 331 && isIdentifier(node.callee.object) 279 - && (node.callee.object.name === 'vi' 280 - || node.callee.object.name === 'vitest') 332 + && utilsObjectNames.includes(node.callee.object.name) 281 333 && isIdentifier(node.callee.property) 282 334 ) { 283 335 const methodName = node.callee.property.name 284 336 285 - if (methodName === 'mock' || methodName === 'unmock') { 337 + if (hoistableMockMethodNames.includes(methodName)) { 286 338 const method = `${node.callee.object.name}.${methodName}` 287 339 assertNotDefaultExport( 288 340 node, ··· 297 349 } 298 350 hoistedNodes.push(node) 299 351 } 300 - 301 352 // vi.doMock(import('./path')) -> vi.doMock('./path') 302 353 // vi.doMock(await import('./path')) -> vi.doMock('./path') 303 - if (methodName === 'doMock' || methodName === 'doUnmock') { 354 + else if (dynamicImportMockMethodNames.includes(methodName)) { 304 355 const moduleInfo = node.arguments[0] as Positioned<Expression> 305 356 let source: Positioned<Expression> | null = null 306 357 if (moduleInfo.type === 'ImportExpression') { ··· 321 372 } 322 373 } 323 374 324 - if (methodName === 'hoisted') { 375 + if (hoistedMethodNames.includes(methodName)) { 325 376 assertNotDefaultExport( 326 377 node, 327 378 'Cannot export hoisted variable. You can control hoisting behavior by placing the import from this file first.', ··· 445 496 if ( 446 497 node.type === 'CallExpression' 447 498 && node.callee.type === 'MemberExpression' 448 - && ((node.callee.property as Identifier).name === 'mock' 449 - || (node.callee.property as Identifier).name === 'unmock') 499 + && dynamicImportMockMethodNames.includes((node.callee.property as Identifier).name) 450 500 ) { 451 501 const moduleInfo = node.arguments[0] as Positioned<Expression> 452 502 // vi.mock(import('./path')) -> vi.mock('./path') ··· 479 529 }) 480 530 .join('') 481 531 482 - if (hoistedCode || hoistedVitestImports) { 532 + if (hoistedCode || hoistedModuleImported) { 483 533 s.prepend( 484 - hoistedVitestImports 485 - + (!hoistedVitestImports && hoistedCode ? API_NOT_FOUND_CHECK : '') 534 + (!hoistedModuleImported && hoistedCode ? API_NOT_FOUND_CHECK(utilsObjectNames) : '') 486 535 + hoistedCode, 487 536 ) 488 537 }
+14 -22
packages/vitest/src/node/plugins/mocks.ts
··· 1 1 import type { Plugin } from 'vite' 2 - import { cleanUrl } from 'vite-node/utils' 3 - import { hoistMocks } from '../hoistMocks' 2 + import { automockPlugin, hoistMocksPlugin } from '@vitest/mocker/node' 4 3 import { distDir } from '../../paths' 5 - import { automockModule } from '../automock' 4 + import { generateCodeFrame } from '../error' 6 5 7 6 export function MocksPlugins(): Plugin[] { 8 7 return [ 9 - { 10 - name: 'vitest:mocks', 11 - enforce: 'post', 12 - transform(code, id) { 8 + hoistMocksPlugin({ 9 + filter(id) { 13 10 if (id.includes(distDir)) { 14 - return 11 + return false 15 12 } 16 - return hoistMocks(code, id, this.parse) 13 + return true 17 14 }, 18 - }, 19 - { 20 - name: 'vitest:automock', 21 - enforce: 'post', 22 - transform(code, id) { 23 - if (id.includes('mock=auto')) { 24 - const ms = automockModule(code, this.parse) 25 - return { 26 - code: ms.toString(), 27 - map: ms.generateMap({ hires: true, source: cleanUrl(id) }), 28 - } 29 - } 15 + codeFrameGenerator(node, id, code) { 16 + return generateCodeFrame( 17 + code, 18 + 4, 19 + node.start + 1, 20 + ) 30 21 }, 31 - }, 22 + }), 23 + automockPlugin(), 32 24 ] 33 25 }
+1
packages/vitest/src/public/mocker.ts
··· 1 + export * from '@vitest/mocker'
-6
packages/vitest/src/runtime/execute.ts
··· 13 13 import { normalize, relative } from 'pathe' 14 14 import { processError } from '@vitest/utils/error' 15 15 import { distDir } from '../paths' 16 - import type { MockMap } from '../types/mocker' 17 16 import type { WorkerGlobalState } from '../types/worker' 18 17 import { VitestMocker } from './mocker' 19 18 import type { ExternalModulesExecutor } from './external-executor' ··· 21 20 const { readFileSync } = fs 22 21 23 22 export interface ExecuteOptions extends ViteNodeRunnerOptions { 24 - mockMap: MockMap 25 23 moduleDirectories?: string[] 26 24 state: WorkerGlobalState 27 25 context?: vm.Context ··· 40 38 const externalizeMap = new Map<string, string>() 41 39 42 40 export interface ContextExecutorOptions { 43 - mockMap?: MockMap 44 41 moduleCache?: ModuleCacheMap 45 42 context?: vm.Context 46 43 externalModulesExecutor?: ExternalModulesExecutor ··· 138 135 }, 139 136 get moduleCache() { 140 137 return state().moduleCache 141 - }, 142 - get mockMap() { 143 - return state().mockMap 144 138 }, 145 139 get interopDefault() { 146 140 return state().config.deps.interopDefault
+109 -320
packages/vitest/src/runtime/mocker.ts
··· 1 - import fs from 'node:fs' 2 1 import vm from 'node:vm' 3 - import { basename, dirname, extname, isAbsolute, join, resolve } from 'pathe' 4 - import { getType, highlight } from '@vitest/utils' 5 - import { isNodeBuiltin } from 'vite-node/utils' 2 + import { isAbsolute, resolve } from 'pathe' 3 + import { highlight } from '@vitest/utils' 4 + import type { ManualMockedModule, MockedModuleType } from '@vitest/mocker' 5 + import { AutomockedModule, MockerRegistry, RedirectedModule, mockObject } from '@vitest/mocker' 6 + import { findMockRedirect } from '@vitest/mocker/redirect' 6 7 import { distDir } from '../paths' 7 - import { getAllMockableProperties } from '../utils/base' 8 - import type { MockFactory, PendingSuiteMock } from '../types/mocker' 8 + import type { MockFactory, MockOptions, PendingSuiteMock } from '../types/mocker' 9 9 import type { VitestExecutor } from './execute' 10 - 11 - const { existsSync, readdirSync } = fs 12 10 13 11 const spyModulePath = resolve(distDir, 'spy.js') 14 12 15 - class RefTracker { 16 - private idMap = new Map<any, number>() 17 - private mockedValueMap = new Map<number, any>() 18 - 19 - public getId(value: any) { 20 - return this.idMap.get(value) 21 - } 22 - 23 - public getMockedValue(id: number) { 24 - return this.mockedValueMap.get(id) 25 - } 26 - 27 - public track(originalValue: any, mockedValue: any): number { 28 - const newId = this.idMap.size 29 - this.idMap.set(originalValue, newId) 30 - this.mockedValueMap.set(newId, mockedValue) 31 - return newId 32 - } 33 - } 34 - 35 - type Key = string | symbol 36 - 37 13 interface MockContext { 38 14 /** 39 15 * When mocking with a factory, this refers to the module that imported the mock. ··· 41 17 callstack: null | string[] 42 18 } 43 19 44 - function isSpecialProp(prop: Key, parentType: string) { 45 - return ( 46 - parentType.includes('Function') 47 - && typeof prop === 'string' 48 - && ['arguments', 'callee', 'caller', 'length', 'name'].includes(prop) 49 - ) 50 - } 51 - 52 20 export class VitestMocker { 53 21 static pendingIds: PendingSuiteMock[] = [] 54 22 private spyModule?: typeof import('@vitest/spy') 55 - private resolveCache = new Map<string, Record<string, string>>() 56 23 private primitives: { 57 24 Object: typeof Object 58 25 Function: typeof Function ··· 64 31 } 65 32 66 33 private filterPublicKeys: (symbol | string)[] 34 + 35 + private registries = new Map<string, MockerRegistry>() 67 36 68 37 private mockContext: MockContext = { 69 38 callstack: null, ··· 113 82 return this.executor.options.root 114 83 } 115 84 116 - private get mockMap() { 117 - return this.executor.options.mockMap 118 - } 119 - 120 85 private get moduleCache() { 121 86 return this.executor.moduleCache 122 87 } ··· 129 94 this.spyModule = await this.executor.executeId(spyModulePath) 130 95 } 131 96 97 + private getMockerRegistry() { 98 + const suite = this.getSuiteFilepath() 99 + if (!this.registries.has(suite)) { 100 + this.registries.set(suite, new MockerRegistry()) 101 + } 102 + return this.registries.get(suite)! 103 + } 104 + 105 + public reset() { 106 + this.registries.clear() 107 + } 108 + 132 109 private deleteCachedItem(id: string) { 133 110 const mockId = this.getMockPath(id) 134 111 if (this.moduleCache.has(mockId)) { ··· 149 126 const error = new Error(message) 150 127 Object.assign(error, { codeFrame }) 151 128 return error 152 - } 153 - 154 - public getMocks() { 155 - const suite = this.getSuiteFilepath() 156 - const suiteMocks = this.mockMap.get(suite) 157 - const globalMocks = this.mockMap.get('global') 158 - 159 - return { 160 - ...globalMocks, 161 - ...suiteMocks, 162 - } 163 129 } 164 130 165 131 private async resolvePath(rawId: string, importer: string) { ··· 203 169 mock.id, 204 170 mock.importer, 205 171 ) 206 - if (mock.type === 'unmock') { 172 + if (mock.action === 'unmock') { 207 173 this.unmockPath(fsPath) 208 174 } 209 - if (mock.type === 'mock') { 210 - this.mockPath(mock.id, fsPath, external, mock.factory) 175 + if (mock.action === 'mock') { 176 + this.mockPath( 177 + mock.id, 178 + fsPath, 179 + external, 180 + mock.type, 181 + mock.factory, 182 + ) 211 183 } 212 184 }), 213 185 ) ··· 215 187 VitestMocker.pendingIds = [] 216 188 } 217 189 218 - private async callFunctionMock(dep: string, mock: MockFactory) { 190 + private async callFunctionMock(dep: string, mock: ManualMockedModule) { 219 191 const cached = this.moduleCache.get(dep)?.exports 220 192 if (cached) { 221 193 return cached 222 194 } 223 - let exports: any 224 - try { 225 - exports = await mock() 226 - } 227 - catch (err) { 228 - const vitestError = this.createError( 229 - '[vitest] There was an error when mocking a module. ' 230 - + 'If you are using "vi.mock" factory, make sure there are no top level variables inside, since this call is hoisted to top of the file. ' 231 - + 'Read more: https://vitest.dev/api/vi.html#vi-mock', 232 - ) 233 - vitestError.cause = err 234 - throw vitestError 235 - } 236 - 237 - const filepath = dep.slice(5) 238 - const mockpath 239 - = this.resolveCache.get(this.getSuiteFilepath())?.[filepath] || filepath 240 - 241 - if (exports === null || typeof exports !== 'object') { 242 - throw this.createError( 243 - `[vitest] vi.mock("${mockpath}", factory?: () => unknown) is not returning an object. Did you mean to return an object with a "default" key?`, 244 - ) 245 - } 195 + const exports = await mock.resolve() 246 196 247 197 const moduleExports = new Proxy(exports, { 248 198 get: (target, prop) => { ··· 259 209 return undefined 260 210 } 261 211 throw this.createError( 262 - `[vitest] No "${String( 263 - prop, 264 - )}" export is defined on the "${mockpath}" mock. ` 212 + `[vitest] No "${String(prop)}" export is defined on the "${mock.raw}" mock. ` 265 213 + 'Did you forget to return it from "vi.mock"?' 266 214 + '\nIf you need to partially mock a module, you can use "importOriginal" helper inside:\n', 267 - highlight(`vi.mock("${mockpath}", async (importOriginal) => { 215 + highlight(`vi.mock(import("${mock.raw}"), async (importOriginal) => { 268 216 const actual = await importOriginal() 269 217 return { 270 218 ...actual, ··· 283 231 return moduleExports 284 232 } 285 233 234 + // public method to avoid circular dependency 286 235 public getMockContext() { 287 236 return this.mockContext 288 237 } 289 238 239 + // path used to store mocked dependencies 290 240 public getMockPath(dep: string) { 291 241 return `mock:${dep}` 292 242 } 293 243 294 244 public getDependencyMock(id: string) { 295 - return this.getMocks()[id] 245 + const registry = this.getMockerRegistry() 246 + return registry.get(id) 296 247 } 297 248 298 249 public normalizePath(path: string) { ··· 300 251 } 301 252 302 253 public resolveMockPath(mockPath: string, external: string | null) { 303 - const path = external || mockPath 304 - 305 - // it's a node_module alias 306 - // all mocks should be inside <root>/__mocks__ 307 - if (external || isNodeBuiltin(mockPath) || !existsSync(mockPath)) { 308 - const mockDirname = dirname(path) // for nested mocks: @vueuse/integration/useJwt 309 - const mockFolder = join(this.root, '__mocks__', mockDirname) 310 - 311 - if (!existsSync(mockFolder)) { 312 - return null 313 - } 314 - 315 - const baseOriginal = basename(path) 316 - 317 - function findFile(mockFolder: string, baseOriginal: string): string | null { 318 - const files = readdirSync(mockFolder) 319 - for (const file of files) { 320 - const baseFile = basename(file, extname(file)) 321 - if (baseFile === baseOriginal) { 322 - const path = resolve(mockFolder, file) 323 - // if the same name, return the file 324 - if (fs.statSync(path).isFile()) { 325 - return path 326 - } 327 - else { 328 - // find folder/index.{js,ts} 329 - const indexFile = findFile(path, 'index') 330 - if (indexFile) { 331 - return indexFile 332 - } 333 - } 334 - } 335 - } 336 - return null 337 - } 338 - 339 - return findFile(mockFolder, baseOriginal) 340 - } 341 - 342 - const dir = dirname(path) 343 - const baseId = basename(path) 344 - const fullPath = resolve(dir, '__mocks__', baseId) 345 - return existsSync(fullPath) ? fullPath : null 254 + return findMockRedirect(this.root, mockPath, external) 346 255 } 347 256 348 257 public mockObject( 349 - object: Record<Key, any>, 350 - mockExports: Record<Key, any> = {}, 258 + object: Record<string | symbol, any>, 259 + mockExports: Record<string | symbol, any> = {}, 260 + behavior: MockedModuleType = 'automock', 351 261 ) { 352 - const finalizers = new Array<() => void>() 353 - const refs = new RefTracker() 354 - 355 - const define = (container: Record<Key, any>, key: Key, value: any) => { 356 - try { 357 - container[key] = value 358 - return true 359 - } 360 - catch { 361 - return false 362 - } 262 + const spyOn = this.spyModule?.spyOn 263 + if (!spyOn) { 264 + throw this.createError( 265 + '[vitest] `spyModule` is not defined. This is a Vitest error. Please open a new issue with reproduction.', 266 + ) 363 267 } 364 - 365 - const mockPropertiesOf = ( 366 - container: Record<Key, any>, 367 - newContainer: Record<Key, any>, 368 - ) => { 369 - const containerType = getType(container) 370 - const isModule = containerType === 'Module' || !!container.__esModule 371 - for (const { key: property, descriptor } of getAllMockableProperties( 372 - container, 373 - isModule, 374 - this.primitives, 375 - )) { 376 - // Modules define their exports as getters. We want to process those. 377 - if (!isModule && descriptor.get) { 378 - try { 379 - Object.defineProperty(newContainer, property, descriptor) 380 - } 381 - catch { 382 - // Ignore errors, just move on to the next prop. 383 - } 384 - continue 385 - } 386 - 387 - // Skip special read-only props, we don't want to mess with those. 388 - if (isSpecialProp(property, containerType)) { 389 - continue 390 - } 391 - 392 - const value = container[property] 393 - 394 - // Special handling of references we've seen before to prevent infinite 395 - // recursion in circular objects. 396 - const refId = refs.getId(value) 397 - if (refId !== undefined) { 398 - finalizers.push(() => 399 - define(newContainer, property, refs.getMockedValue(refId)), 400 - ) 401 - continue 402 - } 403 - 404 - const type = getType(value) 405 - 406 - if (Array.isArray(value)) { 407 - define(newContainer, property, []) 408 - continue 409 - } 410 - 411 - const isFunction 412 - = type.includes('Function') && typeof value === 'function' 413 - if ( 414 - (!isFunction || value.__isMockFunction) 415 - && type !== 'Object' 416 - && type !== 'Module' 417 - ) { 418 - define(newContainer, property, value) 419 - continue 420 - } 421 - 422 - // Sometimes this assignment fails for some unknown reason. If it does, 423 - // just move along. 424 - if (!define(newContainer, property, isFunction ? value : {})) { 425 - continue 426 - } 427 - 428 - if (isFunction) { 429 - if (!this.spyModule) { 430 - throw this.createError( 431 - '[vitest] `spyModule` is not defined. This is Vitest error. Please open a new issue with reproduction.', 432 - ) 433 - } 434 - const spyModule = this.spyModule 435 - const primitives = this.primitives 436 - function mockFunction(this: any) { 437 - // detect constructor call and mock each instance's methods 438 - // so that mock states between prototype/instances don't affect each other 439 - // (jest reference https://github.com/jestjs/jest/blob/2c3d2409879952157433de215ae0eee5188a4384/packages/jest-mock/src/index.ts#L678-L691) 440 - if (this instanceof newContainer[property]) { 441 - for (const { key, descriptor } of getAllMockableProperties( 442 - this, 443 - false, 444 - primitives, 445 - )) { 446 - // skip getter since it's not mocked on prototype as well 447 - if (descriptor.get) { 448 - continue 449 - } 450 - 451 - const value = this[key] 452 - const type = getType(value) 453 - const isFunction 454 - = type.includes('Function') && typeof value === 'function' 455 - if (isFunction) { 456 - // mock and delegate calls to original prototype method, which should be also mocked already 457 - const original = this[key] 458 - const mock = spyModule 459 - .spyOn(this, key as string) 460 - .mockImplementation(original) 461 - mock.mockRestore = () => { 462 - mock.mockReset() 463 - mock.mockImplementation(original) 464 - return mock 465 - } 466 - } 467 - } 468 - } 469 - } 470 - const mock = spyModule 471 - .spyOn(newContainer, property) 472 - .mockImplementation(mockFunction) 473 - mock.mockRestore = () => { 474 - mock.mockReset() 475 - mock.mockImplementation(mockFunction) 476 - return mock 477 - } 478 - // tinyspy retains length, but jest doesn't. 479 - Object.defineProperty(newContainer[property], 'length', { value: 0 }) 480 - } 481 - 482 - refs.track(value, newContainer[property]) 483 - mockPropertiesOf(value, newContainer[property]) 484 - } 485 - } 486 - 487 - const mockedObject: Record<Key, any> = mockExports 488 - mockPropertiesOf(object, mockedObject) 489 - 490 - // Plug together refs 491 - for (const finalizer of finalizers) { 492 - finalizer() 493 - } 494 - 495 - return mockedObject 268 + return mockObject({ 269 + globalConstructors: this.primitives, 270 + spyOn, 271 + type: behavior, 272 + }, object, mockExports) 496 273 } 497 274 498 275 public unmockPath(path: string) { 499 - const suitefile = this.getSuiteFilepath() 500 - 276 + const registry = this.getMockerRegistry() 501 277 const id = this.normalizePath(path) 502 278 503 - const mock = this.mockMap.get(suitefile) 504 - if (mock && id in mock) { 505 - delete mock[id] 506 - } 507 - 279 + registry.delete(id) 508 280 this.deleteCachedItem(id) 509 281 } 510 282 ··· 512 284 originalId: string, 513 285 path: string, 514 286 external: string | null, 287 + mockType: MockedModuleType | undefined, 515 288 factory: MockFactory | undefined, 516 289 ) { 290 + const registry = this.getMockerRegistry() 517 291 const id = this.normalizePath(path) 518 292 519 - // const { config } = this.executor.state 520 - // const isIsolatedThreads = config.pool === 'threads' && (config.poolOptions?.threads?.isolate ?? true) 521 - // const isIsolatedForks = config.pool === 'forks' && (config.poolOptions?.forks?.isolate ?? true) 522 - 523 - // TODO: find a good way to throw this error even in non-isolated mode 524 - // if (throwIfExists && (isIsolatedThreads || isIsolatedForks || config.pool === 'vmThreads')) { 525 - // const cached = this.moduleCache.has(id) && this.moduleCache.getByModuleId(id) 526 - // if (cached && cached.importers.size) 527 - // throw new Error(`[vitest] Cannot mock "${originalId}" because it is already loaded by "${[...cached.importers.values()].map(i => relative(this.root, i)).join('", "')}".\n\nPlease, remove the import if you want static imports to be mocked, or clear module cache by calling "vi.resetModules()" before mocking if you are going to import the file again. See: https://vitest.dev/guide/common-errors.html#cannot-mock-mocked-file-js-because-it-is-already-loaded`) 528 - // } 529 - 530 - const suitefile = this.getSuiteFilepath() 531 - const mocks = this.mockMap.get(suitefile) || {} 532 - const resolves = this.resolveCache.get(suitefile) || {} 533 - 534 - mocks[id] = factory || this.resolveMockPath(id, external) 535 - resolves[id] = originalId 293 + if (mockType === 'manual') { 294 + registry.register('manual', originalId, id, factory!) 295 + } 296 + else if (mockType === 'autospy') { 297 + registry.register('autospy', originalId, id) 298 + } 299 + else { 300 + const redirect = this.resolveMockPath(id, external) 301 + if (redirect) { 302 + registry.register('redirect', originalId, id, redirect) 303 + } 304 + else { 305 + registry.register('automock', originalId, id) 306 + } 307 + } 536 308 537 - this.mockMap.set(suitefile, mocks) 538 - this.resolveCache.set(suitefile, resolves) 309 + // every time the mock is registered, we remove the previous one from the cache 539 310 this.deleteCachedItem(id) 540 311 } 541 312 ··· 559 330 const normalizedId = this.normalizePath(fsPath) 560 331 let mock = this.getDependencyMock(normalizedId) 561 332 562 - if (mock === undefined) { 563 - mock = this.resolveMockPath(normalizedId, external) 333 + if (!mock) { 334 + const redirect = this.resolveMockPath(normalizedId, external) 335 + if (redirect) { 336 + mock = new RedirectedModule(rawId, normalizedId, redirect) 337 + } 338 + else { 339 + mock = new AutomockedModule(rawId, normalizedId) 340 + } 564 341 } 565 342 566 - if (mock === null) { 343 + if (mock.type === 'automock' || mock.type === 'autospy') { 567 344 const mod = await this.executor.cachedRequest(id, fsPath, [importee]) 568 - return this.mockObject(mod) 345 + return this.mockObject(mod, {}, mock.type) 569 346 } 570 347 571 - if (typeof mock === 'function') { 348 + if (mock.type === 'manual') { 572 349 return this.callFunctionMock(fsPath, mock) 573 350 } 574 - return this.executor.dependencyRequest(mock, mock, [importee]) 351 + return this.executor.dependencyRequest(mock.redirect, mock.redirect, [importee]) 575 352 } 576 353 577 354 public async requestWithMock(url: string, callstack: string[]) { 578 355 const id = this.normalizePath(url) 579 356 const mock = this.getDependencyMock(id) 580 357 358 + if (!mock) { 359 + return 360 + } 361 + 581 362 const mockPath = this.getMockPath(id) 582 363 583 - if (mock === null) { 364 + if (mock.type === 'automock' || mock.type === 'autospy') { 584 365 const cache = this.moduleCache.get(mockPath) 585 366 if (cache.exports) { 586 367 return cache.exports 587 368 } 588 - 589 369 const exports = {} 590 370 // Assign the empty exports object early to allow for cycles to work. The object will be filled by mockObject() 591 371 this.moduleCache.set(mockPath, { exports }) 592 372 const mod = await this.executor.directRequest(url, url, callstack) 593 - this.mockObject(mod, exports) 373 + this.mockObject(mod, exports, mock.type) 594 374 return exports 595 375 } 596 376 if ( 597 - typeof mock === 'function' 377 + mock.type === 'manual' 598 378 && !callstack.includes(mockPath) 599 379 && !callstack.includes(url) 600 380 ) { ··· 612 392 callstack.splice(indexMock, 1) 613 393 } 614 394 } 615 - if (typeof mock === 'string' && !callstack.includes(mock)) { 616 - return mock 395 + else if (mock.type === 'redirect' && !callstack.includes(mock.redirect)) { 396 + return mock.redirect 617 397 } 618 398 } 619 399 620 400 public queueMock( 621 401 id: string, 622 402 importer: string, 623 - factory?: MockFactory, 624 - throwIfCached = false, 403 + factoryOrOptions?: MockFactory | MockOptions, 625 404 ) { 405 + const mockType = getMockType(factoryOrOptions) 626 406 VitestMocker.pendingIds.push({ 627 - type: 'mock', 407 + action: 'mock', 628 408 id, 629 409 importer, 630 - factory, 631 - throwIfCached, 410 + factory: typeof factoryOrOptions === 'function' ? factoryOrOptions : undefined, 411 + type: mockType, 632 412 }) 633 413 } 634 414 635 - public queueUnmock(id: string, importer: string, throwIfCached = false) { 415 + public queueUnmock(id: string, importer: string) { 636 416 VitestMocker.pendingIds.push({ 637 - type: 'unmock', 417 + action: 'unmock', 638 418 id, 639 419 importer, 640 - throwIfCached, 641 420 }) 642 421 } 643 422 } 423 + 424 + function getMockType(factoryOrOptions?: MockFactory | MockOptions): MockedModuleType { 425 + if (!factoryOrOptions) { 426 + return 'automock' 427 + } 428 + if (typeof factoryOrOptions === 'function') { 429 + return 'manual' 430 + } 431 + return factoryOrOptions.spy ? 'autospy' : 'automock' 432 + }
+1 -1
packages/vitest/src/runtime/runBaseTests.ts
··· 57 57 && (config.poolOptions?.forks?.isolate ?? true) 58 58 59 59 if (isIsolatedThreads || isIsolatedForks) { 60 - workerState.mockMap.clear() 60 + executor.mocker.reset() 61 61 resetModules(workerState.moduleCache, true) 62 62 } 63 63
+2 -3
packages/vitest/src/runtime/worker.ts
··· 3 3 import { ModuleCacheMap } from 'vite-node/client' 4 4 import { loadEnvironment } from '../integrations/env/loader' 5 5 import { isChildProcess, setProcessTitle } from '../utils/base' 6 - import type { ContextRPC } from '../types/worker' 6 + import type { ContextRPC, WorkerGlobalState } from '../types/worker' 7 7 import { setupInspect } from './inspector' 8 8 import { createRuntimeRpc, rpcDone } from './rpc' 9 9 import type { VitestWorker } from './workers/types' ··· 63 63 ctx, 64 64 // here we create a new one, workers can reassign this if they need to keep it non-isolated 65 65 moduleCache: new ModuleCacheMap(), 66 - mockMap: new Map(), 67 66 config: ctx.config, 68 67 onCancel, 69 68 environment, ··· 73 72 }, 74 73 rpc, 75 74 providedContext: ctx.providedContext, 76 - } 75 + } satisfies WorkerGlobalState 77 76 78 77 const methodName = mehtod === 'collect' ? 'collectTests' : 'runTests' 79 78
-3
packages/vitest/src/runtime/workers/base.ts
··· 3 3 import { provideWorkerState } from '../utils' 4 4 import type { ContextExecutorOptions, VitestExecutor } from '../execute' 5 5 import { getDefaultRequestStubs, startVitestExecutor } from '../execute' 6 - import type { MockMap } from '../../types/mocker' 7 6 8 7 let _viteNode: VitestExecutor 9 8 10 9 const moduleCache = new ModuleCacheMap() 11 - const mockMap: MockMap = new Map() 12 10 13 11 async function startViteNode(options: ContextExecutorOptions) { 14 12 if (_viteNode) { ··· 23 21 const { ctx } = state 24 22 // state has new context, but we want to reuse existing ones 25 23 state.moduleCache = moduleCache 26 - state.mockMap = mockMap 27 24 28 25 provideWorkerState(globalThis, state) 29 26
-1
packages/vitest/src/runtime/workers/vm.ts
··· 77 77 const executor = await startVitestExecutor({ 78 78 context, 79 79 moduleCache: state.moduleCache, 80 - mockMap: state.mockMap, 81 80 state, 82 81 externalModulesExecutor, 83 82 requestStubs: stubs,
+7 -4
packages/vitest/src/types/mocker.ts
··· 1 + import type { MockedModuleType } from '@vitest/mocker' 2 + 1 3 type Promisable<T> = T | Promise<T> 2 4 3 5 export type MockFactoryWithHelper<M = unknown> = ( 4 6 importOriginal: <T extends M = M>() => Promise<T> 5 7 ) => Promisable<Partial<M>> 6 8 export type MockFactory = () => any 7 - 8 - export type MockMap = Map<string, Record<string, string | null | MockFactory>> 9 + export interface MockOptions { 10 + spy?: boolean 11 + } 9 12 10 13 export interface PendingSuiteMock { 11 14 id: string 12 15 importer: string 13 - type: 'mock' | 'unmock' 14 - throwIfCached: boolean 16 + action: 'mock' | 'unmock' 17 + type?: MockedModuleType 15 18 factory?: MockFactory 16 19 }
-2
packages/vitest/src/types/worker.ts
··· 3 3 import type { CancelReason, Task } from '@vitest/runner' 4 4 import type { SerializedConfig } from '../runtime/config' 5 5 import type { RunnerRPC, RuntimeRPC } from './rpc' 6 - import type { MockMap } from './mocker' 7 6 import type { TransformMode } from './general' 8 7 import type { Environment } from './environment' 9 8 ··· 43 42 environmentTeardownRun?: boolean 44 43 onCancel: Promise<CancelReason> 45 44 moduleCache: ModuleCacheMap 46 - mockMap: MockMap 47 45 providedContext: Record<string, any> 48 46 durations: { 49 47 environment: number
+1 -2
packages/web-worker/src/utils.ts
··· 81 81 82 82 export function getRunnerOptions(): any { 83 83 const state = getWorkerState() 84 - const { config, rpc, mockMap, moduleCache } = state 84 + const { config, rpc, moduleCache } = state 85 85 86 86 return { 87 87 async fetchModule(id: string) { ··· 96 96 return rpc.resolveId(id, importer, 'web') 97 97 }, 98 98 moduleCache, 99 - mockMap, 100 99 interopDefault: config.deps.interopDefault ?? true, 101 100 moduleDirectories: config.deps.moduleDirectories, 102 101 root: config.root,
+81 -43
pnpm-lock.yaml
··· 231 231 devDependencies: 232 232 '@vitest/browser': 233 233 specifier: latest 234 - version: 2.0.5(playwright@1.46.0)(typescript@5.5.4)(vitest@packages+vitest)(webdriverio@8.32.2(typescript@5.5.4)) 234 + version: 2.0.5(playwright@1.46.1)(typescript@5.5.4)(vitest@packages+vitest)(webdriverio@8.32.2(typescript@5.5.4)) 235 235 '@vitest/ui': 236 236 specifier: latest 237 237 version: 2.0.5(vitest@packages+vitest) ··· 458 458 '@types/ws': 459 459 specifier: ^8.5.12 460 460 version: 8.5.12 461 + '@vitest/mocker': 462 + specifier: workspace:* 463 + version: link:../mocker 461 464 '@vitest/runner': 462 465 specifier: workspace:* 463 466 version: link:../runner ··· 654 657 specifier: ^3.5.0 655 658 version: 3.5.0 656 659 660 + packages/mocker: 661 + dependencies: 662 + '@vitest/spy': 663 + specifier: workspace:^2.1.0-beta.1 664 + version: link:../spy 665 + estree-walker: 666 + specifier: ^3.0.3 667 + version: 3.0.3 668 + magic-string: 669 + specifier: ^0.30.11 670 + version: 0.30.11 671 + devDependencies: 672 + '@types/estree': 673 + specifier: ^1.0.5 674 + version: 1.0.5 675 + '@vitest/utils': 676 + specifier: workspace:* 677 + version: link:../utils 678 + acorn-walk: 679 + specifier: ^8.3.3 680 + version: 8.3.3 681 + msw: 682 + specifier: ^2.3.5 683 + version: 2.3.5(typescript@5.5.4) 684 + pathe: 685 + specifier: ^1.1.2 686 + version: 1.1.2 687 + vite: 688 + specifier: ^5.4.0 689 + version: 5.4.0(@types/node@20.14.15)(terser@5.22.0) 690 + 657 691 packages/pretty-format: 658 692 dependencies: 659 693 tinyrainbow: ··· 824 858 '@vitest/pretty-format': 825 859 specifier: workspace:* 826 860 version: link:../pretty-format 827 - estree-walker: 828 - specifier: ^3.0.3 829 - version: 3.0.3 830 861 loupe: 831 862 specifier: ^3.1.1 832 863 version: 3.1.1 ··· 883 914 '@vitest/expect': 884 915 specifier: workspace:* 885 916 version: link:../expect 917 + '@vitest/mocker': 918 + specifier: workspace:* 919 + version: link:../mocker 886 920 '@vitest/pretty-format': 887 921 specifier: workspace:^ 888 922 version: link:../pretty-format ··· 1180 1214 '@vitest/expect': 1181 1215 specifier: workspace:* 1182 1216 version: link:../../packages/expect 1217 + '@vitest/mocker': 1218 + specifier: workspace:* 1219 + version: link:../../packages/mocker 1183 1220 '@vitest/runner': 1184 1221 specifier: workspace:* 1185 1222 version: link:../../packages/runner ··· 1312 1349 '@vitest/test-dep-url': 1313 1350 specifier: file:./dep-url 1314 1351 version: '@vitest/test-deps-url@file:test/optimize-deps/dep-url' 1352 + vitest: 1353 + specifier: workspace:* 1354 + version: link:../../packages/vitest 1355 + 1356 + test/public-mocker: 1357 + devDependencies: 1358 + '@vitest/browser': 1359 + specifier: workspace:* 1360 + version: link:../../packages/browser 1361 + '@vitest/mocker': 1362 + specifier: workspace:* 1363 + version: link:../../packages/mocker 1364 + playwright: 1365 + specifier: ^1.46.1 1366 + version: 1.46.1 1315 1367 vitest: 1316 1368 specifier: workspace:* 1317 1369 version: link:../../packages/vitest ··· 7224 7276 ms@2.1.3: 7225 7277 resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} 7226 7278 7227 - msw@2.3.4: 7228 - resolution: {integrity: sha512-sHMlwrajgmZSA2l1o7qRSe+azm/I+x9lvVVcOxAzi4vCtH8uVPJk1K5BQYDkzGl+tt0RvM9huEXXdeGrgcc79g==} 7229 - engines: {node: '>=18'} 7230 - hasBin: true 7231 - peerDependencies: 7232 - typescript: '>= 4.7.x' 7233 - peerDependenciesMeta: 7234 - typescript: 7235 - optional: true 7236 - 7237 7279 msw@2.3.5: 7238 7280 resolution: {integrity: sha512-+GUI4gX5YC5Bv33epBrD+BGdmDvBg2XGruiWnI3GbIbRmMMBeZ5gs3mJ51OWSGHgJKztZ8AtZeYMMNMVrje2/Q==} 7239 7281 engines: {node: '>=18'} ··· 7612 7654 engines: {node: '>=18'} 7613 7655 hasBin: true 7614 7656 7657 + playwright-core@1.46.1: 7658 + resolution: {integrity: sha512-h9LqIQaAv+CYvWzsZ+h3RsrqCStkBHlgo6/TJlFst3cOTlLghBQlJwPOZKQJTKNaD3QIB7aAVQ+gfWbN3NXB7A==} 7659 + engines: {node: '>=18'} 7660 + hasBin: true 7661 + 7615 7662 playwright@1.41.0: 7616 7663 resolution: {integrity: sha512-XOsfl5ZtAik/T9oek4V0jAypNlaCNzuKOwVhqhgYT3os6kH34PzbRb74F0VWcLYa5WFdnmxl7qyAHBXvPv7lqQ==} 7617 7664 engines: {node: '>=16'} ··· 7619 7666 7620 7667 playwright@1.46.0: 7621 7668 resolution: {integrity: sha512-XYJ5WvfefWONh1uPAUAi0H2xXV5S3vrtcnXe6uAOgdGi3aSpqOSXX08IAjXW34xitfuOJsvXU5anXZxPSEQiJw==} 7669 + engines: {node: '>=18'} 7670 + hasBin: true 7671 + 7672 + playwright@1.46.1: 7673 + resolution: {integrity: sha512-oPcr1yqoXLCkgKtD5eNUPLiN40rYEM39odNpIb6VE6S7/15gJmA1NzVv6zJYusV0e7tzvkU/utBFNa/Kpxmwng==} 7622 7674 engines: {node: '>=18'} 7623 7675 hasBin: true 7624 7676 ··· 11324 11376 '@jridgewell/gen-mapping@0.3.5': 11325 11377 dependencies: 11326 11378 '@jridgewell/set-array': 1.2.1 11327 - '@jridgewell/sourcemap-codec': 1.4.15 11379 + '@jridgewell/sourcemap-codec': 1.5.0 11328 11380 '@jridgewell/trace-mapping': 0.3.25 11329 11381 11330 11382 '@jridgewell/resolve-uri@3.1.0': {} ··· 11352 11404 '@jridgewell/trace-mapping@0.3.25': 11353 11405 dependencies: 11354 11406 '@jridgewell/resolve-uri': 3.1.1 11355 - '@jridgewell/sourcemap-codec': 1.4.15 11407 + '@jridgewell/sourcemap-codec': 1.5.0 11356 11408 11357 11409 '@jsdevtools/ez-spawn@3.0.4': 11358 11410 dependencies: ··· 11765 11817 devalue: 4.3.2 11766 11818 esm-env: 1.0.0 11767 11819 kleur: 4.1.5 11768 - magic-string: 0.30.10 11820 + magic-string: 0.30.11 11769 11821 mime: 3.0.0 11770 11822 sade: 1.8.1 11771 11823 set-cookie-parser: 2.6.0 ··· 12589 12641 vite: 5.4.0(@types/node@20.14.15)(terser@5.22.0) 12590 12642 vue: 3.4.37(typescript@5.5.4) 12591 12643 12592 - '@vitest/browser@2.0.5(playwright@1.46.0)(typescript@5.5.4)(vitest@packages+vitest)(webdriverio@8.32.2(typescript@5.5.4))': 12644 + '@vitest/browser@2.0.5(playwright@1.46.1)(typescript@5.5.4)(vitest@packages+vitest)(webdriverio@8.32.2(typescript@5.5.4))': 12593 12645 dependencies: 12594 12646 '@testing-library/dom': 10.4.0 12595 12647 '@testing-library/user-event': 14.5.2(@testing-library/dom@10.4.0) 12596 12648 '@vitest/utils': 2.0.5 12597 - magic-string: 0.30.10 12598 - msw: 2.3.4(typescript@5.5.4) 12649 + magic-string: 0.30.11 12650 + msw: 2.3.5(typescript@5.5.4) 12599 12651 sirv: 2.0.4 12600 12652 vitest: link:packages/vitest 12601 12653 ws: 8.18.0 12602 12654 optionalDependencies: 12603 - playwright: 1.46.0 12655 + playwright: 1.46.1 12604 12656 webdriverio: 8.32.2(typescript@5.5.4) 12605 12657 transitivePeerDependencies: 12606 12658 - bufferutil ··· 16499 16551 16500 16552 ms@2.1.3: {} 16501 16553 16502 - msw@2.3.4(typescript@5.5.4): 16503 - dependencies: 16504 - '@bundled-es-modules/cookie': 2.0.0 16505 - '@bundled-es-modules/statuses': 1.0.1 16506 - '@bundled-es-modules/tough-cookie': 0.1.6 16507 - '@inquirer/confirm': 3.1.9 16508 - '@mswjs/interceptors': 0.29.1 16509 - '@open-draft/until': 2.1.0 16510 - '@types/cookie': 0.6.0 16511 - '@types/statuses': 2.0.5 16512 - chalk: 4.1.2 16513 - graphql: 16.8.1 16514 - headers-polyfill: 4.0.3 16515 - is-node-process: 1.2.0 16516 - outvariant: 1.4.2 16517 - path-to-regexp: 6.2.2 16518 - strict-event-emitter: 0.5.1 16519 - type-fest: 4.20.0 16520 - yargs: 17.7.2 16521 - optionalDependencies: 16522 - typescript: 5.5.4 16523 - 16524 16554 msw@2.3.5(typescript@5.5.4): 16525 16555 dependencies: 16526 16556 '@bundled-es-modules/cookie': 2.0.0 ··· 16901 16931 16902 16932 playwright-core@1.46.0: {} 16903 16933 16934 + playwright-core@1.46.1: {} 16935 + 16904 16936 playwright@1.41.0: 16905 16937 dependencies: 16906 16938 playwright-core: 1.41.0 ··· 16910 16942 playwright@1.46.0: 16911 16943 dependencies: 16912 16944 playwright-core: 1.46.0 16945 + optionalDependencies: 16946 + fsevents: 2.3.2 16947 + 16948 + playwright@1.46.1: 16949 + dependencies: 16950 + playwright-core: 1.46.1 16913 16951 optionalDependencies: 16914 16952 fsevents: 2.3.2 16915 16953
+16
test/browser/fixtures/mocking/autospying.test.ts
··· 1 + import { expect, test, vi } from 'vitest' 2 + import { calculator } from './src/calculator' 3 + import * as mocks_calculator from './src/mocks_calculator' 4 + 5 + vi.mock('./src/calculator', { spy: true }) 6 + vi.mock('./src/mocks_calculator', { spy: true }) 7 + 8 + test('correctly spies on a regular module', () => { 9 + expect(calculator('plus', 1, 2)).toBe(3) 10 + expect(calculator).toHaveBeenCalled() 11 + }) 12 + 13 + test('spy options overrides __mocks__ folder', () => { 14 + expect(mocks_calculator.calculator('plus', 1, 2)).toBe(3) 15 + expect(mocks_calculator.calculator).toHaveBeenCalled() 16 + })
+2 -2
test/browser/fixtures/mocking/import-actual-query.test.ts
··· 4 4 vi.mock(import('./src/mocks_factory?raw'), async (importOriginal) => { 5 5 const original = await importOriginal() 6 6 return { 7 - default: original.default.replace('mocked = false', 'mocked = "mocked!"'), 7 + default: original.default.replace('mocked: boolean = false', 'mocked: boolean = "mocked!"'), 8 8 } 9 9 }) 10 10 ··· 14 14 return _a + _b 15 15 } 16 16 17 - export const mocked = "mocked!" 17 + export const mocked: boolean = "mocked!" 18 18 `.trimStart()) 19 19 })
+1 -1
test/browser/fixtures/mocking/import-mock.test.ts
··· 1 - import { test,vi, expect } from 'vitest' 1 + import { test, vi, expect } from 'vitest' 2 2 3 3 vi.mock(import('./src/mocks_factory'), () => { 4 4 return {
+1 -1
test/browser/fixtures/mocking/src/mocks_factory.ts
··· 2 2 return _a + _b 3 3 } 4 4 5 - export const mocked = false 5 + export const mocked: boolean = false
+1
test/core/package.json
··· 14 14 "devDependencies": { 15 15 "@types/debug": "^4.1.12", 16 16 "@vitest/expect": "workspace:*", 17 + "@vitest/mocker": "workspace:*", 17 18 "@vitest/runner": "workspace:*", 18 19 "@vitest/test-dep1": "file:./deps/dep1", 19 20 "@vitest/test-dep2": "file:./deps/dep2",
+30 -30
test/core/test/browserAutomocker.test.ts
··· 1 1 import { parseAst } from 'vite' 2 2 import { expect, it } from 'vitest' 3 - import { automockModule } from 'vitest/src/node/automock.js' 3 + import { automockModule } from '@vitest/mocker/node' 4 4 5 5 function automock(code: string) { 6 - return automockModule(code, parseAst).toString() 6 + return automockModule(code, 'automock', parseAst).toString() 7 7 } 8 8 9 9 it('correctly parses function declaration', () => { ··· 13 13 " 14 14 function test() {} 15 15 16 - const __vitest_es_current_module__ = { 16 + const __vitest_current_es_module__ = { 17 17 __esModule: true, 18 18 ["test"]: test, 19 19 } 20 - const __vitest_mocked_module__ = __vitest_mocker__.mockObject(__vitest_es_current_module__) 20 + const __vitest_mocked_module__ = globalThis["__vitest_mocker__"].mockObject(__vitest_current_es_module__, "automock") 21 21 const __vitest_mocked_0__ = __vitest_mocked_module__["test"] 22 22 export { 23 23 __vitest_mocked_0__ as test, ··· 33 33 " 34 34 class Test {} 35 35 36 - const __vitest_es_current_module__ = { 36 + const __vitest_current_es_module__ = { 37 37 __esModule: true, 38 38 ["Test"]: Test, 39 39 } 40 - const __vitest_mocked_module__ = __vitest_mocker__.mockObject(__vitest_es_current_module__) 40 + const __vitest_mocked_module__ = globalThis["__vitest_mocker__"].mockObject(__vitest_current_es_module__, "automock") 41 41 const __vitest_mocked_0__ = __vitest_mocked_module__["Test"] 42 42 export { 43 43 __vitest_mocked_0__ as Test, ··· 53 53 " 54 54 const __vitest_default = class Test {} 55 55 56 - const __vitest_es_current_module__ = { 56 + const __vitest_current_es_module__ = { 57 57 __esModule: true, 58 58 ["__vitest_default"]: __vitest_default, 59 59 } 60 - const __vitest_mocked_module__ = __vitest_mocker__.mockObject(__vitest_es_current_module__) 60 + const __vitest_mocked_module__ = globalThis["__vitest_mocker__"].mockObject(__vitest_current_es_module__, "automock") 61 61 const __vitest_mocked_0__ = __vitest_mocked_module__["__vitest_default"] 62 62 export { 63 63 __vitest_mocked_0__ as default, ··· 71 71 " 72 72 const __vitest_default = function test() {} 73 73 74 - const __vitest_es_current_module__ = { 74 + const __vitest_current_es_module__ = { 75 75 __esModule: true, 76 76 ["__vitest_default"]: __vitest_default, 77 77 } 78 - const __vitest_mocked_module__ = __vitest_mocker__.mockObject(__vitest_es_current_module__) 78 + const __vitest_mocked_module__ = globalThis["__vitest_mocker__"].mockObject(__vitest_current_es_module__, "automock") 79 79 const __vitest_mocked_0__ = __vitest_mocked_module__["__vitest_default"] 80 80 export { 81 81 __vitest_mocked_0__ as default, ··· 89 89 " 90 90 const __vitest_default = someVariable 91 91 92 - const __vitest_es_current_module__ = { 92 + const __vitest_current_es_module__ = { 93 93 __esModule: true, 94 94 ["__vitest_default"]: __vitest_default, 95 95 } 96 - const __vitest_mocked_module__ = __vitest_mocker__.mockObject(__vitest_es_current_module__) 96 + const __vitest_mocked_module__ = globalThis["__vitest_mocker__"].mockObject(__vitest_current_es_module__, "automock") 97 97 const __vitest_mocked_0__ = __vitest_mocked_module__["__vitest_default"] 98 98 export { 99 99 __vitest_mocked_0__ as default, ··· 107 107 " 108 108 const __vitest_default = 'test' 109 109 110 - const __vitest_es_current_module__ = { 110 + const __vitest_current_es_module__ = { 111 111 __esModule: true, 112 112 ["__vitest_default"]: __vitest_default, 113 113 } 114 - const __vitest_mocked_module__ = __vitest_mocker__.mockObject(__vitest_es_current_module__) 114 + const __vitest_mocked_module__ = globalThis["__vitest_mocker__"].mockObject(__vitest_current_es_module__, "automock") 115 115 const __vitest_mocked_0__ = __vitest_mocked_module__["__vitest_default"] 116 116 export { 117 117 __vitest_mocked_0__ as default, ··· 125 125 " 126 126 const __vitest_default = null 127 127 128 - const __vitest_es_current_module__ = { 128 + const __vitest_current_es_module__ = { 129 129 __esModule: true, 130 130 ["__vitest_default"]: __vitest_default, 131 131 } 132 - const __vitest_mocked_module__ = __vitest_mocker__.mockObject(__vitest_es_current_module__) 132 + const __vitest_mocked_module__ = globalThis["__vitest_mocker__"].mockObject(__vitest_current_es_module__, "automock") 133 133 const __vitest_mocked_0__ = __vitest_mocked_module__["__vitest_default"] 134 134 export { 135 135 __vitest_mocked_0__ as default, ··· 145 145 const test = '123' 146 146 const __vitest_default = test 147 147 148 - const __vitest_es_current_module__ = { 148 + const __vitest_current_es_module__ = { 149 149 __esModule: true, 150 150 ["__vitest_default"]: __vitest_default, 151 151 } 152 - const __vitest_mocked_module__ = __vitest_mocker__.mockObject(__vitest_es_current_module__) 152 + const __vitest_mocked_module__ = globalThis["__vitest_mocker__"].mockObject(__vitest_current_es_module__, "automock") 153 153 const __vitest_mocked_0__ = __vitest_mocked_module__["__vitest_default"] 154 154 export { 155 155 __vitest_mocked_0__ as default, ··· 169 169 const test2 = () => {} 170 170 const test3 = function test4() {} 171 171 172 - const __vitest_es_current_module__ = { 172 + const __vitest_current_es_module__ = { 173 173 __esModule: true, 174 174 ["test"]: test, 175 175 ["test2"]: test2, 176 176 ["test3"]: test3, 177 177 } 178 - const __vitest_mocked_module__ = __vitest_mocker__.mockObject(__vitest_es_current_module__) 178 + const __vitest_mocked_module__ = globalThis["__vitest_mocker__"].mockObject(__vitest_current_es_module__, "automock") 179 179 const __vitest_mocked_0__ = __vitest_mocked_module__["test"] 180 180 const __vitest_mocked_1__ = __vitest_mocked_module__["test2"] 181 181 const __vitest_mocked_2__ = __vitest_mocked_module__["test3"] ··· 197 197 const [test, ...rest] = [] 198 198 const [...rest2] = [] 199 199 200 - const __vitest_es_current_module__ = { 200 + const __vitest_current_es_module__ = { 201 201 __esModule: true, 202 202 ["test"]: test, 203 203 ["rest"]: rest, 204 204 ["rest2"]: rest2, 205 205 } 206 - const __vitest_mocked_module__ = __vitest_mocker__.mockObject(__vitest_es_current_module__) 206 + const __vitest_mocked_module__ = globalThis["__vitest_mocker__"].mockObject(__vitest_current_es_module__, "automock") 207 207 const __vitest_mocked_0__ = __vitest_mocked_module__["test"] 208 208 const __vitest_mocked_1__ = __vitest_mocked_module__["rest"] 209 209 const __vitest_mocked_2__ = __vitest_mocked_module__["rest2"] ··· 223 223 " 224 224 const test = 2, test2 = 3, test4 = () => {}, test5 = function() {}; 225 225 226 - const __vitest_es_current_module__ = { 226 + const __vitest_current_es_module__ = { 227 227 __esModule: true, 228 228 ["test"]: test, 229 229 ["test2"]: test2, 230 230 ["test4"]: test4, 231 231 ["test5"]: test5, 232 232 } 233 - const __vitest_mocked_module__ = __vitest_mocker__.mockObject(__vitest_es_current_module__) 233 + const __vitest_mocked_module__ = globalThis["__vitest_mocker__"].mockObject(__vitest_current_es_module__, "automock") 234 234 const __vitest_mocked_0__ = __vitest_mocked_module__["test"] 235 235 const __vitest_mocked_1__ = __vitest_mocked_module__["test2"] 236 236 const __vitest_mocked_2__ = __vitest_mocked_module__["test4"] ··· 256 256 const { test: alias } = {} 257 257 const { ...rest2 } = {} 258 258 259 - const __vitest_es_current_module__ = { 259 + const __vitest_current_es_module__ = { 260 260 __esModule: true, 261 261 ["test"]: test, 262 262 ["rest"]: rest, 263 263 ["alias"]: alias, 264 264 ["rest2"]: rest2, 265 265 } 266 - const __vitest_mocked_module__ = __vitest_mocker__.mockObject(__vitest_es_current_module__) 266 + const __vitest_mocked_module__ = globalThis["__vitest_mocker__"].mockObject(__vitest_current_es_module__, "automock") 267 267 const __vitest_mocked_0__ = __vitest_mocked_module__["test"] 268 268 const __vitest_mocked_1__ = __vitest_mocked_module__["rest"] 269 269 const __vitest_mocked_2__ = __vitest_mocked_module__["alias"] ··· 287 287 const test = '1' 288 288 289 289 290 - const __vitest_es_current_module__ = { 290 + const __vitest_current_es_module__ = { 291 291 __esModule: true, 292 292 ["test"]: test, 293 293 ["test"]: test, 294 294 ["test"]: test, 295 295 } 296 - const __vitest_mocked_module__ = __vitest_mocker__.mockObject(__vitest_es_current_module__) 296 + const __vitest_mocked_module__ = globalThis["__vitest_mocker__"].mockObject(__vitest_current_es_module__, "automock") 297 297 const __vitest_mocked_0__ = __vitest_mocked_module__["test"] 298 298 const __vitest_mocked_1__ = __vitest_mocked_module__["test"] 299 299 const __vitest_mocked_2__ = __vitest_mocked_module__["test"] ··· 317 317 import { testing as name4 } from './another-module' 318 318 import { testing as __vitest_imported_3__ } from './another-module' 319 319 320 - const __vitest_es_current_module__ = { 320 + const __vitest_current_es_module__ = { 321 321 __esModule: true, 322 322 ["__vitest_imported_0__"]: __vitest_imported_0__, 323 323 ["__vitest_imported_1__"]: __vitest_imported_1__, 324 324 ["__vitest_imported_2__"]: __vitest_imported_2__, 325 325 ["__vitest_imported_3__"]: __vitest_imported_3__, 326 326 } 327 - const __vitest_mocked_module__ = __vitest_mocker__.mockObject(__vitest_es_current_module__) 327 + const __vitest_mocked_module__ = globalThis["__vitest_mocker__"].mockObject(__vitest_current_es_module__, "automock") 328 328 const __vitest_mocked_0__ = __vitest_mocked_module__["__vitest_imported_0__"] 329 329 const __vitest_mocked_1__ = __vitest_mocked_module__["__vitest_imported_1__"] 330 330 const __vitest_mocked_2__ = __vitest_mocked_module__["__vitest_imported_2__"]
+2 -2
test/core/test/injector-esm.test.ts
··· 1 1 import { parseAst } from 'rollup/parseAst' 2 2 import { expect, test } from 'vitest' 3 - import { injectDynamicImport } from '../../../packages/browser/src/node/esmInjector' 3 + import { injectDynamicImport } from '../../../packages/mocker/src/node/dynamicImportPlugin' 4 4 5 5 function parse(code: string, options: any) { 6 6 return parseAst(code, options) ··· 14 14 const result = injectSimpleCode( 15 15 'export const i = () => import(\'./foo\')', 16 16 ) 17 - expect(result).toMatchInlineSnapshot(`"export const i = () => __vitest_browser_runner__.wrapModule(() => import('./foo'))"`) 17 + expect(result).toMatchInlineSnapshot(`"export const i = () => globalThis["__vitest_mocker__"].wrapDynamicImport(() => import('./foo'))"`) 18 18 })
+155 -183
test/core/test/injector-mock.test.ts
··· 1 1 import { parseAst } from 'rollup/parseAst' 2 2 import { describe, expect, it, test } from 'vitest' 3 3 import stripAnsi from 'strip-ansi' 4 - import { getDefaultColors } from 'tinyrainbow' 5 - import { hoistMocks } from '../../../packages/vitest/src/node/hoistMocks' 4 + import { generateCodeFrame } from 'vitest/src/node/error.js' 5 + import type { HoistMocksPluginOptions } from '../../../packages/mocker/src/node/hoistMocksPlugin' 6 + import { hoistMocks } from '../../../packages/mocker/src/node/hoistMocksPlugin' 6 7 7 8 function parse(code: string, options: any) { 8 9 return parseAst(code, options) 9 10 } 10 11 12 + const hoistMocksOptions: HoistMocksPluginOptions = { 13 + codeFrameGenerator(node: any, id: string, code: string) { 14 + return generateCodeFrame( 15 + code, 16 + 4, 17 + node.start + 1, 18 + ) 19 + }, 20 + } 21 + 11 22 async function hoistSimple(code: string, url = '') { 12 - return hoistMocks(code, url, parse) 23 + return hoistMocks(code, url, parse, hoistMocksOptions) 13 24 } 14 25 15 26 function hoistSimpleCode(code: string) { 16 - return hoistMocks(code, '/test.js', parse)?.code.trim() 27 + return hoistMocks(code, '/test.js', parse, hoistMocksOptions)?.code.trim() 17 28 } 18 29 19 30 test('hoists mock, unmock, hoisted', () => { ··· 22 33 vi.unmock('path') 23 34 vi.hoisted(() => {}) 24 35 `)).toMatchInlineSnapshot(` 25 - "if (typeof globalThis.vi === "undefined" && typeof globalThis.vitest === "undefined") { throw new Error("There are some problems in resolving the mocks API.\\nYou may encounter this issue when importing the mocks API from another module other than 'vitest'.\\nTo fix this issue you can either:\\n- import the mocks API directly from 'vitest'\\n- enable the 'globals' options") } 36 + "if (typeof globalThis["vi"] === "undefined" && typeof globalThis["vitest"] === "undefined") { throw new Error("There are some problems in resolving the mocks API.\\nYou may encounter this issue when importing the mocks API from another module other than 'vitest'.\\nTo fix this issue you can either:\\n- import the mocks API directly from 'vitest'\\n- enable the 'globals' options") } 26 37 vi.mock('path', () => {}) 27 38 vi.unmock('path') 28 39 vi.hoisted(() => {})" ··· 37 48 vi.hoisted(() => {}) 38 49 import { test } from 'vitest' 39 50 `)).toMatchInlineSnapshot(` 40 - "const { vi } = await import('vitest') 41 - const { test } = await import('vitest') 42 - vi.mock('path', () => {}) 51 + "vi.mock('path', () => {}) 43 52 vi.unmock('path') 44 - vi.hoisted(() => {})" 53 + vi.hoisted(() => {}) 54 + 55 + import { vi } from 'vitest' 56 + 57 + 58 + 59 + import { test } from 'vitest'" 45 60 `) 46 61 }) 47 62 ··· 55 70 vi.hoisted(() => {}) 56 71 import { test } from 'vitest' 57 72 `)).toMatchInlineSnapshot(` 58 - "const { vi } = await import('vitest') 59 - const { test } = await import('vitest') 60 - vi.mock('path', () => {}) 73 + "vi.mock('path', () => {}) 61 74 vi.unmock('path') 62 75 vi.hoisted(() => {}) 63 76 const __vi_import_0__ = await import('./path.js') 64 - const __vi_import_1__ = await import('./path2.js')" 77 + const __vi_import_1__ = await import('./path2.js') 78 + 79 + import { vi } from 'vitest' 80 + 81 + 82 + 83 + 84 + 85 + import { test } from 'vitest'" 65 86 `) 66 87 }) 67 88 ··· 71 92 import add, * as AddModule from '../src/add' 72 93 vi.mock('../src/add', () => {}) 73 94 `)).toMatchInlineSnapshot(` 74 - "const { vi } = await import('vitest') 75 - vi.mock('../src/add', () => {}) 76 - const __vi_import_0__ = await import('../src/add')" 95 + "vi.mock('../src/add', () => {}) 96 + const __vi_import_0__ = await import('../src/add') 97 + 98 + import { vi } from 'vitest'" 77 99 `) 78 100 }) 79 101 ··· 84 106 add(); 85 107 vi.mock('../src/add', () => {}) 86 108 `)).toMatchInlineSnapshot(` 87 - "const { vi } = await import('vitest') 88 - vi.mock('../src/add', () => {}) 109 + "vi.mock('../src/add', () => {}) 89 110 const __vi_import_0__ = await import('../src/add') 90 111 91 - 112 + import { vi } from 'vitest' 92 113 93 114 __vi_import_0__.default();" 94 115 `) ··· 96 117 97 118 describe('transform', () => { 98 119 const hoistSimpleCodeWithoutMocks = (code: string) => { 99 - return hoistMocks(`import {vi} from "vitest";\n${code}\nvi.mock('faker');`, '/test.js', parse)?.code.trim() 120 + return hoistMocks(`import {vi} from "vitest";\n${code}\nvi.mock('faker');`, '/test.js', parse, hoistMocksOptions)?.code.trim() 100 121 } 101 122 test('default import', async () => { 102 123 expect( 103 124 hoistSimpleCodeWithoutMocks(`import foo from 'vue';console.log(foo.bar)`), 104 125 ).toMatchInlineSnapshot(` 105 - "const { vi } = await import('vitest') 106 - vi.mock('faker'); 126 + "vi.mock('faker'); 107 127 const __vi_import_0__ = await import('vue') 108 - 128 + import {vi} from "vitest"; 109 129 console.log(__vi_import_0__.default.bar)" 110 130 `) 111 131 }) ··· 122 142 admin: admin, 123 143 })) 124 144 })) 125 - `, './test.js', parse)?.code.trim(), 145 + `, './test.js', parse, hoistMocksOptions)?.code.trim(), 126 146 ).toMatchInlineSnapshot(` 127 - "const { vi } = await import('vitest') 128 - vi.mock('./mock.js', () => ({ 147 + "vi.mock('./mock.js', () => ({ 129 148 getSession: vi.fn().mockImplementation(() => ({ 130 149 user: __vi_import_0__.default, 131 150 admin: __vi_import_1__.admin, 132 151 })) 133 152 })) 134 153 const __vi_import_0__ = await import('./user') 135 - const __vi_import_1__ = await import('./admin')" 154 + const __vi_import_1__ = await import('./admin') 155 + 156 + import { vi } from 'vitest'" 136 157 `) 137 158 }) 138 159 ··· 151 172 admin: admin, 152 173 })) 153 174 }) 154 - `, './test.js', parse)?.code.trim(), 175 + `, './test.js', parse, hoistMocksOptions)?.code.trim(), 155 176 ).toMatchInlineSnapshot(` 156 - "const { vi } = await import('vitest') 157 - const { user, admin } = await vi.hoisted(async () => { 177 + "const { user, admin } = await vi.hoisted(async () => { 158 178 const { default: user } = await import('./user') 159 179 const { admin } = await import('./admin') 160 180 return { user, admin } ··· 164 184 user, 165 185 admin: admin, 166 186 })) 167 - })" 187 + }) 188 + 189 + import { vi } from 'vitest'" 168 190 `) 169 191 }) 170 192 ··· 174 196 `import { ref } from 'vue';function foo() { return ref(0) }`, 175 197 ), 176 198 ).toMatchInlineSnapshot(` 177 - "const { vi } = await import('vitest') 178 - vi.mock('faker'); 199 + "vi.mock('faker'); 179 200 const __vi_import_0__ = await import('vue') 180 - 201 + import {vi} from "vitest"; 181 202 function foo() { return __vi_import_0__.ref(0) }" 182 203 `) 183 204 }) ··· 188 209 `import * as vue from 'vue';function foo() { return vue.ref(0) }`, 189 210 ), 190 211 ).toMatchInlineSnapshot(` 191 - "const { vi } = await import('vitest') 192 - vi.mock('faker'); 212 + "vi.mock('faker'); 193 213 const __vi_import_0__ = await import('vue') 194 - 214 + import {vi} from "vitest"; 195 215 function foo() { return __vi_import_0__.ref(0) }" 196 216 `) 197 217 }) ··· 199 219 test('export function declaration', async () => { 200 220 expect(await hoistSimpleCodeWithoutMocks(`export function foo() {}`)) 201 221 .toMatchInlineSnapshot(` 202 - "const { vi } = await import('vitest') 203 - vi.mock('faker'); 204 - 222 + "vi.mock('faker'); 223 + import {vi} from "vitest"; 205 224 export function foo() {}" 206 225 `) 207 226 }) ··· 209 228 test('export class declaration', async () => { 210 229 expect(await hoistSimpleCodeWithoutMocks(`export class foo {}`)) 211 230 .toMatchInlineSnapshot(` 212 - "const { vi } = await import('vitest') 213 - vi.mock('faker'); 214 - 231 + "vi.mock('faker'); 232 + import {vi} from "vitest"; 215 233 export class foo {}" 216 234 `) 217 235 }) ··· 219 237 test('export var declaration', async () => { 220 238 expect(await hoistSimpleCodeWithoutMocks(`export const a = 1, b = 2`)) 221 239 .toMatchInlineSnapshot(` 222 - "const { vi } = await import('vitest') 223 - vi.mock('faker'); 224 - 240 + "vi.mock('faker'); 241 + import {vi} from "vitest"; 225 242 export const a = 1, b = 2" 226 243 `) 227 244 }) ··· 230 247 expect( 231 248 await hoistSimpleCodeWithoutMocks(`const a = 1, b = 2; export { a, b as c }`), 232 249 ).toMatchInlineSnapshot(` 233 - "const { vi } = await import('vitest') 234 - vi.mock('faker'); 235 - 250 + "vi.mock('faker'); 251 + import {vi} from "vitest"; 236 252 const a = 1, b = 2; export { a, b as c }" 237 253 `) 238 254 }) ··· 241 257 expect( 242 258 await hoistSimpleCodeWithoutMocks(`export { ref, computed as c } from 'vue'`), 243 259 ).toMatchInlineSnapshot(` 244 - "const { vi } = await import('vitest') 245 - vi.mock('faker'); 246 - 260 + "vi.mock('faker'); 261 + import {vi} from "vitest"; 247 262 export { ref, computed as c } from 'vue'" 248 263 `) 249 264 }) ··· 254 269 `import {createApp} from 'vue';export {createApp}`, 255 270 ), 256 271 ).toMatchInlineSnapshot(` 257 - "const { vi } = await import('vitest') 258 - vi.mock('faker'); 272 + "vi.mock('faker'); 259 273 const __vi_import_0__ = await import('vue') 260 - 274 + import {vi} from "vitest"; 261 275 export {createApp}" 262 276 `) 263 277 }) ··· 268 282 `export * from 'vue'\n` + `export * from 'react'`, 269 283 ), 270 284 ).toMatchInlineSnapshot(` 271 - "const { vi } = await import('vitest') 272 - vi.mock('faker'); 273 - 285 + "vi.mock('faker'); 286 + import {vi} from "vitest"; 274 287 export * from 'vue' 275 288 export * from 'react'" 276 289 `) ··· 279 292 test('export * as from', async () => { 280 293 expect(await hoistSimpleCodeWithoutMocks(`export * as foo from 'vue'`)) 281 294 .toMatchInlineSnapshot(` 282 - "const { vi } = await import('vitest') 283 - vi.mock('faker'); 284 - 295 + "vi.mock('faker'); 296 + import {vi} from "vitest"; 285 297 export * as foo from 'vue'" 286 298 `) 287 299 }) ··· 290 302 expect( 291 303 await hoistSimpleCodeWithoutMocks(`export default {}`), 292 304 ).toMatchInlineSnapshot(` 293 - "const { vi } = await import('vitest') 294 - vi.mock('faker'); 295 - 305 + "vi.mock('faker'); 306 + import {vi} from "vitest"; 296 307 export default {}" 297 308 `) 298 309 }) ··· 303 314 `export * from 'vue';import {createApp} from 'vue';`, 304 315 ), 305 316 ).toMatchInlineSnapshot(` 306 - "const { vi } = await import('vitest') 307 - vi.mock('faker'); 317 + "vi.mock('faker'); 308 318 const __vi_import_0__ = await import('vue') 309 - 319 + import {vi} from "vitest"; 310 320 export * from 'vue';" 311 321 `) 312 322 }) ··· 317 327 `path.resolve('server.js');import path from 'node:path';`, 318 328 ), 319 329 ).toMatchInlineSnapshot(` 320 - "const { vi } = await import('vitest') 321 - vi.mock('faker'); 330 + "vi.mock('faker'); 322 331 const __vi_import_0__ = await import('node:path') 323 - 332 + import {vi} from "vitest"; 324 333 __vi_import_0__.default.resolve('server.js');" 325 334 `) 326 335 }) ··· 329 338 expect( 330 339 await hoistSimpleCodeWithoutMocks(`console.log(import.meta.url)`), 331 340 ).toMatchInlineSnapshot(` 332 - "const { vi } = await import('vitest') 333 - vi.mock('faker'); 334 - 341 + "vi.mock('faker'); 342 + import {vi} from "vitest"; 335 343 console.log(import.meta.url)" 336 344 `) 337 345 }) ··· 341 349 `export const i = () => import('./foo')`, 342 350 ) 343 351 expect(result).toMatchInlineSnapshot(` 344 - "const { vi } = await import('vitest') 345 - vi.mock('faker'); 346 - 352 + "vi.mock('faker'); 353 + import {vi} from "vitest"; 347 354 export const i = () => import('./foo')" 348 355 `) 349 356 }) ··· 353 360 `import { fn } from 'vue';class A { fn() { fn() } }`, 354 361 ) 355 362 expect(result).toMatchInlineSnapshot(` 356 - "const { vi } = await import('vitest') 357 - vi.mock('faker'); 363 + "vi.mock('faker'); 358 364 const __vi_import_0__ = await import('vue') 359 - 365 + import {vi} from "vitest"; 360 366 class A { fn() { __vi_import_0__.fn() } }" 361 367 `) 362 368 }) ··· 366 372 `import { fn } from 'vue';function A(){ const fn = () => {}; return { fn }; }`, 367 373 ) 368 374 expect(result).toMatchInlineSnapshot(` 369 - "const { vi } = await import('vitest') 370 - vi.mock('faker'); 375 + "vi.mock('faker'); 371 376 const __vi_import_0__ = await import('vue') 372 - 377 + import {vi} from "vitest"; 373 378 function A(){ const fn = () => {}; return { fn }; }" 374 379 `) 375 380 }) ··· 380 385 `import { fn } from 'vue';function A(){ let {fn, test} = {fn: 'foo', test: 'bar'}; return { fn }; }`, 381 386 ) 382 387 expect(result).toMatchInlineSnapshot(` 383 - "const { vi } = await import('vitest') 384 - vi.mock('faker'); 388 + "vi.mock('faker'); 385 389 const __vi_import_0__ = await import('vue') 386 - 390 + import {vi} from "vitest"; 387 391 function A(){ let {fn, test} = {fn: 'foo', test: 'bar'}; return { fn }; }" 388 392 `) 389 393 }) ··· 394 398 `import { fn } from 'vue';function A(){ let [fn, test] = ['foo', 'bar']; return { fn }; }`, 395 399 ) 396 400 expect(result).toMatchInlineSnapshot(` 397 - "const { vi } = await import('vitest') 398 - vi.mock('faker'); 401 + "vi.mock('faker'); 399 402 const __vi_import_0__ = await import('vue') 400 - 403 + import {vi} from "vitest"; 401 404 function A(){ let [fn, test] = ['foo', 'bar']; return { fn }; }" 402 405 `) 403 406 }) ··· 408 411 `import { fn } from 'vue';function A({foo = \`test\${fn}\`} = {}){ return {}; }`, 409 412 ) 410 413 expect(result).toMatchInlineSnapshot(` 411 - "const { vi } = await import('vitest') 412 - vi.mock('faker'); 414 + "vi.mock('faker'); 413 415 const __vi_import_0__ = await import('vue') 414 - 416 + import {vi} from "vitest"; 415 417 function A({foo = \`test\${__vi_import_0__.fn}\`} = {}){ return {}; }" 416 418 `) 417 419 }) ··· 422 424 `import { fn } from 'vue';function A({foo = fn}){ return {}; }`, 423 425 ) 424 426 expect(result).toMatchInlineSnapshot(` 425 - "const { vi } = await import('vitest') 426 - vi.mock('faker'); 427 + "vi.mock('faker'); 427 428 const __vi_import_0__ = await import('vue') 428 - 429 + import {vi} from "vitest"; 429 430 function A({foo = __vi_import_0__.fn}){ return {}; }" 430 431 `) 431 432 }) ··· 435 436 `import { fn } from 'vue';function A(){ function fn() {}; return { fn }; }`, 436 437 ) 437 438 expect(result).toMatchInlineSnapshot(` 438 - "const { vi } = await import('vitest') 439 - vi.mock('faker'); 439 + "vi.mock('faker'); 440 440 const __vi_import_0__ = await import('vue') 441 - 441 + import {vi} from "vitest"; 442 442 function A(){ function fn() {}; return { fn }; }" 443 443 `) 444 444 }) ··· 448 448 `import {error} from './dependency';try {} catch(error) {}`, 449 449 ) 450 450 expect(result).toMatchInlineSnapshot(` 451 - "const { vi } = await import('vitest') 452 - vi.mock('faker'); 451 + "vi.mock('faker'); 453 452 const __vi_import_0__ = await import('./dependency') 454 - 453 + import {vi} from "vitest"; 455 454 try {} catch(error) {}" 456 455 `) 457 456 }) ··· 463 462 `import { Foo } from './dependency';` + `class A extends Foo {}`, 464 463 ), 465 464 ).toMatchInlineSnapshot(` 466 - "const { vi } = await import('vitest') 467 - vi.mock('faker'); 465 + "vi.mock('faker'); 468 466 const __vi_import_0__ = await import('./dependency') 469 - 467 + import {vi} from "vitest"; 470 468 const Foo = __vi_import_0__.Foo; 471 469 class A extends Foo {}" 472 470 `) ··· 480 478 + `export class B extends Foo {}`, 481 479 ), 482 480 ).toMatchInlineSnapshot(` 483 - "const { vi } = await import('vitest') 484 - vi.mock('faker'); 481 + "vi.mock('faker'); 485 482 const __vi_import_0__ = await import('./dependency') 486 - 483 + import {vi} from "vitest"; 487 484 const Foo = __vi_import_0__.Foo; 488 485 export default class A extends Foo {} 489 486 export class B extends Foo {}" ··· 495 492 // default anonymous functions 496 493 expect(await hoistSimpleCodeWithoutMocks(`export default function() {}\n`)) 497 494 .toMatchInlineSnapshot(` 498 - "const { vi } = await import('vitest') 499 - vi.mock('faker'); 500 - 495 + "vi.mock('faker'); 496 + import {vi} from "vitest"; 501 497 export default function() {}" 502 498 `) 503 499 // default anonymous class 504 500 expect(await hoistSimpleCodeWithoutMocks(`export default class {}\n`)) 505 501 .toMatchInlineSnapshot(` 506 - "const { vi } = await import('vitest') 507 - vi.mock('faker'); 508 - 502 + "vi.mock('faker'); 503 + import {vi} from "vitest"; 509 504 export default class {}" 510 505 `) 511 506 // default named functions ··· 515 510 + `foo.prototype = Object.prototype;`, 516 511 ), 517 512 ).toMatchInlineSnapshot(` 518 - "const { vi } = await import('vitest') 519 - vi.mock('faker'); 520 - 513 + "vi.mock('faker'); 514 + import {vi} from "vitest"; 521 515 export default function foo() {} 522 516 foo.prototype = Object.prototype;" 523 517 `) ··· 527 521 `export default class A {}\n` + `export class B extends A {}`, 528 522 ), 529 523 ).toMatchInlineSnapshot(` 530 - "const { vi } = await import('vitest') 531 - vi.mock('faker'); 532 - 524 + "vi.mock('faker'); 525 + import {vi} from "vitest"; 533 526 export default class A {} 534 527 export class B extends A {}" 535 528 `) ··· 559 552 + `function g() { const f = () => { const inject = true }; console.log(inject) }\n`, 560 553 ), 561 554 ).toMatchInlineSnapshot(` 562 - "const { vi } = await import('vitest') 563 - vi.mock('faker'); 555 + "vi.mock('faker'); 564 556 const __vi_import_0__ = await import('vue') 565 - 557 + import {vi} from "vitest"; 566 558 const a = { inject: __vi_import_0__.inject } 567 559 const b = { test: __vi_import_0__.inject } 568 560 function c() { const { test: inject } = { test: true }; console.log(inject) } ··· 577 569 expect( 578 570 await hoistSimpleCodeWithoutMocks(`const [, LHS, RHS] = inMatch;`), 579 571 ).toMatchInlineSnapshot(` 580 - "const { vi } = await import('vitest') 581 - vi.mock('faker'); 582 - 572 + "vi.mock('faker'); 573 + import {vi} from "vitest"; 583 574 const [, LHS, RHS] = inMatch;" 584 575 `) 585 576 }) ··· 595 586 `, 596 587 ), 597 588 ).toMatchInlineSnapshot(` 598 - "const { vi } = await import('vitest') 599 - vi.mock('faker'); 589 + "vi.mock('faker'); 600 590 const __vi_import_0__ = await import('foo') 601 - 591 + import {vi} from "vitest"; 602 592 603 593 604 594 const a = ({ _ = __vi_import_0__.foo() }) => {} ··· 619 609 `, 620 610 ), 621 611 ).toMatchInlineSnapshot(` 622 - "const { vi } = await import('vitest') 623 - vi.mock('faker'); 612 + "vi.mock('faker'); 624 613 const __vi_import_0__ = await import('foo') 625 - 614 + import {vi} from "vitest"; 626 615 627 616 628 617 const a = () => { ··· 644 633 `, 645 634 ), 646 635 ).toMatchInlineSnapshot(` 647 - "const { vi } = await import('vitest') 648 - vi.mock('faker'); 636 + "vi.mock('faker'); 649 637 const __vi_import_0__ = await import('foo') 650 - 638 + import {vi} from "vitest"; 651 639 652 640 653 641 const foo = {} ··· 689 677 `, 690 678 ), 691 679 ).toMatchInlineSnapshot(` 692 - "const { vi } = await import('vitest') 693 - vi.mock('faker'); 680 + "vi.mock('faker'); 694 681 const __vi_import_0__ = await import('vue') 695 - 682 + import {vi} from "vitest"; 696 683 697 684 698 685 function a() { ··· 740 727 `, 741 728 ), 742 729 ).toMatchInlineSnapshot(` 743 - "const { vi } = await import('vitest') 744 - vi.mock('faker'); 730 + "vi.mock('faker'); 745 731 const __vi_import_0__ = await import('foo') 746 - 732 + import {vi} from "vitest"; 747 733 748 734 749 735 const bar = 'bar' ··· 773 759 `, 774 760 ), 775 761 ).toMatchInlineSnapshot(` 776 - "const { vi } = await import('vitest') 777 - vi.mock('faker'); 762 + "vi.mock('faker'); 778 763 const __vi_import_0__ = await import('vue') 779 - 764 + import {vi} from "vitest"; 780 765 781 766 782 767 const add = __vi_import_0__.add; ··· 806 791 `, 807 792 ), 808 793 ).toMatchInlineSnapshot(` 809 - "const { vi } = await import('vitest') 810 - vi.mock('faker'); 794 + "vi.mock('faker'); 811 795 const __vi_import_0__ = await import('foo') 812 - 796 + import {vi} from "vitest"; 813 797 814 798 815 799 const bar = 'bar' ··· 853 837 `, 854 838 ), 855 839 ).toMatchInlineSnapshot(` 856 - "const { vi } = await import('vitest') 857 - vi.mock('faker'); 840 + "vi.mock('faker'); 858 841 const __vi_import_0__ = await import('vue') 859 - 842 + import {vi} from "vitest"; 860 843 861 844 862 845 function foobar() { ··· 892 875 `, 893 876 ), 894 877 ).toMatchInlineSnapshot(` 895 - "const { vi } = await import('vitest') 896 - vi.mock('faker'); 897 - 878 + "vi.mock('faker'); 879 + import {vi} from "vitest"; 898 880 899 881 export function fn1() { 900 882 }export function fn2() { ··· 915 897 `.trim() 916 898 917 899 expect(await hoistSimpleCodeWithoutMocks(code)).toMatchInlineSnapshot(` 918 - "const { vi } = await import('vitest') 919 - vi.mock('faker'); 920 - 900 + "vi.mock('faker'); 901 + import {vi} from "vitest"; 921 902 export default (function getRandom() { 922 903 return Math.random(); 923 904 });" ··· 926 907 expect( 927 908 await hoistSimpleCodeWithoutMocks(`export default (class A {});`), 928 909 ).toMatchInlineSnapshot(` 929 - "const { vi } = await import('vitest') 930 - vi.mock('faker'); 931 - 910 + "vi.mock('faker'); 911 + import {vi} from "vitest"; 932 912 export default (class A {});" 933 913 `) 934 914 }) ··· 983 963 };`.trim() 984 964 985 965 expect(await hoistSimpleCodeWithoutMocks(code)).toMatchInlineSnapshot(` 986 - "const { vi } = await import('vitest') 987 - vi.mock('faker'); 966 + "vi.mock('faker'); 988 967 const __vi_import_0__ = await import('foobar') 989 - 968 + import {vi} from "vitest"; 990 969 991 970 if (false) { 992 971 const foo = 'foo' ··· 1027 1006 return [foo, bar] 1028 1007 }`), 1029 1008 ).toMatchInlineSnapshot(` 1030 - "const { vi } = await import('vitest') 1031 - vi.mock('faker'); 1009 + "vi.mock('faker'); 1032 1010 const __vi_import_0__ = await import('foobar') 1033 - 1011 + import {vi} from "vitest"; 1034 1012 1035 1013 1036 1014 function test() { ··· 1057 1035 return bar; 1058 1036 }`), 1059 1037 ).toMatchInlineSnapshot(` 1060 - "const { vi } = await import('vitest') 1061 - vi.mock('faker'); 1038 + "vi.mock('faker'); 1062 1039 const __vi_import_0__ = await import('foobar') 1063 - 1040 + import {vi} from "vitest"; 1064 1041 1065 1042 1066 1043 function test() { ··· 1092 1069 console.log(test) 1093 1070 }`), 1094 1071 ).toMatchInlineSnapshot(` 1095 - "const { vi } = await import('vitest') 1096 - vi.mock('faker'); 1072 + "vi.mock('faker'); 1097 1073 const __vi_import_0__ = await import('./test.js') 1098 - 1074 + import {vi} from "vitest"; 1099 1075 1100 1076 1101 1077 for (const test of tests) { ··· 1126 1102 `, 1127 1103 ) 1128 1104 expect(result).toMatchInlineSnapshot(` 1129 - "const { vi } = await import('vitest') 1130 - vi.mock('faker'); 1105 + "vi.mock('faker'); 1131 1106 const __vi_import_0__ = await import('./foo') 1132 - 1107 + import {vi} from "vitest"; 1133 1108 1134 1109 1135 1110 console.log(__vi_import_0__.default, __vi_import_0__.Bar); ··· 1148 1123 import('./bar.json', { with: { type: 'json' } }); 1149 1124 `), 1150 1125 ).toMatchInlineSnapshot(` 1151 - "const { vi } = await import('vitest') 1152 - vi.mock('faker'); 1126 + "vi.mock('faker'); 1153 1127 const __vi_import_0__ = await import('./foo.json') 1154 - 1128 + import {vi} from "vitest"; 1155 1129 1156 1130 1157 1131 import('./bar.json', { with: { type: 'json' } });" ··· 1171 1145 console.log(foo + 2) 1172 1146 `), 1173 1147 ).toMatchInlineSnapshot(` 1174 - "const { vi } = await import('vitest') 1175 - vi.mock('faker'); 1148 + "vi.mock('faker'); 1176 1149 const __vi_import_0__ = await import('./foo') 1177 - 1150 + import {vi} from "vitest"; 1178 1151 1179 1152 console.log(__vi_import_0__.foo + 1) 1180 1153 export * from './a' ··· 1193 1166 .hoisted(() => {}); 1194 1167 `), 1195 1168 ).toMatchInlineSnapshot(` 1196 - "const { vi } = await import('vitest') 1197 - await vi 1169 + "await vi 1198 1170 .hoisted(() => {}); 1199 1171 1200 - 1172 + import { vi } from 'vitest'; 1201 1173 1234;" 1202 1174 `) 1203 1175 }) ··· 1238 1210 vi.mock(await import(\`./path\`), () => {}); 1239 1211 `), 1240 1212 ).toMatchInlineSnapshot(` 1241 - "if (typeof globalThis.vi === "undefined" && typeof globalThis.vitest === "undefined") { throw new Error("There are some problems in resolving the mocks API.\\nYou may encounter this issue when importing the mocks API from another module other than 'vitest'.\\nTo fix this issue you can either:\\n- import the mocks API directly from 'vitest'\\n- enable the 'globals' options") } 1213 + "if (typeof globalThis["vi"] === "undefined" && typeof globalThis["vitest"] === "undefined") { throw new Error("There are some problems in resolving the mocks API.\\nYou may encounter this issue when importing the mocks API from another module other than 'vitest'.\\nTo fix this issue you can either:\\n- import the mocks API directly from 'vitest'\\n- enable the 'globals' options") } 1242 1214 vi.mock('./path') 1243 1215 vi.mock(somePath) 1244 1216 vi.mock(\`./path\`) ··· 1304 1276 describe('throws an error when nodes are incompatible', () => { 1305 1277 const getErrorWhileHoisting = (code: string) => { 1306 1278 try { 1307 - hoistMocks(code, '/test.js', parse, getDefaultColors())?.code.trim() 1279 + hoistMocks(code, '/test.js', parse, hoistMocksOptions)?.code.trim() 1308 1280 } 1309 1281 catch (err: any) { 1310 1282 return err
+24
test/core/test/mocking/autospying.test.ts
··· 1 + import { expect, test, vi } from 'vitest' 2 + import axios from 'axios' 3 + import { getAuthToken } from '../../src/env' 4 + 5 + vi.mock(import('../../src/env'), { spy: true }) 6 + 7 + vi.mock('axios', { spy: true }) 8 + 9 + test('getAuthToken is spied', async () => { 10 + import.meta.env.AUTH_TOKEN = '123' 11 + const token = getAuthToken() 12 + expect(token).toBe('123') 13 + expect(getAuthToken).toHaveBeenCalledTimes(1) 14 + vi.mocked(getAuthToken).mockRestore() 15 + expect(vi.isMockFunction(getAuthToken)).toBe(false) 16 + }) 17 + 18 + test('package in __mocks__ has lower priority', async () => { 19 + expect(vi.isMockFunction(axios.get)).toBe(true) 20 + 21 + // isAxiosError is not defined in __mocks__ 22 + expect(axios.isAxiosError(new Error('test'))).toBe(false) 23 + expect(axios.isAxiosError).toHaveBeenCalled() 24 + })
+12
test/public-mocker/fixtures/automock/index.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8"> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 + <title>Document</title> 7 + </head> 8 + <body> 9 + <script type="module" src="./index.js"></script> 10 + <div id="mocked"></div> 11 + </body> 12 + </html>
+8
test/public-mocker/fixtures/automock/index.js
··· 1 + import { mocker } from 'virtual:mocker' 2 + import { calculate } from './test' 3 + 4 + mocker.customMock(import('./test'), { spy: true }) 5 + 6 + calculate.mockReturnValue(42) 7 + 8 + document.querySelector('#mocked').textContent = calculate(1, 2)
+3
test/public-mocker/fixtures/automock/test.js
··· 1 + export function calculate (a, b) { 2 + return a + b 3 + }
+12
test/public-mocker/fixtures/autospy/index.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8"> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 + <title>Document</title> 7 + </head> 8 + <body> 9 + <script type="module" src="./index.js"></script> 10 + <div id="mocked"></div> 11 + </body> 12 + </html>
+10
test/public-mocker/fixtures/autospy/index.js
··· 1 + import { mocker } from 'virtual:mocker' 2 + import { calculate } from './test' 3 + 4 + mocker.customMock(import('./test')) 5 + 6 + document.querySelector('#mocked').textContent = calculate(1, 2) 7 + 8 + calculate.mockReturnValue(42) 9 + 10 + document.querySelector('#mocked').textContent += `, ${calculate(1, 2)}`
+3
test/public-mocker/fixtures/autospy/test.js
··· 1 + export function calculate (a, b) { 2 + return a + b 3 + }
+12
test/public-mocker/fixtures/manual-mock/index.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8"> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 + <title>Document</title> 7 + </head> 8 + <body> 9 + <script type="module" src="./index.js"></script> 10 + <div id="mocked"></div> 11 + </body> 12 + </html>
+8
test/public-mocker/fixtures/manual-mock/index.js
··· 1 + import { mocker } from 'virtual:mocker' 2 + import { mocked } from './test' 3 + 4 + mocker.customMock(import('./test'), () => { 5 + return { mocked: true } 6 + }) 7 + 8 + document.querySelector('#mocked').textContent = mocked
+1
test/public-mocker/fixtures/manual-mock/test.js
··· 1 + export const mocked = false
+3
test/public-mocker/fixtures/redirect/__mocks__/test.js
··· 1 + export function calculate() { 2 + return 42 3 + }
+12
test/public-mocker/fixtures/redirect/index.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8"> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 + <title>Document</title> 7 + </head> 8 + <body> 9 + <script type="module" src="./index.js"></script> 10 + <div id="mocked"></div> 11 + </body> 12 + </html>
+6
test/public-mocker/fixtures/redirect/index.js
··· 1 + import { mocker } from 'virtual:mocker' 2 + import { calculate } from './test' 3 + 4 + mocker.customMock(import('./test')) 5 + 6 + document.querySelector('#mocked').textContent = calculate(1, 2)
+3
test/public-mocker/fixtures/redirect/test.js
··· 1 + export function calculate (a, b) { 2 + return a + b 3 + }
+15
test/public-mocker/package.json
··· 1 + { 2 + "name": "@vitest/test-public-mocker", 3 + "type": "module", 4 + "private": true, 5 + "scripts": { 6 + "test": "vitest run", 7 + "dev": "vite" 8 + }, 9 + "devDependencies": { 10 + "@vitest/browser": "workspace:*", 11 + "@vitest/mocker": "workspace:*", 12 + "playwright": "^1.46.1", 13 + "vitest": "workspace:*" 14 + } 15 + }
+105
test/public-mocker/test/mocker.test.ts
··· 1 + import { mockerPlugin } from '@vitest/mocker/node' 2 + import type { UserConfig } from 'vite' 3 + import { createServer } from 'vite' 4 + import { beforeAll, expect, it, onTestFinished } from 'vitest' 5 + import type { Browser } from 'playwright' 6 + import { chromium } from 'playwright' 7 + 8 + let browser: Browser 9 + beforeAll(async () => { 10 + browser = await chromium.launch() 11 + return async () => { 12 + await browser.close() 13 + browser = null as any 14 + } 15 + }) 16 + 17 + it('default server manual mocker works correctly', async () => { 18 + const { page } = await createTestServer({ 19 + root: 'fixtures/manual-mock', 20 + }) 21 + 22 + await expect.poll(() => page.locator('css=#mocked').textContent()).toBe('true') 23 + }) 24 + 25 + it('automock works correctly', async () => { 26 + const { page } = await createTestServer({ 27 + root: 'fixtures/automock', 28 + }) 29 + 30 + await expect.poll(() => page.locator('css=#mocked').textContent()).toBe('42') 31 + }) 32 + 33 + it('autospy works correctly', async () => { 34 + const { page } = await createTestServer({ 35 + root: 'fixtures/autospy', 36 + }) 37 + 38 + await expect.poll(() => page.locator('css=#mocked').textContent()).toBe('3, 42') 39 + }) 40 + 41 + it('redirect works correctly', async () => { 42 + const { page } = await createTestServer({ 43 + root: 'fixtures/redirect', 44 + }) 45 + 46 + await expect.poll(() => page.locator('css=#mocked').textContent()).toBe('42') 47 + }) 48 + 49 + async function createTestServer(config: UserConfig) { 50 + const server = await createServer({ 51 + ...config, 52 + plugins: [ 53 + mockerPlugin({ 54 + globalThisAccessor: 'Symbol.for("vitest.mocker")', 55 + hoistMocks: { 56 + utilsObjectNames: ['mocker'], 57 + hoistedModules: ['virtual:mocker'], 58 + hoistableMockMethodNames: ['customMock'], 59 + dynamicImportMockMethodNames: ['customMock'], 60 + hoistedMethodNames: ['customHoisted'], 61 + }, 62 + }), 63 + { 64 + name: 'vi:resolver', 65 + enforce: 'pre', 66 + resolveId(id) { 67 + if (id === 'virtual:mocker') { 68 + return id 69 + } 70 + }, 71 + load(id) { 72 + if (id === 'virtual:mocker') { 73 + return ` 74 + import { registerModuleMocker } from '@vitest/mocker/register' 75 + import { ModuleMockerServerInterceptor } from '@vitest/mocker/browser' 76 + 77 + const _mocker = registerModuleMocker( 78 + () => new ModuleMockerServerInterceptor() 79 + ) 80 + 81 + export const mocker = { 82 + customMock: _mocker.mock, 83 + customHoisted: _mocker.hoisted, 84 + } 85 + ` 86 + } 87 + }, 88 + }, 89 + ], 90 + }) 91 + await server.listen() 92 + onTestFinished(async () => { 93 + await server.close() 94 + }) 95 + const page = await browser.newPage() 96 + onTestFinished(async () => { 97 + await page.close() 98 + }) 99 + await page.goto(server.resolvedUrls!.local[0]) 100 + 101 + return { 102 + server, 103 + page, 104 + } 105 + }
+44
test/public-mocker/vite.config.ts
··· 1 + import { defineConfig } from 'vite' 2 + import { mockerPlugin } from '@vitest/mocker/node' 3 + 4 + export default defineConfig({ 5 + root: 'fixtures/redirect', 6 + plugins: [ 7 + mockerPlugin({ 8 + globalThisAccessor: 'Symbol.for("vitest.mocker")', 9 + hoistMocks: { 10 + utilsObjectNames: ['mocker'], 11 + hoistedModules: ['virtual:mocker'], 12 + hoistableMockMethodNames: ['customMock'], 13 + dynamicImportMockMethodNames: ['customMock'], 14 + hoistedMethodNames: ['customHoisted'], 15 + }, 16 + }), 17 + { 18 + name: 'vi:resolver', 19 + enforce: 'pre', 20 + resolveId(id) { 21 + if (id === 'virtual:mocker') { 22 + return id 23 + } 24 + }, 25 + load(id) { 26 + if (id === 'virtual:mocker') { 27 + return ` 28 + import { registerModuleMocker } from '@vitest/mocker/register' 29 + import { ModuleMockerServerInterceptor } from '@vitest/mocker/browser' 30 + 31 + const _mocker = registerModuleMocker( 32 + () => new ModuleMockerServerInterceptor() 33 + ) 34 + 35 + export const mocker = { 36 + customMock: _mocker.mock, 37 + customHoisted: _mocker.hoisted, 38 + } 39 + ` 40 + } 41 + }, 42 + }, 43 + ], 44 + })
+13
test/public-mocker/vitest.config.ts
··· 1 + import { defineConfig } from 'vitest/config' 2 + 3 + export default defineConfig({ 4 + test: { 5 + isolate: false, 6 + environment: 'node', 7 + expect: { 8 + poll: { 9 + timeout: 30_000, 10 + }, 11 + }, 12 + }, 13 + })
+3
tsconfig.base.json
··· 14 14 "@vitest/snapshot": ["./packages/snapshot/src/index.ts"], 15 15 "@vitest/snapshot/*": ["./packages/snapshot/src/*"], 16 16 "@vitest/expect": ["./packages/expect/src/index.ts"], 17 + "@vitest/mocker": ["./packages/mocker/src/index.ts"], 18 + "@vitest/mocker/node": ["./packages/mocker/src/node/index.ts"], 19 + "@vitest/mocker/browser": ["./packages/mocker/src/browser/index.ts"], 17 20 "@vitest/runner": ["./packages/runner/src/index.ts"], 18 21 "@vitest/runner/*": ["./packages/runner/src/*"], 19 22 "@vitest/browser": ["./packages/browser/src/node/index.ts"],