Skip to content

Commit fac977c

Browse files
committed
test: check support for custom manual mocks
1 parent 3d8c345 commit fac977c

File tree

16 files changed

+938
-350
lines changed

16 files changed

+938
-350
lines changed

docs/api/vi.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -435,7 +435,7 @@ console.log(cart.getApples()) // still 42!
435435
:::
436436

437437
::: tip
438-
It is not possible to spy on an 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.
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.
439439

440440
```ts
441441
import { calculator } from './src/calculator.ts'

packages/mocker/EXPORTS.md

+289
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
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(new ModuleMockerMSWInterceptor())
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+
```

0 commit comments

Comments
 (0)