Skip to content

Commit

Permalink
Add support for async matchers
Browse files Browse the repository at this point in the history
  • Loading branch information
bilby91 committed Apr 4, 2018
1 parent 0d58623 commit 3e84b42
Show file tree
Hide file tree
Showing 6 changed files with 151 additions and 64 deletions.
68 changes: 36 additions & 32 deletions flow-typed/npm/jest_v21.x.x.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ type JestMockFn<TArguments: $ReadOnlyArray<*>, TReturn> = {
* An array that contains all the object instances that have been
* instantiated from this mock function.
*/
instances: Array<TReturn>
instances: Array<TReturn>,
},
/**
* Resets all information stored in the mockFn.mock.calls and
Expand Down Expand Up @@ -45,15 +45,15 @@ type JestMockFn<TArguments: $ReadOnlyArray<*>, TReturn> = {
* will also be executed when the mock is called.
*/
mockImplementation(
fn: (...args: TArguments) => TReturn
fn: (...args: TArguments) => TReturn,
): JestMockFn<TArguments, TReturn>,
/**
* Accepts a function that will be used as an implementation of the mock for
* one call to the mocked function. Can be chained so that multiple function
* calls produce different results.
*/
mockImplementationOnce(
fn: (...args: TArguments) => TReturn
fn: (...args: TArguments) => TReturn,
): JestMockFn<TArguments, TReturn>,
/**
* Just a simple sugar function for returning `this`
Expand All @@ -66,14 +66,14 @@ type JestMockFn<TArguments: $ReadOnlyArray<*>, TReturn> = {
/**
* Sugar for only returning a value once inside your mock
*/
mockReturnValueOnce(value: TReturn): JestMockFn<TArguments, TReturn>
mockReturnValueOnce(value: TReturn): JestMockFn<TArguments, TReturn>,
};

type JestAsymmetricEqualityType = {
/**
* A custom Jasmine equality tester
*/
asymmetricMatch(value: mixed): boolean
asymmetricMatch(value: mixed): boolean,
};

type JestCallsType = {
Expand All @@ -83,21 +83,25 @@ type JestCallsType = {
count(): number,
first(): mixed,
mostRecent(): mixed,
reset(): void
reset(): void,
};

type JestClockType = {
install(): void,
mockDate(date: Date): void,
tick(milliseconds?: number): void,
uninstall(): void
uninstall(): void,
};

type JestMatcherResult = {
type JestMatcherSyncResult = {
message?: string | (() => string),
pass: boolean
pass: boolean,
};

type JestMatcherAsyncResult = Promise<JestMatcherSyncResult>;

type JestMatcherResult = JestMatcherSyncResult | JestMatcherAsyncResult;

type JestMatcher = (actual: any, expected: any) => JestMatcherResult;

type JestPromiseType = {
Expand All @@ -110,7 +114,7 @@ type JestPromiseType = {
* Use resolves to unwrap the value of a fulfilled promise so any other
* matcher can be chained. If the promise is rejected the assertion fails.
*/
resolves: JestExpectType
resolves: JestExpectType,
};

/**
Expand All @@ -133,7 +137,7 @@ type EnzymeMatchersType = {
toIncludeText(text: string): void,
toHaveValue(value: any): void,
toMatchElement(element: React$Element<any>): void,
toMatchSelector(selector: string): void
toMatchSelector(selector: string): void,
};

type JestExpectType = {
Expand Down Expand Up @@ -277,7 +281,7 @@ type JestExpectType = {
* Use .toThrowErrorMatchingSnapshot to test that a function throws a error
* matching the most recent snapshot when it is called.
*/
toThrowErrorMatchingSnapshot(): void
toThrowErrorMatchingSnapshot(): void,
};

type JestObjectType = {
Expand Down Expand Up @@ -329,7 +333,7 @@ type JestObjectType = {
* implementation.
*/
fn<TArguments: $ReadOnlyArray<*>, TReturn>(
implementation?: (...args: TArguments) => TReturn
implementation?: (...args: TArguments) => TReturn,
): JestMockFn<TArguments, TReturn>,
/**
* Determines if the given function is a mocked function.
Expand All @@ -352,7 +356,7 @@ type JestObjectType = {
mock(
moduleName: string,
moduleFactory?: any,
options?: Object
options?: Object,
): JestObjectType,
/**
* Returns the actual module instead of a mock, bypassing all checks on
Expand Down Expand Up @@ -420,32 +424,32 @@ type JestObjectType = {
* Creates a mock function similar to jest.fn but also tracks calls to
* object[methodName].
*/
spyOn(object: Object, methodName: string): JestMockFn<any, any>
spyOn(object: Object, methodName: string): JestMockFn<any, any>,
};

type JestSpyType = {
calls: JestCallsType
calls: JestCallsType,
};

/** Runs this function after every test inside this context */
declare function afterEach(
fn: (done: () => void) => ?Promise<mixed>,
timeout?: number
timeout?: number,
): void;
/** Runs this function before every test inside this context */
declare function beforeEach(
fn: (done: () => void) => ?Promise<mixed>,
timeout?: number
timeout?: number,
): void;
/** Runs this function after all tests have finished inside this context */
declare function afterAll(
fn: (done: () => void) => ?Promise<mixed>,
timeout?: number
timeout?: number,
): void;
/** Runs this function before any tests have started inside this context */
declare function beforeAll(
fn: (done: () => void) => ?Promise<mixed>,
timeout?: number
timeout?: number,
): void;

/** A context for grouping tests together */
Expand All @@ -463,7 +467,7 @@ declare var describe: {
/**
* Skip running this describe block
*/
skip(name: string, fn: () => void): void
skip(name: string, fn: () => void): void,
};

/** An individual test unit */
Expand All @@ -478,7 +482,7 @@ declare var it: {
(
name: string,
fn?: (done: () => void) => ?Promise<mixed>,
timeout?: number
timeout?: number,
): void,
/**
* Only run this test
Expand All @@ -490,7 +494,7 @@ declare var it: {
only(
name: string,
fn?: (done: () => void) => ?Promise<mixed>,
timeout?: number
timeout?: number,
): void,
/**
* Skip running this test
Expand All @@ -502,7 +506,7 @@ declare var it: {
skip(
name: string,
fn?: (done: () => void) => ?Promise<mixed>,
timeout?: number
timeout?: number,
): void,
/**
* Run the test concurrently
Expand All @@ -514,13 +518,13 @@ declare var it: {
concurrent(
name: string,
fn?: (done: () => void) => ?Promise<mixed>,
timeout?: number
): void
timeout?: number,
): void,
};
declare function fit(
name: string,
fn: (done: () => void) => ?Promise<mixed>,
timeout?: number
timeout?: number,
): void;
/** An individual test unit */
declare var test: typeof it;
Expand All @@ -538,7 +542,7 @@ declare var expect: {
/** The object that you want to make assertions against */
(value: any): JestExpectType & JestPromiseType & EnzymeMatchersType,
/** Add additional Jasmine matchers to Jest's roster */
extend(matchers: { [name: string]: JestMatcher }): void,
extend(matchers: {[name: string]: JestMatcher}): void,
/** Add a module that formats application-specific data structures. */
addSnapshotSerializer(serializer: (input: Object) => string): void,
assertions(expectedAssertions: number): void,
Expand All @@ -549,7 +553,7 @@ declare var expect: {
objectContaining(value: Object): void,
/** Matches any received string that contains the exact expected string. */
stringContaining(value: string): void,
stringMatching(value: string | RegExp): void
stringMatching(value: string | RegExp): void,
};

// TODO handle return type
Expand All @@ -572,8 +576,8 @@ declare var jasmine: {
createSpy(name: string): JestSpyType,
createSpyObj(
baseName: string,
methodNames: Array<string>
): { [methodName: string]: JestSpyType },
methodNames: Array<string>,
): {[methodName: string]: JestSpyType},
objectContaining(value: Object): void,
stringMatching(value: string): void
stringMatching(value: string): void,
};
60 changes: 60 additions & 0 deletions integration-tests/__tests__/expect_async_matcher.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/

'use strict';

async function toHaveLengthAsync(
received: any,
lengthPromise: Promise<number>,
) {
const length = await lengthPromise;

const pass = received.length === length;
const message = pass
? () =>
`Expected value to not have length:\n` +
` ${length}\n` +
`Received:\n` +
` ${received}\n` +
`received.length:\n` +
` ${received.length}`
: () =>
`Expected value to have length:\n` +
` ${length}\n` +
`Received:\n` +
` ${received}\n` +
`received.length:\n` +
` ${received.length}`;

return {message, pass};
}

expect.extend({
toHaveLengthAsync,
});

it('works with expected non promise values', async () => {
await (expect([1]): any).toHaveLengthAsync(Promise.resolve(1));
});

it('works with expected non promise values and not', async () => {
await (expect([1, 2]).not: any).toHaveLengthAsync(Promise.resolve(1));
});

it('works with expected promise values', async () => {
await (expect(Promise.resolve([1])).resolves: any).toHaveLengthAsync(
Promise.resolve(1),
);
});

it('works with expected promise values and not', async () => {
await (expect(Promise.resolve([1, 2])).resolves.not: any).toHaveLengthAsync(
Promise.resolve(1),
);
});
69 changes: 42 additions & 27 deletions packages/expect/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import type {
Expect,
ExpectationObject,
AsyncExpectationResult,
SyncExpectationResult,
ExpectationResult,
MatcherState,
MatchersObject,
Expand Down Expand Up @@ -212,10 +214,48 @@ const makeThrowingMatcher = (
utils,
},
);
let result: ExpectationResult;

const processResult = (result: SyncExpectationResult) => {
_validateResult(result);

getState().assertionCalls++;

if ((result.pass && isNot) || (!result.pass && !isNot)) {
// XOR
const message = getMessage(result.message);
const error = new JestAssertionError(message);
// Passing the result of the matcher with the error so that a custom
// reporter could access the actual and expected objects of the result
// for example in order to display a custom visual diff
error.matcherResult = result;
// Try to remove this function from the stack trace frame.
// Guard for some environments (browsers) that do not support this feature.
if (Error.captureStackTrace) {
Error.captureStackTrace(error, throwingMatcher);
}

if (throws) {
throw error;
} else {
getState().suppressedErrors.push(error);
}
}
};

let potentialResult: ExpectationResult;

try {
result = matcher.apply(matcherContext, [actual].concat(args));
potentialResult = matcher.apply(matcherContext, [actual].concat(args));

if (isPromise((potentialResult: any))) {
const asyncResult = ((potentialResult: any): AsyncExpectationResult);

asyncResult.then(aResult => processResult(aResult));
} else {
const syncResult = ((potentialResult: any): SyncExpectationResult);

processResult(syncResult);
}
} catch (error) {
if (
matcher[INTERNAL_MATCHER_FLAG] === true &&
Expand All @@ -229,31 +269,6 @@ const makeThrowingMatcher = (
}
throw error;
}

_validateResult(result);

getState().assertionCalls++;

if ((result.pass && isNot) || (!result.pass && !isNot)) {
// XOR
const message = getMessage(result.message);
const error = new JestAssertionError(message);
// Passing the result of the matcher with the error so that a custom
// reporter could access the actual and expected objects of the result
// for example in order to display a custom visual diff
error.matcherResult = result;
// Try to remove this function from the stack trace frame.
// Guard for some environments (browsers) that do not support this feature.
if (Error.captureStackTrace) {
Error.captureStackTrace(error, throwingMatcher);
}

if (throws) {
throw error;
} else {
getState().suppressedErrors.push(error);
}
}
};
};

Expand Down
Loading

0 comments on commit 3e84b42

Please sign in to comment.