Skip to content

Commit

Permalink
fix(browser): initiate MSW in the same frame as tests (#6772)
Browse files Browse the repository at this point in the history
  • Loading branch information
sheremet-va authored Oct 28, 2024
1 parent 39041ee commit 2444ff2
Show file tree
Hide file tree
Showing 13 changed files with 76 additions and 216 deletions.
51 changes: 1 addition & 50 deletions packages/browser/src/client/channel.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { MockedModuleSerialized } from '@vitest/mocker'
import type { CancelReason } from '@vitest/runner'
import { getBrowserState } from './utils'

Expand All @@ -23,46 +22,6 @@ export interface IframeViewportEvent {
id: string
}

export interface IframeMockEvent {
type: 'mock'
module: MockedModuleSerialized
}

export interface IframeUnmockEvent {
type: 'unmock'
url: string
}

export interface IframeMockingDoneEvent {
type: 'mock:done' | 'unmock:done'
}

export interface IframeMockFactoryRequestEvent {
type: 'mock-factory:request'
eventId: string
id: string
}

export interface IframeMockFactoryResponseEvent {
type: 'mock-factory:response'
eventId: string
exports: string[]
}

export interface IframeMockFactoryErrorEvent {
type: 'mock-factory:error'
eventId: string
error: any
}

export interface IframeViewportChannelEvent {
type: 'viewport:done' | 'viewport:fail'
}

export interface IframeMockInvalidateEvent {
type: 'mock:invalidate'
}

export interface GlobalChannelTestRunCanceledEvent {
type: 'cancel'
reason: CancelReason
Expand All @@ -74,16 +33,8 @@ export type IframeChannelIncomingEvent =
| IframeViewportEvent
| IframeErrorEvent
| IframeDoneEvent
| IframeMockEvent
| IframeUnmockEvent
| IframeMockFactoryResponseEvent
| IframeMockFactoryErrorEvent
| IframeMockInvalidateEvent

export type IframeChannelOutgoingEvent =
| IframeMockFactoryRequestEvent
| IframeViewportChannelEvent
| IframeMockingDoneEvent
export type IframeChannelOutgoingEvent = never

export type IframeChannelEvent =
| IframeChannelIncomingEvent
Expand Down
15 changes: 0 additions & 15 deletions packages/browser/src/client/orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { channel, client } from '@vitest/browser/client'
import { globalChannel, type GlobalChannelIncomingEvent, type IframeChannelEvent, type IframeChannelIncomingEvent } from '@vitest/browser/client'
import { generateHash } from '@vitest/runner/utils'
import { relative } from 'pathe'
import { createModuleMockerInterceptor } from './tester/msw'
import { getUiAPI } from './ui'
import { getBrowserState, getConfig } from './utils'

Expand All @@ -13,7 +12,6 @@ const ID_ALL = '__vitest_all__'
class IframeOrchestrator {
private cancelled = false
private runningFiles = new Set<string>()
private interceptor = createModuleMockerInterceptor()
private iframes = new Map<string, HTMLIFrameElement>()

public async init() {
Expand Down Expand Up @@ -186,19 +184,6 @@ class IframeOrchestrator {
}
break
}
case 'mock:invalidate':
this.interceptor.invalidate()
break
case 'unmock':
await this.interceptor.delete(e.data.url)
break
case 'mock':
await this.interceptor.register(e.data.module)
break
case 'mock-factory:error':
case 'mock-factory:response':
// handled manually
break
default: {
e.data satisfies never

Expand Down
32 changes: 0 additions & 32 deletions packages/browser/src/client/tester/mocker.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,7 @@
import type { IframeChannelOutgoingEvent, IframeMockFactoryErrorEvent, IframeMockFactoryResponseEvent } from '@vitest/browser/client'
import { channel } from '@vitest/browser/client'
import { ModuleMocker } from '@vitest/mocker/browser'
import { getBrowserState } from '../utils'

export class VitestBrowserClientMocker extends ModuleMocker {
setupWorker() {
channel.addEventListener(
'message',
async (e: MessageEvent<IframeChannelOutgoingEvent>) => {
if (e.data.type === 'mock-factory:request') {
try {
const module = await this.resolveFactoryModule(e.data.id)
const exports = Object.keys(module)
channel.postMessage({
type: 'mock-factory:response',
eventId: e.data.eventId,
exports,
} satisfies IframeMockFactoryResponseEvent)
}
catch (err: any) {
channel.postMessage({
type: 'mock-factory:error',
eventId: e.data.eventId,
error: {
name: err.name,
message: err.message,
stack: err.stack,
},
} satisfies IframeMockFactoryErrorEvent)
}
}
},
)
}

// default "vi" utility tries to access mock context to avoid circular dependencies
public getMockContext() {
return { callstack: null }
Expand Down
61 changes: 4 additions & 57 deletions packages/browser/src/client/tester/msw.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,9 @@
import type {
IframeChannelEvent,
IframeMockFactoryRequestEvent,
IframeMockingDoneEvent,
} from '@vitest/browser/client'
import type { MockedModuleSerialized } from '@vitest/mocker'
import { channel } from '@vitest/browser/client'
import { ManualMockedModule } from '@vitest/mocker'
import { ModuleMockerMSWInterceptor } from '@vitest/mocker/browser'
import { nanoid } from '@vitest/utils'

export class VitestBrowserModuleMockerInterceptor extends ModuleMockerMSWInterceptor {
override async register(event: MockedModuleSerialized): Promise<void> {
if (event.type === 'manual') {
const module = ManualMockedModule.fromJSON(event, async () => {
const keys = await getFactoryExports(event.url)
return Object.fromEntries(keys.map(key => [key, null]))
})
await super.register(module)
}
else {
await this.init()
this.mocks.register(event)
}
channel.postMessage(<IframeMockingDoneEvent>{ type: 'mock:done' })
}

override async delete(url: string): Promise<void> {
await super.delete(url)
channel.postMessage(<IframeMockingDoneEvent>{ type: 'unmock:done' })
}
}
import { getConfig } from '../utils'

export function createModuleMockerInterceptor() {
return new VitestBrowserModuleMockerInterceptor({
const debug = getConfig().env.VITEST_BROWSER_DEBUG
return new ModuleMockerMSWInterceptor({
globalThisAccessor: '"__vitest_mocker__"',
mswOptions: {
serviceWorker: {
Expand All @@ -42,31 +13,7 @@ export function createModuleMockerInterceptor() {
},
},
onUnhandledRequest: 'bypass',
quiet: true,
quiet: !(debug && debug !== 'false'),
},
})
}

function getFactoryExports(id: string) {
const eventId = nanoid()
channel.postMessage({
type: 'mock-factory:request',
eventId,
id,
} satisfies IframeMockFactoryRequestEvent)
return new Promise<string[]>((resolve, reject) => {
channel.addEventListener(
'message',
function onMessage(e: MessageEvent<IframeChannelEvent>) {
if (e.data.type === 'mock-factory:response' && e.data.eventId === eventId) {
resolve(e.data.exports)
channel.removeEventListener('message', onMessage)
}
if (e.data.type === 'mock-factory:error' && e.data.eventId === eventId) {
reject(e.data.error)
channel.removeEventListener('message', onMessage)
}
},
)
})
}
30 changes: 5 additions & 25 deletions packages/browser/src/client/tester/tester.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import type { IframeMockEvent, IframeMockInvalidateEvent, IframeUnmockEvent } from '@vitest/browser/client'
import { channel, client, onCancel, waitForChannel } from '@vitest/browser/client'
import { channel, client, onCancel } from '@vitest/browser/client'
import { page, userEvent } from '@vitest/browser/context'
import { collectTests, setupCommonEnv, SpyModule, startCoverageInsideWorker, startTests, stopCoverageInsideWorker } from 'vitest/browser'
import { executor, getBrowserState, getConfig, getWorkerState } from '../utils'
import { setupDialogsSpy } from './dialog'
import { setupExpectDom } from './expect-element'
import { setupConsoleLogSpy } from './logger'
import { VitestBrowserClientMocker } from './mocker'
import { createModuleMockerInterceptor } from './msw'
import { createSafeRpc } from './rpc'
import { browserHashMap, initiateRunner } from './runner'

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

// TODO: expose `worker`
const interceptor = createModuleMockerInterceptor()
const mocker = new VitestBrowserClientMocker(
{
async delete(url: string) {
channel.postMessage({
type: 'unmock',
url,
} satisfies IframeUnmockEvent)
await waitForChannel('unmock:done')
},
async register(module) {
channel.postMessage({
type: 'mock',
module: module.toJSON(),
} satisfies IframeMockEvent)
await waitForChannel('mock:done')
},
invalidate() {
channel.postMessage({
type: 'mock:invalidate',
} satisfies IframeMockInvalidateEvent)
},
},
interceptor,
rpc,
SpyModule.spyOn,
{
Expand All @@ -79,8 +61,6 @@ async function prepareTestEnvironment(files: string[]) {
}
})

mocker.setupWorker()

onCancel.then((reason) => {
runner.onCancel?.(reason)
})
Expand Down
5 changes: 5 additions & 0 deletions packages/browser/src/node/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { fileURLToPath } from 'node:url'
import { resolve } from 'pathe'

const pkgRoot = resolve(fileURLToPath(import.meta.url), '../..')
export const distRoot = resolve(pkgRoot, 'dist')
2 changes: 2 additions & 0 deletions packages/browser/src/node/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import BrowserPlugin from './plugin'
import { setupBrowserRpc } from './rpc'
import { BrowserServer } from './server'

export { distRoot } from './constants'
export { createBrowserPool } from './pool'

export type { BrowserServer } from './server'

export async function createBrowserServer(
Expand Down
30 changes: 27 additions & 3 deletions packages/browser/src/node/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,24 @@ import type { WorkspaceProject } from 'vitest/node'
import type { BrowserServer } from './server'
import { lstatSync, readFileSync } from 'node:fs'
import { createRequire } from 'node:module'
import { fileURLToPath } from 'node:url'
import { dynamicImportPlugin } from '@vitest/mocker/node'
import { toArray } from '@vitest/utils'
import MagicString from 'magic-string'
import { basename, dirname, extname, resolve } from 'pathe'
import sirv from 'sirv'
import { coverageConfigDefaults, type Plugin } from 'vitest/config'
import { getFilePoolName, resolveApiServerConfig, resolveFsAllow, distDir as vitestDist } from 'vitest/node'
import { distRoot } from './constants'
import BrowserContext from './plugins/pluginContext'
import { resolveOrchestrator } from './serverOrchestrator'
import { resolveTester } from './serverTester'

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

const versionRegexp = /(?:\?|&)v=\w{8}/

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

function isPackageExists(pkg: string, root: string) {
Expand Down Expand Up @@ -160,6 +160,24 @@ export default (browserServer: BrowserServer, base = '/'): Plugin[] => {
res.end(buffer)
})
}
server.middlewares.use((req, res, next) => {
// 9000 mega head move
// Vite always caches optimized dependencies, but users might mock
// them in _some_ tests, while keeping original modules in others
// there is no way to configure that in Vite, so we patch it here
// to always ignore the cache-control set by Vite in the next middleware
if (req.url && versionRegexp.test(req.url) && !req.url.includes('chunk-')) {
res.setHeader('Cache-Control', 'no-cache')
const setHeader = res.setHeader.bind(res)
res.setHeader = function (name, value) {
if (name === 'Cache-Control') {
return res
}
return setHeader(name, value)
}
}
next()
})
},
},
{
Expand Down Expand Up @@ -325,6 +343,12 @@ export default (browserServer: BrowserServer, base = '/'): Plugin[] => {
BrowserContext(browserServer),
dynamicImportPlugin({
globalThisAccessor: '"__vitest_browser_runner__"',
filter(id) {
if (id.includes(distRoot)) {
return false
}
return true
},
}),
{
name: 'vitest:browser:config',
Expand Down
Loading

0 comments on commit 2444ff2

Please sign in to comment.