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
This commit is contained in:
Andrew Clark
2018-10-03 18:37:41 -06:00
committed by GitHub
parent 7ba1e21dbc
commit bc1a29df20
15 changed files with 536 additions and 150 deletions
+3
View File
@@ -0,0 +1,3 @@
# `jest-react`
Jest matchers and utilities for testing React Test Renderer.
+10
View File
@@ -0,0 +1,10 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
export * from './src/JestReact';
+7
View File
@@ -0,0 +1,7 @@
'use strict';
if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/jest-react.production.min.js');
} else {
module.exports = require('./cjs/jest-react.development.js');
}
+28
View File
@@ -0,0 +1,28 @@
{
"name": "jest-react",
"version": "0.2.0",
"description": "Jest matchers and utilities for testing React components.",
"main": "index.js",
"repository": "facebook/react",
"keywords": [
"react",
"jest",
"react-testing"
],
"license": "MIT",
"bugs": {
"url": "https://github.com/facebook/react/issues"
},
"homepage": "https://reactjs.org/",
"peerDependencies": {
"jest": "^23.0.1",
"react": "^16.0.0",
"react-test-renderer": "^16.0.0"
},
"files": [
"LICENSE",
"README.md",
"index.js",
"cjs/"
]
}
+168
View File
@@ -0,0 +1,168 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {REACT_ELEMENT_TYPE, REACT_FRAGMENT_TYPE} from 'shared/ReactSymbols';
import invariant from 'shared/invariant';
function captureAssertion(fn) {
// Trick to use a Jest matcher inside another Jest matcher. `fn` contains an
// assertion; if it throws, we capture the error and return it, so the stack
// trace presented to the user points to the original assertion in the
// test file.
try {
fn();
} catch (error) {
return {
pass: false,
message: () => error.message,
};
}
return {pass: true};
}
function assertYieldsWereCleared(root) {
const actualYields = root.unstable_clearYields();
invariant(
actualYields.length === 0,
'Log of yielded values is not empty. ' +
'Call expect(ReactTestRenderer).toHaveYielded(...) first.',
);
}
export function toFlushAndYield(root, expectedYields) {
return captureAssertion(() => {
assertYieldsWereCleared(root);
const actualYields = root.unstable_flushAll();
expect(actualYields).toEqual(expectedYields);
});
}
export function toFlushAndYieldThrough(root, expectedYields) {
return captureAssertion(() => {
assertYieldsWereCleared(root);
const actualYields = root.unstable_flushNumberOfYields(
expectedYields.length,
);
expect(actualYields).toEqual(expectedYields);
});
}
export function toFlushWithoutYielding(root) {
return toFlushAndYield(root, []);
}
export function toHaveYielded(ReactTestRenderer, expectedYields) {
return captureAssertion(() => {
if (
ReactTestRenderer === null ||
typeof ReactTestRenderer !== 'object' ||
typeof ReactTestRenderer.unstable_setNowImplementation !== 'function'
) {
invariant(
false,
'The matcher `toHaveYielded` expects an instance of React Test ' +
'Renderer.\n\nTry: ' +
'expect(ReactTestRenderer).toHaveYielded(expectedYields)',
);
}
const actualYields = ReactTestRenderer.unstable_clearYields();
expect(actualYields).toEqual(expectedYields);
});
}
export function toFlushAndThrow(root, ...rest) {
return captureAssertion(() => {
assertYieldsWereCleared(root);
expect(() => {
root.unstable_flushAll();
}).toThrow(...rest);
});
}
export function toMatchRenderedOutput(root, expectedJSX) {
const actualJSON = root.toJSON();
let actualJSX;
if (actualJSON === null || typeof actualJSON === 'string') {
actualJSX = actualJSON;
} else if (Array.isArray(actualJSON)) {
if (actualJSON.length === 0) {
actualJSX = null;
} else if (actualJSON.length === 1) {
actualJSX = jsonChildToJSXChild(actualJSON[0]);
} else {
const actualJSXChildren = jsonChildrenToJSXChildren(actualJSON);
if (actualJSXChildren === null || typeof actualJSXChildren === 'string') {
actualJSX = actualJSXChildren;
} else {
actualJSX = {
$$typeof: REACT_ELEMENT_TYPE,
type: REACT_FRAGMENT_TYPE,
key: null,
ref: null,
props: {
children: actualJSXChildren,
},
_owner: null,
_store: __DEV__ ? {} : undefined,
};
}
}
} else {
actualJSX = jsonChildToJSXChild(actualJSON);
}
return captureAssertion(() => {
expect(actualJSX).toEqual(expectedJSX);
});
}
function jsonChildToJSXChild(jsonChild) {
if (jsonChild === null || typeof jsonChild === 'string') {
return jsonChild;
} else {
const jsxChildren = jsonChildrenToJSXChildren(jsonChild.children);
return {
$$typeof: REACT_ELEMENT_TYPE,
type: jsonChild.type,
key: null,
ref: null,
props:
jsxChildren === null
? jsonChild.props
: {...jsonChild.props, children: jsxChildren},
_owner: null,
_store: __DEV__ ? {} : undefined,
};
}
}
function jsonChildrenToJSXChildren(jsonChildren) {
if (jsonChildren !== null) {
if (jsonChildren.length === 1) {
return jsonChildToJSXChild(jsonChildren[0]);
} else if (jsonChildren.length > 1) {
let jsxChildren = [];
let allJSXChildrenAreStrings = true;
let jsxChildrenString = '';
for (let i = 0; i < jsonChildren.length; i++) {
const jsxChild = jsonChildrenToJSXChildren(jsonChildren[i]);
jsxChildren.push(jsxChild);
if (allJSXChildrenAreStrings) {
if (typeof jsxChild === 'string') {
jsxChildrenString += jsxChild;
} else if (jsxChild !== null) {
allJSXChildrenAreStrings = false;
}
}
}
return allJSXChildrenAreStrings ? jsxChildrenString : jsxChildren;
}
}
return null;
}
@@ -1,5 +1,3 @@
const jestDiff = require('jest-diff');
describe('ErrorBoundaryReconciliation', () => {
let BrokenRender;
let DidCatchErrorBoundary;
@@ -48,22 +46,6 @@ describe('ErrorBoundaryReconciliation', () => {
const InvalidType = undefined;
BrokenRender = ({fail}) =>
fail ? <InvalidType /> : <span prop="BrokenRender" />;
function toHaveRenderedChildren(renderer, children) {
let actual, expected;
try {
actual = renderer.toJSON();
expected = ReactTestRenderer.create(children).toJSON();
expect(actual).toEqual(expected);
} catch (error) {
return {
message: () => jestDiff(expected, actual),
pass: false,
};
}
return {pass: true};
}
expect.extend({toHaveRenderedChildren});
});
[true, false].forEach(isConcurrent => {
@@ -77,7 +59,7 @@ describe('ErrorBoundaryReconciliation', () => {
if (isConcurrent) {
renderer.unstable_flushAll();
}
expect(renderer).toHaveRenderedChildren(<span prop="BrokenRender" />);
expect(renderer).toMatchRenderedOutput(<span prop="BrokenRender" />);
expect(() => {
renderer.update(
@@ -89,9 +71,8 @@ describe('ErrorBoundaryReconciliation', () => {
renderer.unstable_flushAll();
}
}).toWarnDev(isConcurrent ? ['invalid', 'invalid'] : ['invalid']);
expect(renderer).toHaveRenderedChildren(
React.createElement(fallbackTagName, {prop: 'ErrorBoundary'}),
);
const Fallback = fallbackTagName;
expect(renderer).toMatchRenderedOutput(<Fallback prop="ErrorBoundary" />);
}
describe(isConcurrent ? 'concurrent' : 'sync', () => {
@@ -0,0 +1,193 @@
let React;
let ReactTestRenderer;
let ReactFeatureFlags;
let ReactCache;
let Placeholder;
// let JestReact;
let cache;
let TextResource;
let textResourceShouldFail;
// The tests in the ReactSuspense suite use ReactNoop. We want to use
// ReactTestRenderer for most of our tests, so we're gradually migrating the
// Suspense tests over to this file.
describe('ReactSuspenseWithTestRenderer', () => {
beforeEach(() => {
jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
ReactFeatureFlags.enableSuspense = true;
React = require('react');
ReactTestRenderer = require('react-test-renderer');
// JestReact = require('jest-react');
ReactCache = require('react-cache');
Placeholder = React.Placeholder;
function invalidateCache() {
cache = ReactCache.createCache(invalidateCache);
}
invalidateCache();
TextResource = ReactCache.createResource(([text, ms = 0]) => {
let listeners = null;
let status = 'pending';
let value = null;
return {
then(resolve, reject) {
switch (status) {
case 'pending': {
if (listeners === null) {
listeners = [{resolve, reject}];
setTimeout(() => {
if (textResourceShouldFail) {
ReactTestRenderer.unstable_yield(
`Promise rejected [${text}]`,
);
status = 'rejected';
value = new Error('Failed to load: ' + text);
listeners.forEach(listener => listener.reject(value));
} else {
ReactTestRenderer.unstable_yield(
`Promise resolved [${text}]`,
);
status = 'resolved';
value = text;
listeners.forEach(listener => listener.resolve(value));
}
}, ms);
} else {
listeners.push({resolve, reject});
}
break;
}
case 'resolved': {
resolve(value);
break;
}
case 'rejected': {
reject(value);
break;
}
}
},
};
}, ([text, ms]) => text);
textResourceShouldFail = false;
});
function Text(props) {
ReactTestRenderer.unstable_yield(props.text);
return props.text;
}
function AsyncText(props) {
const text = props.text;
try {
TextResource.read(cache, [props.text, props.ms]);
ReactTestRenderer.unstable_yield(text);
return text;
} catch (promise) {
if (typeof promise.then === 'function') {
ReactTestRenderer.unstable_yield(`Suspend! [${text}]`);
} else {
ReactTestRenderer.unstable_yield(`Error! [${text}]`);
}
throw promise;
}
}
it('suspends rendering and continues later', () => {
function Bar(props) {
ReactTestRenderer.unstable_yield('Bar');
return props.children;
}
function Foo() {
ReactTestRenderer.unstable_yield('Foo');
return (
<Placeholder>
<Bar>
<AsyncText text="A" ms={100} />
<Text text="B" />
</Bar>
</Placeholder>
);
}
const root = ReactTestRenderer.create(<Foo />, {
unstable_isConcurrent: true,
});
expect(root).toFlushAndYield([
'Foo',
'Bar',
// A suspends
'Suspend! [A]',
// But we keep rendering the siblings
'B',
]);
expect(root).toMatchRenderedOutput(null);
// Flush some of the time
jest.advanceTimersByTime(50);
// Still nothing...
expect(root).toFlushWithoutYielding();
expect(root).toMatchRenderedOutput(null);
// Flush the promise completely
jest.advanceTimersByTime(50);
// Renders successfully
expect(ReactTestRenderer).toHaveYielded(['Promise resolved [A]']);
expect(root).toFlushAndYield(['Foo', 'Bar', 'A', 'B']);
expect(root).toMatchRenderedOutput('AB');
});
it('suspends siblings and later recovers each independently', () => {
// Render two sibling Placeholder components
const root = ReactTestRenderer.create(
<React.Fragment>
<Placeholder delayMs={1000} fallback={<Text text="Loading A..." />}>
<AsyncText text="A" ms={5000} />
</Placeholder>
<Placeholder delayMs={3000} fallback={<Text text="Loading B..." />}>
<AsyncText text="B" ms={6000} />
</Placeholder>
</React.Fragment>,
{
unstable_isConcurrent: true,
},
);
expect(root).toFlushAndYield([
'Suspend! [A]',
'Loading A...',
'Suspend! [B]',
'Loading B...',
]);
expect(root).toMatchRenderedOutput(null);
// Advance time by enough to timeout both components and commit their placeholders
jest.advanceTimersByTime(4000);
expect(root).toFlushWithoutYielding();
expect(root).toMatchRenderedOutput('Loading A...Loading B...');
// Advance time by enough that the first Placeholder's promise resolves and
// switches back to the normal view. The second Placeholder should still
// show the placeholder
jest.advanceTimersByTime(1000);
// TODO: Should we throw if you forget to call toHaveYielded?
expect(ReactTestRenderer).toHaveYielded(['Promise resolved [A]']);
expect(root).toFlushAndYield(['A']);
expect(root).toMatchRenderedOutput('ALoading B...');
// Advance time by enough that the second Placeholder's promise resolves
// and switches back to the normal view
jest.advanceTimersByTime(1000);
expect(ReactTestRenderer).toHaveYielded(['Promise resolved [B]']);
expect(root).toFlushAndYield(['B']);
expect(root).toMatchRenderedOutput('AB');
});
});
@@ -47,7 +47,9 @@ export function flushAll(): Array<mixed> {
didTimeout: false,
});
}
return yieldedValues;
const values = yieldedValues;
yieldedValues = [];
return values;
}
export function flushNumberOfYields(count: number): Array<mixed> {
@@ -72,7 +74,9 @@ export function flushNumberOfYields(count: number): Array<mixed> {
didTimeout: false,
});
}
return yieldedValues;
const values = yieldedValues;
yieldedValues = [];
return values;
}
export function yieldValue(value: mixed): void {
@@ -32,7 +32,7 @@ describe('ReactTestRendererAsync', () => {
expect(renderer.toJSON()).toEqual(null);
// Flush initial mount.
expect(renderer).toFlushAll([]);
expect(renderer).toFlushWithoutYielding();
expect(renderer.toJSON()).toEqual('Hi');
// Update
@@ -40,7 +40,7 @@ describe('ReactTestRendererAsync', () => {
// Not yet updated.
expect(renderer.toJSON()).toEqual('Hi');
// Flush update.
expect(renderer).toFlushAll([]);
expect(renderer).toFlushWithoutYielding();
expect(renderer.toJSON()).toEqual('Bye');
});
@@ -62,11 +62,11 @@ describe('ReactTestRendererAsync', () => {
unstable_isConcurrent: true,
});
expect(renderer).toFlushAll(['A:1', 'B:1', 'C:1']);
expect(renderer).toFlushAndYield(['A:1', 'B:1', 'C:1']);
expect(renderer.toJSON()).toEqual(['A:1', 'B:1', 'C:1']);
renderer.update(<Parent step={2} />);
expect(renderer).toFlushAll(['A:2', 'B:2', 'C:2']);
expect(renderer).toFlushAndYield(['A:2', 'B:2', 'C:2']);
expect(renderer.toJSON()).toEqual(['A:2', 'B:2', 'C:2']);
});
@@ -89,12 +89,12 @@ describe('ReactTestRendererAsync', () => {
});
// Flush the first two siblings
expect(renderer).toFlushThrough(['A:1', 'B:1']);
expect(renderer).toFlushAndYieldThrough(['A:1', 'B:1']);
// Did not commit yet.
expect(renderer.toJSON()).toEqual(null);
// Flush the remaining work
expect(renderer).toFlushAll(['C:1']);
expect(renderer).toFlushAndYield(['C:1']);
expect(renderer.toJSON()).toEqual(['A:1', 'B:1', 'C:1']);
});
@@ -126,7 +126,7 @@ describe('ReactTestRendererAsync', () => {
});
// Flush the some of the changes, but don't commit
expect(renderer).toFlushThrough(['A:1']);
expect(renderer).toFlushAndYieldThrough(['A:1']);
expect(renderer.toJSON()).toEqual(null);
// Interrupt with higher priority properties
@@ -139,7 +139,7 @@ describe('ReactTestRendererAsync', () => {
});
describe('Jest matchers', () => {
it('toFlushThrough', () => {
it('toFlushAndYieldThrough', () => {
const Yield = ({id}) => {
ReactTestRenderer.unstable_yield(id);
return id;
@@ -156,12 +156,12 @@ describe('ReactTestRendererAsync', () => {
},
);
expect(() => expect(renderer).toFlushThrough(['foo', 'baz'])).toThrow(
'Expected value to equal:',
);
expect(() =>
expect(renderer).toFlushAndYieldThrough(['foo', 'baz']),
).toThrow('Expected value to equal:');
});
it('toFlushAll', () => {
it('toFlushAndYield', () => {
const Yield = ({id}) => {
ReactTestRenderer.unstable_yield(id);
return id;
@@ -178,7 +178,7 @@ describe('ReactTestRendererAsync', () => {
},
);
expect(() => expect(renderer).toFlushAll([])).toThrowError(
expect(() => expect(renderer).toFlushWithoutYielding()).toThrowError(
'Expected value to equal:',
);
@@ -190,7 +190,7 @@ describe('ReactTestRendererAsync', () => {
</div>,
);
expect(() => expect(renderer).toFlushAll(['foo', 'baz'])).toThrow(
expect(() => expect(renderer).toFlushAndYield(['foo', 'baz'])).toThrow(
'Expected value to equal:',
);
});
@@ -221,35 +221,38 @@ describe('ReactTestRendererAsync', () => {
unstable_isConcurrent: true,
});
expect(() => {
expect(renderer).toFlushAndThrow(
// Wrong expected values
['A', 'B'],
'Oh no!',
);
}).toThrow('Expected value to equal:');
expect(renderer).toFlushAndThrow('Oh no!');
expect(ReactTestRenderer).toHaveYielded([
'A',
'B',
'C',
'D',
'A',
'B',
'C',
'D',
]);
renderer.update(<App />);
expect(() => {
expect(renderer).toFlushAndThrow(
['A', 'B', 'C', 'D', 'A', 'B', 'C', 'D'],
// Wrong error message
'Oops!',
);
}).toThrow('Expected the function to throw an error matching:');
expect(renderer).toFlushAndThrow('Oh no!');
expect(ReactTestRenderer).toHaveYielded([
'A',
'B',
'C',
'D',
'A',
'B',
'C',
'D',
]);
renderer.update(<App />);
// Passes
expect(renderer).toFlushAndThrow(
['A', 'B', 'C', 'D', 'A', 'B', 'C', 'D'],
'Oh no!',
);
expect(renderer).toFlushAndThrow('Oh no!');
});
});
it('toClearYields', () => {
it('toHaveYielded', () => {
const Yield = ({id}) => {
ReactTestRenderer.unstable_yield(id);
return id;
@@ -266,8 +269,18 @@ describe('ReactTestRendererAsync', () => {
}
ReactTestRenderer.create(<App />);
expect(() => expect(ReactTestRenderer).toClearYields(['A', 'B'])).toThrow(
expect(() => expect(ReactTestRenderer).toHaveYielded(['A', 'B'])).toThrow(
'Expected value to equal:',
);
});
it('flush methods throw if log is not empty', () => {
const renderer = ReactTestRenderer.create(<div />, {
unstable_isConcurrent: true,
});
ReactTestRenderer.unstable_yield('Something');
expect(() => expect(renderer).toFlushWithoutYielding()).toThrow(
'Log of yielded values is not empty.',
);
});
});
@@ -227,9 +227,9 @@ describe('Profiler', () => {
);
// Times are logged until a render is committed.
expect(renderer).toFlushThrough(['first']);
expect(renderer).toFlushAndYieldThrough(['first']);
expect(callback).toHaveBeenCalledTimes(0);
expect(renderer).toFlushAll(['last']);
expect(renderer).toFlushAndYield(['last']);
expect(callback).toHaveBeenCalledTimes(1);
});
@@ -579,11 +579,11 @@ describe('Profiler', () => {
</React.unstable_Profiler>,
{unstable_isConcurrent: true},
);
expect(renderer).toFlushThrough(['Yield:2']);
expect(renderer).toFlushAndYieldThrough(['Yield:2']);
expect(callback).toHaveBeenCalledTimes(0);
// Resume render for remaining children.
expect(renderer).toFlushAll(['Yield:3']);
expect(renderer).toFlushAndYield(['Yield:3']);
// Verify that logged times include both durations above.
expect(callback).toHaveBeenCalledTimes(1);
@@ -617,7 +617,7 @@ describe('Profiler', () => {
</React.unstable_Profiler>,
{unstable_isConcurrent: true},
);
expect(renderer).toFlushThrough(['Yield:5']);
expect(renderer).toFlushAndYieldThrough(['Yield:5']);
expect(callback).toHaveBeenCalledTimes(0);
// Simulate time moving forward while frame is paused.
@@ -625,7 +625,7 @@ describe('Profiler', () => {
// Flush the remaninig work,
// Which should take an additional 10ms of simulated time.
expect(renderer).toFlushAll(['Yield:10', 'Yield:17']);
expect(renderer).toFlushAndYield(['Yield:10', 'Yield:17']);
expect(callback).toHaveBeenCalledTimes(2);
const [innerCall, outerCall] = callback.mock.calls;
@@ -664,7 +664,7 @@ describe('Profiler', () => {
</React.unstable_Profiler>,
{unstable_isConcurrent: true},
);
expect(renderer).toFlushThrough(['Yield:10']);
expect(renderer).toFlushAndYieldThrough(['Yield:10']);
expect(callback).toHaveBeenCalledTimes(0);
// Simulate time moving forward while frame is paused.
@@ -679,7 +679,7 @@ describe('Profiler', () => {
</React.unstable_Profiler>,
);
});
expect(ReactTestRenderer).toClearYields(['Yield:5']);
expect(ReactTestRenderer).toHaveYielded(['Yield:5']);
// The initial work was thrown away in this case,
// So the actual and base times should only include the final rendered tree times.
@@ -693,7 +693,7 @@ describe('Profiler', () => {
callback.mockReset();
// Verify no more unexpected callbacks from low priority work
expect(renderer).toFlushAll([]);
expect(renderer).toFlushWithoutYielding();
expect(callback).toHaveBeenCalledTimes(0);
});
@@ -718,7 +718,7 @@ describe('Profiler', () => {
// Render everything initially.
// This should take 21 seconds of actual and base time.
expect(renderer).toFlushAll(['Yield:6', 'Yield:15']);
expect(renderer).toFlushAndYield(['Yield:6', 'Yield:15']);
expect(callback).toHaveBeenCalledTimes(1);
let call = callback.mock.calls[0];
expect(call[2]).toBe(21); // actual time
@@ -739,14 +739,14 @@ describe('Profiler', () => {
<Yield renderTime={9} />
</React.unstable_Profiler>,
);
expect(renderer).toFlushThrough(['Yield:3']);
expect(renderer).toFlushAndYieldThrough(['Yield:3']);
expect(callback).toHaveBeenCalledTimes(0);
// Simulate time moving forward while frame is paused.
advanceTimeBy(100); // 59 -> 159
// Render another 5ms of simulated time.
expect(renderer).toFlushThrough(['Yield:5']);
expect(renderer).toFlushAndYieldThrough(['Yield:5']);
expect(callback).toHaveBeenCalledTimes(0);
// Simulate time moving forward while frame is paused.
@@ -761,7 +761,7 @@ describe('Profiler', () => {
</React.unstable_Profiler>,
);
});
expect(ReactTestRenderer).toClearYields(['Yield:11']);
expect(ReactTestRenderer).toHaveYielded(['Yield:11']);
// The actual time should include only the most recent render,
// Because this lets us avoid a lot of commit phase reset complexity.
@@ -774,7 +774,7 @@ describe('Profiler', () => {
expect(call[5]).toBe(275); // commit time
// Verify no more unexpected callbacks from low priority work
expect(renderer).toFlushAll([]);
expect(renderer).toFlushAndYield([]);
expect(callback).toHaveBeenCalledTimes(1);
});
@@ -825,7 +825,7 @@ describe('Profiler', () => {
// Render everything initially.
// This simulates a total of 14ms of actual render time.
// The base render time is also 14ms for the initial render.
expect(renderer).toFlushAll([
expect(renderer).toFlushAndYield([
'FirstComponent:1',
'Yield:4',
'SecondComponent:2',
@@ -845,7 +845,7 @@ describe('Profiler', () => {
// Render a partially update, but don't finish.
// This partial render will take 10ms of actual render time.
first.setState({renderTime: 10});
expect(renderer).toFlushThrough(['FirstComponent:10']);
expect(renderer).toFlushAndYieldThrough(['FirstComponent:10']);
expect(callback).toHaveBeenCalledTimes(0);
// Simulate time moving forward while frame is paused.
@@ -854,7 +854,7 @@ describe('Profiler', () => {
// Interrupt with higher priority work.
// This simulates a total of 37ms of actual render time.
renderer.unstable_flushSync(() => second.setState({renderTime: 30}));
expect(ReactTestRenderer).toClearYields([
expect(ReactTestRenderer).toHaveYielded([
'SecondComponent:30',
'Yield:7',
]);
@@ -881,7 +881,7 @@ describe('Profiler', () => {
// The tree contains 42ms of base render time at this point,
// Reflecting the most recent (longer) render durations.
// TODO: This actual time should decrease by 10ms once the scheduler supports resuming.
expect(renderer).toFlushAll(['FirstComponent:10', 'Yield:4']);
expect(renderer).toFlushAndYield(['FirstComponent:10', 'Yield:4']);
expect(callback).toHaveBeenCalledTimes(1);
call = callback.mock.calls[0];
expect(call[2]).toBe(14); // actual time
@@ -1305,7 +1305,7 @@ describe('Profiler', () => {
renderer = ReactTestRenderer.create(<Component>succeed</Component>, {
unstable_isConcurrent: true,
});
expect(renderer).toFlushAll(['Component:succeed']);
expect(renderer).toFlushAndYield(['Component:succeed']);
const tree = renderer.toTree();
expect(tree.type).toBe(Component);
expect(tree.props.children).toBe('succeed');
@@ -1327,14 +1327,13 @@ describe('Profiler', () => {
// Errors that happen inside of a subscriber should throw,
throwInOnWorkStarted = true;
expect(() => {
expect(renderer).toFlushAll(['Component:text']);
}).toThrow('Expected error onWorkStarted');
expect(renderer).toFlushAndThrow('Expected error onWorkStarted');
expect(ReactTestRenderer).toHaveYielded(['Component:text']);
throwInOnWorkStarted = false;
expect(onWorkStarted).toHaveBeenCalled();
// But the React work should have still been processed.
expect(renderer).toFlushAll([]);
expect(renderer).toFlushAndYield([]);
const tree = renderer.toTree();
expect(tree.type).toBe(Component);
expect(tree.props.children).toBe('text');
@@ -1565,7 +1564,7 @@ describe('Profiler', () => {
]);
expect(onWorkScheduled.mock.calls[0][1] > 0).toBe(true);
expect(renderer).toFlushThrough(['first']);
expect(renderer).toFlushAndYieldThrough(['first']);
expect(onRender).not.toHaveBeenCalled();
expect(onInteractionTraced).toHaveBeenCalledTimes(2);
@@ -1750,7 +1749,7 @@ describe('Profiler', () => {
interactionLowPri,
]);
expect(renderer).toFlushThrough(['FirstComponent']);
expect(renderer).toFlushAndYieldThrough(['FirstComponent']);
expect(onRender).not.toHaveBeenCalled();
expect(onInteractionTraced).toHaveBeenCalledTimes(1);
@@ -1794,7 +1793,7 @@ describe('Profiler', () => {
},
);
});
expect(ReactTestRenderer).toClearYields(['SecondComponent']);
expect(ReactTestRenderer).toHaveYielded(['SecondComponent']);
expect(onInteractionTraced).toHaveBeenCalledTimes(2);
expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
@@ -2421,13 +2420,17 @@ describe('Profiler', () => {
advanceTimeBy(1500);
await awaitableAdvanceTimers(1500);
expect(renderer).toFlushAll(['Suspend [loaded]', 'Text [loading]']);
expect(renderer).toFlushAndYield([
'Suspend [loaded]',
'Text [loading]',
]);
expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
advanceTimeBy(2500);
await awaitableAdvanceTimers(2500);
expect(renderer).toFlushAll(['AsyncText [loaded]']);
expect(ReactTestRenderer).toHaveYielded(['Promise resolved [loaded]']);
expect(renderer).toFlushAndYield(['AsyncText [loaded]']);
expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
expect(
onInteractionScheduledWorkCompleted,
@@ -2590,7 +2593,7 @@ describe('Profiler', () => {
);
},
);
expect(renderer).toFlushAll([
expect(renderer).toFlushAndYield([
'Suspend [loaded]',
'Text [loading]',
'Text [initial]',
@@ -2644,7 +2647,13 @@ describe('Profiler', () => {
advanceTimeBy(500);
jest.advanceTimersByTime(500);
await originalPromise;
expect(renderer).toFlushAll(['AsyncText [loaded]']);
expect(ReactTestRenderer).toHaveYielded([
'Suspend [loaded]',
'Text [loading]',
'Text [updated]',
'Promise resolved [loaded]',
]);
expect(renderer).toFlushAndYield(['AsyncText [loaded]']);
expect(renderer.toJSON()).toEqual(['loaded', 'updated']);
expect(onRender).toHaveBeenCalledTimes(1);
-57
View File
@@ -1,57 +0,0 @@
'use strict';
function captureAssertion(fn) {
// Trick to use a Jest matcher inside another Jest matcher. `fn` contains an
// assertion; if it throws, we capture the error and return it, so the stack
// trace presented to the user points to the original assertion in the
// test file.
try {
fn();
} catch (error) {
return {
pass: false,
message: () => error.message,
};
}
return {pass: true};
}
function toFlushAll(renderer, expectedYields) {
const actualYields = renderer.unstable_flushAll();
return captureAssertion(() => expect(actualYields).toEqual(expectedYields));
}
function toFlushThrough(renderer, expectedYields) {
const actualYields = renderer.unstable_flushNumberOfYields(
expectedYields.length
);
return captureAssertion(() => expect(actualYields).toEqual(expectedYields));
}
function toClearYields(ReactTestRenderer, expectedYields) {
const actualYields = ReactTestRenderer.unstable_clearYields();
return captureAssertion(() => expect(actualYields).toEqual(expectedYields));
}
function toFlushAndThrow(renderer, expectedYields, ...rest) {
return captureAssertion(() => {
try {
expect(() => {
renderer.unstable_flushAll();
}).toThrow(...rest);
} catch (error) {
const actualYields = renderer.unstable_clearYields();
expect(actualYields).toEqual(expectedYields);
throw error;
}
const actualYields = renderer.unstable_clearYields();
expect(actualYields).toEqual(expectedYields);
});
}
module.exports = {
toFlushAll,
toFlushThrough,
toFlushAndThrow,
toClearYields,
};
+8 -1
View File
@@ -43,10 +43,17 @@ if (process.env.REACT_CLASS_EQUIVALENCE_TEST) {
global.spyOnDevAndProd = spyOn;
}
const JestReact = require('jest-react');
expect.extend({
...require('./matchers/interactionTracing'),
...require('./matchers/toWarnDev'),
...require('./matchers/testRenderer'),
toFlushWithoutYielding: JestReact.toFlushWithoutYielding,
toFlushAndYield: JestReact.toFlushAndYield,
toFlushAndYieldThrough: JestReact.toFlushAndYieldThrough,
toHaveYielded: JestReact.toHaveYielded,
toFlushAndThrow: JestReact.toFlushAndThrow,
toMatchRenderedOutput: JestReact.toMatchRenderedOutput,
});
// We have a Babel transform that inserts guards against infinite loops.
@@ -45,10 +45,17 @@ global.spyOnProd = function(...args) {
}
};
const JestReact = require('jest-react');
expect.extend({
...require('../matchers/interactionTracing'),
...require('../matchers/toWarnDev'),
...require('../matchers/testRenderer'),
toFlushWithoutYielding: JestReact.toFlushWithoutYielding,
toFlushAndYield: JestReact.toFlushAndYield,
toFlushAndYieldThrough: JestReact.toFlushAndYieldThrough,
toHaveYielded: JestReact.toHaveYielded,
toFlushAndThrow: JestReact.toFlushAndThrow,
toMatchRenderedOutput: JestReact.toMatchRenderedOutput,
});
beforeEach(() => (numExpectations = 0));
+10
View File
@@ -400,6 +400,16 @@ const bundles = [
externals: [],
},
/******* Jest React (experimental) *******/
{
label: 'jest-react',
bundleTypes: [NODE_DEV, NODE_PROD, FB_WWW_DEV, FB_WWW_PROD],
moduleType: ISOMORPHIC,
entry: 'jest-react',
global: 'JestReact',
externals: ['jest-diff'],
},
{
label: 'scheduler-tracing',
bundleTypes: [
+4 -1
View File
@@ -1,4 +1,7 @@
# Don't validate noop renderer bundle.
# Unlike others, it currently uses ES6 syntax (generators).
# We also don't use or publish it.
**/react-noop-renderer/**
**/react-noop-renderer/**
JestReact-prod.js
JestReact-dev.js
**/jest-react/**