Skip to content

Commit

Permalink
feat: Print promise values in error messages
Browse files Browse the repository at this point in the history
Previously all expectations on promises were being printed as
`thenReturn({})`. Now the promise inner value will be correctly printed
for `thenResolve` and `thenReject` expectations. `thenReturn(promise)`
will still print `{}` as there's no way to peek inside the promise.
  • Loading branch information
NiGhTTraX committed Feb 11, 2021
1 parent 95827ef commit bc0301f
Show file tree
Hide file tree
Showing 19 changed files with 316 additions and 139 deletions.
4 changes: 2 additions & 2 deletions src/base-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export abstract class BaseRepository implements ExpectationRepository {
if (propertyExpectation) {
this.countAndConsume(propertyExpectation);

return propertyExpectation.expectation.returnValue;
return propertyExpectation.expectation.returnValue.value;
}

return (...args: any[]) => {
Expand All @@ -63,7 +63,7 @@ export abstract class BaseRepository implements ExpectationRepository {
this.recordExpected(property, args);
this.countAndConsume(callExpectation);

return callExpectation.expectation.returnValue;
return callExpectation.expectation.returnValue.value;
}

this.recordUnexpected(property, args);
Expand Down
2 changes: 0 additions & 2 deletions src/expectation-repository.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { Expectation } from './expectation';

export type ReturnValue = { returnValue: any };

export type Call = {
arguments: any[] | undefined;
};
Expand Down
9 changes: 8 additions & 1 deletion src/expectation.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
export type ReturnValue = {
value: any;
isPromise?: boolean;
promiseValue?: any;
isError?: boolean;
};

export interface Expectation {
property: PropertyKey;

Expand All @@ -7,7 +14,7 @@ export interface Expectation {
*/
args: any[] | undefined;

returnValue: any;
returnValue: ReturnValue;

min: number;

Expand Down
8 changes: 4 additions & 4 deletions src/pending-expectation.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import { MissingWhen, UnfinishedExpectation } from './errors';
import { Expectation } from './expectation';
import { Expectation, ReturnValue } from './expectation';
import { ExpectationRepository } from './expectation-repository';
import { printWhen } from './print';

export type ExpectationFactory = (
property: PropertyKey,
args: any[] | undefined,
returnValue: any
returnValue: ReturnValue
) => Expectation;

export interface PendingExpectation {
// TODO: get rid of repo
start(repo: ExpectationRepository): void;

finish(returnValue: any): Expectation;
finish(returnValue: ReturnValue): Expectation;

clear(): void;

Expand Down Expand Up @@ -54,7 +54,7 @@ export class RepoSideEffectPendingExpectation implements PendingExpectation {
this._args = value;
}

finish(returnValue: any): Expectation {
finish(returnValue: ReturnValue): Expectation {
if (!this._repo) {
throw new MissingWhen();
}
Expand Down
28 changes: 22 additions & 6 deletions src/print.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { EXPECTED_COLOR, printExpected } from 'jest-matcher-utils';
import { ApplyProp, Expectation } from './expectation';
import { ApplyProp, Expectation, ReturnValue } from './expectation';
import { isMatcher } from './matcher';

export const printProperty = (property: PropertyKey) => {
Expand All @@ -24,11 +24,27 @@ export const printCall = (property: PropertyKey, args: any[]) => {
return `${prettyProperty}(${prettyArgs})`;
};

export const printReturns = (returnValue: any, min: number, max: number) => {
const isError = returnValue instanceof Error;
export const printReturns = (
{ isError, isPromise, value, promiseValue }: ReturnValue,
min: number,
max: number
) => {
let thenPrefix = '';

if (isPromise) {
if (isError) {
thenPrefix += 'thenReject';
} else {
thenPrefix += 'thenResolve';
}
} else if (isError) {
thenPrefix += 'thenThrow';
} else {
thenPrefix += 'thenReturn';
}

return `.${isError ? 'thenThrow' : 'thenReturn'}(${printExpected(
isError ? returnValue.message : returnValue
return `.${thenPrefix}(${printExpected(
promiseValue || value
)}).between(${min}, ${max})`;
};

Expand All @@ -43,7 +59,7 @@ export const printWhen = (property: PropertyKey, args: any[] | undefined) => {
export const printExpectation = (
property: PropertyKey,
args: any[] | undefined,
returnValue: any,
returnValue: ReturnValue,
min: number,
max: number
) => `${printWhen(property, args)}${printReturns(returnValue, min, max)}`;
Expand Down
40 changes: 33 additions & 7 deletions src/returns.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ReturnValue } from './expectation';
import { createInvocationCount, InvocationCount } from './invocation-count';
import { PendingExpectation } from './pending-expectation';

Expand Down Expand Up @@ -83,7 +84,7 @@ export type Stub<T> = [T] extends [Promise<infer U>]
* Set a return value for the currently pending expectation.
*/
const finishPendingExpectation = (
returnValue: any,
returnValue: ReturnValue,
pendingExpectation: PendingExpectation
) => {
const finishedExpectation = pendingExpectation.finish(returnValue);
Expand Down Expand Up @@ -115,30 +116,55 @@ export const createReturns = <R>(
// TODO: should probably fix this
/* istanbul ignore next: because it will be overridden by
* promiseStub and the types are compatible */
return finishPendingExpectation(returnValue, pendingExpectation);
return finishPendingExpectation(
{ value: returnValue, isError: false, isPromise: false },
pendingExpectation
);
},
thenThrow: (errorOrMessage?: Error | string): InvocationCount =>
finishPendingExpectation(getError(errorOrMessage), pendingExpectation),
finishPendingExpectation(
{ value: getError(errorOrMessage), isError: true, isPromise: false },
pendingExpectation
),
};

const promiseStub: PromiseStub<any> = {
thenReturn: (promise: Promise<any>): InvocationCount =>
finishPendingExpectation(promise, pendingExpectation),
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
),

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

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

// @ts-ignore TODO: because the return type is a conditional and
// @ts-expect-error TODO: because the return type is a conditional and
// we're doing something fishy here that TS doesn't like
return { ...nonPromiseStub, ...promiseStub };
};
4 changes: 2 additions & 2 deletions src/strong-expectation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import isEqual from 'lodash/isEqual';
import { Expectation } from './expectation';
import { Expectation, ReturnValue } from './expectation';
import { isMatcher } from './matcher';
import { printExpectation } from './print';

Expand All @@ -25,7 +25,7 @@ export class StrongExpectation implements Expectation {
constructor(
public property: PropertyKey,
public args: any[] | undefined,
public returnValue: any
public returnValue: ReturnValue
) {}

setInvocationCount(min: number, max = 1) {
Expand Down
8 changes: 6 additions & 2 deletions tests/errors.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,12 @@ foobar`

describe('UnexpectedCalls', () => {
it('should print the unexpected calls and remaining expectations', () => {
const e1 = new NotMatchingExpectation(':irrelevant:', undefined);
const e2 = new NotMatchingExpectation(':irrelevant:', undefined);
const e1 = new NotMatchingExpectation(':irrelevant:', {
value: undefined,
});
const e2 = new NotMatchingExpectation(':irrelevant:', {
value: undefined,
});
e1.toJSON = () => 'e1';
e2.toJSON = () => 'e2';

Expand Down
9 changes: 3 additions & 6 deletions tests/expectation-repository.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
/* eslint-disable class-methods-use-this */
import { Expectation } from '../src/expectation';
import {
ExpectationRepository,
ReturnValue,
} from '../src/expectation-repository';
import { Expectation, ReturnValue } from '../src/expectation';
import { ExpectationRepository } from '../src/expectation-repository';

export class OneIncomingExpectationRepository implements ExpectationRepository {
public expectation: Expectation | undefined;
Expand All @@ -13,7 +10,7 @@ export class OneIncomingExpectationRepository implements ExpectationRepository {
}

get(): ReturnValue | undefined {
return this.expectation && { returnValue: this.expectation.returnValue };
return this.expectation?.returnValue;
}

getUnmet() {
Expand Down
16 changes: 8 additions & 8 deletions tests/expectations.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Expectation } from '../src/expectation';
import { Expectation, ReturnValue } from '../src/expectation';
import { ExpectationRepository } from '../src/expectation-repository';
import {
ExpectationFactory,
Expand All @@ -18,7 +18,7 @@ export class NeverMatchingExpectation implements Expectation {

property = 'bar';

returnValue = undefined;
returnValue = { value: undefined };

matches = () => false;
}
Expand All @@ -36,7 +36,7 @@ export class OneUseAlwaysMatchingExpectation implements Expectation {

property = 'bar';

returnValue = 42;
returnValue = { value: 42 };

matches = () => true;
}
Expand All @@ -56,7 +56,7 @@ export class SpyExpectation implements Expectation {
constructor(
public property: PropertyKey,
public args: any[] | undefined,
public returnValue: any
public returnValue: ReturnValue
) {}

matches = () => false;
Expand All @@ -75,7 +75,7 @@ export class SpyPendingExpectation implements PendingExpectation {

public clearCalled = false;

public finishCalledWith: any;
public finishCalledWith: ReturnValue | undefined;

public propertyCalledWith: PropertyKey | undefined;

Expand All @@ -89,7 +89,7 @@ export class SpyPendingExpectation implements PendingExpectation {
this.clearCalled = true;
}

finish(returnValue: any) {
finish(returnValue: ReturnValue) {
this.finishCalledWith = returnValue;
return new OneUseAlwaysMatchingExpectation();
}
Expand All @@ -104,7 +104,7 @@ export class SpyPendingExpectation implements PendingExpectation {
}

export class MatchingPropertyExpectation implements Expectation {
constructor(public property: PropertyKey, public returnValue: any) {}
constructor(public property: PropertyKey, public returnValue: ReturnValue) {}

args = undefined;

Expand All @@ -123,7 +123,7 @@ export class MatchingPropertyExpectation implements Expectation {
}

export class MatchingCallExpectation implements Expectation {
constructor(public property: PropertyKey, public returnValue: any) {}
constructor(public property: PropertyKey, public returnValue: ReturnValue) {}

args = [];

Expand Down
4 changes: 3 additions & 1 deletion tests/invocation-count.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import { SpyExpectation } from './expectations';

describe('invocation count', () => {
it('between should set the min and max', () => {
const expectation = new SpyExpectation('bar', undefined, undefined);
const expectation = new SpyExpectation('bar', undefined, {
value: undefined,
});

const invocationCount = createInvocationCount(expectation);
invocationCount.between(2, 8);
Expand Down
Loading

0 comments on commit bc0301f

Please sign in to comment.