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: implement module mocking in browser mode #5765

Merged
merged 33 commits into from
May 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
87736f3
feat: implement module mocking in the browser
sheremet-va May 23, 2024
e90b2f4
chore: cleanup
sheremet-va May 23, 2024
f6e724a
fix: correctly invalidate mocked module
sheremet-va May 23, 2024
74d14ad
feat: support __mocks__ folder
sheremet-va May 23, 2024
ffaf094
feat: support automocking
sheremet-va May 23, 2024
0c4de84
fix: remove slowHijackESM
sheremet-va May 23, 2024
f5c00fe
fix: add uniqe name to the error
sheremet-va May 23, 2024
38e9028
test: update module browser tests
sheremet-va May 24, 2024
4b0969f
chore: todo
sheremet-va May 24, 2024
b944a4b
chore: expose browser name
sheremet-va May 24, 2024
01aae91
chore: fix the testResults count
sheremet-va May 24, 2024
a727812
chore: fix mocking in --no-isolate mode
sheremet-va May 24, 2024
ab8d756
test: add tests for mocking
sheremet-va May 24, 2024
1634d0d
test: add test for nested dependencies
sheremet-va May 24, 2024
47ef4a0
chore: don't bundle tinyspy and tinybecnh with std-env
sheremet-va May 24, 2024
6b932d3
chore: cleanup
sheremet-va May 24, 2024
83ffe11
chore: print better debug error
sheremet-va May 24, 2024
3fe6f59
chore: remove exportAll util
sheremet-va May 24, 2024
97b7ecf
chore: remove scripts from the resolved config
sheremet-va May 24, 2024
d0896e6
fix: do not hoist vi.mock in vitest package
sheremet-va May 24, 2024
5e904df
chore: only ignore the Vitest dist
sheremet-va May 24, 2024
d9fcdc5
chore: fix typing issue
sheremet-va May 24, 2024
95b1430
feat: split the ws rpc for the browser mode
sheremet-va May 25, 2024
d7beeda
chore: fix types
sheremet-va May 25, 2024
ddca7a1
feat: support all export syntaxes for automocking
sheremet-va May 25, 2024
9677d06
chore: remove Symbol from automocker
sheremet-va May 28, 2024
5b06fb5
chore: remove await
sheremet-va May 28, 2024
a77531e
feat: support vi.importActual
sheremet-va May 28, 2024
958e066
chore: remove browser mock section
sheremet-va May 28, 2024
2bb081b
chore: move test
sheremet-va May 31, 2024
cbc9567
feat: support vi.importMock
sheremet-va May 31, 2024
ab8af86
test: add tets for doMock
sheremet-va May 31, 2024
927449e
fix: correctly parse url on Windows
sheremet-va May 31, 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
4 changes: 0 additions & 4 deletions docs/api/vi.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,6 @@ 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.
:::

::: warning
The [browser mode](/guide/browser) does not presently support mocking modules. You can track this feature in the GitHub [issue](https://github.com/vitest-dev/vitest/issues/3046).
:::

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.

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.
Expand Down
13 changes: 1 addition & 12 deletions docs/config/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -1487,7 +1487,7 @@ Listen to port and serve API. When set to true, the default port is 51204

### browser {#browser}

- **Type:** `{ enabled?, name?, provider?, headless?, api?, slowHijackESM? }`
- **Type:** `{ enabled?, name?, provider?, headless?, api? }`
- **Default:** `{ enabled: false, headless: process.env.CI, api: 63315 }`
- **CLI:** `--browser`, `--browser=<name>`, `--browser.name=chrome --browser.headless`

Expand Down Expand Up @@ -1601,17 +1601,6 @@ To have a better type safety when using built-in providers, you can add one of t
```
:::

#### browser.slowHijackESM {#browser-slowhijackesm}

- **Type:** `boolean`
- **Default:** `false`

When running tests in Node.js Vitest can use its own module resolution to easily mock modules with `vi.mock` syntax. However it's not so easy to replicate ES module resolution in browser, so we need to transform your source files before browser can consume it.

This option has no effect on tests running inside Node.js.

If you rely on spying on ES modules with `vi.spyOn`, you can enable this experimental feature to allow spying on module exports.

#### browser.ui {#browser-ui}

- **Type:** `boolean`
Expand Down
1 change: 0 additions & 1 deletion docs/guide/cli-table.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@
| `--browser.api.strictPort` | Set to true to exit if port is already in use, instead of automatically trying the next available port |
| `--browser.provider <name>` | Provider used to run browser tests. Some browsers are only available for specific providers. Can be "webdriverio", "playwright", or the path to a custom provider. Visit [`browser.provider`](https://vitest.dev/config/#browser-provider) for more information (default: `"webdriverio"`) |
| `--browser.providerOptions <options>` | Options that are passed down to a browser provider. Visit [`browser.providerOptions`](https://vitest.dev/config/#browser-provideroptions) for more information |
| `--browser.slowHijackESM` | Let Vitest use its own module resolution on the browser to enable APIs such as vi.mock and vi.spyOn. Visit [`browser.slowHijackESM`](https://vitest.dev/config/#browser-slowhijackesm) for more information (default: `false`) |
| `--browser.isolate` | Run every browser test file in isolation. To disable isolation, use `--browser.isolate=false` (default: `true`) |
| `--pool <pool>` | Specify pool, if not running in the browser (default: `threads`) |
| `--poolOptions.threads.isolate` | Isolate tests in threads pool (default: `true`) |
Expand Down
4 changes: 4 additions & 0 deletions packages/browser/context.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ export const server: {
* Name of the browser provider.
*/
provider: string
/**
* Name of the current browser.
*/
browser: string
/**
* Available commands for the browser.
* @see {@link https://vitest.dev/guide/browser#commands}
Expand Down
2 changes: 2 additions & 0 deletions packages/browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@
"@vitest/ui": "workspace:*",
"@vitest/ws-client": "workspace:*",
"@wdio/protocols": "^8.32.0",
"birpc": "0.2.17",
"flatted": "^3.3.1",
"periscopic": "^4.0.2",
"playwright": "^1.44.0",
"playwright-core": "^1.44.0",
Expand Down
107 changes: 101 additions & 6 deletions packages/browser/src/client/client.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,116 @@
import type { CancelReason } from '@vitest/runner'
import { createClient } from '@vitest/ws-client'
import { type BirpcReturn, createBirpc } from 'birpc'
import type { WebSocketBrowserEvents, WebSocketBrowserHandlers } from 'vitest'
import { parse, stringify } from 'flatted'
import type { VitestBrowserClientMocker } from './mocker'
import { getBrowserState } from './utils'

export const PORT = import.meta.hot ? '51204' : location.port
export const HOST = [location.hostname, PORT].filter(Boolean).join(':')
export const SESSION_ID = crypto.randomUUID()
export const ENTRY_URL = `${
location.protocol === 'https:' ? 'wss:' : 'ws:'
}//${HOST}/__vitest_api__`
}//${HOST}/__vitest_browser_api__?type=${getBrowserState().type}&sessionId=${SESSION_ID}`

let setCancel = (_: CancelReason) => {}
export const onCancel = new Promise<CancelReason>((resolve) => {
setCancel = resolve
})

export const client = createClient(ENTRY_URL, {
handlers: {
export interface VitestBrowserClient {
rpc: BrowserRPC
ws: WebSocket
waitForConnection: () => Promise<void>
}

type BrowserRPC = BirpcReturn<WebSocketBrowserHandlers, WebSocketBrowserEvents>

function createClient() {
const autoReconnect = true
const reconnectInterval = 2000
const reconnectTries = 10
const connectTimeout = 60000

let tries = reconnectTries

const ctx: VitestBrowserClient = {
ws: new WebSocket(ENTRY_URL),
waitForConnection,
} as VitestBrowserClient

let onMessage: Function

ctx.rpc = createBirpc<WebSocketBrowserHandlers, WebSocketBrowserEvents>({
onCancel: setCancel,
},
})
async startMocking(id: string) {
// @ts-expect-error not typed global
if (typeof __vitest_mocker__ === 'undefined')
throw new Error(`Cannot mock modules in the orchestrator process`)
// @ts-expect-error not typed global
const mocker = __vitest_mocker__ as VitestBrowserClientMocker
const exports = await mocker.resolve(id)
return Object.keys(exports)
},
}, {
post: msg => ctx.ws.send(msg),
on: fn => (onMessage = fn),
serialize: e => stringify(e, (_, v) => {
if (v instanceof Error) {
return {
name: v.name,
message: v.message,
stack: v.stack,
}
}
return v
}),
deserialize: parse,
onTimeoutError(functionName) {
throw new Error(`[vitest-browser]: Timeout calling "${functionName}"`)
},
})

let openPromise: Promise<void>

function reconnect(reset = false) {
if (reset)
tries = reconnectTries
ctx.ws = new WebSocket(ENTRY_URL)
registerWS()
}

function registerWS() {
openPromise = new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error(`Cannot connect to the server in ${connectTimeout / 1000} seconds`))
}, connectTimeout)?.unref?.()
if (ctx.ws.OPEN === ctx.ws.readyState)
resolve()
// still have a listener even if it's already open to update tries
ctx.ws.addEventListener('open', () => {
tries = reconnectTries
resolve()
clearTimeout(timeout)
})
})
ctx.ws.addEventListener('message', (v) => {
onMessage(v.data)
})
ctx.ws.addEventListener('close', () => {
tries -= 1
if (autoReconnect && tries > 0)
setTimeout(reconnect, reconnectInterval)
})
}

registerWS()

function waitForConnection() {
return openPromise
}

return ctx
}

export const client = createClient()
export const channel = new BroadcastChannel('vitest')
7 changes: 5 additions & 2 deletions packages/browser/src/client/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,11 @@ export async function setupConsoleLogSpy() {

console.trace = (...args: unknown[]) => {
const content = processLog(args)
const error = new Error('Trace')
const stack = (error.stack || '').split('\n').slice(2).join('\n')
const error = new Error('$$Trace')
const stack = (error.stack || '')
.split('\n')
.slice(error.stack?.includes('$$Trace') ? 2 : 1)
.join('\n')
sendLog('stdout', `${content}\n${stack}`)
return trace(...args)
}
Expand Down
13 changes: 8 additions & 5 deletions packages/browser/src/client/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,15 @@ client.ws.addEventListener('open', async () => {
const filenames = e.data.filenames
filenames.forEach(filename => runningFiles.delete(filename))

const iframeId = filenames.length > 1 ? ID_ALL : filenames[0]
iframes.get(iframeId)?.remove()
iframes.delete(iframeId)

if (!runningFiles.size)
if (!runningFiles.size) {
await done()
}
else {
// keep the last iframe
const iframeId = filenames.length > 1 ? ID_ALL : filenames[0]
iframes.get(iframeId)?.remove()
iframes.delete(iframeId)
}
break
}
// error happened at the top level, this should never happen in user code, but it can trigger during development
Expand Down
Loading
Loading