Skip to content

Commit 6c138d6

Browse files
committed
fix(browser): initiate MSW in the same frame as tests
1 parent 9d9bad5 commit 6c138d6

File tree

15 files changed

+155
-201
lines changed

15 files changed

+155
-201
lines changed

packages/browser/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@
8989
"@vitest/mocker": "workspace:*",
9090
"@vitest/utils": "workspace:*",
9191
"magic-string": "^0.30.12",
92-
"msw": "^2.4.9",
92+
"msw": "^2.4.13",
9393
"sirv": "^3.0.0",
9494
"tinyrainbow": "^1.2.0",
9595
"ws": "^8.18.0"

packages/browser/src/client/channel.ts

Lines changed: 1 addition & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import type { CancelReason } from '@vitest/runner'
2-
import type { MockedModuleSerialized } from '@vitest/mocker'
32
import { getBrowserState } from './utils'
43

54
export interface IframeDoneEvent {
@@ -23,46 +22,6 @@ export interface IframeViewportEvent {
2322
id: string
2423
}
2524

26-
export interface IframeMockEvent {
27-
type: 'mock'
28-
module: MockedModuleSerialized
29-
}
30-
31-
export interface IframeUnmockEvent {
32-
type: 'unmock'
33-
url: string
34-
}
35-
36-
export interface IframeMockingDoneEvent {
37-
type: 'mock:done' | 'unmock:done'
38-
}
39-
40-
export interface IframeMockFactoryRequestEvent {
41-
type: 'mock-factory:request'
42-
eventId: string
43-
id: string
44-
}
45-
46-
export interface IframeMockFactoryResponseEvent {
47-
type: 'mock-factory:response'
48-
eventId: string
49-
exports: string[]
50-
}
51-
52-
export interface IframeMockFactoryErrorEvent {
53-
type: 'mock-factory:error'
54-
eventId: string
55-
error: any
56-
}
57-
58-
export interface IframeViewportChannelEvent {
59-
type: 'viewport:done' | 'viewport:fail'
60-
}
61-
62-
export interface IframeMockInvalidateEvent {
63-
type: 'mock:invalidate'
64-
}
65-
6625
export interface GlobalChannelTestRunCanceledEvent {
6726
type: 'cancel'
6827
reason: CancelReason
@@ -74,16 +33,8 @@ export type IframeChannelIncomingEvent =
7433
| IframeViewportEvent
7534
| IframeErrorEvent
7635
| IframeDoneEvent
77-
| IframeMockEvent
78-
| IframeUnmockEvent
79-
| IframeMockFactoryResponseEvent
80-
| IframeMockFactoryErrorEvent
81-
| IframeMockInvalidateEvent
8236

83-
export type IframeChannelOutgoingEvent =
84-
| IframeMockFactoryRequestEvent
85-
| IframeViewportChannelEvent
86-
| IframeMockingDoneEvent
37+
export type IframeChannelOutgoingEvent = never
8738

8839
export type IframeChannelEvent =
8940
| IframeChannelIncomingEvent

packages/browser/src/client/orchestrator.ts

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,13 @@ import { relative } from 'pathe'
55
import type { SerializedConfig } from 'vitest'
66
import { getBrowserState, getConfig } from './utils'
77
import { getUiAPI } from './ui'
8-
import { createModuleMockerInterceptor } from './tester/msw'
98

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

1312
class IframeOrchestrator {
1413
private cancelled = false
1514
private runningFiles = new Set<string>()
16-
private interceptor = createModuleMockerInterceptor()
1715
private iframes = new Map<string, HTMLIFrameElement>()
1816

1917
public async init() {
@@ -186,19 +184,6 @@ class IframeOrchestrator {
186184
}
187185
break
188186
}
189-
case 'mock:invalidate':
190-
this.interceptor.invalidate()
191-
break
192-
case 'unmock':
193-
await this.interceptor.delete(e.data.url)
194-
break
195-
case 'mock':
196-
await this.interceptor.register(e.data.module)
197-
break
198-
case 'mock-factory:error':
199-
case 'mock-factory:response':
200-
// handled manually
201-
break
202187
default: {
203188
e.data satisfies never
204189

packages/browser/src/client/tester/mocker.ts

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,7 @@
1-
import type { IframeChannelOutgoingEvent, IframeMockFactoryErrorEvent, IframeMockFactoryResponseEvent } from '@vitest/browser/client'
2-
import { channel } from '@vitest/browser/client'
31
import { ModuleMocker } from '@vitest/mocker/browser'
42
import { getBrowserState } from '../utils'
53

64
export class VitestBrowserClientMocker extends ModuleMocker {
7-
setupWorker() {
8-
channel.addEventListener(
9-
'message',
10-
async (e: MessageEvent<IframeChannelOutgoingEvent>) => {
11-
if (e.data.type === 'mock-factory:request') {
12-
try {
13-
const module = await this.resolveFactoryModule(e.data.id)
14-
const exports = Object.keys(module)
15-
channel.postMessage({
16-
type: 'mock-factory:response',
17-
eventId: e.data.eventId,
18-
exports,
19-
} satisfies IframeMockFactoryResponseEvent)
20-
}
21-
catch (err: any) {
22-
channel.postMessage({
23-
type: 'mock-factory:error',
24-
eventId: e.data.eventId,
25-
error: {
26-
name: err.name,
27-
message: err.message,
28-
stack: err.stack,
29-
},
30-
} satisfies IframeMockFactoryErrorEvent)
31-
}
32-
}
33-
},
34-
)
35-
}
36-
375
// default "vi" utility tries to access mock context to avoid circular dependencies
386
public getMockContext() {
397
return { callstack: null }
Lines changed: 4 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,9 @@
1-
import { channel } from '@vitest/browser/client'
2-
import type {
3-
IframeChannelEvent,
4-
IframeMockFactoryRequestEvent,
5-
IframeMockingDoneEvent,
6-
} from '@vitest/browser/client'
7-
import type { MockedModuleSerialized } from '@vitest/mocker'
8-
import { ManualMockedModule } from '@vitest/mocker'
91
import { ModuleMockerMSWInterceptor } from '@vitest/mocker/browser'
10-
import { nanoid } from '@vitest/utils'
11-
12-
export class VitestBrowserModuleMockerInterceptor extends ModuleMockerMSWInterceptor {
13-
override async register(event: MockedModuleSerialized): Promise<void> {
14-
if (event.type === 'manual') {
15-
const module = ManualMockedModule.fromJSON(event, async () => {
16-
const keys = await getFactoryExports(event.url)
17-
return Object.fromEntries(keys.map(key => [key, null]))
18-
})
19-
await super.register(module)
20-
}
21-
else {
22-
await this.init()
23-
this.mocks.register(event)
24-
}
25-
channel.postMessage(<IframeMockingDoneEvent>{ type: 'mock:done' })
26-
}
27-
28-
override async delete(url: string): Promise<void> {
29-
await super.delete(url)
30-
channel.postMessage(<IframeMockingDoneEvent>{ type: 'unmock:done' })
31-
}
32-
}
2+
import { getConfig } from '../utils'
333

344
export function createModuleMockerInterceptor() {
35-
return new VitestBrowserModuleMockerInterceptor({
5+
const debug = getConfig().env.VITEST_BROWSER_DEBUG
6+
return new ModuleMockerMSWInterceptor({
367
globalThisAccessor: '"__vitest_mocker__"',
378
mswOptions: {
389
serviceWorker: {
@@ -42,31 +13,7 @@ export function createModuleMockerInterceptor() {
4213
},
4314
},
4415
onUnhandledRequest: 'bypass',
45-
quiet: true,
16+
quiet: !(debug && debug !== 'false'),
4617
},
4718
})
4819
}
49-
50-
function getFactoryExports(id: string) {
51-
const eventId = nanoid()
52-
channel.postMessage({
53-
type: 'mock-factory:request',
54-
eventId,
55-
id,
56-
} satisfies IframeMockFactoryRequestEvent)
57-
return new Promise<string[]>((resolve, reject) => {
58-
channel.addEventListener(
59-
'message',
60-
function onMessage(e: MessageEvent<IframeChannelEvent>) {
61-
if (e.data.type === 'mock-factory:response' && e.data.eventId === eventId) {
62-
resolve(e.data.exports)
63-
channel.removeEventListener('message', onMessage)
64-
}
65-
if (e.data.type === 'mock-factory:error' && e.data.eventId === eventId) {
66-
reject(e.data.error)
67-
channel.removeEventListener('message', onMessage)
68-
}
69-
},
70-
)
71-
})
72-
}

packages/browser/src/client/tester/tester.ts

Lines changed: 5 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { SpyModule, collectTests, setupCommonEnv, startCoverageInsideWorker, startTests, stopCoverageInsideWorker } from 'vitest/browser'
22
import { page, userEvent } from '@vitest/browser/context'
3-
import type { IframeMockEvent, IframeMockInvalidateEvent, IframeUnmockEvent } from '@vitest/browser/client'
4-
import { channel, client, onCancel, waitForChannel } from '@vitest/browser/client'
3+
import { channel, client, onCancel } from '@vitest/browser/client'
54
import { executor, getBrowserState, getConfig, getWorkerState } from '../utils'
65
import { setupDialogsSpy } from './dialog'
76
import { setupConsoleLogSpy } from './logger'
87
import { createSafeRpc } from './rpc'
98
import { browserHashMap, initiateRunner } from './runner'
109
import { VitestBrowserClientMocker } from './mocker'
1110
import { setupExpectDom } from './expect-element'
11+
import { createModuleMockerInterceptor } from './msw'
1212

1313
const cleanupSymbol = Symbol.for('vitest:component-cleanup')
1414

@@ -34,28 +34,10 @@ async function prepareTestEnvironment(files: string[]) {
3434
state.onCancel = onCancel
3535
state.rpc = rpc as any
3636

37+
// TODO: expose `worker`
38+
const interceptor = createModuleMockerInterceptor()
3739
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-
},
40+
interceptor,
5941
rpc,
6042
SpyModule.spyOn,
6143
{
@@ -79,8 +61,6 @@ async function prepareTestEnvironment(files: string[]) {
7961
}
8062
})
8163

82-
mocker.setupWorker()
83-
8464
onCancel.then((reason) => {
8565
runner.onCancel?.(reason)
8666
})
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { fileURLToPath } from 'node:url'
2+
import { resolve } from 'pathe'
3+
4+
const pkgRoot = resolve(fileURLToPath(import.meta.url), '../..')
5+
export const distRoot = resolve(pkgRoot, 'dist')

packages/browser/src/node/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import BrowserPlugin from './plugin'
1010
export type { BrowserServer } from './server'
1111
export { createBrowserPool } from './pool'
1212

13+
export { distRoot } from './constants'
14+
1315
export async function createBrowserServer(
1416
project: WorkspaceProject,
1517
configFile: string | undefined,

packages/browser/src/node/plugin.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { fileURLToPath } from 'node:url'
21
import { createRequire } from 'node:module'
32
import { lstatSync, readFileSync } from 'node:fs'
43
import type { Stats } from 'node:fs'
@@ -14,13 +13,12 @@ import BrowserContext from './plugins/pluginContext'
1413
import type { BrowserServer } from './server'
1514
import { resolveOrchestrator } from './serverOrchestrator'
1615
import { resolveTester } from './serverTester'
16+
import { distRoot } from './constants'
1717

1818
export type { BrowserCommand } from 'vitest/node'
1919
export { defineBrowserCommand } from './commands/utils'
2020

2121
export default (browserServer: BrowserServer, base = '/'): Plugin[] => {
22-
const pkgRoot = resolve(fileURLToPath(import.meta.url), '../..')
23-
const distRoot = resolve(pkgRoot, 'dist')
2422
const project = browserServer.project
2523

2624
function isPackageExists(pkg: string, root: string) {
@@ -322,6 +320,12 @@ export default (browserServer: BrowserServer, base = '/'): Plugin[] => {
322320
BrowserContext(browserServer),
323321
dynamicImportPlugin({
324322
globalThisAccessor: '"__vitest_browser_runner__"',
323+
filter(id) {
324+
if (id.includes(distRoot)) {
325+
return false
326+
}
327+
return true
328+
},
325329
}),
326330
{
327331
name: 'vitest:browser:config',

packages/mocker/src/browser/interceptor-msw.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ export interface ModuleMockerMSWInterceptorOptions {
3434
export class ModuleMockerMSWInterceptor implements ModuleMockerInterceptor {
3535
protected readonly mocks: MockerRegistry = new MockerRegistry()
3636

37-
private started = false
38-
private startPromise: undefined | Promise<unknown>
37+
private startPromise: undefined | Promise<SetupWorker>
38+
private worker: undefined | SetupWorker
3939

4040
constructor(
4141
private readonly options: ModuleMockerMSWInterceptorOptions = {},
@@ -78,9 +78,9 @@ export class ModuleMockerMSWInterceptor implements ModuleMockerInterceptor {
7878
})
7979
}
8080

81-
protected async init(): Promise<unknown> {
82-
if (this.started) {
83-
return
81+
protected async init(): Promise<SetupWorker> {
82+
if (this.worker) {
83+
return this.worker
8484
}
8585
if (this.startPromise) {
8686
return this.startPromise
@@ -126,13 +126,13 @@ export class ModuleMockerMSWInterceptor implements ModuleMockerInterceptor {
126126
}
127127
}),
128128
)
129-
return worker.start(this.options.mswOptions)
129+
return worker.start(this.options.mswOptions).then(() => worker)
130130
})
131131
.finally(() => {
132-
this.started = true
132+
this.worker = worker
133133
this.startPromise = undefined
134134
})
135-
await this.startPromise
135+
return await this.startPromise
136136
}
137137
}
138138

0 commit comments

Comments
 (0)