From d3acbd8b967d19361a3235222316edc98b8ff65e Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Tue, 4 Feb 2025 22:38:15 +0900 Subject: [PATCH] fix(browser): fix mocking modules out of root (#7415) --- packages/mocker/src/browser/mocker.ts | 11 ++++++----- packages/mocker/src/node/resolver.ts | 19 +++++++++++++------ .../project1/basic.test.js | 10 ++++++++++ .../project1/vitest.config.ts | 18 ++++++++++++++++++ .../mocking-out-of-root/project2/index.js | 1 + test/browser/specs/mocking.test.ts | 13 +++++++++++++ 6 files changed, 61 insertions(+), 11 deletions(-) create mode 100644 test/browser/fixtures/mocking-out-of-root/project1/basic.test.js create mode 100644 test/browser/fixtures/mocking-out-of-root/project1/vitest.config.ts create mode 100644 test/browser/fixtures/mocking-out-of-root/project2/index.js diff --git a/packages/mocker/src/browser/mocker.ts b/packages/mocker/src/browser/mocker.ts index 292de0811..14a2c785d 100644 --- a/packages/mocker/src/browser/mocker.ts +++ b/packages/mocker/src/browser/mocker.ts @@ -82,13 +82,13 @@ export class ModuleMocker { public async importMock(rawId: string, importer: string): Promise { await this.prepare() - const { resolvedId, redirectUrl } = await this.rpc.resolveMock( + const { resolvedId, resolvedUrl, redirectUrl } = await this.rpc.resolveMock( rawId, importer, { mock: 'auto' }, ) - const mockUrl = this.resolveMockPath(cleanVersion(resolvedId)) + const mockUrl = this.resolveMockPath(cleanVersion(resolvedUrl)) let mock = this.registry.get(mockUrl) if (!mock) { @@ -139,8 +139,8 @@ export class ModuleMocker { ? 'factory' : factoryOrOptions?.spy ? 'spy' : 'auto', }) - .then(async ({ redirectUrl, resolvedId, needsInterop, mockType }) => { - const mockUrl = this.resolveMockPath(cleanVersion(resolvedId)) + .then(async ({ redirectUrl, resolvedId, resolvedUrl, needsInterop, mockType }) => { + const mockUrl = this.resolveMockPath(cleanVersion(resolvedUrl)) this.mockedIds.add(resolvedId) const factory = typeof factoryOrOptions === 'function' ? async () => { @@ -185,7 +185,7 @@ export class ModuleMocker { if (!resolved) { return } - const mockUrl = this.resolveMockPath(cleanVersion(resolved.id)) + const mockUrl = this.resolveMockPath(cleanVersion(resolved.url)) this.mockedIds.add(resolved.id) this.registry.delete(mockUrl) await this.interceptor.delete(mockUrl) @@ -241,6 +241,7 @@ export interface ResolveIdResult { export interface ResolveMockResult { mockType: MockedModuleType resolvedId: string + resolvedUrl: string redirectUrl?: string | null needsInterop?: boolean } diff --git a/packages/mocker/src/node/resolver.ts b/packages/mocker/src/node/resolver.ts index 274c7c22c..bd1d11827 100644 --- a/packages/mocker/src/node/resolver.ts +++ b/packages/mocker/src/node/resolver.ts @@ -26,15 +26,16 @@ export class ServerMockResolver { options: { mock: 'spy' | 'factory' | 'auto' }, ): Promise { const { id, fsPath, external } = await this.resolveMockId(rawId, importer) + const resolvedUrl = this.normalizeResolveIdToUrl({ id }).url if (options.mock === 'factory') { const manifest = getViteDepsManifest(this.server.config) const needsInterop = manifest?.[fsPath]?.needsInterop ?? false - return { mockType: 'manual', resolvedId: id, needsInterop } + return { mockType: 'manual', resolvedId: id, resolvedUrl, needsInterop } } if (options.mock === 'spy') { - return { mockType: 'autospy', resolvedId: id } + return { mockType: 'autospy', resolvedId: id, resolvedUrl } } const redirectUrl = findMockRedirect(this.server.config.root, fsPath, external) @@ -43,6 +44,7 @@ export class ServerMockResolver { mockType: redirectUrl === null ? 'automock' : 'redirect', redirectUrl, resolvedId: id, + resolvedUrl, } } @@ -67,10 +69,14 @@ export class ServerMockResolver { if (!resolved) { return null } + return this.normalizeResolveIdToUrl(resolved) + } + + private normalizeResolveIdToUrl(resolved: { id: string }) { const isOptimized = resolved.id.startsWith(withTrailingSlash(this.server.config.cacheDir)) let url: string // normalise the URL to be acceptable by the browser - // https://github.com/vitejs/vite/blob/e833edf026d495609558fd4fb471cf46809dc369/packages/vite/src/node/plugins/importAnalysis.ts#L335 + // https://github.com/vitejs/vite/blob/14027b0f2a9b01c14815c38aab22baf5b29594bb/packages/vite/src/node/plugins/importAnalysis.ts#L103 const root = this.server.config.root if (resolved.id.startsWith(withTrailingSlash(root))) { url = resolved.id.slice(root.length) @@ -86,9 +92,9 @@ export class ServerMockResolver { url = resolved.id } if (url[0] !== '.' && url[0] !== '/') { - url = id.startsWith(VALID_ID_PREFIX) - ? id - : VALID_ID_PREFIX + id.replace('\0', '__x00__') + url = resolved.id.startsWith(VALID_ID_PREFIX) + ? resolved.id + : VALID_ID_PREFIX + resolved.id.replace('\0', '__x00__') } return { id: resolved.id, @@ -177,6 +183,7 @@ function withTrailingSlash(path: string): string { export interface ServerMockResolution { mockType: 'manual' | 'redirect' | 'automock' | 'autospy' resolvedId: string + resolvedUrl: string needsInterop?: boolean redirectUrl?: string | null } diff --git a/test/browser/fixtures/mocking-out-of-root/project1/basic.test.js b/test/browser/fixtures/mocking-out-of-root/project1/basic.test.js new file mode 100644 index 000000000..3aed726e6 --- /dev/null +++ b/test/browser/fixtures/mocking-out-of-root/project1/basic.test.js @@ -0,0 +1,10 @@ +import { test, expect, vi } from 'vitest'; +import project2 from "../project2/index.js" + +vi.mock("../project2/index.js", () => ({ + default: 'project2-mocked' +})) + +test("basic", () => { + expect(project2).toMatchInlineSnapshot(`"project2-mocked"`) +}) diff --git a/test/browser/fixtures/mocking-out-of-root/project1/vitest.config.ts b/test/browser/fixtures/mocking-out-of-root/project1/vitest.config.ts new file mode 100644 index 000000000..87eb62502 --- /dev/null +++ b/test/browser/fixtures/mocking-out-of-root/project1/vitest.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from 'vitest/config'; +import { fileURLToPath } from 'node:url' +import { instances, provider } from '../../../settings' + +// BROWSER=chromium pnpm -C test/browser test-fixtures --root fixtures/mocking-out-of-root/project1 + +export default defineConfig({ + cacheDir: fileURLToPath(new URL("./node_modules/.vite", import.meta.url)), + root: import.meta.dirname, + test: { + browser: { + enabled: true, + provider: provider, + screenshotFailures: false, + instances, + }, + }, +}) diff --git a/test/browser/fixtures/mocking-out-of-root/project2/index.js b/test/browser/fixtures/mocking-out-of-root/project2/index.js new file mode 100644 index 000000000..4b93f9909 --- /dev/null +++ b/test/browser/fixtures/mocking-out-of-root/project2/index.js @@ -0,0 +1 @@ +export default "project2" diff --git a/test/browser/specs/mocking.test.ts b/test/browser/specs/mocking.test.ts index 1f2a8855f..350ec0569 100644 --- a/test/browser/specs/mocking.test.ts +++ b/test/browser/specs/mocking.test.ts @@ -63,3 +63,16 @@ test('mocking dependency correctly invalidates it on rerun', async () => { expect(vitest.stdout).not.toReportPassedTest('2_not-mocked-import.test.ts', browser) }) }) + +test('mocking out of root', async () => { + const { vitest, ctx } = await runVitest({ + root: 'fixtures/mocking-out-of-root/project1', + }) + onTestFinished(async () => { + await ctx.close() + }) + expect(vitest.stderr).toReportNoErrors() + instances.forEach(({ browser }) => { + expect(vitest.stdout).toReportPassedTest('basic.test.js', browser) + }) +}) -- 2.51.2