@samitouri / QOS-React / commits / 96bcae9d50

Jest + test renderer helpers for concurrent mode (#13751)

* Jest + test renderer helpers for concurrent mode Most of our concurrent React tests use the noop renderer. But most of those tests don't test the renderer API, and could instead be written with the test renderer. We should switch to using the test renderer whenever possible, because that's what we expect product devs and library authors to do. If test renderer is sufficient for writing most React core tests, it should be sufficient for others, too. (The converse isn't true but we should aim to dogfood test renderer as much as possible.) This PR adds a new package, jest-react (thanks @cpojer). I've moved our existing Jest matchers into that package and added some new ones. I'm not expecting to figure out the final API in this PR. My goal is to land something good enough that we can start dogfooding in www. TODO: Continue migrating Suspense tests, decide on better API names * Add additional invariants to prevent common errors - Errors if user attempts to flush when log of yields is not empty - Throws if argument passed to toClearYields is not ReactTestRenderer * Better method names - toFlushAll -> toFlushAndYield - toFlushAndYieldThrough -> - toClearYields -> toHaveYielded Also added toFlushWithoutYielding * Fix jest-react exports * Tweak README

Andrew Clark committed Oct 3, 2018 at 18:37 UTC 96bcae9d50cae41ab87539075dceeaf3a0e0a36d
15 files changed +536 -150
packages/jest-react/README.md new
+3
@@ -0,0 +1,3 @@
1 +# `jest-react`
2 +
3 +Jest matchers and utilities for testing React Test Renderer.
\ No newline at end of file
packages/jest-react/index.js new
+10
@@ -0,0 +1,10 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
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 +
8 +'use strict';
9 +
10 +export * from './src/JestReact';
packages/jest-react/npm/index.js new
+7
@@ -0,0 +1,7 @@
1 +'use strict';
2 +
3 +if (process.env.NODE_ENV === 'production') {
4 + module.exports = require('./cjs/jest-react.production.min.js');
5 +} else {
6 + module.exports = require('./cjs/jest-react.development.js');
7 +}
packages/jest-react/package.json new
+28
@@ -0,0 +1,28 @@
1 +{
2 + "name": "jest-react",
3 + "version": "0.2.0",
4 + "description": "Jest matchers and utilities for testing React components.",
5 + "main": "index.js",
6 + "repository": "facebook/react",
7 + "keywords": [
8 + "react",
9 + "jest",
10 + "react-testing"
11 + ],
12 + "license": "MIT",
13 + "bugs": {
14 + "url": "https://github.com/facebook/react/issues"
15 + },
16 + "homepage": "https://reactjs.org/",
17 + "peerDependencies": {
18 + "jest": "^23.0.1",
19 + "react": "^16.0.0",
20 + "react-test-renderer": "^16.0.0"
21 + },
22 + "files": [
23 + "LICENSE",
24 + "README.md",
25 + "index.js",
26 + "cjs/"
27 + ]
28 +}
packages/jest-react/src/JestReact.js new
+168
@@ -0,0 +1,168 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
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 +
8 +import {REACT_ELEMENT_TYPE, REACT_FRAGMENT_TYPE} from 'shared/ReactSymbols';
9 +
10 +import invariant from 'shared/invariant';
11 +
12 +function captureAssertion(fn) {
13 + // Trick to use a Jest matcher inside another Jest matcher. `fn` contains an
14 + // assertion; if it throws, we capture the error and return it, so the stack
15 + // trace presented to the user points to the original assertion in the
16 + // test file.
17 + try {
18 + fn();
19 + } catch (error) {
20 + return {
21 + pass: false,
22 + message: () => error.message,
23 + };
24 + }
25 + return {pass: true};
26 +}
27 +
28 +function assertYieldsWereCleared(root) {
29 + const actualYields = root.unstable_clearYields();
30 + invariant(
31 + actualYields.length === 0,
32 + 'Log of yielded values is not empty. ' +
33 + 'Call expect(ReactTestRenderer).toHaveYielded(...) first.',
34 + );
35 +}
36 +
37 +export function toFlushAndYield(root, expectedYields) {
38 + return captureAssertion(() => {
39 + assertYieldsWereCleared(root);
40 + const actualYields = root.unstable_flushAll();
41 + expect(actualYields).toEqual(expectedYields);
42 + });
43 +}
44 +
45 +export function toFlushAndYieldThrough(root, expectedYields) {
46 + return captureAssertion(() => {
47 + assertYieldsWereCleared(root);
48 + const actualYields = root.unstable_flushNumberOfYields(
49 + expectedYields.length,
50 + );
51 + expect(actualYields).toEqual(expectedYields);
52 + });
53 +}
54 +
55 +export function toFlushWithoutYielding(root) {
56 + return toFlushAndYield(root, []);
57 +}
58 +
59 +export function toHaveYielded(ReactTestRenderer, expectedYields) {
60 + return captureAssertion(() => {
61 + if (
62 + ReactTestRenderer === null ||
63 + typeof ReactTestRenderer !== 'object' ||
64 + typeof ReactTestRenderer.unstable_setNowImplementation !== 'function'
65 + ) {
66 + invariant(
67 + false,
68 + 'The matcher `toHaveYielded` expects an instance of React Test ' +
69 + 'Renderer.\n\nTry: ' +
70 + 'expect(ReactTestRenderer).toHaveYielded(expectedYields)',
71 + );
72 + }
73 + const actualYields = ReactTestRenderer.unstable_clearYields();
74 + expect(actualYields).toEqual(expectedYields);
75 + });
76 +}
77 +
78 +export function toFlushAndThrow(root, ...rest) {
79 + return captureAssertion(() => {
80 + assertYieldsWereCleared(root);
81 + expect(() => {
82 + root.unstable_flushAll();
83 + }).toThrow(...rest);
84 + });
85 +}
86 +
87 +export function toMatchRenderedOutput(root, expectedJSX) {
88 + const actualJSON = root.toJSON();
89 +
90 + let actualJSX;
91 + if (actualJSON === null || typeof actualJSON === 'string') {
92 + actualJSX = actualJSON;
93 + } else if (Array.isArray(actualJSON)) {
94 + if (actualJSON.length === 0) {
95 + actualJSX = null;
96 + } else if (actualJSON.length === 1) {
97 + actualJSX = jsonChildToJSXChild(actualJSON[0]);
98 + } else {
99 + const actualJSXChildren = jsonChildrenToJSXChildren(actualJSON);
100 + if (actualJSXChildren === null || typeof actualJSXChildren === 'string') {
101 + actualJSX = actualJSXChildren;
102 + } else {
103 + actualJSX = {
104 + $$typeof: REACT_ELEMENT_TYPE,
105 + type: REACT_FRAGMENT_TYPE,
106 + key: null,
107 + ref: null,
108 + props: {
109 + children: actualJSXChildren,
110 + },
111 + _owner: null,
112 + _store: __DEV__ ? {} : undefined,
113 + };
114 + }
115 + }
116 + } else {
117 + actualJSX = jsonChildToJSXChild(actualJSON);
118 + }
119 +
120 + return captureAssertion(() => {
121 + expect(actualJSX).toEqual(expectedJSX);
122 + });
123 +}
124 +
125 +function jsonChildToJSXChild(jsonChild) {
126 + if (jsonChild === null || typeof jsonChild === 'string') {
127 + return jsonChild;
128 + } else {
129 + const jsxChildren = jsonChildrenToJSXChildren(jsonChild.children);
130 + return {
131 + $$typeof: REACT_ELEMENT_TYPE,
132 + type: jsonChild.type,
133 + key: null,
134 + ref: null,
135 + props:
136 + jsxChildren === null
137 + ? jsonChild.props
138 + : {...jsonChild.props, children: jsxChildren},
139 + _owner: null,
140 + _store: __DEV__ ? {} : undefined,
141 + };
142 + }
143 +}
144 +
145 +function jsonChildrenToJSXChildren(jsonChildren) {
146 + if (jsonChildren !== null) {
147 + if (jsonChildren.length === 1) {
148 + return jsonChildToJSXChild(jsonChildren[0]);
149 + } else if (jsonChildren.length > 1) {
150 + let jsxChildren = [];
151 + let allJSXChildrenAreStrings = true;
152 + let jsxChildrenString = '';
153 + for (let i = 0; i < jsonChildren.length; i++) {
154 + const jsxChild = jsonChildrenToJSXChildren(jsonChildren[i]);
155 + jsxChildren.push(jsxChild);
156 + if (allJSXChildrenAreStrings) {
157 + if (typeof jsxChild === 'string') {
158 + jsxChildrenString += jsxChild;
159 + } else if (jsxChild !== null) {
160 + allJSXChildrenAreStrings = false;
161 + }
162 + }
163 + }
164 + return allJSXChildrenAreStrings ? jsxChildrenString : jsxChildren;
165 + }
166 + }
167 + return null;
168 +}
packages/react-reconciler/src/__tests__/ErrorBoundaryReconciliation-test.internal.js
+3 -22
@@ -1,5 +1,3 @@
1 -const jestDiff = require('jest-diff');
2 -
1 describe('ErrorBoundaryReconciliation', () => {
2 let BrokenRender;
3 let DidCatchErrorBoundary;
@@ -48,22 +46,6 @@ describe('ErrorBoundaryReconciliation', () => {
46 const InvalidType = undefined;
47 BrokenRender = ({fail}) =>
48 fail ? <InvalidType /> : <span prop="BrokenRender" />;
51 -
52 - function toHaveRenderedChildren(renderer, children) {
53 - let actual, expected;
54 - try {
55 - actual = renderer.toJSON();
56 - expected = ReactTestRenderer.create(children).toJSON();
57 - expect(actual).toEqual(expected);
58 - } catch (error) {
59 - return {
60 - message: () => jestDiff(expected, actual),
61 - pass: false,
62 - };
63 - }
64 - return {pass: true};
65 - }
66 - expect.extend({toHaveRenderedChildren});
49 });
50
51 [true, false].forEach(isConcurrent => {
@@ -77,7 +59,7 @@ describe('ErrorBoundaryReconciliation', () => {
59 if (isConcurrent) {
60 renderer.unstable_flushAll();
61 }
80 - expect(renderer).toHaveRenderedChildren(<span prop="BrokenRender" />);
62 + expect(renderer).toMatchRenderedOutput(<span prop="BrokenRender" />);
63
64 expect(() => {
65 renderer.update(
@@ -89,9 +71,8 @@ describe('ErrorBoundaryReconciliation', () => {
71 renderer.unstable_flushAll();
72 }
73 }).toWarnDev(isConcurrent ? ['invalid', 'invalid'] : ['invalid']);
92 - expect(renderer).toHaveRenderedChildren(
93 - React.createElement(fallbackTagName, {prop: 'ErrorBoundary'}),
94 - );
74 + const Fallback = fallbackTagName;
75 + expect(renderer).toMatchRenderedOutput(<Fallback prop="ErrorBoundary" />);
76 }
77
78 describe(isConcurrent ? 'concurrent' : 'sync', () => {
packages/react-reconciler/src/__tests__/ReactSuspenseWithTestRenderer-test.internal.js new
+193
@@ -0,0 +1,193 @@
1 +let React;
2 +let ReactTestRenderer;
3 +let ReactFeatureFlags;
4 +let ReactCache;
5 +let Placeholder;
6 +
7 +// let JestReact;
8 +
9 +let cache;
10 +let TextResource;
11 +let textResourceShouldFail;
12 +
13 +// The tests in the ReactSuspense suite use ReactNoop. We want to use
14 +// ReactTestRenderer for most of our tests, so we're gradually migrating the
15 +// Suspense tests over to this file.
16 +describe('ReactSuspenseWithTestRenderer', () => {
17 + beforeEach(() => {
18 + jest.resetModules();
19 + ReactFeatureFlags = require('shared/ReactFeatureFlags');
20 + ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
21 + ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
22 + ReactFeatureFlags.enableSuspense = true;
23 + React = require('react');
24 + ReactTestRenderer = require('react-test-renderer');
25 + // JestReact = require('jest-react');
26 + ReactCache = require('react-cache');
27 +
28 + Placeholder = React.Placeholder;
29 +
30 + function invalidateCache() {
31 + cache = ReactCache.createCache(invalidateCache);
32 + }
33 + invalidateCache();
34 + TextResource = ReactCache.createResource(([text, ms = 0]) => {
35 + let listeners = null;
36 + let status = 'pending';
37 + let value = null;
38 + return {
39 + then(resolve, reject) {
40 + switch (status) {
41 + case 'pending': {
42 + if (listeners === null) {
43 + listeners = [{resolve, reject}];
44 + setTimeout(() => {
45 + if (textResourceShouldFail) {
46 + ReactTestRenderer.unstable_yield(
47 + `Promise rejected [${text}]`,
48 + );
49 + status = 'rejected';
50 + value = new Error('Failed to load: ' + text);
51 + listeners.forEach(listener => listener.reject(value));
52 + } else {
53 + ReactTestRenderer.unstable_yield(
54 + `Promise resolved [${text}]`,
55 + );
56 + status = 'resolved';
57 + value = text;
58 + listeners.forEach(listener => listener.resolve(value));
59 + }
60 + }, ms);
61 + } else {
62 + listeners.push({resolve, reject});
63 + }
64 + break;
65 + }
66 + case 'resolved': {
67 + resolve(value);
68 + break;
69 + }
70 + case 'rejected': {
71 + reject(value);
72 + break;
73 + }
74 + }
75 + },
76 + };
77 + }, ([text, ms]) => text);
78 + textResourceShouldFail = false;
79 + });
80 +
81 + function Text(props) {
82 + ReactTestRenderer.unstable_yield(props.text);
83 + return props.text;
84 + }
85 +
86 + function AsyncText(props) {
87 + const text = props.text;
88 + try {
89 + TextResource.read(cache, [props.text, props.ms]);
90 + ReactTestRenderer.unstable_yield(text);
91 + return text;
92 + } catch (promise) {
93 + if (typeof promise.then === 'function') {
94 + ReactTestRenderer.unstable_yield(`Suspend! [${text}]`);
95 + } else {
96 + ReactTestRenderer.unstable_yield(`Error! [${text}]`);
97 + }
98 + throw promise;
99 + }
100 + }
101 +
102 + it('suspends rendering and continues later', () => {
103 + function Bar(props) {
104 + ReactTestRenderer.unstable_yield('Bar');
105 + return props.children;
106 + }
107 +
108 + function Foo() {
109 + ReactTestRenderer.unstable_yield('Foo');
110 + return (
111 + <Placeholder>
112 + <Bar>
113 + <AsyncText text="A" ms={100} />
114 + <Text text="B" />
115 + </Bar>
116 + </Placeholder>
117 + );
118 + }
119 +
120 + const root = ReactTestRenderer.create(<Foo />, {
121 + unstable_isConcurrent: true,
122 + });
123 +
124 + expect(root).toFlushAndYield([
125 + 'Foo',
126 + 'Bar',
127 + // A suspends
128 + 'Suspend! [A]',
129 + // But we keep rendering the siblings
130 + 'B',
131 + ]);
132 + expect(root).toMatchRenderedOutput(null);
133 +
134 + // Flush some of the time
135 + jest.advanceTimersByTime(50);
136 + // Still nothing...
137 + expect(root).toFlushWithoutYielding();
138 + expect(root).toMatchRenderedOutput(null);
139 +
140 + // Flush the promise completely
141 + jest.advanceTimersByTime(50);
142 + // Renders successfully
143 + expect(ReactTestRenderer).toHaveYielded(['Promise resolved [A]']);
144 + expect(root).toFlushAndYield(['Foo', 'Bar', 'A', 'B']);
145 + expect(root).toMatchRenderedOutput('AB');
146 + });
147 +
148 + it('suspends siblings and later recovers each independently', () => {
149 + // Render two sibling Placeholder components
150 + const root = ReactTestRenderer.create(
151 + <React.Fragment>
152 + <Placeholder delayMs={1000} fallback={<Text text="Loading A..." />}>
153 + <AsyncText text="A" ms={5000} />
154 + </Placeholder>
155 + <Placeholder delayMs={3000} fallback={<Text text="Loading B..." />}>
156 + <AsyncText text="B" ms={6000} />
157 + </Placeholder>
158 + </React.Fragment>,
159 + {
160 + unstable_isConcurrent: true,
161 + },
162 + );
163 +
164 + expect(root).toFlushAndYield([
165 + 'Suspend! [A]',
166 + 'Loading A...',
167 + 'Suspend! [B]',
168 + 'Loading B...',
169 + ]);
170 + expect(root).toMatchRenderedOutput(null);
171 +
172 + // Advance time by enough to timeout both components and commit their placeholders
173 + jest.advanceTimersByTime(4000);
174 + expect(root).toFlushWithoutYielding();
175 + expect(root).toMatchRenderedOutput('Loading A...Loading B...');
176 +
177 + // Advance time by enough that the first Placeholder's promise resolves and
178 + // switches back to the normal view. The second Placeholder should still
179 + // show the placeholder
180 + jest.advanceTimersByTime(1000);
181 + // TODO: Should we throw if you forget to call toHaveYielded?
182 + expect(ReactTestRenderer).toHaveYielded(['Promise resolved [A]']);
183 + expect(root).toFlushAndYield(['A']);
184 + expect(root).toMatchRenderedOutput('ALoading B...');
185 +
186 + // Advance time by enough that the second Placeholder's promise resolves
187 + // and switches back to the normal view
188 + jest.advanceTimersByTime(1000);
189 + expect(ReactTestRenderer).toHaveYielded(['Promise resolved [B]']);
190 + expect(root).toFlushAndYield(['B']);
191 + expect(root).toMatchRenderedOutput('AB');
192 + });
193 +});
packages/react-test-renderer/src/ReactTestRendererScheduling.js
+6 -2
@@ -47,7 +47,9 @@ export function flushAll(): Array<mixed> {
47 didTimeout: false,
48 });
49 }
50 - return yieldedValues;
50 + const values = yieldedValues;
51 + yieldedValues = [];
52 + return values;
53 }
54
55 export function flushNumberOfYields(count: number): Array<mixed> {
@@ -72,7 +74,9 @@ export function flushNumberOfYields(count: number): Array<mixed> {
74 didTimeout: false,
75 });
76 }
75 - return yieldedValues;
77 + const values = yieldedValues;
78 + yieldedValues = [];
79 + return values;
80 }
81
82 export function yieldValue(value: mixed): void {
packages/react-test-renderer/src/__tests__/ReactTestRendererAsync-test.js
+49 -36
@@ -32,7 +32,7 @@ describe('ReactTestRendererAsync', () => {
32 expect(renderer.toJSON()).toEqual(null);
33
34 // Flush initial mount.
35 - expect(renderer).toFlushAll([]);
35 + expect(renderer).toFlushWithoutYielding();
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 - expect(renderer).toFlushAll([]);
43 + expect(renderer).toFlushWithoutYielding();
44 expect(renderer.toJSON()).toEqual('Bye');
45 });
46
@@ -62,11 +62,11 @@ describe('ReactTestRendererAsync', () => {
62 unstable_isConcurrent: true,
63 });
64
65 - expect(renderer).toFlushAll(['A:1', 'B:1', 'C:1']);
65 + expect(renderer).toFlushAndYield(['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 - expect(renderer).toFlushAll(['A:2', 'B:2', 'C:2']);
69 + expect(renderer).toFlushAndYield(['A:2', 'B:2', 'C:2']);
70 expect(renderer.toJSON()).toEqual(['A:2', 'B:2', 'C:2']);
71 });
72
@@ -89,12 +89,12 @@ describe('ReactTestRendererAsync', () => {
89 });
90
91 // Flush the first two siblings
92 - expect(renderer).toFlushThrough(['A:1', 'B:1']);
92 + expect(renderer).toFlushAndYieldThrough(['A:1', 'B:1']);
93 // Did not commit yet.
94 expect(renderer.toJSON()).toEqual(null);
95
96 // Flush the remaining work
97 - expect(renderer).toFlushAll(['C:1']);
97 + expect(renderer).toFlushAndYield(['C:1']);
98 expect(renderer.toJSON()).toEqual(['A:1', 'B:1', 'C:1']);
99 });
100
@@ -126,7 +126,7 @@ describe('ReactTestRendererAsync', () => {
126 });
127
128 // Flush the some of the changes, but don't commit
129 - expect(renderer).toFlushThrough(['A:1']);
129 + expect(renderer).toFlushAndYieldThrough(['A:1']);
130 expect(renderer.toJSON()).toEqual(null);
131
132 // Interrupt with higher priority properties
@@ -139,7 +139,7 @@ describe('ReactTestRendererAsync', () => {
139 });
140
141 describe('Jest matchers', () => {
142 - it('toFlushThrough', () => {
142 + it('toFlushAndYieldThrough', () => {
143 const Yield = ({id}) => {
144 ReactTestRenderer.unstable_yield(id);
145 return id;
@@ -156,12 +156,12 @@ describe('ReactTestRendererAsync', () => {
156 },
157 );
158
159 - expect(() => expect(renderer).toFlushThrough(['foo', 'baz'])).toThrow(
160 - 'Expected value to equal:',
161 - );
159 + expect(() =>
160 + expect(renderer).toFlushAndYieldThrough(['foo', 'baz']),
161 + ).toThrow('Expected value to equal:');
162 });
163
164 - it('toFlushAll', () => {
164 + it('toFlushAndYield', () => {
165 const Yield = ({id}) => {
166 ReactTestRenderer.unstable_yield(id);
167 return id;
@@ -178,7 +178,7 @@ describe('ReactTestRendererAsync', () => {
178 },
179 );
180
181 - expect(() => expect(renderer).toFlushAll([])).toThrowError(
181 + expect(() => expect(renderer).toFlushWithoutYielding()).toThrowError(
182 'Expected value to equal:',
183 );
184
@@ -190,7 +190,7 @@ describe('ReactTestRendererAsync', () => {
190 </div>,
191 );
192
193 - expect(() => expect(renderer).toFlushAll(['foo', 'baz'])).toThrow(
193 + expect(() => expect(renderer).toFlushAndYield(['foo', 'baz'])).toThrow(
194 'Expected value to equal:',
195 );
196 });
@@ -221,35 +221,38 @@ describe('ReactTestRendererAsync', () => {
221 unstable_isConcurrent: true,
222 });
223
224 - expect(() => {
225 - expect(renderer).toFlushAndThrow(
226 - // Wrong expected values
227 - ['A', 'B'],
228 - 'Oh no!',
229 - );
230 - }).toThrow('Expected value to equal:');
224 + expect(renderer).toFlushAndThrow('Oh no!');
225 + expect(ReactTestRenderer).toHaveYielded([
226 + 'A',
227 + 'B',
228 + 'C',
229 + 'D',
230 + 'A',
231 + 'B',
232 + 'C',
233 + 'D',
234 + ]);
235
236 renderer.update(<App />);
237
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:');
238 + expect(renderer).toFlushAndThrow('Oh no!');
239 + expect(ReactTestRenderer).toHaveYielded([
240 + 'A',
241 + 'B',
242 + 'C',
243 + 'D',
244 + 'A',
245 + 'B',
246 + 'C',
247 + 'D',
248 + ]);
249
250 renderer.update(<App />);
243 -
244 - // Passes
245 - expect(renderer).toFlushAndThrow(
246 - ['A', 'B', 'C', 'D', 'A', 'B', 'C', 'D'],
247 - 'Oh no!',
248 - );
251 + expect(renderer).toFlushAndThrow('Oh no!');
252 });
253 });
254
252 - it('toClearYields', () => {
255 + it('toHaveYielded', () => {
256 const Yield = ({id}) => {
257 ReactTestRenderer.unstable_yield(id);
258 return id;
@@ -266,8 +269,18 @@ describe('ReactTestRendererAsync', () => {
269 }
270
271 ReactTestRenderer.create(<App />);
269 - expect(() => expect(ReactTestRenderer).toClearYields(['A', 'B'])).toThrow(
272 + expect(() => expect(ReactTestRenderer).toHaveYielded(['A', 'B'])).toThrow(
273 'Expected value to equal:',
274 );
275 });
276 +
277 + it('flush methods throw if log is not empty', () => {
278 + const renderer = ReactTestRenderer.create(<div />, {
279 + unstable_isConcurrent: true,
280 + });
281 + ReactTestRenderer.unstable_yield('Something');
282 + expect(() => expect(renderer).toFlushWithoutYielding()).toThrow(
283 + 'Log of yielded values is not empty.',
284 + );
285 + });
286 });
packages/react/src/__tests__/ReactProfiler-test.internal.js
+39 -30
@@ -227,9 +227,9 @@ describe('Profiler', () => {
227 );
228
229 // Times are logged until a render is committed.
230 - expect(renderer).toFlushThrough(['first']);
230 + expect(renderer).toFlushAndYieldThrough(['first']);
231 expect(callback).toHaveBeenCalledTimes(0);
232 - expect(renderer).toFlushAll(['last']);
232 + expect(renderer).toFlushAndYield(['last']);
233 expect(callback).toHaveBeenCalledTimes(1);
234 });
235
@@ -579,11 +579,11 @@ describe('Profiler', () => {
579 </React.unstable_Profiler>,
580 {unstable_isConcurrent: true},
581 );
582 - expect(renderer).toFlushThrough(['Yield:2']);
582 + expect(renderer).toFlushAndYieldThrough(['Yield:2']);
583 expect(callback).toHaveBeenCalledTimes(0);
584
585 // Resume render for remaining children.
586 - expect(renderer).toFlushAll(['Yield:3']);
586 + expect(renderer).toFlushAndYield(['Yield:3']);
587
588 // Verify that logged times include both durations above.
589 expect(callback).toHaveBeenCalledTimes(1);
@@ -617,7 +617,7 @@ describe('Profiler', () => {
617 </React.unstable_Profiler>,
618 {unstable_isConcurrent: true},
619 );
620 - expect(renderer).toFlushThrough(['Yield:5']);
620 + expect(renderer).toFlushAndYieldThrough(['Yield:5']);
621 expect(callback).toHaveBeenCalledTimes(0);
622
623 // Simulate time moving forward while frame is paused.
@@ -625,7 +625,7 @@ describe('Profiler', () => {
625
626 // Flush the remaninig work,
627 // Which should take an additional 10ms of simulated time.
628 - expect(renderer).toFlushAll(['Yield:10', 'Yield:17']);
628 + expect(renderer).toFlushAndYield(['Yield:10', 'Yield:17']);
629 expect(callback).toHaveBeenCalledTimes(2);
630
631 const [innerCall, outerCall] = callback.mock.calls;
@@ -664,7 +664,7 @@ describe('Profiler', () => {
664 </React.unstable_Profiler>,
665 {unstable_isConcurrent: true},
666 );
667 - expect(renderer).toFlushThrough(['Yield:10']);
667 + expect(renderer).toFlushAndYieldThrough(['Yield:10']);
668 expect(callback).toHaveBeenCalledTimes(0);
669
670 // Simulate time moving forward while frame is paused.
@@ -679,7 +679,7 @@ describe('Profiler', () => {
679 </React.unstable_Profiler>,
680 );
681 });
682 - expect(ReactTestRenderer).toClearYields(['Yield:5']);
682 + expect(ReactTestRenderer).toHaveYielded(['Yield:5']);
683
684 // The initial work was thrown away in this case,
685 // So the actual and base times should only include the final rendered tree times.
@@ -693,7 +693,7 @@ describe('Profiler', () => {
693 callback.mockReset();
694
695 // Verify no more unexpected callbacks from low priority work
696 - expect(renderer).toFlushAll([]);
696 + expect(renderer).toFlushWithoutYielding();
697 expect(callback).toHaveBeenCalledTimes(0);
698 });
699
@@ -718,7 +718,7 @@ describe('Profiler', () => {
718
719 // Render everything initially.
720 // This should take 21 seconds of actual and base time.
721 - expect(renderer).toFlushAll(['Yield:6', 'Yield:15']);
721 + expect(renderer).toFlushAndYield(['Yield:6', 'Yield:15']);
722 expect(callback).toHaveBeenCalledTimes(1);
723 let call = callback.mock.calls[0];
724 expect(call[2]).toBe(21); // actual time
@@ -739,14 +739,14 @@ describe('Profiler', () => {
739 <Yield renderTime={9} />
740 </React.unstable_Profiler>,
741 );
742 - expect(renderer).toFlushThrough(['Yield:3']);
742 + expect(renderer).toFlushAndYieldThrough(['Yield:3']);
743 expect(callback).toHaveBeenCalledTimes(0);
744
745 // Simulate time moving forward while frame is paused.
746 advanceTimeBy(100); // 59 -> 159
747
748 // Render another 5ms of simulated time.
749 - expect(renderer).toFlushThrough(['Yield:5']);
749 + expect(renderer).toFlushAndYieldThrough(['Yield:5']);
750 expect(callback).toHaveBeenCalledTimes(0);
751
752 // Simulate time moving forward while frame is paused.
@@ -761,7 +761,7 @@ describe('Profiler', () => {
761 </React.unstable_Profiler>,
762 );
763 });
764 - expect(ReactTestRenderer).toClearYields(['Yield:11']);
764 + expect(ReactTestRenderer).toHaveYielded(['Yield:11']);
765
766 // The actual time should include only the most recent render,
767 // Because this lets us avoid a lot of commit phase reset complexity.
@@ -774,7 +774,7 @@ describe('Profiler', () => {
774 expect(call[5]).toBe(275); // commit time
775
776 // Verify no more unexpected callbacks from low priority work
777 - expect(renderer).toFlushAll([]);
777 + expect(renderer).toFlushAndYield([]);
778 expect(callback).toHaveBeenCalledTimes(1);
779 });
780
@@ -825,7 +825,7 @@ describe('Profiler', () => {
825 // Render everything initially.
826 // This simulates a total of 14ms of actual render time.
827 // The base render time is also 14ms for the initial render.
828 - expect(renderer).toFlushAll([
828 + expect(renderer).toFlushAndYield([
829 'FirstComponent:1',
830 'Yield:4',
831 'SecondComponent:2',
@@ -845,7 +845,7 @@ describe('Profiler', () => {
845 // Render a partially update, but don't finish.
846 // This partial render will take 10ms of actual render time.
847 first.setState({renderTime: 10});
848 - expect(renderer).toFlushThrough(['FirstComponent:10']);
848 + expect(renderer).toFlushAndYieldThrough(['FirstComponent:10']);
849 expect(callback).toHaveBeenCalledTimes(0);
850
851 // Simulate time moving forward while frame is paused.
@@ -854,7 +854,7 @@ describe('Profiler', () => {
854 // Interrupt with higher priority work.
855 // This simulates a total of 37ms of actual render time.
856 renderer.unstable_flushSync(() => second.setState({renderTime: 30}));
857 - expect(ReactTestRenderer).toClearYields([
857 + expect(ReactTestRenderer).toHaveYielded([
858 'SecondComponent:30',
859 'Yield:7',
860 ]);
@@ -881,7 +881,7 @@ describe('Profiler', () => {
881 // The tree contains 42ms of base render time at this point,
882 // Reflecting the most recent (longer) render durations.
883 // TODO: This actual time should decrease by 10ms once the scheduler supports resuming.
884 - expect(renderer).toFlushAll(['FirstComponent:10', 'Yield:4']);
884 + expect(renderer).toFlushAndYield(['FirstComponent:10', 'Yield:4']);
885 expect(callback).toHaveBeenCalledTimes(1);
886 call = callback.mock.calls[0];
887 expect(call[2]).toBe(14); // actual time
@@ -1305,7 +1305,7 @@ describe('Profiler', () => {
1305 renderer = ReactTestRenderer.create(<Component>succeed</Component>, {
1306 unstable_isConcurrent: true,
1307 });
1308 - expect(renderer).toFlushAll(['Component:succeed']);
1308 + expect(renderer).toFlushAndYield(['Component:succeed']);
1309 const tree = renderer.toTree();
1310 expect(tree.type).toBe(Component);
1311 expect(tree.props.children).toBe('succeed');
@@ -1327,14 +1327,13 @@ describe('Profiler', () => {
1327
1328 // Errors that happen inside of a subscriber should throw,
1329 throwInOnWorkStarted = true;
1330 - expect(() => {
1331 - expect(renderer).toFlushAll(['Component:text']);
1332 - }).toThrow('Expected error onWorkStarted');
1330 + expect(renderer).toFlushAndThrow('Expected error onWorkStarted');
1331 + expect(ReactTestRenderer).toHaveYielded(['Component:text']);
1332 throwInOnWorkStarted = false;
1333 expect(onWorkStarted).toHaveBeenCalled();
1334
1335 // But the React work should have still been processed.
1337 - expect(renderer).toFlushAll([]);
1336 + expect(renderer).toFlushAndYield([]);
1337 const tree = renderer.toTree();
1338 expect(tree.type).toBe(Component);
1339 expect(tree.props.children).toBe('text');
@@ -1565,7 +1564,7 @@ describe('Profiler', () => {
1564 ]);
1565 expect(onWorkScheduled.mock.calls[0][1] > 0).toBe(true);
1566
1568 - expect(renderer).toFlushThrough(['first']);
1567 + expect(renderer).toFlushAndYieldThrough(['first']);
1568 expect(onRender).not.toHaveBeenCalled();
1569
1570 expect(onInteractionTraced).toHaveBeenCalledTimes(2);
@@ -1750,7 +1749,7 @@ describe('Profiler', () => {
1749 interactionLowPri,
1750 ]);
1751
1753 - expect(renderer).toFlushThrough(['FirstComponent']);
1752 + expect(renderer).toFlushAndYieldThrough(['FirstComponent']);
1753 expect(onRender).not.toHaveBeenCalled();
1754
1755 expect(onInteractionTraced).toHaveBeenCalledTimes(1);
@@ -1794,7 +1793,7 @@ describe('Profiler', () => {
1793 },
1794 );
1795 });
1797 - expect(ReactTestRenderer).toClearYields(['SecondComponent']);
1796 + expect(ReactTestRenderer).toHaveYielded(['SecondComponent']);
1797
1798 expect(onInteractionTraced).toHaveBeenCalledTimes(2);
1799 expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
@@ -2421,13 +2420,17 @@ describe('Profiler', () => {
2420 advanceTimeBy(1500);
2421 await awaitableAdvanceTimers(1500);
2422
2424 - expect(renderer).toFlushAll(['Suspend [loaded]', 'Text [loading]']);
2423 + expect(renderer).toFlushAndYield([
2424 + 'Suspend [loaded]',
2425 + 'Text [loading]',
2426 + ]);
2427 expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
2428
2429 advanceTimeBy(2500);
2430 await awaitableAdvanceTimers(2500);
2431
2430 - expect(renderer).toFlushAll(['AsyncText [loaded]']);
2432 + expect(ReactTestRenderer).toHaveYielded(['Promise resolved [loaded]']);
2433 + expect(renderer).toFlushAndYield(['AsyncText [loaded]']);
2434 expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
2435 expect(
2436 onInteractionScheduledWorkCompleted,
@@ -2590,7 +2593,7 @@ describe('Profiler', () => {
2593 );
2594 },
2595 );
2593 - expect(renderer).toFlushAll([
2596 + expect(renderer).toFlushAndYield([
2597 'Suspend [loaded]',
2598 'Text [loading]',
2599 'Text [initial]',
@@ -2644,7 +2647,13 @@ describe('Profiler', () => {
2647 advanceTimeBy(500);
2648 jest.advanceTimersByTime(500);
2649 await originalPromise;
2647 - expect(renderer).toFlushAll(['AsyncText [loaded]']);
2650 + expect(ReactTestRenderer).toHaveYielded([
2651 + 'Suspend [loaded]',
2652 + 'Text [loading]',
2653 + 'Text [updated]',
2654 + 'Promise resolved [loaded]',
2655 + ]);
2656 + expect(renderer).toFlushAndYield(['AsyncText [loaded]']);
2657 expect(renderer.toJSON()).toEqual(['loaded', 'updated']);
2658
2659 expect(onRender).toHaveBeenCalledTimes(1);
scripts/jest/matchers/testRenderer.js deleted
-57
@@ -1,57 +0,0 @@
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
+8 -1
@@ -43,10 +43,17 @@ if (process.env.REACT_CLASS_EQUIVALENCE_TEST) {
43 global.spyOnDevAndProd = spyOn;
44 }
45
46 + const JestReact = require('jest-react');
47 expect.extend({
48 ...require('./matchers/interactionTracing'),
49 ...require('./matchers/toWarnDev'),
49 - ...require('./matchers/testRenderer'),
50 +
51 + toFlushWithoutYielding: JestReact.toFlushWithoutYielding,
52 + toFlushAndYield: JestReact.toFlushAndYield,
53 + toFlushAndYieldThrough: JestReact.toFlushAndYieldThrough,
54 + toHaveYielded: JestReact.toHaveYielded,
55 + toFlushAndThrow: JestReact.toFlushAndThrow,
56 + toMatchRenderedOutput: JestReact.toMatchRenderedOutput,
57 });
58
59 // We have a Babel transform that inserts guards against infinite loops.
scripts/jest/spec-equivalence-reporter/setupTests.js
+8 -1
@@ -45,10 +45,17 @@ global.spyOnProd = function(...args) {
45 }
46 };
47
48 +const JestReact = require('jest-react');
49 expect.extend({
50 ...require('../matchers/interactionTracing'),
51 ...require('../matchers/toWarnDev'),
51 - ...require('../matchers/testRenderer'),
52 +
53 + toFlushWithoutYielding: JestReact.toFlushWithoutYielding,
54 + toFlushAndYield: JestReact.toFlushAndYield,
55 + toFlushAndYieldThrough: JestReact.toFlushAndYieldThrough,
56 + toHaveYielded: JestReact.toHaveYielded,
57 + toFlushAndThrow: JestReact.toFlushAndThrow,
58 + toMatchRenderedOutput: JestReact.toMatchRenderedOutput,
59 });
60
61 beforeEach(() => (numExpectations = 0));
scripts/rollup/bundles.js
+10
@@ -400,6 +400,16 @@ const bundles = [
400 externals: [],
401 },
402
403 + /******* Jest React (experimental) *******/
404 + {
405 + label: 'jest-react',
406 + bundleTypes: [NODE_DEV, NODE_PROD, FB_WWW_DEV, FB_WWW_PROD],
407 + moduleType: ISOMORPHIC,
408 + entry: 'jest-react',
409 + global: 'JestReact',
410 + externals: ['jest-diff'],
411 + },
412 +
413 {
414 label: 'scheduler-tracing',
415 bundleTypes: [
scripts/rollup/validate/eslintignore
+4 -1
@@ -1,4 +1,7 @@
1 # Don't validate noop renderer bundle.
2 # Unlike others, it currently uses ES6 syntax (generators).
3 # We also don't use or publish it.
4 -**/react-noop-renderer/**
\ No newline at end of file
4 +**/react-noop-renderer/**
5 +JestReact-prod.js
6 +JestReact-dev.js
7 +**/jest-react/**
\ No newline at end of file