Skip to content

Commit 3e84b42

Browse files
committed
Add support for async matchers
1 parent 0d58623 commit 3e84b42

File tree

6 files changed

+151
-64
lines changed

6 files changed

+151
-64
lines changed

flow-typed/npm/jest_v21.x.x.js

Lines changed: 36 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ type JestMockFn<TArguments: $ReadOnlyArray<*>, TReturn> = {
1717
* An array that contains all the object instances that have been
1818
* instantiated from this mock function.
1919
*/
20-
instances: Array<TReturn>
20+
instances: Array<TReturn>,
2121
},
2222
/**
2323
* Resets all information stored in the mockFn.mock.calls and
@@ -45,15 +45,15 @@ type JestMockFn<TArguments: $ReadOnlyArray<*>, TReturn> = {
4545
* will also be executed when the mock is called.
4646
*/
4747
mockImplementation(
48-
fn: (...args: TArguments) => TReturn
48+
fn: (...args: TArguments) => TReturn,
4949
): JestMockFn<TArguments, TReturn>,
5050
/**
5151
* Accepts a function that will be used as an implementation of the mock for
5252
* one call to the mocked function. Can be chained so that multiple function
5353
* calls produce different results.
5454
*/
5555
mockImplementationOnce(
56-
fn: (...args: TArguments) => TReturn
56+
fn: (...args: TArguments) => TReturn,
5757
): JestMockFn<TArguments, TReturn>,
5858
/**
5959
* Just a simple sugar function for returning `this`
@@ -66,14 +66,14 @@ type JestMockFn<TArguments: $ReadOnlyArray<*>, TReturn> = {
6666
/**
6767
* Sugar for only returning a value once inside your mock
6868
*/
69-
mockReturnValueOnce(value: TReturn): JestMockFn<TArguments, TReturn>
69+
mockReturnValueOnce(value: TReturn): JestMockFn<TArguments, TReturn>,
7070
};
7171

7272
type JestAsymmetricEqualityType = {
7373
/**
7474
* A custom Jasmine equality tester
7575
*/
76-
asymmetricMatch(value: mixed): boolean
76+
asymmetricMatch(value: mixed): boolean,
7777
};
7878

7979
type JestCallsType = {
@@ -83,21 +83,25 @@ type JestCallsType = {
8383
count(): number,
8484
first(): mixed,
8585
mostRecent(): mixed,
86-
reset(): void
86+
reset(): void,
8787
};
8888

8989
type JestClockType = {
9090
install(): void,
9191
mockDate(date: Date): void,
9292
tick(milliseconds?: number): void,
93-
uninstall(): void
93+
uninstall(): void,
9494
};
9595

96-
type JestMatcherResult = {
96+
type JestMatcherSyncResult = {
9797
message?: string | (() => string),
98-
pass: boolean
98+
pass: boolean,
9999
};
100100

101+
type JestMatcherAsyncResult = Promise<JestMatcherSyncResult>;
102+
103+
type JestMatcherResult = JestMatcherSyncResult | JestMatcherAsyncResult;
104+
101105
type JestMatcher = (actual: any, expected: any) => JestMatcherResult;
102106

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

116120
/**
@@ -133,7 +137,7 @@ type EnzymeMatchersType = {
133137
toIncludeText(text: string): void,
134138
toHaveValue(value: any): void,
135139
toMatchElement(element: React$Element<any>): void,
136-
toMatchSelector(selector: string): void
140+
toMatchSelector(selector: string): void,
137141
};
138142

139143
type JestExpectType = {
@@ -277,7 +281,7 @@ type JestExpectType = {
277281
* Use .toThrowErrorMatchingSnapshot to test that a function throws a error
278282
* matching the most recent snapshot when it is called.
279283
*/
280-
toThrowErrorMatchingSnapshot(): void
284+
toThrowErrorMatchingSnapshot(): void,
281285
};
282286

283287
type JestObjectType = {
@@ -329,7 +333,7 @@ type JestObjectType = {
329333
* implementation.
330334
*/
331335
fn<TArguments: $ReadOnlyArray<*>, TReturn>(
332-
implementation?: (...args: TArguments) => TReturn
336+
implementation?: (...args: TArguments) => TReturn,
333337
): JestMockFn<TArguments, TReturn>,
334338
/**
335339
* Determines if the given function is a mocked function.
@@ -352,7 +356,7 @@ type JestObjectType = {
352356
mock(
353357
moduleName: string,
354358
moduleFactory?: any,
355-
options?: Object
359+
options?: Object,
356360
): JestObjectType,
357361
/**
358362
* Returns the actual module instead of a mock, bypassing all checks on
@@ -420,32 +424,32 @@ type JestObjectType = {
420424
* Creates a mock function similar to jest.fn but also tracks calls to
421425
* object[methodName].
422426
*/
423-
spyOn(object: Object, methodName: string): JestMockFn<any, any>
427+
spyOn(object: Object, methodName: string): JestMockFn<any, any>,
424428
};
425429

426430
type JestSpyType = {
427-
calls: JestCallsType
431+
calls: JestCallsType,
428432
};
429433

430434
/** Runs this function after every test inside this context */
431435
declare function afterEach(
432436
fn: (done: () => void) => ?Promise<mixed>,
433-
timeout?: number
437+
timeout?: number,
434438
): void;
435439
/** Runs this function before every test inside this context */
436440
declare function beforeEach(
437441
fn: (done: () => void) => ?Promise<mixed>,
438-
timeout?: number
442+
timeout?: number,
439443
): void;
440444
/** Runs this function after all tests have finished inside this context */
441445
declare function afterAll(
442446
fn: (done: () => void) => ?Promise<mixed>,
443-
timeout?: number
447+
timeout?: number,
444448
): void;
445449
/** Runs this function before any tests have started inside this context */
446450
declare function beforeAll(
447451
fn: (done: () => void) => ?Promise<mixed>,
448-
timeout?: number
452+
timeout?: number,
449453
): void;
450454

451455
/** A context for grouping tests together */
@@ -463,7 +467,7 @@ declare var describe: {
463467
/**
464468
* Skip running this describe block
465469
*/
466-
skip(name: string, fn: () => void): void
470+
skip(name: string, fn: () => void): void,
467471
};
468472

469473
/** An individual test unit */
@@ -478,7 +482,7 @@ declare var it: {
478482
(
479483
name: string,
480484
fn?: (done: () => void) => ?Promise<mixed>,
481-
timeout?: number
485+
timeout?: number,
482486
): void,
483487
/**
484488
* Only run this test
@@ -490,7 +494,7 @@ declare var it: {
490494
only(
491495
name: string,
492496
fn?: (done: () => void) => ?Promise<mixed>,
493-
timeout?: number
497+
timeout?: number,
494498
): void,
495499
/**
496500
* Skip running this test
@@ -502,7 +506,7 @@ declare var it: {
502506
skip(
503507
name: string,
504508
fn?: (done: () => void) => ?Promise<mixed>,
505-
timeout?: number
509+
timeout?: number,
506510
): void,
507511
/**
508512
* Run the test concurrently
@@ -514,13 +518,13 @@ declare var it: {
514518
concurrent(
515519
name: string,
516520
fn?: (done: () => void) => ?Promise<mixed>,
517-
timeout?: number
518-
): void
521+
timeout?: number,
522+
): void,
519523
};
520524
declare function fit(
521525
name: string,
522526
fn: (done: () => void) => ?Promise<mixed>,
523-
timeout?: number
527+
timeout?: number,
524528
): void;
525529
/** An individual test unit */
526530
declare var test: typeof it;
@@ -538,7 +542,7 @@ declare var expect: {
538542
/** The object that you want to make assertions against */
539543
(value: any): JestExpectType & JestPromiseType & EnzymeMatchersType,
540544
/** Add additional Jasmine matchers to Jest's roster */
541-
extend(matchers: { [name: string]: JestMatcher }): void,
545+
extend(matchers: {[name: string]: JestMatcher}): void,
542546
/** Add a module that formats application-specific data structures. */
543547
addSnapshotSerializer(serializer: (input: Object) => string): void,
544548
assertions(expectedAssertions: number): void,
@@ -549,7 +553,7 @@ declare var expect: {
549553
objectContaining(value: Object): void,
550554
/** Matches any received string that contains the exact expected string. */
551555
stringContaining(value: string): void,
552-
stringMatching(value: string | RegExp): void
556+
stringMatching(value: string | RegExp): void,
553557
};
554558

555559
// TODO handle return type
@@ -572,8 +576,8 @@ declare var jasmine: {
572576
createSpy(name: string): JestSpyType,
573577
createSpyObj(
574578
baseName: string,
575-
methodNames: Array<string>
576-
): { [methodName: string]: JestSpyType },
579+
methodNames: Array<string>,
580+
): {[methodName: string]: JestSpyType},
577581
objectContaining(value: Object): void,
578-
stringMatching(value: string): void
582+
stringMatching(value: string): void,
579583
};
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/**
2+
* Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
'use strict';
11+
12+
async function toHaveLengthAsync(
13+
received: any,
14+
lengthPromise: Promise<number>,
15+
) {
16+
const length = await lengthPromise;
17+
18+
const pass = received.length === length;
19+
const message = pass
20+
? () =>
21+
`Expected value to not have length:\n` +
22+
` ${length}\n` +
23+
`Received:\n` +
24+
` ${received}\n` +
25+
`received.length:\n` +
26+
` ${received.length}`
27+
: () =>
28+
`Expected value to have length:\n` +
29+
` ${length}\n` +
30+
`Received:\n` +
31+
` ${received}\n` +
32+
`received.length:\n` +
33+
` ${received.length}`;
34+
35+
return {message, pass};
36+
}
37+
38+
expect.extend({
39+
toHaveLengthAsync,
40+
});
41+
42+
it('works with expected non promise values', async () => {
43+
await (expect([1]): any).toHaveLengthAsync(Promise.resolve(1));
44+
});
45+
46+
it('works with expected non promise values and not', async () => {
47+
await (expect([1, 2]).not: any).toHaveLengthAsync(Promise.resolve(1));
48+
});
49+
50+
it('works with expected promise values', async () => {
51+
await (expect(Promise.resolve([1])).resolves: any).toHaveLengthAsync(
52+
Promise.resolve(1),
53+
);
54+
});
55+
56+
it('works with expected promise values and not', async () => {
57+
await (expect(Promise.resolve([1, 2])).resolves.not: any).toHaveLengthAsync(
58+
Promise.resolve(1),
59+
);
60+
});

packages/expect/src/index.js

Lines changed: 42 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
import type {
1111
Expect,
1212
ExpectationObject,
13+
AsyncExpectationResult,
14+
SyncExpectationResult,
1315
ExpectationResult,
1416
MatcherState,
1517
MatchersObject,
@@ -212,10 +214,48 @@ const makeThrowingMatcher = (
212214
utils,
213215
},
214216
);
215-
let result: ExpectationResult;
217+
218+
const processResult = (result: SyncExpectationResult) => {
219+
_validateResult(result);
220+
221+
getState().assertionCalls++;
222+
223+
if ((result.pass && isNot) || (!result.pass && !isNot)) {
224+
// XOR
225+
const message = getMessage(result.message);
226+
const error = new JestAssertionError(message);
227+
// Passing the result of the matcher with the error so that a custom
228+
// reporter could access the actual and expected objects of the result
229+
// for example in order to display a custom visual diff
230+
error.matcherResult = result;
231+
// Try to remove this function from the stack trace frame.
232+
// Guard for some environments (browsers) that do not support this feature.
233+
if (Error.captureStackTrace) {
234+
Error.captureStackTrace(error, throwingMatcher);
235+
}
236+
237+
if (throws) {
238+
throw error;
239+
} else {
240+
getState().suppressedErrors.push(error);
241+
}
242+
}
243+
};
244+
245+
let potentialResult: ExpectationResult;
216246

217247
try {
218-
result = matcher.apply(matcherContext, [actual].concat(args));
248+
potentialResult = matcher.apply(matcherContext, [actual].concat(args));
249+
250+
if (isPromise((potentialResult: any))) {
251+
const asyncResult = ((potentialResult: any): AsyncExpectationResult);
252+
253+
asyncResult.then(aResult => processResult(aResult));
254+
} else {
255+
const syncResult = ((potentialResult: any): SyncExpectationResult);
256+
257+
processResult(syncResult);
258+
}
219259
} catch (error) {
220260
if (
221261
matcher[INTERNAL_MATCHER_FLAG] === true &&
@@ -229,31 +269,6 @@ const makeThrowingMatcher = (
229269
}
230270
throw error;
231271
}
232-
233-
_validateResult(result);
234-
235-
getState().assertionCalls++;
236-
237-
if ((result.pass && isNot) || (!result.pass && !isNot)) {
238-
// XOR
239-
const message = getMessage(result.message);
240-
const error = new JestAssertionError(message);
241-
// Passing the result of the matcher with the error so that a custom
242-
// reporter could access the actual and expected objects of the result
243-
// for example in order to display a custom visual diff
244-
error.matcherResult = result;
245-
// Try to remove this function from the stack trace frame.
246-
// Guard for some environments (browsers) that do not support this feature.
247-
if (Error.captureStackTrace) {
248-
Error.captureStackTrace(error, throwingMatcher);
249-
}
250-
251-
if (throws) {
252-
throw error;
253-
} else {
254-
getState().suppressedErrors.push(error);
255-
}
256-
}
257272
};
258273
};
259274

0 commit comments

Comments
 (0)