diff --git a/packages/jest-mock/src/__tests__/jest_mock.test.js b/packages/jest-mock/src/__tests__/index.test.ts similarity index 99% rename from packages/jest-mock/src/__tests__/jest_mock.test.js rename to packages/jest-mock/src/__tests__/index.test.ts index bd38cb281806..7337442c1ef6 100644 --- a/packages/jest-mock/src/__tests__/jest_mock.test.js +++ b/packages/jest-mock/src/__tests__/index.test.ts @@ -8,7 +8,7 @@ 'use strict'; -const vm = require('vm'); +import vm from 'vm'; describe('moduleMocker', () => { let moduleMocker; @@ -182,6 +182,7 @@ describe('moduleMocker', () => { it('mocks ES2015 non-enumerable static properties and methods', () => { class ClassFoo { static foo() {} + static fooProp: Function; } ClassFoo.fooProp = () => {}; diff --git a/packages/jest-mock/src/index.js b/packages/jest-mock/src/index.ts similarity index 70% rename from packages/jest-mock/src/index.js rename to packages/jest-mock/src/index.ts index 9a95f5d9f2e1..c6f041f2befa 100644 --- a/packages/jest-mock/src/index.js +++ b/packages/jest-mock/src/index.ts @@ -3,22 +3,33 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - * - * @flow */ -import type {Global} from 'types/Global'; - -type Mock = any; -export type MockFunctionMetadata = { - ref?: any, - members?: {[key: string]: MockFunctionMetadata}, - mockImpl?: () => any, - name?: string, - refID?: string | number, - type?: string, - value?: any, - length?: number, +type Global = NodeJS.Global; + +type MockFunctionMetadataType = + | 'object' + | 'array' + | 'regexp' + | 'function' + | 'constant' + | 'collection' + | 'null' + | 'undefined'; + +export type MockFunctionMetadata< + T, + Y extends unknown[], + Type = MockFunctionMetadataType +> = { + ref?: number; + members?: {[key: string]: MockFunctionMetadata}; + mockImpl?: (...args: Y) => T; + name?: string; + refID?: number; + type?: Type; + value?: T; + length?: number; }; /** @@ -38,33 +49,65 @@ type MockFunctionResult = { /** * Indicates how the call completed. */ - type: MockFunctionResultType, + type: MockFunctionResultType; /** * The value that was either thrown or returned by the function. * Undefined when type === 'incomplete'. */ - value: any, + value: unknown; }; -type MockFunctionState = { - instances: Array, - calls: Array>, +type MockFunctionState = { + calls: Array; + instances: Array; + invocationCallOrder: Array; /** * List of results of calls to the mock function. */ - results: Array, - invocationCallOrder: Array, + results: Array; }; type MockFunctionConfig = { - isReturnValueLastSet: boolean, - defaultReturnValue: any, - mockImpl: any, - mockName: string, - specificReturnValues: Array, - specificMockImpls: Array, + isReturnValueLastSet: boolean; + defaultReturnValue: unknown; + mockImpl: Function | undefined; + mockName: string; + specificReturnValues: Array; + specificMockImpls: Array; }; +interface Mock + extends Function, + MockInstance { + new (...args: Y): T; + (...args: Y): T; +} + +interface SpyInstance extends MockInstance {} + +interface MockInstance { + _isMockFunction: boolean; + _protoImpl: Function; + getMockName(): string; + getMockImplementation(): Function | undefined; + mock: MockFunctionState; + mockClear(): void; + mockReset(): void; + mockRestore(): void; + mockImplementation(fn: (...args: Y) => T): Mock; + mockImplementation(fn: () => Promise): Mock; + mockImplementationOnce(fn: (...args: Y) => T): Mock; + mockImplementationOnce(fn: () => Promise): Mock; + mockName(name: string): Mock; + mockReturnThis(): Mock; + mockReturnValue(value: T): Mock; + mockReturnValueOnce(value: T): Mock; + mockResolvedValue(value: T): Mock; + mockResolvedValueOnce(value: T): Mock; + mockRejectedValue(value: T): Mock; + mockRejectedValueOnce(value: T): Mock; +} + const MOCK_CONSTRUCTOR_NAME = 'mockConstructor'; const FUNCTION_NAME_RESERVED_PATTERN = /[\s!-\/:-@\[-`{-~]/; @@ -124,57 +167,113 @@ const RESERVED_KEYWORDS = new Set([ 'yield', ]); -function matchArity(fn: any, length: number): any { +function matchArity(fn: Function, length: number): Function { let mockConstructor; switch (length) { case 1: - mockConstructor = function(a) { + mockConstructor = function(this: unknown, _a: unknown) { return fn.apply(this, arguments); }; break; case 2: - mockConstructor = function(a, b) { + mockConstructor = function(this: unknown, _a: unknown, _b: unknown) { return fn.apply(this, arguments); }; break; case 3: - mockConstructor = function(a, b, c) { + mockConstructor = function( + this: unknown, + _a: unknown, + _b: unknown, + _c: unknown, + ) { return fn.apply(this, arguments); }; break; case 4: - mockConstructor = function(a, b, c, d) { + mockConstructor = function( + this: unknown, + _a: unknown, + _b: unknown, + _c: unknown, + _d: unknown, + ) { return fn.apply(this, arguments); }; break; case 5: - mockConstructor = function(a, b, c, d, e) { + mockConstructor = function( + this: unknown, + _a: unknown, + _b: unknown, + _c: unknown, + _d: unknown, + _e: unknown, + ) { return fn.apply(this, arguments); }; break; case 6: - mockConstructor = function(a, b, c, d, e, f) { + mockConstructor = function( + this: unknown, + _a: unknown, + _b: unknown, + _c: unknown, + _d: unknown, + _e: unknown, + _f: unknown, + ) { return fn.apply(this, arguments); }; break; case 7: - mockConstructor = function(a, b, c, d, e, f, g) { + mockConstructor = function( + this: unknown, + _a: unknown, + _b: unknown, + _c: unknown, + _d: unknown, + _e: unknown, + _f: unknown, + _g: unknown, + ) { return fn.apply(this, arguments); }; break; case 8: - mockConstructor = function(a, b, c, d, e, f, g, h) { + mockConstructor = function( + this: unknown, + _a: unknown, + _b: unknown, + _c: unknown, + _d: unknown, + _e: unknown, + _f: unknown, + _g: unknown, + _h: unknown, + ) { return fn.apply(this, arguments); }; break; case 9: - mockConstructor = function(a, b, c, d, e, f, g, h, i) { + mockConstructor = function( + this: unknown, + _a: unknown, + _b: unknown, + _c: unknown, + _d: unknown, + _e: unknown, + _f: unknown, + _g: unknown, + _h: unknown, + _i: unknown, + ) { return fn.apply(this, arguments); }; break; default: - mockConstructor = function() { + mockConstructor = function(this: unknown) { return fn.apply(this, arguments); }; break; @@ -183,11 +282,11 @@ function matchArity(fn: any, length: number): any { return mockConstructor; } -function getObjectType(value: any): string { +function getObjectType(value: unknown): string { return Object.prototype.toString.apply(value).slice(8, -1); } -function getType(ref?: any): string | null { +function getType(ref?: unknown): MockFunctionMetadataType | null { const typeName = getObjectType(ref); if ( typeName === 'Function' || @@ -253,10 +352,10 @@ function isReadonlyProp(object: any, prop: string): boolean { class ModuleMockerClass { _environmentGlobal: Global; - _mockState: WeakMap; + _mockState: WeakMap, MockFunctionState>; _mockConfigRegistry: WeakMap; _spyState: Set<() => void>; - ModuleMocker: Class; + ModuleMocker: typeof ModuleMockerClass; _invocationCallCounter: number; /** @@ -307,7 +406,7 @@ class ModuleMockerClass { if (!isReadonlyProp(object, prop)) { const propDesc = Object.getOwnPropertyDescriptor(object, prop); - + // @ts-ignore Object.__esModule if ((propDesc !== undefined && !propDesc.get) || object.__esModule) { slots.add(prop); } @@ -320,7 +419,7 @@ class ModuleMockerClass { return Array.from(slots); } - _ensureMockConfig(f: Mock): MockFunctionConfig { + _ensureMockConfig(f: Mock): MockFunctionConfig { let config = this._mockConfigRegistry.get(f); if (!config) { config = this._defaultMockConfig(); @@ -329,7 +428,9 @@ class ModuleMockerClass { return config; } - _ensureMockState(f: Mock): MockFunctionState { + _ensureMockState( + f: Mock, + ): MockFunctionState { let state = this._mockState.get(f); if (!state) { state = this._defaultMockState(); @@ -349,7 +450,7 @@ class ModuleMockerClass { }; } - _defaultMockState(): MockFunctionState { + _defaultMockState(): MockFunctionState { return { calls: [], instances: [], @@ -358,7 +459,34 @@ class ModuleMockerClass { }; } - _makeComponent(metadata: MockFunctionMetadata, restore?: () => void): Mock { + _makeComponent( + metadata: MockFunctionMetadata, + restore?: () => void, + ): Object; + _makeComponent( + metadata: MockFunctionMetadata, + restore?: () => void, + ): Array; + _makeComponent( + metadata: MockFunctionMetadata, + restore?: () => void, + ): RegExp; + _makeComponent( + metadata: MockFunctionMetadata< + T, + Y, + 'constant' | 'collection' | 'null' | 'undefined' + >, + restore?: () => void, + ): T; + _makeComponent( + metadata: MockFunctionMetadata, + restore?: () => void, + ): Mock; + _makeComponent( + metadata: MockFunctionMetadata, + restore?: () => void, + ): Object | Array | RegExp | T | undefined | Mock { if (metadata.type === 'object') { return new this._environmentGlobal.Object(); } else if (metadata.type === 'array') { @@ -373,9 +501,7 @@ class ModuleMockerClass { ) { return metadata.value; } else if (metadata.type === 'function') { - /* eslint-disable prefer-const */ - let f; - /* eslint-enable prefer-const */ + let f: Mock; const prototype = (metadata.members && @@ -384,17 +510,17 @@ class ModuleMockerClass { {}; const prototypeSlots = this._getSlots(prototype); const mocker = this; - const mockConstructor = matchArity(function() { + const mockConstructor = matchArity(function(this: T, ...args: Y) { const mockState = mocker._ensureMockState(f); const mockConfig = mocker._ensureMockConfig(f); mockState.instances.push(this); - mockState.calls.push(Array.prototype.slice.call(arguments)); + mockState.calls.push(args); // Create and record an "incomplete" mock result immediately upon // calling rather than waiting for the mock to return. This avoids // issues caused by recursion where results can be recorded in the // wrong order. const mockResult = { - type: 'incomplete', + type: ('incomplete' as unknown) as MockFunctionResultType, value: undefined, }; mockState.results.push(mockResult); @@ -422,8 +548,11 @@ class ModuleMockerClass { // it easier to interact with mock instance call and // return values if (prototype[slot].type === 'function') { + // @ts-ignore no index signature const protoImpl = this[slot]; + // @ts-ignore no index signature this[slot] = mocker.generateFromMetadata(prototype[slot]); + // @ts-ignore no index signature this[slot]._protoImpl = protoImpl; } }); @@ -487,7 +616,10 @@ class ModuleMockerClass { return finalReturnValue; }, metadata.length || 0); - f = this._createMockFunction(metadata, mockConstructor); + f = (this._createMockFunction( + metadata, + mockConstructor, + ) as unknown) as Mock; f._isMockFunction = true; f.getMockImplementation = () => this._ensureMockConfig(f).mockImpl; @@ -495,10 +627,9 @@ class ModuleMockerClass { this._spyState.add(restore); } - this._mockState.set(f, this._defaultMockState()); + this._mockState.set(f, this._defaultMockState()); this._mockConfigRegistry.set(f, this._defaultMockConfig()); - // $FlowFixMe - defineProperty getters not supported Object.defineProperty(f, 'mock', { configurable: false, enumerable: true, @@ -522,20 +653,20 @@ class ModuleMockerClass { return restore ? restore() : undefined; }; - f.mockReturnValueOnce = value => { + f.mockReturnValueOnce = (value: T) => { // next function call will return this value or default return value const mockConfig = this._ensureMockConfig(f); mockConfig.specificReturnValues.push(value); return f; }; - f.mockResolvedValueOnce = value => + f.mockResolvedValueOnce = (value: T) => f.mockImplementationOnce(() => Promise.resolve(value)); - f.mockRejectedValueOnce = value => + f.mockRejectedValueOnce = (value: T) => f.mockImplementationOnce(() => Promise.reject(value)); - f.mockReturnValue = value => { + f.mockReturnValue = (value: T) => { // next function call will return specified return value or this one const mockConfig = this._ensureMockConfig(f); mockConfig.isReturnValueLastSet = true; @@ -543,13 +674,15 @@ class ModuleMockerClass { return f; }; - f.mockResolvedValue = value => + f.mockResolvedValue = (value: T) => f.mockImplementation(() => Promise.resolve(value)); - f.mockRejectedValue = value => + f.mockRejectedValue = (value: T) => f.mockImplementation(() => Promise.reject(value)); - f.mockImplementationOnce = fn => { + f.mockImplementationOnce = ( + fn: ((...args: Y) => T) | (() => Promise), + ): Mock => { // next function call will use this mock implementation return value // or default mock implementation return value const mockConfig = this._ensureMockConfig(f); @@ -558,7 +691,9 @@ class ModuleMockerClass { return f; }; - f.mockImplementation = fn => { + f.mockImplementation = ( + fn: ((...args: Y) => T) | (() => Promise), + ): Mock => { // next function call will use mock implementation return value const mockConfig = this._ensureMockConfig(f); mockConfig.isReturnValueLastSet = false; @@ -568,11 +703,11 @@ class ModuleMockerClass { }; f.mockReturnThis = () => - f.mockImplementation(function() { + f.mockImplementation(function(this: T) { return this; }); - f.mockName = name => { + f.mockName = (name: string) => { if (name) { const mockConfig = this._ensureMockConfig(f); mockConfig.mockName = name; @@ -596,10 +731,10 @@ class ModuleMockerClass { } } - _createMockFunction( - metadata: MockFunctionMetadata, - mockConstructor: () => any, - ): any { + _createMockFunction( + metadata: MockFunctionMetadata, + mockConstructor: Function, + ): Function { let name = metadata.name; if (!name) { return mockConstructor; @@ -656,11 +791,22 @@ class ModuleMockerClass { return createConstructor(mockConstructor); } - _generateMock( - metadata: MockFunctionMetadata, - callbacks: Array<() => any>, - refs: Object, - ): Mock { + _generateMock( + metadata: MockFunctionMetadata, + callbacks: Array, + refs: { + [key: string]: + | Object + | Array + | RegExp + | T + | undefined + | Mock; + }, + ): Mock { + // metadata not compatible but it's the same type, maybe problem with + // overloading of _makeComponent and not _generateMock? + // @ts-ignore const mock = this._makeComponent(metadata); if (metadata.refID != null) { refs[metadata.refID] = mock; @@ -669,7 +815,11 @@ class ModuleMockerClass { this._getSlots(metadata.members).forEach(slot => { const slotMetadata = (metadata.members && metadata.members[slot]) || {}; if (slotMetadata.ref != null) { - callbacks.push(() => (mock[slot] = refs[slotMetadata.ref])); + callbacks.push( + (function(ref) { + return () => (mock[slot] = refs[ref]); + })(slotMetadata.ref), + ); } else { mock[slot] = this._generateMock(slotMetadata, callbacks, refs); } @@ -691,8 +841,10 @@ class ModuleMockerClass { * @param _metadata Metadata for the mock in the schema returned by the * getMetadata method of this module. */ - generateFromMetadata(_metadata: MockFunctionMetadata): Mock { - const callbacks = []; + generateFromMetadata( + _metadata: MockFunctionMetadata, + ): Mock { + const callbacks: Function[] = []; const refs = {}; const mock = this._generateMock(_metadata, callbacks, refs); callbacks.forEach(setter => setter()); @@ -703,8 +855,11 @@ class ModuleMockerClass { * @see README.md * @param component The component for which to retrieve metadata. */ - getMetadata(component: any, _refs?: Map): ?MockFunctionMetadata { - const refs = _refs || new Map(); + getMetadata( + component: T, + _refs?: Map, + ): MockFunctionMetadata | null { + const refs = _refs || new Map(); const ref = refs.get(component); if (ref != null) { return {ref}; @@ -715,7 +870,7 @@ class ModuleMockerClass { return null; } - const metadata: MockFunctionMetadata = {type}; + const metadata: MockFunctionMetadata = {type}; if ( type === 'constant' || type === 'collection' || @@ -725,8 +880,11 @@ class ModuleMockerClass { metadata.value = component; return metadata; } else if (type === 'function') { + // @ts-ignore this is a function so it has a name metadata.name = component.name; + // @ts-ignore may be a mock if (component._isMockFunction === true) { + // @ts-ignore may be a mock metadata.mockImpl = component.getMockImplementation(); } } @@ -734,28 +892,27 @@ class ModuleMockerClass { metadata.refID = refs.size; refs.set(component, metadata.refID); - let members = null; + let members: {[key: string]: MockFunctionMetadata} | null = null; // Leave arrays alone if (type !== 'array') { - if (type !== 'undefined') { - this._getSlots(component).forEach(slot => { - if ( - type === 'function' && - component._isMockFunction === true && - slot.match(/^mock/) - ) { - return; - } - - const slotMetadata = this.getMetadata(component[slot], refs); - if (slotMetadata) { - if (!members) { - members = {}; - } - members[slot] = slotMetadata; + this._getSlots(component).forEach(slot => { + if ( + type === 'function' && + // @ts-ignore may be a mock + component._isMockFunction === true && + slot.match(/^mock/) + ) { + return; + } + // @ts-ignore no index signature + const slotMetadata = this.getMetadata(component[slot], refs); + if (slotMetadata) { + if (!members) { + members = {}; } - }); - } + members[slot] = slotMetadata; + } + }); } if (members) { @@ -769,16 +926,39 @@ class ModuleMockerClass { return !!fn && fn._isMockFunction === true; } - fn(implementation?: any): any { + fn(implementation?: (...args: Y) => T): Mock { const length = implementation ? implementation.length : 0; - const fn = this._makeComponent({length, type: 'function'}); + const fn = this._makeComponent({length, type: 'function'}); if (implementation) { fn.mockImplementation(implementation); } return fn; } - spyOn(object: any, methodName: any, accessType?: string): any { + spyOn( + object: T, + methodName: M, + accessType: 'get', + ): SpyInstance; + + spyOn( + object: T, + methodName: M, + accessType: 'set', + ): SpyInstance; + + spyOn( + object: T, + methodName: M, + ): T[M] extends (...args: any[]) => any + ? SpyInstance, ArgsType> + : never; + + spyOn( + object: T, + methodName: M, + accessType?: 'get' | 'set', + ) { if (accessType) { return this._spyOnProperty(object, methodName, accessType); } @@ -802,11 +982,13 @@ class ModuleMockerClass { ); } + // @ts-ignore overriding original method with a Mock object[methodName] = this._makeComponent({type: 'function'}, () => { object[methodName] = original; }); - object[methodName].mockImplementation(function() { + // @ts-ignore original method is now a Mock + object[methodName].mockImplementation(function(this: unknown) { return original.apply(this, arguments); }); } @@ -814,7 +996,11 @@ class ModuleMockerClass { return object[methodName]; } - _spyOnProperty(obj: any, propertyName: any, accessType: string = 'get'): any { + _spyOnProperty( + obj: T, + propertyName: M, + accessType: 'get' | 'set' = 'get', + ): Mock { if (typeof obj !== 'object' && typeof obj !== 'function') { throw new Error( 'Cannot spyOn on a primitive value; ' + this._typeOf(obj) + ' given', @@ -867,19 +1053,23 @@ class ModuleMockerClass { } descriptor[accessType] = this._makeComponent({type: 'function'}, () => { - // $FlowFixMe + if (!descriptor) { + return; + } descriptor[accessType] = original; - // $FlowFixMe Object.defineProperty(obj, propertyName, descriptor); }); - descriptor[accessType].mockImplementation(function() { + (descriptor[accessType] as Mock).mockImplementation(function( + this: unknown, + ) { + // @ts-ignore return original.apply(this, arguments); }); } Object.defineProperty(obj, propertyName, descriptor); - return descriptor[accessType]; + return descriptor[accessType] as Mock; } clearAllMocks() { diff --git a/packages/jest-mock/tsconfig.json b/packages/jest-mock/tsconfig.json new file mode 100644 index 000000000000..7bb06bce6d20 --- /dev/null +++ b/packages/jest-mock/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "build" + } +}