Skip to content

Commit 675b434

Browse files
toxikZelys-DFKH
andauthored
fix(browser): remove orphaned Playwright route when same module is mocked via multiple ids [backport to v4] (#10474)
Co-authored-by: Zelys <zelys@dfkhelper.com>
1 parent e4067b3 commit 675b434

7 files changed

Lines changed: 71 additions & 10 deletions

File tree

packages/browser-playwright/src/playwright.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -261,9 +261,9 @@ export class PlaywrightBrowserProvider implements BrowserProvider {
261261

262262
private createMocker(): BrowserModuleMocker {
263263
const idPredicates = new Map<string, (url: URL) => boolean>()
264-
const sessionIds = new Map<string, string[]>()
264+
const sessionIds = new Map<string, Set<string>>()
265265

266-
function createPredicate(sessionId: string, url: string) {
266+
function createPredicate(url: string) {
267267
const moduleUrl = new URL(url, 'http://localhost')
268268
const predicate = (url: URL) => {
269269
if (url.searchParams.has('_vitest_original')) {
@@ -293,11 +293,7 @@ export class PlaywrightBrowserProvider implements BrowserProvider {
293293

294294
return true
295295
}
296-
const ids = sessionIds.get(sessionId) || []
297-
ids.push(moduleUrl.href)
298-
sessionIds.set(sessionId, ids)
299-
idPredicates.set(predicateKey(sessionId, moduleUrl.href), predicate)
300-
return predicate
296+
return { url: moduleUrl.href, predicate }
301297
}
302298

303299
function predicateKey(sessionId: string, url: string) {
@@ -307,7 +303,17 @@ export class PlaywrightBrowserProvider implements BrowserProvider {
307303
return {
308304
register: async (sessionId: string, module: MockedModule): Promise<void> => {
309305
const page = this.getPage(sessionId)
310-
await page.context().route(createPredicate(sessionId, module.url), async (route) => {
306+
const { url: moduleUrl, predicate } = createPredicate(module.url)
307+
const key = predicateKey(sessionId, moduleUrl)
308+
const existingPredicate = idPredicates.get(key)
309+
if (existingPredicate) {
310+
await page.context().unroute(existingPredicate)
311+
}
312+
const ids = sessionIds.get(sessionId) ?? new Set<string>()
313+
ids.add(moduleUrl)
314+
sessionIds.set(sessionId, ids)
315+
idPredicates.set(key, predicate)
316+
await page.context().route(predicate, async (route) => {
311317
if (module.type === 'manual') {
312318
const exports = Object.keys(await module.resolve())
313319
const body = createManualModuleSource(module.url, exports)
@@ -381,8 +387,8 @@ export class PlaywrightBrowserProvider implements BrowserProvider {
381387
},
382388
clear: async (sessionId: string): Promise<void> => {
383389
const page = this.getPage(sessionId)
384-
const ids = sessionIds.get(sessionId) || []
385-
const promises = ids.map((id) => {
390+
const ids = sessionIds.get(sessionId) ?? new Set<string>()
391+
const promises = [...ids].map((id) => {
386392
const key = predicateKey(sessionId, id)
387393
const predicate = idPredicates.get(key)
388394
if (predicate) {
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { expect, test, vi } from 'vitest'
2+
import { loadModalSource } from './dual-id/consumer'
3+
4+
vi.mock('~/dual-id/modal', () => ({
5+
readModalSource: () => 'mocked modal',
6+
}))
7+
8+
vi.mock('./dual-id/modal', () => ({
9+
readModalSource: () => 'mocked modal',
10+
}))
11+
12+
test('dual-id mock is active in the file that registered it', () => {
13+
expect(loadModalSource()).not.toBe('actual modal')
14+
})
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { readModalSource } from '~/dual-id/modal'
2+
3+
export function loadModalSource() {
4+
return readModalSource()
5+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function readModalSource() {
2+
return 'actual modal'
3+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { expect, test } from 'vitest'
2+
import { loadModalSource } from './dual-id/consumer'
3+
4+
test('dual-id mock from the previous file does not leak into this one', () => {
5+
expect(loadModalSource()).toBe('actual modal')
6+
})

test/browser/fixtures/mocking/vitest.config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ import { defineConfig } from 'vitest/config'
33
import { instances, provider } from '../../settings'
44

55
export default defineConfig({
6+
resolve: {
7+
alias: {
8+
'~': fileURLToPath(new URL('./src', import.meta.url)),
9+
},
10+
},
611
optimizeDeps: {
712
include: ['@vitest/cjs-lib'],
813
needsInterop: ['@vitest/cjs-lib'],

test/browser/specs/mocking.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,35 @@ test.each([true/* , false */])('mocking works correctly - isolated %s', async (i
2828
expect(result.stdout).toReportPassedTest('import-actual-in-mock.test.ts', browser)
2929
expect(result.stdout).toReportPassedTest('import-actual-query.test.ts', browser)
3030
expect(result.stdout).toReportPassedTest('import-mock.test.ts', browser)
31+
expect(result.stdout).toReportPassedTest('src/aaa-dual-id-probe.test.ts', browser)
32+
expect(result.stdout).toReportPassedTest('src/zzz-dual-id-target.test.ts', browser)
3133
expect(result.stdout).toReportPassedTest('mocked-do-mock-factory.test.ts', browser)
3234
expect(result.stdout).toReportPassedTest('import-actual-dep.test.ts', browser)
3335
})
3436

3537
expect(result.exitCode).toBe(0)
3638
})
3739

40+
test('manual mocks do not leak across browser files when alias and relative ids resolve to the same module', async () => {
41+
const result = await runVitest({
42+
root: 'fixtures/mocking',
43+
}, ['src/aaa-dual-id-probe.test.ts', 'src/zzz-dual-id-target.test.ts'])
44+
45+
onTestFailed(() => {
46+
console.error(result.stdout)
47+
console.error(result.stderr)
48+
})
49+
50+
expect(result.stderr).toReportNoErrors()
51+
52+
instances.forEach(({ browser }) => {
53+
expect(result.stdout).toReportPassedTest('src/aaa-dual-id-probe.test.ts', browser)
54+
expect(result.stdout).toReportPassedTest('src/zzz-dual-id-target.test.ts', browser)
55+
})
56+
57+
expect(result.exitCode).toBe(0)
58+
}, 60_000)
59+
3860
test('mocking dependency correctly invalidates it on rerun', async () => {
3961
const { vitest, ctx } = await runVitest({
4062
root: 'fixtures/mocking-watch',

0 commit comments

Comments
 (0)