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

chore: extend and improve type tests for jest object #12442

Merged
merged 9 commits into from Feb 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -12,6 +12,7 @@
- `[jest-environment-node]` [**BREAKING**] Add default `node` and `node-addon` conditions to `exportConditions` for `node` environment ([#11924](https://github.com/facebook/jest/pull/11924))
- `[@jest/expect]` New module which extends `expect` with `jest-snapshot` matchers ([#12404](https://github.com/facebook/jest/pull/12404), [#12410](https://github.com/facebook/jest/pull/12410), [#12418](https://github.com/facebook/jest/pull/12418))
- `[@jest/expect-utils]` New module exporting utils for `expect` ([#12323](https://github.com/facebook/jest/pull/12323))
- `[jest-mock]` Improve `isMockFunction` to infer types of passed function ([#12442](https://github.com/facebook/jest/pull/12442))
- `[jest-resolve]` [**BREAKING**] Add support for `package.json` `exports` ([#11961](https://github.com/facebook/jest/pull/11961), [#12373](https://github.com/facebook/jest/pull/12373))
- `[jest-resolve, jest-runtime]` Add support for `data:` URI import and mock ([#12392](https://github.com/facebook/jest/pull/12392))
- `[@jest/schemas]` New module for JSON schemas for Jest's config ([#12384](https://github.com/facebook/jest/pull/12384))
Expand All @@ -27,6 +28,7 @@
- `[jest-haste-map]` Don't use partial results if file crawl errors ([#12420](https://github.com/facebook/jest/pull/12420))
- `[jest-jasmine2, jest-types]` [**BREAKING**] Move all `jasmine` specific types from `@jest/types` to its own package ([#12125](https://github.com/facebook/jest/pull/12125))
- `[jest-matcher-utils]` Pass maxWidth to `pretty-format` to avoid printing every element in arrays by default ([#12402](https://github.com/facebook/jest/pull/12402))
- `[jest-mock]` Fix function overloads for `spyOn` to allow more correct type inference in complex object ([#12442](https://github.com/facebook/jest/pull/12442))

### Chore & Maintenance

Expand Down
17 changes: 5 additions & 12 deletions packages/jest-environment/src/index.ts
Expand Up @@ -8,12 +8,7 @@
import type {Context} from 'vm';
import type {LegacyFakeTimers, ModernFakeTimers} from '@jest/fake-timers';
import type {Circus, Config, Global} from '@jest/types';
import type {
fn as JestMockFn,
mocked as JestMockMocked,
spyOn as JestMockSpyOn,
ModuleMocker,
} from 'jest-mock';
import type {ModuleMocker} from 'jest-mock';

export type EnvironmentContext = {
console: Console;
Expand Down Expand Up @@ -110,7 +105,7 @@ export interface Jest {
/**
* Creates a mock function. Optionally takes a mock implementation.
*/
fn: typeof JestMockFn;
fn: ModuleMocker['fn'];
/**
* Given the name of a module, use the automatic mocking system to generate a
* mocked version of the module for you.
Expand All @@ -132,9 +127,7 @@ export interface Jest {
/**
* Determines if the given function is a mocked function.
*/
isMockFunction(
fn: (...args: Array<any>) => unknown,
): fn is ReturnType<typeof JestMockFn>;
isMockFunction: ModuleMocker['isMockFunction'];
/**
* Mocks a module with an auto-mocked version when it is being required.
*/
Expand Down Expand Up @@ -196,7 +189,7 @@ export interface Jest {
* jest.spyOn; other mocks will require you to manually restore them.
*/
restoreAllMocks(): Jest;
mocked: typeof JestMockMocked;
mocked: ModuleMocker['mocked'];
/**
* Runs failed tests n-times until they pass or until the max number of
* retries is exhausted. This only works with `jest-circus`!
Expand Down Expand Up @@ -259,7 +252,7 @@ export interface Jest {
* Note: By default, jest.spyOn also calls the spied method. This is
* different behavior from most other test libraries.
*/
spyOn: typeof JestMockSpyOn;
spyOn: ModuleMocker['spyOn'];
/**
* Indicates that the module system should never return a mocked version of
* the specified module from require() (e.g. that it should always return the
Expand Down
32 changes: 23 additions & 9 deletions packages/jest-mock/src/index.ts
Expand Up @@ -192,6 +192,10 @@ type FunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends (...args: Array<any>) => any ? K : never;
}[keyof T] &
string;
type ConstructorPropertyNames<T> = {
[K in keyof T]: T[K] extends new (...args: Array<any>) => any ? K : never;
}[keyof T] &
string;

const MOCK_CONSTRUCTOR_NAME = 'mockConstructor';

Expand Down Expand Up @@ -988,6 +992,10 @@ export class ModuleMocker {
return metadata;
}

isMockFunction<T, Y extends Array<unknown> = Array<unknown>>(
fn: (...args: Y) => T,
): fn is Mock<T, Y>;
isMockFunction(fn: unknown): fn is Mock<unknown>;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@SimenB Might be this is better idea. See tests. Thanks for good questions!

Copy link
Member

Choose a reason for hiding this comment

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

perfect, thanks!

isMockFunction<T>(fn: unknown): fn is Mock<T> {
return !!fn && (fn as any)._isMockFunction === true;
}
Expand All @@ -1003,27 +1011,34 @@ export class ModuleMocker {
return fn;
}

spyOn<T extends {}, M extends NonFunctionPropertyNames<T>>(
spyOn<T extends object, M extends NonFunctionPropertyNames<T>>(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

{} as a constrain type allows any non-nullish value. See typescript-eslint/typescript-eslint#2063 (comment). Here it is almost the same as any, because string or boolean are allowed. Might be better to use object (any non-primitive value).

object: T,
methodName: M,
accessType: 'get',
): SpyInstance<T[M], []>;

spyOn<T extends {}, M extends NonFunctionPropertyNames<T>>(
spyOn<T extends object, M extends NonFunctionPropertyNames<T>>(
object: T,
methodName: M,
accessType: 'set',
): SpyInstance<void, [T[M]]>;

spyOn<T extends {}, M extends FunctionPropertyNames<T>>(
spyOn<T extends object, M extends ConstructorPropertyNames<Required<T>>>(
object: T,
methodName: M,
): T[M] extends new (...args: Array<any>) => any
? SpyInstance<InstanceType<T[M]>, ConstructorParameters<T[M]>>
: never;

spyOn<T extends object, M extends FunctionPropertyNames<T>>(
object: T,
methodName: M,
): T[M] extends (...args: Array<any>) => any
? SpyInstance<ReturnType<T[M]>, Parameters<T[M]>>
: never;

// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
spyOn<T extends {}, M extends NonFunctionPropertyNames<T>>(
spyOn<T extends object, M extends NonFunctionPropertyNames<T>>(
object: T,
methodName: M,
accessType?: 'get' | 'set',
Expand Down Expand Up @@ -1094,11 +1109,10 @@ export class ModuleMocker {
return object[methodName];
}

private _spyOnProperty<T extends {}, M extends NonFunctionPropertyNames<T>>(
obj: T,
propertyName: M,
accessType: 'get' | 'set' = 'get',
): Mock<T> {
private _spyOnProperty<
T extends object,
M extends NonFunctionPropertyNames<T>,
>(obj: T, propertyName: M, accessType: 'get' | 'set' = 'get'): Mock<T> {
if (typeof obj !== 'object' && typeof obj !== 'function') {
throw new Error(
'Cannot spyOn on a primitive value; ' + this._typeOf(obj) + ' given',
Expand Down