Skip to content

Commit

Permalink
[Feature]: add isolateModulesAsync (#13680)
Browse files Browse the repository at this point in the history
  • Loading branch information
Michele Mancioppi committed Dec 31, 2022
1 parent 5da283d commit abc64a6
Show file tree
Hide file tree
Showing 7 changed files with 182 additions and 3 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions docs/JestObjectAPI.md
Expand Up @@ -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?)`
Expand Down
6 changes: 6 additions & 0 deletions packages/jest-environment/src/index.ts
Expand Up @@ -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<void>): Promise<void>;
/**
* Mocks a module with an auto-mocked version when it is being required.
*/
Expand Down
Expand Up @@ -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,
});
Expand Down Expand Up @@ -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.',
);
});

Expand Down Expand Up @@ -325,6 +325,7 @@ describe('isolateModules', () => {
beforeEach(() => {
jest.isolateModules(() => {
exports = require('./test_root/ModuleWithState');
exports.set(1); // Ensure idempotency with the isolateModulesAsync test
});
});

Expand All @@ -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);
});
});
});
Expand Up @@ -8,6 +8,10 @@

let state = 1;

export const set = i => {
state = i;
};

export const increment = () => {
state += 1;
};
Expand Down
22 changes: 21 additions & 1 deletion packages/jest-runtime/src/index.ts
Expand Up @@ -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();
Expand All @@ -1141,6 +1141,25 @@ export default class Runtime {
}
}

async isolateModulesAsync(fn: () => Promise<void>): Promise<void> {
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();
Expand Down Expand Up @@ -2226,6 +2245,7 @@ export default class Runtime {
getTimerCount: () => _getFakeTimers().getTimerCount(),
isMockFunction: this._moduleMocker.isMockFunction,
isolateModules,
isolateModulesAsync: this.isolateModulesAsync,
mock,
mocked,
now: () => _getFakeTimers().now(),
Expand Down
4 changes: 4 additions & 0 deletions packages/jest-types/__typetests__/jest.test.ts
Expand Up @@ -98,6 +98,10 @@ expectError(jest.enableAutomock('moduleName'));
expectType<typeof jest>(jest.isolateModules(() => {}));
expectError(jest.isolateModules());

expectType<Promise<void>>(jest.isolateModulesAsync(async () => {}));
expectError(jest.isolateModulesAsync(() => {}));
expectError(jest.isolateModulesAsync());

expectType<typeof jest>(jest.mock('moduleName'));
expectType<typeof jest>(jest.mock('moduleName', jest.fn()));
expectType<typeof jest>(
Expand Down

0 comments on commit abc64a6

Please sign in to comment.