diff --git a/CHANGELOG.md b/CHANGELOG.md index 12947b26e001..2af6cc2ac6ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### Features +- `[jest-runtime]` Add `jest.isolateModulesAsync` for scoped module initialization of asynchronous functions ([#13680](https://github.com/facebook/jest/pull/13680)) - `[jest-test-result]` Added `skipped` and `focused` status to `FormattedTestResult` ([#13700](https://github.com/facebook/jest/pull/13700)) ### Fixes diff --git a/docs/JestObjectAPI.md b/docs/JestObjectAPI.md index a961dab0fd0d..58634a135c85 100644 --- a/docs/JestObjectAPI.md +++ b/docs/JestObjectAPI.md @@ -568,6 +568,20 @@ jest.isolateModules(() => { const otherCopyOfMyModule = require('myModule'); ``` +### `jest.isolateModulesAsync(fn)` + +`jest.isolateModulesAsync()` is the equivalent of `jest.isolateModules()`, but for async callbacks. The caller is expected to `await` the completion of `isolateModulesAsync`. + +```js +let myModule; +await jest.isolateModulesAsync(async () => { + myModule = await import('myModule'); + // do async stuff here +}); + +const otherCopyOfMyModule = await import('myModule'); +``` + ## Mock Functions ### `jest.fn(implementation?)` diff --git a/packages/jest-environment/src/index.ts b/packages/jest-environment/src/index.ts index df50111e37f5..f2b3bf4ee961 100644 --- a/packages/jest-environment/src/index.ts +++ b/packages/jest-environment/src/index.ts @@ -172,6 +172,12 @@ export interface Jest { * local module state doesn't conflict between tests. */ isolateModules(fn: () => void): Jest; + /** + * `jest.isolateModulesAsync()` is the equivalent of `jest.isolateModules()`, but for + * async functions to be wrapped. The caller is expected to `await` the completion of + * `isolateModulesAsync`. + */ + isolateModulesAsync(fn: () => Promise): Promise; /** * Mocks a module with an auto-mocked version when it is being required. */ diff --git a/packages/jest-runtime/src/__tests__/runtime_require_module_or_mock.test.js b/packages/jest-runtime/src/__tests__/runtime_require_module_or_mock.test.js index 8e28832c9d6a..e49457ed67eb 100644 --- a/packages/jest-runtime/src/__tests__/runtime_require_module_or_mock.test.js +++ b/packages/jest-runtime/src/__tests__/runtime_require_module_or_mock.test.js @@ -214,7 +214,7 @@ describe('resetModules', () => { }); describe('isolateModules', () => { - it("keeps it's registry isolated from global one", async () => { + it('keeps its registry isolated from global one', async () => { const runtime = await createRuntime(__filename, { moduleNameMapper, }); @@ -287,7 +287,7 @@ describe('isolateModules', () => { runtime.isolateModules(() => {}); }); }).toThrow( - 'isolateModules cannot be nested inside another isolateModules.', + 'isolateModules cannot be nested inside another isolateModules or isolateModulesAsync.', ); }); @@ -325,6 +325,7 @@ describe('isolateModules', () => { beforeEach(() => { jest.isolateModules(() => { exports = require('./test_root/ModuleWithState'); + exports.set(1); // Ensure idempotency with the isolateModulesAsync test }); }); @@ -340,3 +341,132 @@ describe('isolateModules', () => { }); }); }); + +describe('isolateModulesAsync', () => { + it('keeps its registry isolated from global one', async () => { + const runtime = await createRuntime(__filename, { + moduleNameMapper, + }); + let exports; + exports = runtime.requireModuleOrMock( + runtime.__mockRootPath, + 'ModuleWithState', + ); + exports.increment(); + expect(exports.getState()).toBe(2); + + await runtime.isolateModulesAsync(async () => { + exports = runtime.requireModuleOrMock( + runtime.__mockRootPath, + 'ModuleWithState', + ); + expect(exports.getState()).toBe(1); + }); + + exports = runtime.requireModuleOrMock( + runtime.__mockRootPath, + 'ModuleWithState', + ); + expect(exports.getState()).toBe(2); + }); + + it('resets all modules after the block', async () => { + const runtime = await createRuntime(__filename, { + moduleNameMapper, + }); + let exports; + await runtime.isolateModulesAsync(async () => { + exports = runtime.requireModuleOrMock( + runtime.__mockRootPath, + 'ModuleWithState', + ); + expect(exports.getState()).toBe(1); + exports.increment(); + expect(exports.getState()).toBe(2); + }); + + exports = runtime.requireModuleOrMock( + runtime.__mockRootPath, + 'ModuleWithState', + ); + expect(exports.getState()).toBe(1); + }); + + it('resets module after failing', async () => { + const runtime = await createRuntime(__filename, { + moduleNameMapper, + }); + await expect( + runtime.isolateModulesAsync(async () => { + throw new Error('Error from isolated module'); + }), + ).rejects.toThrow('Error from isolated module'); + + await runtime.isolateModulesAsync(async () => { + expect(true).toBe(true); + }); + }); + + it('cannot nest isolateModulesAsync blocks', async () => { + const runtime = await createRuntime(__filename, { + moduleNameMapper, + }); + await expect(async () => { + await runtime.isolateModulesAsync(async () => { + await runtime.isolateModulesAsync(() => Promise.resolve()); + }); + }).rejects.toThrow( + 'isolateModulesAsync cannot be nested inside another isolateModulesAsync or isolateModules.', + ); + }); + + it('can call resetModules within a isolateModules block', async () => { + const runtime = await createRuntime(__filename, { + moduleNameMapper, + }); + let exports; + await runtime.isolateModulesAsync(async () => { + exports = runtime.requireModuleOrMock( + runtime.__mockRootPath, + 'ModuleWithState', + ); + expect(exports.getState()).toBe(1); + + exports.increment(); + runtime.resetModules(); + + exports = runtime.requireModuleOrMock( + runtime.__mockRootPath, + 'ModuleWithState', + ); + expect(exports.getState()).toBe(1); + }); + + exports = runtime.requireModuleOrMock( + runtime.__mockRootPath, + 'ModuleWithState', + ); + expect(exports.getState()).toBe(1); + }); + + describe('can use isolateModulesAsync from a beforeEach block', () => { + let exports; + beforeEach(async () => { + await jest.isolateModulesAsync(async () => { + exports = require('./test_root/ModuleWithState'); + exports.set(1); // Ensure idempotency with the isolateModules test + }); + }); + + it('can use the required module from beforeEach and re-require it', () => { + expect(exports.getState()).toBe(1); + exports.increment(); + expect(exports.getState()).toBe(2); + + exports = require('./test_root/ModuleWithState'); + expect(exports.getState()).toBe(2); + exports.increment(); + expect(exports.getState()).toBe(3); + }); + }); +}); diff --git a/packages/jest-runtime/src/__tests__/test_root/ModuleWithState.js b/packages/jest-runtime/src/__tests__/test_root/ModuleWithState.js index 220e11d1b0fe..b8c0236109be 100644 --- a/packages/jest-runtime/src/__tests__/test_root/ModuleWithState.js +++ b/packages/jest-runtime/src/__tests__/test_root/ModuleWithState.js @@ -8,6 +8,10 @@ let state = 1; +export const set = i => { + state = i; +}; + export const increment = () => { state += 1; }; diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index c278d77052c5..b9250afcd3bd 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -1125,7 +1125,7 @@ export default class Runtime { isolateModules(fn: () => void): void { if (this._isolatedModuleRegistry || this._isolatedMockRegistry) { throw new Error( - 'isolateModules cannot be nested inside another isolateModules.', + 'isolateModules cannot be nested inside another isolateModules or isolateModulesAsync.', ); } this._isolatedModuleRegistry = new Map(); @@ -1141,6 +1141,25 @@ export default class Runtime { } } + async isolateModulesAsync(fn: () => Promise): Promise { + if (this._isolatedModuleRegistry || this._isolatedMockRegistry) { + throw new Error( + 'isolateModulesAsync cannot be nested inside another isolateModulesAsync or isolateModules.', + ); + } + this._isolatedModuleRegistry = new Map(); + this._isolatedMockRegistry = new Map(); + try { + await fn(); + } finally { + // might be cleared within the callback + this._isolatedModuleRegistry?.clear(); + this._isolatedMockRegistry?.clear(); + this._isolatedModuleRegistry = null; + this._isolatedMockRegistry = null; + } + } + resetModules(): void { this._isolatedModuleRegistry?.clear(); this._isolatedMockRegistry?.clear(); @@ -2226,6 +2245,7 @@ export default class Runtime { getTimerCount: () => _getFakeTimers().getTimerCount(), isMockFunction: this._moduleMocker.isMockFunction, isolateModules, + isolateModulesAsync: this.isolateModulesAsync, mock, mocked, now: () => _getFakeTimers().now(), diff --git a/packages/jest-types/__typetests__/jest.test.ts b/packages/jest-types/__typetests__/jest.test.ts index ad5717d43645..691bcd330f9a 100644 --- a/packages/jest-types/__typetests__/jest.test.ts +++ b/packages/jest-types/__typetests__/jest.test.ts @@ -98,6 +98,10 @@ expectError(jest.enableAutomock('moduleName')); expectType(jest.isolateModules(() => {})); expectError(jest.isolateModules()); +expectType>(jest.isolateModulesAsync(async () => {})); +expectError(jest.isolateModulesAsync(() => {})); +expectError(jest.isolateModulesAsync()); + expectType(jest.mock('moduleName')); expectType(jest.mock('moduleName', jest.fn())); expectType(