mirror of
https://github.com/hanzoai/react.git
synced 2026-09-24 17:02:27 +00:00
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:
@@ -0,0 +1,3 @@
|
||||
# `jest-react`
|
||||
|
||||
Jest matchers and utilities for testing React Test Renderer.
|
||||
@@ -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';
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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/"
|
||||
]
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
+3
-22
@@ -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', () => {
|
||||
|
||||
+193
@@ -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 {
|
||||
|
||||
+49
-36
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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));
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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/**
|
||||
Reference in New Issue
Block a user