Skip to content
58 changes: 39 additions & 19 deletions packages/vitest/src/runtime/moduleRunner/bareModuleMocker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,27 +152,33 @@ export class BareModuleMocker implements TestModuleMocker {
return
}

await Promise.all(
BareModuleMocker.pendingIds.map(async (mock) => {
const { id, url, external } = await this.resolveId(
const resolveMock = async (mock: PendingSuiteMock) => {
const { id, url, external } = await this.resolveId(
mock.id,
mock.importer,
)
if (mock.action === 'unmock') {
this.unmockPath(id)
}
if (mock.action === 'mock') {
this.mockPath(
mock.id,
mock.importer,
id,
url,
external,
mock.type,
mock.factory,
)
if (mock.action === 'unmock') {
this.unmockPath(id)
}
if (mock.action === 'mock') {
this.mockPath(
mock.id,
id,
url,
external,
mock.type,
mock.factory,
)
}
}),
)
}
}

// group consecutive mocks of the same action type together,
// resolve in parallel inside each group, but run groups sequentially
// to preserve mock/unmock ordering
const groups = groupByConsecutiveAction(BareModuleMocker.pendingIds)
for (const group of groups) {
await Promise.all(group.map(resolveMock))
}

BareModuleMocker.pendingIds = []
}
Expand Down Expand Up @@ -368,6 +374,20 @@ function slash(p: string): string {
return p.replace(windowsSlashRE, '/')
}

function groupByConsecutiveAction(mocks: PendingSuiteMock[]): PendingSuiteMock[][] {
const groups: PendingSuiteMock[][] = []
for (const mock of mocks) {
const last = groups.at(-1)
if (last?.[0].action === mock.action) {
last.push(mock)
}
else {
groups.push([mock])
}
}
return groups
}

const multipleSlashRe = /^\/+/
// module-runner incorrectly replaces file:///path with `///path`
function fixLeadingSlashes(id: string): string {
Expand Down
41 changes: 41 additions & 0 deletions test/cli/test/mocking.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,47 @@ test('mock works without loading original', () => {
`)
})

test('doMock/doUnmock ordering is preserved in resolveMocks', async () => {
// This tests repeats doUnmock + doMock
// vi.doUnmock('/mock-lib-0');
// vi.doMock('/mock-lib-0', () => ({ value: 0 }));
// vi.doUnmock('/mock-lib-1');
// vi.doMock('/mock-lib-1', () => ({ value: 1 }));
// ...
// then, all modules should be mocked
// import('/mock-lib-0') // => { value: 0 }
// import('/mock-lib-1') // => { value: 1 }
// ...
const N = 20
const mockEntries = Array.from({ length: N }, (_, i) => `\
vi.doUnmock('/mock-lib-${i}');
vi.doMock('/mock-lib-${i}', () => ({ value: ${i} }));
`).join('\n')
const importChecks = Array.from({ length: N }, (_, i) => `\
await expect(import('/mock-lib-${i}')).resolves.toEqual({ value: ${i} });
`).join('\n')

const { stderr, errorTree } = await runInlineTests({
'./basic.test.js': `
import { test, expect, vi } from 'vitest'

test('many unmock + mock (all should mocked)', async () => {
${mockEntries}
${importChecks}
})
`,
})

expect(stderr).toBe('')
expect(errorTree()).toMatchInlineSnapshot(`
{
"basic.test.js": {
"many unmock + mock (all should mocked)": "passed",
},
}
`)
})

test.for([
'node',
'playwright',
Expand Down
Loading