diff --git a/packages/browser/src/client/tester/mocker.ts b/packages/browser/src/client/tester/mocker.ts index 7b6dd34dee53..b5f95f4c3657 100644 --- a/packages/browser/src/client/tester/mocker.ts +++ b/packages/browser/src/client/tester/mocker.ts @@ -59,12 +59,19 @@ export class VitestBrowserClientMocker { ) } const ext = extname(resolved.id) - const url = new URL(`/@id/${resolved.id}`, location.href) - const query = `_vitest_original&ext.${ext}` + const url = new URL(resolved.url, location.href) + const query = `_vitest_original&ext${ext}` const actualUrl = `${url.pathname}${ url.search ? `${url.search}&${query}` : `?${query}` }${url.hash}` - return getBrowserState().wrapModule(() => import(/* @vite-ignore */ actualUrl)) + return getBrowserState().wrapModule(() => import(/* @vite-ignore */ actualUrl)).then((mod) => { + if (!resolved.optimized || typeof mod.default === 'undefined') { + return mod + } + // vite injects this helper for optimized modules, so we try to follow the same behavior + const m = mod.default + return m?.__esModule ? m : { ...((typeof m === 'object' && !Array.isArray(m)) || typeof m === 'function' ? m : {}), default: m } + }) } public async importMock(rawId: string, importer: string) { diff --git a/packages/browser/src/node/rpc.ts b/packages/browser/src/node/rpc.ts index dd7991141622..9db8a86490cc 100644 --- a/packages/browser/src/node/rpc.ts +++ b/packages/browser/src/node/rpc.ts @@ -1,5 +1,5 @@ import { existsSync, promises as fs } from 'node:fs' -import { dirname } from 'pathe' +import { dirname, isAbsolute, join } from 'pathe' import { createBirpc } from 'birpc' import { parse, stringify } from 'flatted' import type { WebSocket } from 'ws' @@ -8,11 +8,12 @@ import type { BrowserCommandContext } from 'vitest/node' import { createDebugger, isFileServingAllowed } from 'vitest/node' import type { WebSocketBrowserEvents, WebSocketBrowserHandlers } from './types' import type { BrowserServer } from './server' -import { resolveMock } from './resolveMock' +import { cleanUrl, resolveMock } from './resolveMock' const debug = createDebugger('vitest:browser:api') const BROWSER_API_PATH = '/__vitest_browser_api__' +const VALID_ID_PREFIX = '/@id/' export function setupBrowserRpc( server: BrowserServer, @@ -118,14 +119,44 @@ export function setupBrowserRpc( ctx.cancelCurrentRun(reason) }, async resolveId(id, importer) { - const result = await project.server.pluginContainer.resolveId( + const resolved = await vite.pluginContainer.resolveId( id, importer, { ssr: false, }, ) - return result + if (!resolved) { + return null + } + const isOptimized = resolved.id.startsWith(withTrailingSlash(vite.config.cacheDir)) + let url: string + // normalise the URL to be acceptible by the browser + // https://github.com/vitejs/vite/blob/e833edf026d495609558fd4fb471cf46809dc369/packages/vite/src/node/plugins/importAnalysis.ts#L335 + const root = vite.config.root + if (resolved.id.startsWith(withTrailingSlash(root))) { + url = resolved.id.slice(root.length) + } + else if ( + resolved.id !== '/@react-refresh' + && isAbsolute(resolved.id) + && existsSync(cleanUrl(resolved.id)) + ) { + url = join('/@fs/', resolved.id) + } + else { + url = resolved.id + } + if (url[0] !== '.' && url[0] !== '/') { + url = id.startsWith(VALID_ID_PREFIX) + ? id + : VALID_ID_PREFIX + id.replace('\0', '__x00__') + } + return { + id: resolved.id, + url, + optimized: isOptimized, + } }, debug(...args) { ctx.logger.console.debug(...args) @@ -242,3 +273,11 @@ export function stringifyReplace(key: string, value: any) { return value } } + +function withTrailingSlash(path: string): string { + if (path[path.length - 1] !== '/') { + return `${path}/` + } + + return path +} diff --git a/packages/browser/src/node/types.ts b/packages/browser/src/node/types.ts index 051b0b7bf8b6..8f783cdc34fe 100644 --- a/packages/browser/src/node/types.ts +++ b/packages/browser/src/node/types.ts @@ -20,7 +20,7 @@ export interface WebSocketBrowserHandlers { resolveId: ( id: string, importer?: string - ) => Promise<{ id: string } | null> + ) => Promise<{ id: string; url: string; optimized: boolean } | null> triggerCommand: ( contextId: string, command: string, diff --git a/test/browser/fixtures/mocking/import-actual-dep.test.ts b/test/browser/fixtures/mocking/import-actual-dep.test.ts new file mode 100644 index 000000000000..f87f0d3d0a41 --- /dev/null +++ b/test/browser/fixtures/mocking/import-actual-dep.test.ts @@ -0,0 +1,14 @@ +import { a, b } from '@vitest/cjs-lib' +import { expect, test, vi } from 'vitest' + +vi.mock(import('@vitest/cjs-lib'), async (importOriginal) => { + const original = await importOriginal() + return { + ...await importOriginal(), + } +}) + +test('mocking works correctly', () => { + expect(a).toBe('a') + expect(b).toBe('b') +}) diff --git a/test/browser/specs/mocking.test.ts b/test/browser/specs/mocking.test.ts index 040a7e764066..a42fc91957fd 100644 --- a/test/browser/specs/mocking.test.ts +++ b/test/browser/specs/mocking.test.ts @@ -24,6 +24,7 @@ test.each([true, false])('mocking works correctly - isolated %s', async (isolate expect(result.stdout).toContain('import-actual-query.test.ts') expect(result.stdout).toContain('import-mock.test.ts') expect(result.stdout).toContain('mocked-do-mock-factory.test.ts') + expect(result.stdout).toContain('import-actual-dep.test.ts') expect(result.exitCode).toBe(0) })