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
26 changes: 26 additions & 0 deletions docs/JestObjectAPI.md
Expand Up @@ -568,6 +568,32 @@ 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 () => {
myModule = await import('myModule');
// do async stuff here
});

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

`jest.isolateModulesAsync()` can also run synchronous callbacks:
mmanciop marked this conversation as resolved.
Show resolved Hide resolved

```js
let myModule;
await jest.isolateModulesAsync(() => {
myModule = require('myModule');
// do sync stuff here
});

const otherCopyOfMyModule = require('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,242 @@ describe('isolateModules', () => {
});
});
});

describe('isolateModulesAsync', () => {
describe('with sync callback', () => {
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(() => {
throw new Error('Error from isolated module');
}),
).rejects.toThrow('Error from isolated module');

await runtime.isolateModulesAsync(() => {
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 isolateModulesAsync or isolateModules.',
);
});

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('with async callback', () => {
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> | 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
expectType<Promise<void>>(jest.isolateModulesAsync(() => {}));
Copy link
Contributor

Choose a reason for hiding this comment

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

Try running type tests. This assertion is currently failing. Should be:

Suggested change
expectType<Promise<void>>(jest.isolateModulesAsync(() => {}));
expectError(jest.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.

mmm I did run it and it did not fail:

mmanciop@Micheles-MacBook-Pro jest % yarn test-types
Running one project: type-tests
 PASS   type-tests  packages/expect/__typetests__/expect.test.ts
 PASS   type-tests  packages/jest-worker/__typetests__/jest-worker.test.ts
 PASS   type-tests  packages/jest-mock/__typetests__/utility-types.test.ts
 PASS   type-tests  packages/jest-mock/__typetests__/Mocked.test.ts
 PASS   type-tests  packages/jest-types/__typetests__/each.test.ts
 PASS   type-tests  packages/jest-types/__typetests__/globals.test.ts
 PASS   type-tests  packages/jest-types/__typetests__/expect.test.ts
 PASS   type-tests  packages/jest-types/__typetests__/jest.test.ts
 PASS   type-tests  packages/jest-reporters/__typetests__/jest-reporters.test.ts
 PASS   type-tests  packages/jest-resolve/__typetests__/resolver.test.ts
 PASS   type-tests  packages/jest-runner/__typetests__/jest-runner.test.ts
 PASS   type-tests  packages/jest-mock/__typetests__/mock-functions.test.ts
 PASS   type-tests  packages/jest-snapshot/__typetests__/SnapshotResolver.test.ts
 PASS   type-tests  packages/jest-snapshot/__typetests__/matchers.test.ts
 PASS   type-tests  packages/expect-utils/__typetests__/utils.test.ts
 PASS   type-tests  packages/jest-types/__typetests__/config.test.ts
 PASS   type-tests  packages/jest-expect/__typetests__/jest-expect.test.ts
 PASS   type-tests  packages/jest-mock/__typetests__/ModuleMocker.test.ts
 PASS   type-tests  packages/jest/__typetests__/jest.test.ts

Test Suites: 19 passed, 19 total
Tests:       1283 passed, 1283 total
Snapshots:   0 total
Time:        1.951 s, estimated 2 s
Ran all test suites.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@mrazauskas I am not managing to replicate the type error. I must be doing something wrong here. I updated the signature as I understand you suggested, can you please check again?

Copy link
Contributor

Choose a reason for hiding this comment

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

The repo was not rebuild? Tests import from @jest/globals, not from src. That is why rebuilding is important after making changes in src.

expectError(jest.isolateModulesAsync());

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