[react-test-renderer] Jest matchers for async tests (#13236)
Adds custom Jest matchers that help with writing async tests: - `toFlushThrough` - `toFlushAll` - `toFlushAndThrow` - `toClearYields` Each one accepts an array of expected yielded values, to prevent false negatives. Eventually I imagine we'll want to publish this on npm.
Andrew Clark committed
Jul 19, 2018 at 10:26 UTC
71b4e99901a98396ca34655e0cc1c03eadcbc49b
7 files changed
+202
-144
packages/react-test-renderer/src/ReactTestRenderer.js
+1
-1
@@ -472,7 +472,7 @@ const ReactTestRendererFiber = {
472
TestRendererScheduling.clearYields();
473
return TestRenderer.flushSync(fn);
474
},
475
- unstable_flushThrough: TestRendererScheduling.flushThrough,
475
+ unstable_flushNumberOfYields: TestRendererScheduling.flushNumberOfYields,
476
unstable_clearYields: TestRendererScheduling.clearYields,
477
};
478
packages/react-test-renderer/src/ReactTestRendererScheduling.js
+3
-36
@@ -31,38 +31,7 @@ export function setNowImplementation(implementation: () => number): void {
31
nowImplementation = implementation;
32
}
33
34
-function verifyExpectedValues(expectedValues: Array<mixed>): void {
35
- for (let i = 0; i < expectedValues.length; i++) {
36
- const expectedValue = `"${(expectedValues[i]: any)}"`;
37
- const yieldedValue =
38
- i < yieldedValues.length ? `"${(yieldedValues[i]: any)}"` : 'nothing';
39
- if (yieldedValue !== expectedValue) {
40
- const error = new Error(
41
- `Flush expected to yield ${(expectedValue: any)}, but ${(yieldedValue: any)} was yielded`,
42
- );
43
- // Attach expected and yielded arrays,
44
- // So the caller could pretty print the diff (if desired).
45
- (error: any).expectedValues = expectedValues;
46
- (error: any).actualValues = yieldedValues;
47
- throw error;
48
- }
49
- }
50
-
51
- if (expectedValues.length !== yieldedValues.length) {
52
- const error = new Error(
53
- `Flush expected to yield ${expectedValues.length} values, but yielded ${
54
- yieldedValues.length
55
- }`,
56
- );
57
- // Attach expected and yielded arrays,
58
- // So the caller could pretty print the diff (if desired).
59
- (error: any).expectedValues = expectedValues;
60
- (error: any).actualValues = yieldedValues;
61
- throw error;
62
- }
63
-}
64
-
65
-export function flushAll(expectedValues: Array<mixed>): Array<mixed> {
34
+export function flushAll(): Array<mixed> {
35
yieldedValues = [];
36
while (scheduledCallback !== null) {
37
const cb = scheduledCallback;
@@ -78,11 +47,10 @@ export function flushAll(expectedValues: Array<mixed>): Array<mixed> {
47
didTimeout: false,
48
});
49
}
81
- verifyExpectedValues(expectedValues);
50
return yieldedValues;
51
}
52
85
-export function flushThrough(expectedValues: Array<mixed>): Array<mixed> {
53
+export function flushNumberOfYields(count: number): Array<mixed> {
54
let didStop = false;
55
yieldedValues = [];
56
while (scheduledCallback !== null && !didStop) {
@@ -90,7 +58,7 @@ export function flushThrough(expectedValues: Array<mixed>): Array<mixed> {
58
scheduledCallback = null;
59
cb({
60
timeRemaining() {
93
- if (yieldedValues.length >= expectedValues.length) {
61
+ if (yieldedValues.length >= count) {
62
// We at least as many values as expected. Stop rendering.
63
didStop = true;
64
return 0;
@@ -104,7 +72,6 @@ export function flushThrough(expectedValues: Array<mixed>): Array<mixed> {
72
didTimeout: false,
73
});
74
}
107
- verifyExpectedValues(expectedValues);
75
return yieldedValues;
76
}
77
packages/react-test-renderer/src/__tests__/ReactTestRendererAsync-test.js
+121
-77
@@ -32,7 +32,7 @@ describe('ReactTestRendererAsync', () => {
32
expect(renderer.toJSON()).toEqual(null);
33
34
// Flush initial mount.
35
- renderer.unstable_flushAll([]);
35
+ expect(renderer).toFlushAll([]);
36
expect(renderer.toJSON()).toEqual('Hi');
37
38
// Update
@@ -40,7 +40,7 @@ describe('ReactTestRendererAsync', () => {
40
// Not yet updated.
41
expect(renderer.toJSON()).toEqual('Hi');
42
// Flush update.
43
- renderer.unstable_flushAll([]);
43
+ expect(renderer).toFlushAll([]);
44
expect(renderer.toJSON()).toEqual('Bye');
45
});
46
@@ -62,11 +62,11 @@ describe('ReactTestRendererAsync', () => {
62
unstable_isAsync: true,
63
});
64
65
- renderer.unstable_flushAll(['A:1', 'B:1', 'C:1']);
65
+ expect(renderer).toFlushAll(['A:1', 'B:1', 'C:1']);
66
expect(renderer.toJSON()).toEqual(['A:1', 'B:1', 'C:1']);
67
68
renderer.update(<Parent step={2} />);
69
- renderer.unstable_flushAll(['A:2', 'B:2', 'C:2']);
69
+ expect(renderer).toFlushAll(['A:2', 'B:2', 'C:2']);
70
expect(renderer.toJSON()).toEqual(['A:2', 'B:2', 'C:2']);
71
});
72
@@ -89,15 +89,12 @@ describe('ReactTestRendererAsync', () => {
89
});
90
91
// Flush the first two siblings
92
- expect(renderer.unstable_flushThrough(['A:1', 'B:1'])).toEqual([
93
- 'A:1',
94
- 'B:1',
95
- ]);
92
+ expect(renderer).toFlushThrough(['A:1', 'B:1']);
93
// Did not commit yet.
94
expect(renderer.toJSON()).toEqual(null);
95
96
// Flush the remaining work
100
- renderer.unstable_flushAll(['C:1']);
97
+ expect(renderer).toFlushAll(['C:1']);
98
expect(renderer.toJSON()).toEqual(['A:1', 'B:1', 'C:1']);
99
});
100
@@ -129,7 +126,7 @@ describe('ReactTestRendererAsync', () => {
126
});
127
128
// Flush the some of the changes, but don't commit
132
- expect(renderer.unstable_flushThrough(['A:1'])).toEqual(['A:1']);
129
+ expect(renderer).toFlushThrough(['A:1']);
130
expect(renderer.toJSON()).toEqual(null);
131
132
// Interrupt with higher priority properties
@@ -141,89 +138,136 @@ describe('ReactTestRendererAsync', () => {
138
expect(renderer.toJSON()).toEqual(['A:2', 'B:2']);
139
});
140
144
- it('should error if flushThrough params dont match yielded values', () => {
145
- const Yield = ({id}) => {
146
- ReactTestRenderer.unstable_yield(id);
147
- return id;
148
- };
141
+ describe('Jest matchers', () => {
142
+ it('toFlushThrough', () => {
143
+ const Yield = ({id}) => {
144
+ ReactTestRenderer.unstable_yield(id);
145
+ return id;
146
+ };
147
+
148
+ const renderer = ReactTestRenderer.create(
149
+ <div>
150
+ <Yield id="foo" />
151
+ <Yield id="bar" />
152
+ <Yield id="baz" />
153
+ </div>,
154
+ {
155
+ unstable_isAsync: true,
156
+ },
157
+ );
158
150
- const renderer = ReactTestRenderer.create(
151
- <div>
152
- <Yield id="foo" />
153
- <Yield id="bar" />
154
- <Yield id="baz" />
155
- </div>,
156
- {
157
- unstable_isAsync: true,
158
- },
159
- );
159
+ expect(() => expect(renderer).toFlushThrough(['foo', 'baz'])).toThrow(
160
+ 'Expected value to equal:',
161
+ );
162
+ });
163
161
- expect(() => renderer.unstable_flushThrough(['foo', 'baz'])).toThrow(
162
- 'Flush expected to yield "baz", but "bar" was yielded',
163
- );
164
- });
164
+ it('toFlushAll', () => {
165
+ const Yield = ({id}) => {
166
+ ReactTestRenderer.unstable_yield(id);
167
+ return id;
168
+ };
169
+
170
+ const renderer = ReactTestRenderer.create(
171
+ <div>
172
+ <Yield id="foo" />
173
+ <Yield id="bar" />
174
+ <Yield id="baz" />
175
+ </div>,
176
+ {
177
+ unstable_isAsync: true,
178
+ },
179
+ );
180
166
- it('should error if flushAll params dont match yielded values', () => {
167
- const Yield = ({id}) => {
168
- ReactTestRenderer.unstable_yield(id);
169
- return id;
170
- };
181
+ expect(() => expect(renderer).toFlushAll([])).toThrowError(
182
+ 'Expected value to equal:',
183
+ );
184
+
185
+ renderer.update(
186
+ <div>
187
+ <Yield id="foo" />
188
+ <Yield id="bar" />
189
+ <Yield id="baz" />
190
+ </div>,
191
+ );
192
+
193
+ expect(() => expect(renderer).toFlushAll(['foo', 'baz'])).toThrow(
194
+ 'Expected value to equal:',
195
+ );
196
+ });
197
+
198
+ it('toFlushAndThrow', () => {
199
+ const Yield = ({id}) => {
200
+ ReactTestRenderer.unstable_yield(id);
201
+ return id;
202
+ };
203
+
204
+ function BadRender() {
205
+ throw new Error('Oh no!');
206
+ }
207
172
- const renderer = ReactTestRenderer.create(
173
- <div>
174
- <Yield id="foo" />
175
- <Yield id="bar" />
176
- <Yield id="baz" />
177
- </div>,
178
- {
208
+ function App() {
209
+ return (
210
+ <div>
211
+ <Yield id="A" />
212
+ <Yield id="B" />
213
+ <BadRender />
214
+ <Yield id="C" />
215
+ <Yield id="D" />
216
+ </div>
217
+ );
218
+ }
219
+
220
+ const renderer = ReactTestRenderer.create(<App />, {
221
unstable_isAsync: true,
180
- },
181
- );
222
+ });
223
183
- expect(() => renderer.unstable_flushAll([])).toThrow(
184
- 'Flush expected to yield 0 values, but yielded 3',
185
- );
224
+ expect(() => {
225
+ expect(renderer).toFlushAndThrow(
226
+ // Wrong expected values
227
+ ['A', 'B'],
228
+ 'Oh no!',
229
+ );
230
+ }).toThrow('Expected value to equal:');
231
187
- renderer.update(
188
- <div>
189
- <Yield id="foo" />
190
- <Yield id="bar" />
191
- <Yield id="baz" />
192
- </div>,
193
- );
232
+ renderer.update(<App />);
233
195
- expect(() => renderer.unstable_flushAll(['foo', 'baz'])).toThrow(
196
- 'Flush expected to yield "baz", but "bar" was yielded',
197
- );
234
+ expect(() => {
235
+ expect(renderer).toFlushAndThrow(
236
+ ['A', 'B', 'C', 'D', 'A', 'B', 'C', 'D'],
237
+ // Wrong error message
238
+ 'Oops!',
239
+ );
240
+ }).toThrow('Expected the function to throw an error matching:');
241
+
242
+ renderer.update(<App />);
243
+
244
+ // Passes
245
+ expect(renderer).toFlushAndThrow(
246
+ ['A', 'B', 'C', 'D', 'A', 'B', 'C', 'D'],
247
+ 'Oh no!',
248
+ );
249
+ });
250
});
251
200
- it('should error if flushThrough yields the wrong number of values', () => {
252
+ it('toClearYields', () => {
253
const Yield = ({id}) => {
254
ReactTestRenderer.unstable_yield(id);
255
return id;
256
};
257
206
- const renderer = ReactTestRenderer.create(
207
- <div>
208
- <Yield id="foo" />
209
- </div>,
210
- {
211
- unstable_isAsync: true,
212
- },
213
- );
214
-
215
- expect(() => renderer.unstable_flushThrough(['foo', 'bar'])).toThrow(
216
- 'Flush expected to yield "bar", but nothing was yielded',
217
- );
218
- });
219
-
220
- it('should error if flushThrough yields no values', () => {
221
- const renderer = ReactTestRenderer.create(null, {
222
- unstable_isAsync: true,
223
- });
258
+ function App() {
259
+ return (
260
+ <div>
261
+ <Yield id="A" />
262
+ <Yield id="B" />
263
+ <Yield id="C" />
264
+ </div>
265
+ );
266
+ }
267
225
- expect(() => renderer.unstable_flushThrough(['foo'])).toThrow(
226
- 'Flush expected to yield "foo", but nothing was yielded',
268
+ ReactTestRenderer.create(<App />);
269
+ expect(() => expect(ReactTestRenderer).toClearYields(['A', 'B'])).toThrow(
270
+ 'Expected value to equal:',
271
);
272
});
273
});
packages/react/src/__tests__/ReactProfiler-test.internal.js
+18
-30
@@ -178,9 +178,9 @@ describe('Profiler', () => {
178
);
179
180
// Times are logged until a render is committed.
181
- renderer.unstable_flushThrough(['first']);
181
+ expect(renderer).toFlushThrough(['first']);
182
expect(callback).toHaveBeenCalledTimes(0);
183
- renderer.unstable_flushAll(['last']);
183
+ expect(renderer).toFlushAll(['last']);
184
expect(callback).toHaveBeenCalledTimes(1);
185
});
186
@@ -528,13 +528,11 @@ describe('Profiler', () => {
528
</React.unstable_Profiler>,
529
{unstable_isAsync: true},
530
);
531
- expect(renderer.unstable_flushThrough(['Yield:2'])).toEqual([
532
- 'Yield:2',
533
- ]);
531
+ expect(renderer).toFlushThrough(['Yield:2']);
532
expect(callback).toHaveBeenCalledTimes(0);
533
534
// Resume render for remaining children.
537
- renderer.unstable_flushAll(['Yield:3']);
535
+ expect(renderer).toFlushAll(['Yield:3']);
536
537
// Verify that logged times include both durations above.
538
expect(callback).toHaveBeenCalledTimes(1);
@@ -568,9 +566,7 @@ describe('Profiler', () => {
566
</React.unstable_Profiler>,
567
{unstable_isAsync: true},
568
);
571
- expect(renderer.unstable_flushThrough(['Yield:5'])).toEqual([
572
- 'Yield:5',
573
- ]);
569
+ expect(renderer).toFlushThrough(['Yield:5']);
570
expect(callback).toHaveBeenCalledTimes(0);
571
572
// Simulate time moving forward while frame is paused.
@@ -578,7 +574,7 @@ describe('Profiler', () => {
574
575
// Flush the remaninig work,
576
// Which should take an additional 10ms of simulated time.
581
- renderer.unstable_flushAll(['Yield:10', 'Yield:17']);
577
+ expect(renderer).toFlushAll(['Yield:10', 'Yield:17']);
578
expect(callback).toHaveBeenCalledTimes(2);
579
580
const [innerCall, outerCall] = callback.mock.calls;
@@ -617,9 +613,7 @@ describe('Profiler', () => {
613
</React.unstable_Profiler>,
614
{unstable_isAsync: true},
615
);
620
- expect(renderer.unstable_flushThrough(['Yield:10'])).toEqual([
621
- 'Yield:10',
622
- ]);
616
+ expect(renderer).toFlushThrough(['Yield:10']);
617
expect(callback).toHaveBeenCalledTimes(0);
618
619
// Simulate time moving forward while frame is paused.
@@ -634,7 +628,7 @@ describe('Profiler', () => {
628
</React.unstable_Profiler>,
629
);
630
});
637
- expect(ReactTestRenderer.unstable_clearYields()).toEqual(['Yield:5']);
631
+ expect(ReactTestRenderer).toClearYields(['Yield:5']);
632
633
// The initial work was thrown away in this case,
634
// So the actual and base times should only include the final rendered tree times.
@@ -648,7 +642,7 @@ describe('Profiler', () => {
642
callback.mockReset();
643
644
// Verify no more unexpected callbacks from low priority work
651
- renderer.unstable_flushAll([]);
645
+ expect(renderer).toFlushAll([]);
646
expect(callback).toHaveBeenCalledTimes(0);
647
});
648
@@ -673,7 +667,7 @@ describe('Profiler', () => {
667
668
// Render everything initially.
669
// This should take 21 seconds of actual and base time.
676
- renderer.unstable_flushAll(['Yield:6', 'Yield:15']);
670
+ expect(renderer).toFlushAll(['Yield:6', 'Yield:15']);
671
expect(callback).toHaveBeenCalledTimes(1);
672
let call = callback.mock.calls[0];
673
expect(call[2]).toBe(21); // actual time
@@ -694,18 +688,14 @@ describe('Profiler', () => {
688
<Yield renderTime={9} />
689
</React.unstable_Profiler>,
690
);
697
- expect(renderer.unstable_flushThrough(['Yield:3'])).toEqual([
698
- 'Yield:3',
699
- ]);
691
+ expect(renderer).toFlushThrough(['Yield:3']);
692
expect(callback).toHaveBeenCalledTimes(0);
693
694
// Simulate time moving forward while frame is paused.
695
advanceTimeBy(100); // 59 -> 159
696
697
// Render another 5ms of simulated time.
706
- expect(renderer.unstable_flushThrough(['Yield:5'])).toEqual([
707
- 'Yield:5',
708
- ]);
698
+ expect(renderer).toFlushThrough(['Yield:5']);
699
expect(callback).toHaveBeenCalledTimes(0);
700
701
// Simulate time moving forward while frame is paused.
@@ -720,7 +710,7 @@ describe('Profiler', () => {
710
</React.unstable_Profiler>,
711
);
712
});
723
- expect(ReactTestRenderer.unstable_clearYields()).toEqual(['Yield:11']);
713
+ expect(ReactTestRenderer).toClearYields(['Yield:11']);
714
715
// The actual time should include only the most recent render,
716
// Because this lets us avoid a lot of commit phase reset complexity.
@@ -733,7 +723,7 @@ describe('Profiler', () => {
723
expect(call[5]).toBe(275); // commit time
724
725
// Verify no more unexpected callbacks from low priority work
736
- renderer.unstable_flushAll([]);
726
+ expect(renderer).toFlushAll([]);
727
expect(callback).toHaveBeenCalledTimes(1);
728
});
729
@@ -784,7 +774,7 @@ describe('Profiler', () => {
774
// Render everything initially.
775
// This simulates a total of 14ms of actual render time.
776
// The base render time is also 14ms for the initial render.
787
- renderer.unstable_flushAll([
777
+ expect(renderer).toFlushAll([
778
'FirstComponent:1',
779
'Yield:4',
780
'SecondComponent:2',
@@ -804,9 +794,7 @@ describe('Profiler', () => {
794
// Render a partially update, but don't finish.
795
// This partial render will take 10ms of actual render time.
796
first.setState({renderTime: 10});
807
- expect(renderer.unstable_flushThrough(['FirstComponent:10'])).toEqual([
808
- 'FirstComponent:10',
809
- ]);
797
+ expect(renderer).toFlushThrough(['FirstComponent:10']);
798
expect(callback).toHaveBeenCalledTimes(0);
799
800
// Simulate time moving forward while frame is paused.
@@ -815,7 +803,7 @@ describe('Profiler', () => {
803
// Interrupt with higher priority work.
804
// This simulates a total of 37ms of actual render time.
805
renderer.unstable_flushSync(() => second.setState({renderTime: 30}));
818
- expect(ReactTestRenderer.unstable_clearYields()).toEqual([
806
+ expect(ReactTestRenderer).toClearYields([
807
'SecondComponent:30',
808
'Yield:7',
809
]);
@@ -842,7 +830,7 @@ describe('Profiler', () => {
830
// The tree contains 42ms of base render time at this point,
831
// Reflecting the most recent (longer) render durations.
832
// TODO: This actual time should decrease by 10ms once the scheduler supports resuming.
845
- renderer.unstable_flushAll(['FirstComponent:10', 'Yield:4']);
833
+ expect(renderer).toFlushAll(['FirstComponent:10', 'Yield:4']);
834
expect(callback).toHaveBeenCalledTimes(1);
835
call = callback.mock.calls[0];
836
expect(call[2]).toBe(14); // actual time
scripts/jest/matchers/testRenderer.js
new
+57
@@ -0,0 +1,57 @@
1
+'use strict';
2
+
3
+function captureAssertion(fn) {
4
+ // Trick to use a Jest matcher inside another Jest matcher. `fn` contains an
5
+ // assertion; if it throws, we capture the error and return it, so the stack
6
+ // trace presented to the user points to the original assertion in the
7
+ // test file.
8
+ try {
9
+ fn();
10
+ } catch (error) {
11
+ return {
12
+ pass: false,
13
+ message: () => error.message,
14
+ };
15
+ }
16
+ return {pass: true};
17
+}
18
+
19
+function toFlushAll(renderer, expectedYields) {
20
+ const actualYields = renderer.unstable_flushAll();
21
+ return captureAssertion(() => expect(actualYields).toEqual(expectedYields));
22
+}
23
+
24
+function toFlushThrough(renderer, expectedYields) {
25
+ const actualYields = renderer.unstable_flushNumberOfYields(
26
+ expectedYields.length
27
+ );
28
+ return captureAssertion(() => expect(actualYields).toEqual(expectedYields));
29
+}
30
+
31
+function toClearYields(ReactTestRenderer, expectedYields) {
32
+ const actualYields = ReactTestRenderer.unstable_clearYields();
33
+ return captureAssertion(() => expect(actualYields).toEqual(expectedYields));
34
+}
35
+
36
+function toFlushAndThrow(renderer, expectedYields, ...rest) {
37
+ return captureAssertion(() => {
38
+ try {
39
+ expect(() => {
40
+ renderer.unstable_flushAll();
41
+ }).toThrow(...rest);
42
+ } catch (error) {
43
+ const actualYields = renderer.unstable_clearYields();
44
+ expect(actualYields).toEqual(expectedYields);
45
+ throw error;
46
+ }
47
+ const actualYields = renderer.unstable_clearYields();
48
+ expect(actualYields).toEqual(expectedYields);
49
+ });
50
+}
51
+
52
+module.exports = {
53
+ toFlushAll,
54
+ toFlushThrough,
55
+ toFlushAndThrow,
56
+ toClearYields,
57
+};
scripts/jest/setupTests.js
+1
@@ -44,6 +44,7 @@ if (process.env.REACT_CLASS_EQUIVALENCE_TEST) {
44
45
expect.extend({
46
...require('./matchers/toWarnDev'),
47
+ ...require('./matchers/testRenderer'),
48
});
49
50
// We have a Babel transform that inserts guards against infinite loops.
scripts/jest/spec-equivalence-reporter/setupTests.js
+1
@@ -47,6 +47,7 @@ global.spyOnProd = function(...args) {
47
48
expect.extend({
49
...require('../matchers/toWarnDev'),
50
+ ...require('../matchers/testRenderer'),
51
});
52
53
beforeEach(() => (numExpectations = 0));