Skip to content

Commit

Permalink
fix: Don't treat any returns as promises
Browse files Browse the repository at this point in the history
Previously, a `() => any` mocked function would activate the
`thenResolve` helpers and require a Promise in `thenReturns`. This is no
 longer the case, and now `thenReturns` will accept `any`, and the
 `thenResolve` helper will no longer be available. You can of course
 still return a promise with `thenReturns(Promise.resolve())`.
  • Loading branch information
NiGhTTraX committed Aug 28, 2022
1 parent 7150e8f commit 9304492
Show file tree
Hide file tree
Showing 4 changed files with 63 additions and 90 deletions.
28 changes: 13 additions & 15 deletions src/return/returns.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ describe('returns', () => {
})
).thenReturn(expectation);

createReturns<number>(
createReturns(
SM.instance(pendingExpectation),
SM.instance(repo)
).thenReturn(23);
Expand All @@ -40,10 +40,9 @@ describe('returns', () => {
})
).thenReturn(expectation);

createReturns<number>(
SM.instance(pendingExpectation),
SM.instance(repo)
).thenThrow(error);
createReturns(SM.instance(pendingExpectation), SM.instance(repo)).thenThrow(
error
);
});

it('should set an empty exception', () => {
Expand All @@ -55,7 +54,7 @@ describe('returns', () => {
})
).thenReturn(expectation);

createReturns<number>(
createReturns(
SM.instance(pendingExpectation),
SM.instance(repo)
).thenThrow();
Expand All @@ -70,10 +69,9 @@ describe('returns', () => {
})
).thenReturn(expectation);

createReturns<number>(
SM.instance(pendingExpectation),
SM.instance(repo)
).thenThrow('foobar');
createReturns(SM.instance(pendingExpectation), SM.instance(repo)).thenThrow(
'foobar'
);
});

it('should set a return promise', async () => {
Expand All @@ -87,7 +85,7 @@ describe('returns', () => {
})
).thenReturn(expectation);

createReturns<Promise<number>>(
createReturns(
SM.instance(pendingExpectation),
SM.instance(repo)
).thenReturn(promise);
Expand All @@ -102,7 +100,7 @@ describe('returns', () => {
})
).thenReturn(expectation);

createReturns<Promise<number>>(
createReturns(
SM.instance(pendingExpectation),
SM.instance(repo)
).thenResolve(23);
Expand All @@ -119,7 +117,7 @@ describe('returns', () => {
})
).thenReturn(expectation);

createReturns<Promise<number>>(
createReturns(
SM.instance(pendingExpectation),
SM.instance(repo)
).thenReject(error);
Expand All @@ -134,7 +132,7 @@ describe('returns', () => {
})
).thenReturn(expectation);

createReturns<Promise<number>>(
createReturns(
SM.instance(pendingExpectation),
SM.instance(repo)
).thenReject();
Expand All @@ -149,7 +147,7 @@ describe('returns', () => {
})
).thenReturn(expectation);

createReturns<Promise<number>>(
createReturns(
SM.instance(pendingExpectation),
SM.instance(repo)
).thenReject('foobar');
Expand Down
110 changes: 40 additions & 70 deletions src/return/returns.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { ReturnValue } from '../expectation/repository/return-value';
import { ExpectationRepository } from '../expectation/repository/expectation-repository';
import { ReturnValue } from '../expectation/repository/return-value';
import { PendingExpectation } from '../when/pending-expectation';
import { createInvocationCount, InvocationCount } from './invocation-count';

type PromiseStub<R, P> = {
export type PromiseStub<R, P> = {
/**
* Set the return value for the current call.
*
Expand Down Expand Up @@ -49,7 +49,7 @@ type PromiseStub<R, P> = {
thenReject(): InvocationCount;
};

type NonPromiseStub<R> = {
export type NonPromiseStub<R> = {
/**
* Set the return value for the current call.
*
Expand Down Expand Up @@ -80,11 +80,6 @@ type NonPromiseStub<R> = {
thenThrow(): InvocationCount;
};

// Wrap T in a tuple to prevent distribution in case it's a union.
export type Stub<T> = [T] extends [Promise<infer U>]
? PromiseStub<U, T>
: NonPromiseStub<T>;

const finishPendingExpectation = (
returnValue: ReturnValue,
pendingExpectation: PendingExpectation,
Expand All @@ -109,67 +104,42 @@ const getError = (errorOrMessage: Error | string | undefined): Error => {
return new Error();
};

export const createReturns = <R>(
export const createReturns = (
pendingExpectation: PendingExpectation,
repository: ExpectationRepository
): Stub<R> => {
const nonPromiseStub: NonPromiseStub<any> = {
// TODO: merge this with the promise version
thenReturn:
/* istanbul ignore next: because this is overwritten by the promise version */ (
returnValue: any
): InvocationCount =>
finishPendingExpectation(
{ value: returnValue, isError: false, isPromise: false },
pendingExpectation,
repository
),
thenThrow: (errorOrMessage?: Error | string): InvocationCount =>
finishPendingExpectation(
{ value: getError(errorOrMessage), isError: true, isPromise: false },
pendingExpectation,
repository
),
};

const promiseStub: PromiseStub<any, any> = {
thenReturn: (promise: Promise<any>): InvocationCount =>
finishPendingExpectation(
{
value: promise,
isError: false,
// We're setting this to false because we can't distinguish between a
// promise thenReturn and a normal thenReturn.
isPromise: false,
},
pendingExpectation,
repository
),

thenResolve: (promiseValue: any): InvocationCount =>
finishPendingExpectation(
{
value: promiseValue,
isError: false,
isPromise: true,
},
pendingExpectation,
repository
),

thenReject: (errorOrMessage?: Error | string): InvocationCount =>
finishPendingExpectation(
{
value: getError(errorOrMessage),
isError: true,
isPromise: true,
},
pendingExpectation,
repository
),
};

// @ts-expect-error because the return type is a conditional, and we're merging
// both branches here
return { ...nonPromiseStub, ...promiseStub };
};
) => ({
thenReturn: (returnValue: any): InvocationCount =>
finishPendingExpectation(
// This will handle both thenReturn(23) and thenReturn(Promise.resolve(3)).
{ value: returnValue, isError: false, isPromise: false },
pendingExpectation,
repository
),
thenThrow: (errorOrMessage?: Error | string): InvocationCount =>
finishPendingExpectation(
{ value: getError(errorOrMessage), isError: true, isPromise: false },
pendingExpectation,
repository
),
thenResolve: (promiseValue: any): InvocationCount =>
finishPendingExpectation(
{
value: promiseValue,
isError: false,
isPromise: true,
},
pendingExpectation,
repository
),

thenReject: (errorOrMessage?: Error | string): InvocationCount =>
finishPendingExpectation(
{
value: getError(errorOrMessage),
isError: true,
isPromise: true,
},
pendingExpectation,
repository
),
});
11 changes: 7 additions & 4 deletions src/when/when.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { getActiveMock, getMockState } from '../mock/map';
import { Mode, setMode } from '../mock/mock';
import { createReturns, Stub } from '../return/returns';
import { createReturns, NonPromiseStub, PromiseStub } from '../return/returns';

interface When {
<R>(expectation: () => Promise<R>): PromiseStub<R, Promise<R>>;
<R>(expectation: () => R): NonPromiseStub<R>;
}
/**
* Set an expectation on a mock.
*
Expand All @@ -27,13 +31,12 @@ import { createReturns, Stub } from '../return/returns';
* const fn = mock<(x: number) => Promise<number>();
* when(() => fn(23)).thenResolve(42);
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars
export const when = <R>(expectation: () => R): Stub<R> => {
export const when: When = <R>(expectation: () => R) => {
setMode(Mode.EXPECT);
expectation();
setMode(Mode.CALL);

const { pendingExpectation, repository } = getMockState(getActiveMock());

return createReturns<R>(pendingExpectation, repository);
return createReturns(pendingExpectation, repository);
};
4 changes: 3 additions & 1 deletion tests/types.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,11 @@ it('type safety', () => {
// @ts-expect-error promises only reject
when(() => fnp()).thenThrow;

// any will be treated like a promise but should allow any return value.
// any should not enable the promise helpers
const fnany = mock<() => any>();
when(() => fnany()).thenReturn(23);
// @ts-expect-error
when(() => fnany()).thenResolve(23);
}

function matcherSafety() {
Expand Down

0 comments on commit 9304492

Please sign in to comment.