Skip to content

Commit 7c06598

Browse files
hi-ogawaclaude
andauthored
fix: ensure sequential mock/unmock resolution (#9830)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent f54abad commit 7c06598

File tree

2 files changed

+80
-19
lines changed

2 files changed

+80
-19
lines changed

packages/vitest/src/runtime/moduleRunner/bareModuleMocker.ts

Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -152,27 +152,33 @@ export class BareModuleMocker implements TestModuleMocker {
152152
return
153153
}
154154

155-
await Promise.all(
156-
BareModuleMocker.pendingIds.map(async (mock) => {
157-
const { id, url, external } = await this.resolveId(
155+
const resolveMock = async (mock: PendingSuiteMock) => {
156+
const { id, url, external } = await this.resolveId(
157+
mock.id,
158+
mock.importer,
159+
)
160+
if (mock.action === 'unmock') {
161+
this.unmockPath(id)
162+
}
163+
if (mock.action === 'mock') {
164+
this.mockPath(
158165
mock.id,
159-
mock.importer,
166+
id,
167+
url,
168+
external,
169+
mock.type,
170+
mock.factory,
160171
)
161-
if (mock.action === 'unmock') {
162-
this.unmockPath(id)
163-
}
164-
if (mock.action === 'mock') {
165-
this.mockPath(
166-
mock.id,
167-
id,
168-
url,
169-
external,
170-
mock.type,
171-
mock.factory,
172-
)
173-
}
174-
}),
175-
)
172+
}
173+
}
174+
175+
// group consecutive mocks of the same action type together,
176+
// resolve in parallel inside each group, but run groups sequentially
177+
// to preserve mock/unmock ordering
178+
const groups = groupByConsecutiveAction(BareModuleMocker.pendingIds)
179+
for (const group of groups) {
180+
await Promise.all(group.map(resolveMock))
181+
}
176182

177183
BareModuleMocker.pendingIds = []
178184
}
@@ -368,6 +374,20 @@ function slash(p: string): string {
368374
return p.replace(windowsSlashRE, '/')
369375
}
370376

377+
function groupByConsecutiveAction(mocks: PendingSuiteMock[]): PendingSuiteMock[][] {
378+
const groups: PendingSuiteMock[][] = []
379+
for (const mock of mocks) {
380+
const last = groups.at(-1)
381+
if (last?.[0].action === mock.action) {
382+
last.push(mock)
383+
}
384+
else {
385+
groups.push([mock])
386+
}
387+
}
388+
return groups
389+
}
390+
371391
const multipleSlashRe = /^\/+/
372392
// module-runner incorrectly replaces file:///path with `///path`
373393
function fixLeadingSlashes(id: string): string {

test/cli/test/mocking.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,47 @@ test('mock works without loading original', () => {
392392
`)
393393
})
394394

395+
test('doMock/doUnmock ordering is preserved in resolveMocks', async () => {
396+
// This tests repeats doUnmock + doMock
397+
// vi.doUnmock('/mock-lib-0');
398+
// vi.doMock('/mock-lib-0', () => ({ value: 0 }));
399+
// vi.doUnmock('/mock-lib-1');
400+
// vi.doMock('/mock-lib-1', () => ({ value: 1 }));
401+
// ...
402+
// then, all modules should be mocked
403+
// import('/mock-lib-0') // => { value: 0 }
404+
// import('/mock-lib-1') // => { value: 1 }
405+
// ...
406+
const N = 20
407+
const mockEntries = Array.from({ length: N }, (_, i) => `\
408+
vi.doUnmock('/mock-lib-${i}');
409+
vi.doMock('/mock-lib-${i}', () => ({ value: ${i} }));
410+
`).join('\n')
411+
const importChecks = Array.from({ length: N }, (_, i) => `\
412+
await expect(import('/mock-lib-${i}')).resolves.toEqual({ value: ${i} });
413+
`).join('\n')
414+
415+
const { stderr, errorTree } = await runInlineTests({
416+
'./basic.test.js': `
417+
import { test, expect, vi } from 'vitest'
418+
419+
test('many unmock + mock (all should mocked)', async () => {
420+
${mockEntries}
421+
${importChecks}
422+
})
423+
`,
424+
})
425+
426+
expect(stderr).toBe('')
427+
expect(errorTree()).toMatchInlineSnapshot(`
428+
{
429+
"basic.test.js": {
430+
"many unmock + mock (all should mocked)": "passed",
431+
},
432+
}
433+
`)
434+
})
435+
395436
test.for([
396437
'node',
397438
'playwright',

0 commit comments

Comments
 (0)