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

fix(browser): initiate MSW in the same frame as tests #6772

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
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 @@ -3,24 +3,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 @@ -157,6 +157,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 @@ -322,6 +340,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