diff --git a/packages/browser/src/client/tester/mocker.ts b/packages/browser/src/client/tester/mocker.ts index 8ff6b7f05..5aad7c1e9 100644 --- a/packages/browser/src/client/tester/mocker.ts +++ b/packages/browser/src/client/tester/mocker.ts @@ -138,16 +138,24 @@ export class VitestBrowserClientMocker { public queueMock(id: string, importer: string, factory?: () => any) { const promise = rpc() .resolveMock(id, importer, !!factory) - .then(async ({ mockPath, resolvedId }) => { + .then(async ({ mockPath, resolvedId, needsInterop }) => { this.ids.add(resolvedId) - const urlPaths = resolveMockPaths(resolvedId) + const urlPaths = resolveMockPaths(cleanVersion(resolvedId)) const resolvedMock = typeof mockPath === 'string' - ? new URL(resolvedMockedPath(mockPath), location.href).toString() + ? new URL(resolvedMockedPath(cleanVersion(mockPath)), location.href).toString() : mockPath + const _factory = factory && needsInterop + ? async () => { + const data = await factory() + return { default: data } + } + : factory urlPaths.forEach((url) => { this.mocks[url] = resolvedMock - this.factories[url] = factory! + if (_factory) { + this.factories[url] = _factory + } }) channel.postMessage({ type: 'mock', @@ -170,7 +178,7 @@ export class VitestBrowserClientMocker { return } this.ids.delete(resolved.id) - const urlPaths = resolveMockPaths(resolved.id) + const urlPaths = resolveMockPaths(cleanVersion(resolved.id)) urlPaths.forEach((url) => { delete this.mocks[url] delete this.factories[url] @@ -445,3 +453,8 @@ function resolveMockPaths(path: string) { return paths } + +const versionRegexp = /(\?|&)v=\w{8}/ +function cleanVersion(url: string) { + return url.replace(versionRegexp, '') +} diff --git a/packages/browser/src/client/tester/msw.ts b/packages/browser/src/client/tester/msw.ts index da09efd98..f66738b96 100644 --- a/packages/browser/src/client/tester/msw.ts +++ b/packages/browser/src/client/tester/msw.ts @@ -13,7 +13,7 @@ export function createModuleMocker() { const worker = setupWorker( http.get(/.+/, async ({ request }) => { - const path = removeTimestamp(request.url.slice(location.origin.length)) + const path = cleanQuery(request.url.slice(location.origin.length)) if (!mocks.has(path)) { return passthrough() } @@ -112,9 +112,9 @@ function getFactoryExports(id: string) { } const timestampRegexp = /(\?|&)t=\d{13}/ - -function removeTimestamp(url: string) { - return url.replace(timestampRegexp, '') +const versionRegexp = /(\?|&)v=\w{8}/ +function cleanQuery(url: string) { + return url.replace(timestampRegexp, '').replace(versionRegexp, '') } function passthrough() { diff --git a/packages/browser/src/node/resolveMock.ts b/packages/browser/src/node/resolveMock.ts index ff8b148a4..6326883d9 100644 --- a/packages/browser/src/node/resolveMock.ts +++ b/packages/browser/src/node/resolveMock.ts @@ -1,8 +1,9 @@ -import { existsSync, readdirSync } from 'node:fs' +import { existsSync, readFileSync, readdirSync } from 'node:fs' import { builtinModules } from 'node:module' import { basename, dirname, extname, isAbsolute, join, resolve } from 'pathe' import type { PartialResolvedId } from 'rollup' import type { ResolvedConfig } from 'vitest' +import type { ResolvedConfig as ViteConfig } from 'vite' import type { WorkspaceProject } from 'vitest/node' export async function resolveMock( @@ -14,7 +15,9 @@ export async function resolveMock( const { id, fsPath, external } = await resolveId(project, rawId, importer) if (hasFactory) { - return { type: 'factory' as const, resolvedId: id } + const needsInteropMap = viteDepsInteropMap(project.browser!.vite.config) + const needsInterop = needsInteropMap?.get(fsPath) ?? false + return { type: 'factory' as const, resolvedId: id, needsInterop } } const mockPath = resolveMockPath(project.config.root, fsPath, external) @@ -126,3 +129,29 @@ const postfixRE = /[?#].*$/ export function cleanUrl(url: string): string { return url.replace(postfixRE, '') } + +const metadata = new WeakMap>() + +function viteDepsInteropMap(config: ViteConfig) { + if (metadata.has(config)) { + return metadata.get(config)! + } + const cacheDirPath = getDepsCacheDir(config) + const metadataPath = resolve(cacheDirPath, '_metadata.json') + if (!existsSync(metadataPath)) { + return null + } + const { optimized } = JSON.parse(readFileSync(metadataPath, 'utf-8')) + const needsInteropMap = new Map() + for (const name in optimized) { + const dep = optimized[name] + const file = resolve(cacheDirPath, dep.file) + needsInteropMap.set(file, dep.needsInterop) + } + metadata.set(config, needsInteropMap) + return needsInteropMap +} + +function getDepsCacheDir(config: ViteConfig): string { + return resolve(config.cacheDir, 'deps') +} diff --git a/packages/browser/src/node/types.ts b/packages/browser/src/node/types.ts index ea766de7b..051b0b7bf 100644 --- a/packages/browser/src/node/types.ts +++ b/packages/browser/src/node/types.ts @@ -35,6 +35,7 @@ export interface WebSocketBrowserHandlers { type: 'factory' | 'redirect' | 'automock' mockPath?: string | null resolvedId: string + needsInterop?: boolean }> invalidate: (ids: string[]) => void getBrowserFileSourceMap: ( diff --git a/test/browser/fixtures/mocking/mocking-dep.test.ts b/test/browser/fixtures/mocking/mocking-dep.test.ts new file mode 100644 index 000000000..f9fe23aa1 --- /dev/null +++ b/test/browser/fixtures/mocking/mocking-dep.test.ts @@ -0,0 +1,12 @@ +import { a } from '@vitest/cjs-lib' +import { expect, test, vi } from 'vitest' + +vi.mock(import('@vitest/cjs-lib'), () => { + return { + a: 'mocked', + } +}) + +test('mocking works correctly', () => { + expect(a).toBe('mocked') +}) diff --git a/test/browser/fixtures/mocking/vitest.config.ts b/test/browser/fixtures/mocking/vitest.config.ts index 63c6c3b39..9fbb1e0da 100644 --- a/test/browser/fixtures/mocking/vitest.config.ts +++ b/test/browser/fixtures/mocking/vitest.config.ts @@ -5,6 +5,10 @@ const name = process.env.BROWSER || (provider === 'playwright' ? 'chromium' : 'chrome') export default defineConfig({ + optimizeDeps: { + include: ['@vitest/cjs-lib'], + needsInterop: ['@vitest/cjs-lib'], + }, test: { browser: { enabled: true,