Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature]: add isolateModulesAsync #13680

Merged
merged 14 commits into from Dec 31, 2022
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 () => {}));
mmanciop marked this conversation as resolved.
Show resolved Hide resolved
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