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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -2,6 +2,8 @@

### Features

- `[jest-runtime]` Add `jest.isolateModulesAsync` for scoped module initialization of asynchronous functions ([#13680](https://github.com/facebook/jest/pull/13680))

### Fixes

- `[jest-resolve]` add global paths to `require.resolve.paths` ([#13633](https://github.com/facebook/jest/pull/13633))
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 functions that need to be wrapped. The caller is expected to `await` the completion of `isolateModulesAsync`.

```js
let myModule;
await jest.isolateModulesAsync(async () => {
constmyModule = require('myModule');
mmanciop marked this conversation as resolved.
Show resolved Hide resolved
// do async stuff here
});

const otherCopyOfMyModule = require('myModule');
mmanciop marked this conversation as resolved.
Show resolved Hide resolved
```

## 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
25 changes: 24 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 @@ -2161,6 +2180,9 @@ export default class Runtime {
this.isolateModules(fn);
return jestObject;
};
const isolateModulesAsync = async (fn: () => Promise<void>) => {
await this.isolateModulesAsync(fn);
};
const fn = this._moduleMocker.fn.bind(this._moduleMocker);
const spyOn = this._moduleMocker.spyOn.bind(this._moduleMocker);
const mocked =
Expand Down Expand Up @@ -2226,6 +2248,7 @@ export default class Runtime {
getTimerCount: () => _getFakeTimers().getTimerCount(),
isMockFunction: this._moduleMocker.isMockFunction,
isolateModules,
isolateModulesAsync,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn’t it better to simply inline:

Suggested change
isolateModulesAsync,
isolateModulesAsync: this.isolateModulesAsync,

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 36c138e

mock,
mocked,
now: () => _getFakeTimers().now(),
Expand Down
3 changes: 3 additions & 0 deletions packages/jest-types/__typetests__/jest.test.ts
Expand Up @@ -98,6 +98,9 @@ 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());

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