Skip to content
New issue

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

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

Already on GitHub? # to your account

[Feature]: add isolateModulesAsync #13680

Merged
merged 14 commits into from
Dec 31, 2022
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

### 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
6 changes: 6 additions & 0 deletions packages/jest-environment/src/index.ts
Original file line number Diff line number Diff line change
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: () => void): Promise<void>;
/**
* Mocks a module with an auto-mocked version when it is being required.
*/
Expand Down
Original file line number Diff line number Diff line change
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,130 @@ 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(() => {
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(() => {
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(() => {});
});
}).rejects.toThrow(
'isolateModulesAsync cannot be nested inside another isolateModules or isolateModulesAsync.',
);
});

it('can call resetModules within a isolateModules block', async () => {
const runtime = await createRuntime(__filename, {
moduleNameMapper,
});
let exports;
await runtime.isolateModulesAsync(() => {
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);
});
});
});
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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: () => void): Promise<void> {
if (this._isolatedModuleRegistry || this._isolatedMockRegistry) {
throw new Error(
'isolateModulesAsync cannot be nested inside another isolateModules or isolateModulesAsync.',
);
}
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: () => void) => {
return 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
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ expectError(jest.enableAutomock('moduleName'));
expectType<typeof jest>(jest.isolateModules(() => {}));
expectError(jest.isolateModules());

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

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