diff --git a/packages/vitest/src/runtime/moduleRunner/bareModuleMocker.ts b/packages/vitest/src/runtime/moduleRunner/bareModuleMocker.ts index 513d9e8ef..c0f8054c4 100644 --- a/packages/vitest/src/runtime/moduleRunner/bareModuleMocker.ts +++ b/packages/vitest/src/runtime/moduleRunner/bareModuleMocker.ts @@ -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 = [] } @@ -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 { diff --git a/test/cli/test/mocking.test.ts b/test/cli/test/mocking.test.ts index 8bba3e510..3cbb181ed 100644 --- a/test/cli/test/mocking.test.ts +++ b/test/cli/test/mocking.test.ts @@ -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',