Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 41 commits into from
Aug 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
cc44a96
feat(vitest): allow auto spying on all exports
sheremet-va Aug 6, 2024
5369644
test: behaviour overrides __mocks__
sheremet-va Aug 6, 2024
5d3ea95
feat: support autospy in the browser
sheremet-va Aug 6, 2024
e2d3eb4
chore: fix test
sheremet-va Aug 13, 2024
2aa88d8
feat: refactor mocker into @vitest/mocker
sheremet-va Aug 13, 2024
d834687
chore: cleanup
sheremet-va Aug 13, 2024
9e0163f
chore: cleanup
sheremet-va Aug 13, 2024
55415e2
fix(browser): correctly resolve id when mocking
sheremet-va Aug 13, 2024
71aede7
test: fix automockModule tests
sheremet-va Aug 13, 2024
072c2a6
fix: correctly invalidate mocked urls
sheremet-va Aug 13, 2024
8dd772b
refactor: cleanup
sheremet-va Aug 13, 2024
5bc5827
chore: fix typecheck
sheremet-va Aug 13, 2024
1549e56
chore: cleanup
sheremet-va Aug 13, 2024
170133f
docs: add more docs
sheremet-va Aug 13, 2024
502a01b
fix: correctly invalidate mocked modules
sheremet-va Aug 13, 2024
89f93be
refactor: move more functions to @vitest/mocker
sheremet-va Aug 13, 2024
36405e3
chore: remove a warning
sheremet-va Aug 13, 2024
1242a67
feat: move interceptor and browser mocker to @vitest/mocker
sheremet-va Aug 13, 2024
2313614
refactor: use types from vite
sheremet-va Aug 15, 2024
02bfe9d
feat: add a public plugin for module mocking
sheremet-va Aug 15, 2024
0dc548e
chore: more docs
sheremet-va Aug 15, 2024
245e1eb
chore: return compiler hints from register
sheremet-va Aug 15, 2024
64d39a4
chore: add auto-register
sheremet-va Aug 15, 2024
ce1118f
chore: fix typecheck
sheremet-va Aug 15, 2024
77ebc01
chore: cleanup
sheremet-va Aug 15, 2024
3adf4d2
test: fix snapshots
sheremet-va Aug 16, 2024
be1f9a1
chore: don't highlight hoisted errors
sheremet-va Aug 16, 2024
340b2a6
chore: cleanup
sheremet-va Aug 16, 2024
42c28ad
perf: fix import from node
sheremet-va Aug 19, 2024
4fdddc2
feat: support native interceptor
sheremet-va Aug 19, 2024
607eb6b
chore: cleanup
sheremet-va Aug 19, 2024
7d502c1
fix: correctly resolve manual mocks
sheremet-va Aug 19, 2024
93b9528
test: check support for custom manual mocks
sheremet-va Aug 19, 2024
ad89b1f
test: more tests for the server interceptor
sheremet-va Aug 19, 2024
43de9c9
test: fix snapshots
sheremet-va Aug 19, 2024
45c1415
chore: build regexp on init
sheremet-va Aug 20, 2024
c0ebea9
chore: cleanup
sheremet-va Aug 20, 2024
007556c
chore: fix hoistable regexp
sheremet-va Aug 21, 2024
620f2d4
chore: trigger ci
sheremet-va Aug 21, 2024
5ff1964
fix: pass down options
sheremet-va Aug 21, 2024
3bd343e
fix: error on Windows
sheremet-va Aug 21, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 41 additions & 8 deletions docs/api/vi.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ This section describes the API that you can use when [mocking a module](/guide/m

### vi.mock

- **Type**: `(path: string, factory?: (importOriginal: () => unknown) => unknown) => void`
- **Type**: `<T>(path: Promise<T>, factory?: (importOriginal: () => T) => T | Promise<T>) => void`
- **Type**: `(path: string, factory?: MockOptions | ((importOriginal: () => unknown) => unknown)) => void`
- **Type**: `<T>(path: Promise<T>, factory?: MockOptions | ((importOriginal: () => T) => T | Promise<T>)) => void`

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`.

Expand All @@ -29,11 +29,27 @@ In order to hoist `vi.mock`, Vitest statically analyzes your files. It indicates
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.
:::

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.
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.

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.

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).
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.

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

vi.mock('./src/calculator.ts', { spy: true })

// calls the original implementation,
// but allows asserting the behaviour later
const result = calculator(1, 2)

expect(result).toBe(3)
expect(calculator).toHaveBeenCalledWith(1, 2)
expect(calculator).toHaveReturned(3)
```

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).

```ts twoslash
// @filename: ./path/to/module.js
Expand Down Expand Up @@ -103,7 +119,7 @@ vi.mock('./path/to/module.js', () => {
```
:::

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.
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.

For example, you have this file structure:

Expand All @@ -118,7 +134,7 @@ For example, you have this file structure:
- increment.test.js
```

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:
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:

```ts
// increment.test.js
Expand All @@ -144,8 +160,8 @@ If there is no `__mocks__` folder or a factory provided, Vitest will import the

### vi.doMock

- **Type**: `(path: string, factory?: (importOriginal: () => unknown) => unknown) => void`
- **Type**: `<T>(path: Promise<T>, factory?: (importOriginal: () => T) => T | Promise<T>) => void`
- **Type**: `(path: string, factory?: MockOptions | ((importOriginal: () => unknown) => unknown)) => void`
- **Type**: `<T>(path: Promise<T>, factory?: MockOptions | ((importOriginal: () => T) => T | Promise<T>)) => void`

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.

Expand Down Expand Up @@ -418,6 +434,23 @@ console.log(cart.getApples()) // still 42!
```
:::

::: tip
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.

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

vi.mock('./src/calculator.ts', { spy: true })

calculator(1, 2)

expect(calculator).toHaveBeenCalledWith(1, 2)
expect(calculator).toHaveReturned(3)
```

And while it is possible to spy on exports in `jsdom` or other Node.js environments, this might change in the future.
:::

### vi.stubEnv {#vi-stubenv}

- **Type:** `(name: string, value: string) => Vitest`
Expand Down
1 change: 1 addition & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ export default antfu(
{
files: [
`docs/${GLOB_SRC}`,
`**/*.md`,
],
rules: {
'style/max-statements-per-line': 'off',
Expand Down
1 change: 1 addition & 0 deletions packages/browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
"devDependencies": {
"@testing-library/jest-dom": "^6.4.8",
"@types/ws": "^8.5.12",
"@vitest/mocker": "workspace:*",
"@vitest/runner": "workspace:*",
"@vitest/ui": "workspace:*",
"@vitest/ws-client": "workspace:*",
Expand Down
6 changes: 3 additions & 3 deletions packages/browser/src/client/channel.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { CancelReason } from '@vitest/runner'
import type { MockedModuleSerialized } from '@vitest/mocker'
import { getBrowserState } from './utils'

export interface IframeDoneEvent {
Expand All @@ -24,13 +25,12 @@ export interface IframeViewportEvent {

export interface IframeMockEvent {
type: 'mock'
paths: string[]
mock: string | undefined | null
module: MockedModuleSerialized
}

export interface IframeUnmockEvent {
type: 'unmock'
paths: string[]
url: string
}

export interface IframeMockingDoneEvent {
Expand Down
10 changes: 5 additions & 5 deletions packages/browser/src/client/orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ import { relative } from 'pathe'
import type { SerializedConfig } from 'vitest'
import { getBrowserState, getConfig } from './utils'
import { getUiAPI } from './ui'
import { createModuleMocker } from './tester/msw'
import { createModuleMockerInterceptor } from './tester/msw'

const url = new URL(location.href)
const ID_ALL = '__vitest_all__'

class IframeOrchestrator {
private cancelled = false
private runningFiles = new Set<string>()
private mocker = createModuleMocker()
private interceptor = createModuleMockerInterceptor()
private iframes = new Map<string, HTMLIFrameElement>()

public async init() {
Expand Down Expand Up @@ -187,13 +187,13 @@ class IframeOrchestrator {
break
}
case 'mock:invalidate':
this.mocker.invalidate()
this.interceptor.invalidate()
break
case 'unmock':
await this.mocker.unmock(e.data)
await this.interceptor.delete(e.data.url)
break
case 'mock':
await this.mocker.mock(e.data)
await this.interceptor.register(e.data.module)
break
case 'mock-factory:error':
case 'mock-factory:response':
Expand Down
1 change: 1 addition & 0 deletions packages/browser/src/client/public/esm-client-injector.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ function wrapModule(module) {

window.__vitest_browser_runner__ = {
wrapModule,
wrapDynamicImport: wrapModule,
moduleCache,
config: { __VITEST_CONFIG__ },
viteConfig: { __VITEST_VITE_CONFIG__ },
Expand Down
Loading
Loading