From 5c95521730bbc58ab20052f7ff807c29a8fbeb65 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Sat, 10 Oct 2026 10:16:16 +0200 Subject: [PATCH] fix(browser): clean up unmocked and pending mocks (#11537) --- packages/browser-playwright/src/playwright.ts | 3 +- packages/mocker/src/browser/mocker.ts | 2 + .../1-mock-not-loaded.test.ts | 7 +++ .../mocking-unloaded/2-original.test.ts | 6 +++ .../fixtures/mocking-unloaded/src/unloaded.ts | 3 ++ .../mocking-unloaded/vitest.config.ts | 27 ++++++++++++ .../fixtures/mocking-unmock/do-unmock.test.ts | 11 +++++ test/browser/fixtures/mocking-unmock/setup.ts | 4 ++ .../mocking-unmock/src/setup-automock.ts | 3 ++ .../mocking-unmock/src/setup-factory.ts | 3 ++ .../fixtures/mocking-unmock/unmock.test.ts | 11 +++++ .../fixtures/mocking-unmock/vitest.config.ts | 16 +++++++ test/browser/specs/mocking.test.ts | 44 +++++++++++++++++++ 13 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 test/browser/fixtures/mocking-unloaded/1-mock-not-loaded.test.ts create mode 100644 test/browser/fixtures/mocking-unloaded/2-original.test.ts create mode 100644 test/browser/fixtures/mocking-unloaded/src/unloaded.ts create mode 100644 test/browser/fixtures/mocking-unloaded/vitest.config.ts create mode 100644 test/browser/fixtures/mocking-unmock/do-unmock.test.ts create mode 100644 test/browser/fixtures/mocking-unmock/setup.ts create mode 100644 test/browser/fixtures/mocking-unmock/src/setup-automock.ts create mode 100644 test/browser/fixtures/mocking-unmock/src/setup-factory.ts create mode 100644 test/browser/fixtures/mocking-unmock/unmock.test.ts create mode 100644 test/browser/fixtures/mocking-unmock/vitest.config.ts diff --git a/packages/browser-playwright/src/playwright.ts b/packages/browser-playwright/src/playwright.ts index 074ed51e9..baa1451d2 100644 --- a/packages/browser-playwright/src/playwright.ts +++ b/packages/browser-playwright/src/playwright.ts @@ -534,8 +534,9 @@ export class PlaywrightBrowserProvider implements BrowserProvider { return { url: moduleUrl.href, predicate } } + // the tester unmocks by path, but the route is registered by the full url function predicateKey(sessionId: string, url: string) { - return `${sessionId}:${url}` + return `${sessionId}:${new URL(url, 'http://localhost').href}` } return { diff --git a/packages/mocker/src/browser/mocker.ts b/packages/mocker/src/browser/mocker.ts index 8f7a6ae47..1c94d59f3 100644 --- a/packages/mocker/src/browser/mocker.ts +++ b/packages/mocker/src/browser/mocker.ts @@ -56,6 +56,8 @@ export class ModuleMocker implements TestModuleMocker { } public async invalidate(): Promise { + // a file can finish before its mocks are registered if it never imports them + await Promise.allSettled(this.queue) const ids = Array.from(this.mockedIds) if (!ids.length) { return diff --git a/test/browser/fixtures/mocking-unloaded/1-mock-not-loaded.test.ts b/test/browser/fixtures/mocking-unloaded/1-mock-not-loaded.test.ts new file mode 100644 index 000000000..80a986d31 --- /dev/null +++ b/test/browser/fixtures/mocking-unloaded/1-mock-not-loaded.test.ts @@ -0,0 +1,7 @@ +import { expect, test, vi } from 'vitest' + +vi.mock('./src/unloaded', () => ({ source: () => 'mocked' })) + +test('the mocked module is never loaded in this file', () => { + expect(document.body).toBeDefined() +}) diff --git a/test/browser/fixtures/mocking-unloaded/2-original.test.ts b/test/browser/fixtures/mocking-unloaded/2-original.test.ts new file mode 100644 index 000000000..77d081d8d --- /dev/null +++ b/test/browser/fixtures/mocking-unloaded/2-original.test.ts @@ -0,0 +1,6 @@ +import { expect, test } from 'vitest' +import { source } from './src/unloaded' + +test('a mock from the previous file does not apply', () => { + expect(source()).toBe('original') +}) diff --git a/test/browser/fixtures/mocking-unloaded/src/unloaded.ts b/test/browser/fixtures/mocking-unloaded/src/unloaded.ts new file mode 100644 index 000000000..ea744c0ef --- /dev/null +++ b/test/browser/fixtures/mocking-unloaded/src/unloaded.ts @@ -0,0 +1,3 @@ +export function source() { + return 'original' +} diff --git a/test/browser/fixtures/mocking-unloaded/vitest.config.ts b/test/browser/fixtures/mocking-unloaded/vitest.config.ts new file mode 100644 index 000000000..7f6e5e016 --- /dev/null +++ b/test/browser/fixtures/mocking-unloaded/vitest.config.ts @@ -0,0 +1,27 @@ +import type { TestSpecification } from 'vitest/node' +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import { instances, provider } from '../../settings' + +export default defineConfig({ + cacheDir: fileURLToPath(new URL('./node_modules/.vite', import.meta.url)), + test: { + fileParallelism: false, + sequence: { + sequencer: class { + async shard(files: TestSpecification[]) { + return files + } + async sort(files: TestSpecification[]) { + return [...files].sort((a, b) => a.moduleId.localeCompare(b.moduleId)) + } + }, + }, + browser: { + enabled: true, + provider, + instances, + headless: true, + }, + }, +}) diff --git a/test/browser/fixtures/mocking-unmock/do-unmock.test.ts b/test/browser/fixtures/mocking-unmock/do-unmock.test.ts new file mode 100644 index 000000000..e663f5773 --- /dev/null +++ b/test/browser/fixtures/mocking-unmock/do-unmock.test.ts @@ -0,0 +1,11 @@ +import { expect, test, vi } from 'vitest' + +test('vi.doUnmock restores modules mocked in the setup file', async () => { + vi.doUnmock('./src/setup-automock') + vi.doUnmock('./src/setup-factory') + + const { source: automocked } = await import('./src/setup-automock') + const { source: factory } = await import('./src/setup-factory') + expect(automocked()).toBe('original') + expect(factory()).toBe('original') +}) diff --git a/test/browser/fixtures/mocking-unmock/setup.ts b/test/browser/fixtures/mocking-unmock/setup.ts new file mode 100644 index 000000000..002a57e48 --- /dev/null +++ b/test/browser/fixtures/mocking-unmock/setup.ts @@ -0,0 +1,4 @@ +import { vi } from 'vitest' + +vi.mock('./src/setup-automock') +vi.mock('./src/setup-factory', () => ({ source: () => 'factory' })) diff --git a/test/browser/fixtures/mocking-unmock/src/setup-automock.ts b/test/browser/fixtures/mocking-unmock/src/setup-automock.ts new file mode 100644 index 000000000..ea744c0ef --- /dev/null +++ b/test/browser/fixtures/mocking-unmock/src/setup-automock.ts @@ -0,0 +1,3 @@ +export function source() { + return 'original' +} diff --git a/test/browser/fixtures/mocking-unmock/src/setup-factory.ts b/test/browser/fixtures/mocking-unmock/src/setup-factory.ts new file mode 100644 index 000000000..ea744c0ef --- /dev/null +++ b/test/browser/fixtures/mocking-unmock/src/setup-factory.ts @@ -0,0 +1,3 @@ +export function source() { + return 'original' +} diff --git a/test/browser/fixtures/mocking-unmock/unmock.test.ts b/test/browser/fixtures/mocking-unmock/unmock.test.ts new file mode 100644 index 000000000..d35283961 --- /dev/null +++ b/test/browser/fixtures/mocking-unmock/unmock.test.ts @@ -0,0 +1,11 @@ +import { expect, test, vi } from 'vitest' +import { source as automocked } from './src/setup-automock' +import { source as factory } from './src/setup-factory' + +vi.unmock('./src/setup-automock') +vi.unmock('./src/setup-factory') + +test('vi.unmock restores modules mocked in the setup file', () => { + expect(automocked()).toBe('original') + expect(factory()).toBe('original') +}) diff --git a/test/browser/fixtures/mocking-unmock/vitest.config.ts b/test/browser/fixtures/mocking-unmock/vitest.config.ts new file mode 100644 index 000000000..99fabed07 --- /dev/null +++ b/test/browser/fixtures/mocking-unmock/vitest.config.ts @@ -0,0 +1,16 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import { instances, provider } from '../../settings' + +export default defineConfig({ + cacheDir: fileURLToPath(new URL('./node_modules/.vite', import.meta.url)), + test: { + setupFiles: ['./setup.ts'], + browser: { + enabled: true, + provider, + instances, + headless: true, + }, + }, +}) diff --git a/test/browser/specs/mocking.test.ts b/test/browser/specs/mocking.test.ts index 875ec2ea0..dcbafbb6a 100644 --- a/test/browser/specs/mocking.test.ts +++ b/test/browser/specs/mocking.test.ts @@ -127,6 +127,50 @@ test('mocks from a setup file apply to static imports of a test file', async () expect(result.exitCode).toBe(0) }) +test('vi.unmock and vi.doUnmock restore modules mocked in a setup file', async () => { + const result = await runVitest({ + root: 'fixtures/mocking-unmock', + }) + + onTestFailed(() => { + console.error(result.stdout) + console.error(result.stderr) + }) + + expect(result.stderr).toReportNoErrors() + + instances.forEach(({ browser }) => { + expect(result.stdout).toReportPassedTest('unmock.test.ts', browser) + expect(result.stdout).toReportPassedTest('do-unmock.test.ts', browser) + }) + + expect(result.exitCode).toBe(0) +}) + +test.each([true, false])( + 'a mock that a file never loads does not apply to the next file - isolated %s', + async (isolate) => { + const result = await runVitest({ + root: 'fixtures/mocking-unloaded', + isolate, + }) + + onTestFailed(() => { + console.error(result.stdout) + console.error(result.stderr) + }) + + expect(result.stderr).toReportNoErrors() + + instances.forEach(({ browser }) => { + expect(result.stdout).toReportPassedTest('1-mock-not-loaded.test.ts', browser) + expect(result.stdout).toReportPassedTest('2-original.test.ts', browser) + }) + + expect(result.exitCode).toBe(0) + }, +) + // fix https://github.com/vitest-dev/vitest/issues/7788 test('mocks modules next to a test file in a directory with spaces', async () => { const { errorTree } = await runInlineBrowserTests({ -- 2.51.2