Skip to content

Commit

Permalink
Merge pull request #95 from lallenfrancisl/fix/mock-deep-type-issue
Browse files Browse the repository at this point in the history
Fix DeepMockProxy type error for objects which are functions and have fields at the same time
  • Loading branch information
marchaos committed Jul 16, 2022
2 parents e98bc1e + 220d97f commit d0fc0d1
Show file tree
Hide file tree
Showing 2 changed files with 74 additions and 8 deletions.
67 changes: 66 additions & 1 deletion src/Mock.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,26 @@ class Test4 {
constructor(test1: Test1, int: MockInt) {}
}

export interface FunctionWithPropsMockInt {
(arg1: number): number;

prop: number;

nonDeepProp: (arg: Test1) => number;

deepProp: Test2;
}

export class Test6 {
public id: number;
funcValueProp: FunctionWithPropsMockInt;

constructor(funcValueProp: FunctionWithPropsMockInt, id: number) {
this.id = id;
this.funcValueProp = funcValueProp;
}
}

describe('jest-mock-extended', () => {
test('Can be assigned back to itself even when there are private parts', () => {
// No TS errors here
Expand Down Expand Up @@ -226,7 +246,7 @@ describe('jest-mock-extended', () => {
mockObj.getNumberWithMockArg.calledWith(mockArg).mockReturnValue(4);

expect(mockObj.getNumberWithMockArg(mockArg)).toBe(4);
})
});
});

describe('Matchers with toHaveBeenCalledWith', () => {
Expand Down Expand Up @@ -282,6 +302,51 @@ describe('jest-mock-extended', () => {
});
});

describe('Deep mock support for class variables which are functions but also have nested properties and functions', () => {
test('can deep mock members', () => {
const mockObj = mockDeep<Test6>();
const input = new Test1(1);
mockObj.funcValueProp.nonDeepProp.calledWith(input).mockReturnValue(4);

expect(mockObj.funcValueProp.nonDeepProp(input)).toBe(4);
});

test('three or more level deep mock', () => {
const mockObj = mockDeep<Test6>();
mockObj.funcValueProp.deepProp.deeperProp.getNumber.calledWith(1).mockReturnValue(4);

expect(mockObj.funcValueProp.deepProp.deeperProp.getNumber(1)).toBe(4);
});

test('maintains API for deep mocks', () => {
const mockObj = mockDeep<Test6>();
mockObj.funcValueProp.deepProp.getNumber(100);

expect(mockObj.funcValueProp.deepProp.getNumber.mock.calls[0][0]).toBe(100);
});

test('deep expectation work as expected', () => {
const mockObj = mockDeep<Test6>();
mockObj.funcValueProp.deepProp.getNumber(2);

expect(mockObj.funcValueProp.deepProp.getNumber).toHaveBeenCalledTimes(1);
});

test('can mock base function which have properties', () => {
const mockObj = mockDeep<Test6>();
mockObj.funcValueProp.calledWith(1).mockReturnValue(2);

expect(mockObj.funcValueProp(1)).toBe(2);
});

test('base function expectation work as expected', () => {
const mockObj = mockDeep<Test6>();
mockObj.funcValueProp(1);

expect(mockObj.funcValueProp).toHaveBeenCalledTimes(1);
});
});

describe('mock implementation support', () => {
test('can provide mock implementation for props', () => {
const mockObj = mock<Test1>({
Expand Down
15 changes: 8 additions & 7 deletions src/Mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,12 @@ export interface CalledWithMock<T, Y extends any[]> extends jest.Mock<T, Y> {
export type MockProxy<T> = {
// This supports deep mocks in the else branch
[K in keyof T]: T[K] extends (...args: infer A) => infer B ? CalledWithMock<B, A> : T[K];
} &
T;
} & T;

export type DeepMockProxy<T> = {
// This supports deep mocks in the else branch
[K in keyof T]: T[K] extends (...args: infer A) => infer B ? CalledWithMock<B, A> : DeepMockProxy<T[K]>;
} &
T;
[K in keyof T]: T[K] extends (...args: infer A) => infer B ? CalledWithMock<B, A> & DeepMockProxy<T[K]> : DeepMockProxy<T[K]>;
} & T;

export interface MockOpts {
deep?: boolean;
Expand Down Expand Up @@ -89,7 +87,7 @@ export const mockReset = (mock: MockProxy<any>) => {
}
};

export const mockDeep = <T>(mockImplementation?: DeepPartial<T>): DeepMockProxy<T> & T => mock(mockImplementation, { deep: true });
export const mockDeep = <T>(mockImplementation?: DeepPartial<T>) => mock<T, DeepMockProxy<T> & T>(mockImplementation, { deep: true });

const overrideMockImp = (obj: DeepPartial<any>, opts?: MockOpts) => {
const proxy = new Proxy<MockProxy<any>>(obj, handler(opts));
Expand Down Expand Up @@ -154,7 +152,10 @@ const handler = (opts?: MockOpts) => ({
},
});

const mock = <T>(mockImplementation: DeepPartial<T> = {} as DeepPartial<T>, opts?: MockOpts): MockProxy<T> & T => {
const mock = <T, MockedReturn extends MockProxy<T> & T = MockProxy<T> & T>(
mockImplementation: DeepPartial<T> = {} as DeepPartial<T>,
opts?: MockOpts
): MockedReturn => {
// @ts-ignore private
mockImplementation!._isMockObject = true;
return overrideMockImp(mockImplementation, opts);
Expand Down

0 comments on commit d0fc0d1

Please sign in to comment.