Use concurrent root in RTR (#28498)

Based on
- https://github.com/facebook/react/pull/28497
- https://github.com/facebook/react/pull/28419

Reusing the disableLegacyMode flag, we set ReactTestRenderer to always
render with concurrent root where legacy APIs are no longer available.
If disableLegacyMode is false, we continue to allow the
unstable_isConcurrent option determine the root type.

Also checking a global `IS_REACT_NATIVE_TEST_ENVIRONMENT` so we can
maintain the existing behavior for RN until we remove legacy root
support there.
This commit is contained in:
Jack Pope
2024-03-26 18:53:09 -04:00
committed by GitHub
parent 1f9befef5c
commit bb66aa3cef
8 changed files with 536 additions and 274 deletions

View File

@@ -46,45 +46,40 @@ describe('ErrorBoundaryReconciliation', () => {
fail ? <InvalidType /> : <span prop="BrokenRender" />;
});
[true, false].forEach(isConcurrent => {
async function sharedTest(ErrorBoundary, fallbackTagName) {
let renderer;
async function sharedTest(ErrorBoundary, fallbackTagName) {
let renderer;
await act(() => {
renderer = ReactTestRenderer.create(
<ErrorBoundary fallbackTagName={fallbackTagName}>
<BrokenRender fail={false} />
</ErrorBoundary>,
{unstable_isConcurrent: true},
);
});
expect(renderer).toMatchRenderedOutput(<span prop="BrokenRender" />);
await expect(async () => {
await act(() => {
renderer = ReactTestRenderer.create(
renderer.update(
<ErrorBoundary fallbackTagName={fallbackTagName}>
<BrokenRender fail={false} />
<BrokenRender fail={true} />
</ErrorBoundary>,
{unstable_isConcurrent: isConcurrent},
);
});
expect(renderer).toMatchRenderedOutput(<span prop="BrokenRender" />);
}).toErrorDev(['invalid', 'invalid']);
const Fallback = fallbackTagName;
expect(renderer).toMatchRenderedOutput(<Fallback prop="ErrorBoundary" />);
}
await expect(async () => {
await act(() => {
renderer.update(
<ErrorBoundary fallbackTagName={fallbackTagName}>
<BrokenRender fail={true} />
</ErrorBoundary>,
);
});
}).toErrorDev(isConcurrent ? ['invalid', 'invalid'] : ['invalid']);
const Fallback = fallbackTagName;
expect(renderer).toMatchRenderedOutput(<Fallback prop="ErrorBoundary" />);
}
it('componentDidCatch can recover by rendering an element of the same type', () =>
sharedTest(DidCatchErrorBoundary, 'span'));
describe(isConcurrent ? 'concurrent' : 'sync', () => {
it('componentDidCatch can recover by rendering an element of the same type', () =>
sharedTest(DidCatchErrorBoundary, 'span'));
it('componentDidCatch can recover by rendering an element of a different type', () =>
sharedTest(DidCatchErrorBoundary, 'div'));
it('componentDidCatch can recover by rendering an element of a different type', () =>
sharedTest(DidCatchErrorBoundary, 'div'));
it('getDerivedStateFromError can recover by rendering an element of the same type', () =>
sharedTest(GetDerivedErrorBoundary, 'span'));
it('getDerivedStateFromError can recover by rendering an element of the same type', () =>
sharedTest(GetDerivedErrorBoundary, 'span'));
it('getDerivedStateFromError can recover by rendering an element of a different type', () =>
sharedTest(GetDerivedErrorBoundary, 'div'));
});
});
it('getDerivedStateFromError can recover by rendering an element of a different type', () =>
sharedTest(GetDerivedErrorBoundary, 'div'));
});

View File

@@ -1451,6 +1451,7 @@ describe('ReactLazy', () => {
});
describe('legacy mode', () => {
// @gate !disableLegacyMode
it('mount and reorder lazy elements (legacy mode)', async () => {
class Child extends React.Component {
componentDidMount() {
@@ -1520,6 +1521,7 @@ describe('ReactLazy', () => {
expect(root).toMatchRenderedOutput('ba');
});
// @gate !disableLegacyMode
it('mount and reorder lazy types (legacy mode)', async () => {
class Child extends React.Component {
componentDidMount() {

View File

@@ -55,6 +55,7 @@ import {ConcurrentRoot, LegacyRoot} from 'react-reconciler/src/ReactRootTags';
import {
allowConcurrentByDefault,
enableReactTestRendererWarning,
disableLegacyMode,
} from 'shared/ReactFeatureFlags';
const act = React.act;
@@ -485,7 +486,10 @@ function create(
}
let createNodeMock = defaultTestOptions.createNodeMock;
let isConcurrent = false;
const isConcurrentOnly =
disableLegacyMode === true &&
global.IS_REACT_NATIVE_TEST_ENVIRONMENT !== true;
let isConcurrent = isConcurrentOnly;
let isStrictMode = false;
let concurrentUpdatesByDefault = null;
if (typeof options === 'object' && options !== null) {
@@ -493,8 +497,8 @@ function create(
// $FlowFixMe[incompatible-type] found when upgrading Flow
createNodeMock = options.createNodeMock;
}
if (options.unstable_isConcurrent === true) {
isConcurrent = true;
if (isConcurrentOnly === false) {
isConcurrent = options.unstable_isConcurrent;
}
if (options.unstable_strictMode === true) {
isStrictMode = true;

View File

@@ -14,14 +14,19 @@ const ReactFeatureFlags = require('shared/ReactFeatureFlags');
const React = require('react');
const ReactTestRenderer = require('react-test-renderer');
const {format: prettyFormat} = require('pretty-format');
const InternalTestUtils = require('internal-test-utils');
const waitForAll = InternalTestUtils.waitForAll;
const act = InternalTestUtils.act;
const Reconciler = require('react-reconciler/src/ReactFiberReconciler');
const {
ConcurrentRoot,
LegacyRoot,
} = require('react-reconciler/src/ReactRootTags');
// Isolate noop renderer
jest.resetModules();
const ReactNoop = require('react-noop-renderer');
const InternalTestUtils = require('internal-test-utils');
const waitForAll = InternalTestUtils.waitForAll;
// Kind of hacky, but we nullify all the instances to test the tree structure
// with jasmine's deep equality function, and test the instances separate. We
// also delete children props because testing them is more annoying and not
@@ -75,14 +80,63 @@ describe('ReactTestRenderer', () => {
'Warning: react-test-renderer is deprecated. See https://react.dev/warnings/react-test-renderer',
{withoutStack: true},
);
global.IS_REACT_NATIVE_TEST_ENVIRONMENT = false;
});
it('renders a simple component', () => {
describe('root tags', () => {
let createContainerSpy;
beforeEach(() => {
global.IS_REACT_NATIVE_TEST_ENVIRONMENT = false;
createContainerSpy = jest.spyOn(Reconciler, 'createContainer');
});
function expectTag(tag) {
expect(createContainerSpy).toHaveBeenCalledWith(
expect.anything(),
tag,
null,
expect.anything(),
null,
expect.anything(),
expect.anything(),
null,
);
}
// @gate disableLegacyMode
it('should render using concurrent root if disableLegacyMode', () => {
ReactTestRenderer.create(<div />);
expectTag(ConcurrentRoot);
});
// @gate !disableLegacyMode
it('should default to legacy root if not disableLegacyMode', () => {
ReactTestRenderer.create(<div />);
expectTag(LegacyRoot);
});
it('should allow unstable_isConcurrent if not disableLegacyMode', async () => {
ReactTestRenderer.create(<div />, {
unstable_isConcurrent: true,
});
ReactTestRenderer.create(<div />);
expectTag(ConcurrentRoot);
});
it('should render legacy root when RN test environment', async () => {
global.IS_REACT_NATIVE_TEST_ENVIRONMENT = true;
ReactTestRenderer.create(<div />);
expectTag(LegacyRoot);
});
});
it('renders a simple component', async () => {
function Link() {
return <a role="link" />;
}
const renderer = ReactTestRenderer.create(<Link />);
let renderer;
await act(() => {
renderer = ReactTestRenderer.create(<Link />);
});
expect(renderer.toJSON()).toEqual({
type: 'a',
props: {role: 'link'},
@@ -90,19 +144,25 @@ describe('ReactTestRenderer', () => {
});
});
it('renders a top-level empty component', () => {
it('renders a top-level empty component', async () => {
function Empty() {
return null;
}
const renderer = ReactTestRenderer.create(<Empty />);
let renderer;
await act(() => {
renderer = ReactTestRenderer.create(<Empty />);
});
expect(renderer.toJSON()).toEqual(null);
});
it('exposes a type flag', () => {
it('exposes a type flag', async () => {
function Link() {
return <a role="link" />;
}
const renderer = ReactTestRenderer.create(<Link />);
let renderer;
await act(() => {
renderer = ReactTestRenderer.create(<Link />);
});
const object = renderer.toJSON();
expect(object.$$typeof).toBe(Symbol.for('react.test.json'));
@@ -114,7 +174,7 @@ describe('ReactTestRenderer', () => {
}
});
it('can render a composite component', () => {
it('can render a composite component', async () => {
class Component extends React.Component {
render() {
return (
@@ -129,7 +189,10 @@ describe('ReactTestRenderer', () => {
return <moo />;
};
const renderer = ReactTestRenderer.create(<Component />);
let renderer;
await act(() => {
renderer = ReactTestRenderer.create(<Component />);
});
expect(renderer.toJSON()).toEqual({
type: 'div',
props: {className: 'purple'},
@@ -137,7 +200,7 @@ describe('ReactTestRenderer', () => {
});
});
it('renders some basics with an update', () => {
it('renders some basics with an update', async () => {
let renders = 0;
class Component extends React.Component {
@@ -169,7 +232,10 @@ describe('ReactTestRenderer', () => {
return null;
};
const renderer = ReactTestRenderer.create(<Component />);
let renderer;
await act(() => {
renderer = ReactTestRenderer.create(<Component />);
});
expect(renderer.toJSON()).toEqual({
type: 'div',
props: {className: 'purple'},
@@ -178,7 +244,7 @@ describe('ReactTestRenderer', () => {
expect(renders).toBe(6);
});
it('exposes the instance', () => {
it('exposes the instance', async () => {
class Mouse extends React.Component {
constructor() {
super();
@@ -191,7 +257,10 @@ describe('ReactTestRenderer', () => {
return <div>{this.state.mouse}</div>;
}
}
const renderer = ReactTestRenderer.create(<Mouse />);
let renderer;
await act(() => {
renderer = ReactTestRenderer.create(<Mouse />);
});
expect(renderer.toJSON()).toEqual({
type: 'div',
@@ -200,7 +269,9 @@ describe('ReactTestRenderer', () => {
});
const mouse = renderer.getInstance();
mouse.handleMoose();
await act(() => {
mouse.handleMoose();
});
expect(renderer.toJSON()).toEqual({
type: 'div',
children: ['moose'],
@@ -208,15 +279,20 @@ describe('ReactTestRenderer', () => {
});
});
it('updates types', () => {
const renderer = ReactTestRenderer.create(<div>mouse</div>);
it('updates types', async () => {
let renderer;
await act(() => {
renderer = ReactTestRenderer.create(<div>mouse</div>);
});
expect(renderer.toJSON()).toEqual({
type: 'div',
props: {},
children: ['mouse'],
});
renderer.update(<span>mice</span>);
await act(() => {
renderer.update(<span>mice</span>);
});
expect(renderer.toJSON()).toEqual({
type: 'span',
props: {},
@@ -224,14 +300,18 @@ describe('ReactTestRenderer', () => {
});
});
it('updates children', () => {
const renderer = ReactTestRenderer.create(
<div>
<span key="a">A</span>
<span key="b">B</span>
<span key="c">C</span>
</div>,
);
it('updates children', async () => {
let renderer;
await act(() => {
renderer = ReactTestRenderer.create(
<div>
<span key="a">A</span>
<span key="b">B</span>
<span key="c">C</span>
</div>,
);
});
expect(renderer.toJSON()).toEqual({
type: 'div',
props: {},
@@ -242,13 +322,15 @@ describe('ReactTestRenderer', () => {
],
});
renderer.update(
<div>
<span key="d">D</span>
<span key="c">C</span>
<span key="b">B</span>
</div>,
);
await act(() => {
renderer.update(
<div>
<span key="d">D</span>
<span key="c">C</span>
<span key="b">B</span>
</div>,
);
});
expect(renderer.toJSON()).toEqual({
type: 'div',
props: {},
@@ -260,7 +342,7 @@ describe('ReactTestRenderer', () => {
});
});
it('does the full lifecycle', () => {
it('does the full lifecycle', async () => {
const log = [];
class Log extends React.Component {
render() {
@@ -275,9 +357,16 @@ describe('ReactTestRenderer', () => {
}
}
const renderer = ReactTestRenderer.create(<Log key="foo" name="Foo" />);
renderer.update(<Log key="bar" name="Bar" />);
renderer.unmount();
let renderer;
await act(() => {
renderer = ReactTestRenderer.create(<Log key="foo" name="Foo" />);
});
await act(() => {
renderer.update(<Log key="bar" name="Bar" />);
});
await act(() => {
renderer.unmount();
});
expect(log).toEqual([
'render Foo',
@@ -289,14 +378,16 @@ describe('ReactTestRenderer', () => {
]);
});
it('gives a ref to native components', () => {
it('gives a ref to native components', async () => {
const log = [];
ReactTestRenderer.create(<div ref={r => log.push(r)} />);
await act(() => {
ReactTestRenderer.create(<div ref={r => log.push(r)} />);
});
expect(log).toEqual([null]);
});
// @gate !enableRefAsProp || !__DEV__
it('warns correctly for refs on SFCs', () => {
it('warns correctly for refs on SFCs', async () => {
function Bar() {
return <div>Hello, world</div>;
}
@@ -312,8 +403,14 @@ describe('ReactTestRenderer', () => {
return <div ref={this.bazRef} />;
}
}
ReactTestRenderer.create(<Baz />);
expect(() => ReactTestRenderer.create(<Foo />)).toErrorDev(
await act(() => {
ReactTestRenderer.create(<Baz />);
});
await expect(async () => {
await act(() => {
ReactTestRenderer.create(<Foo />);
});
}).toErrorDev(
'Warning: Function components cannot be given refs. Attempts ' +
'to access this ref will fail. ' +
'Did you mean to use React.forwardRef()?\n' +
@@ -322,7 +419,7 @@ describe('ReactTestRenderer', () => {
);
});
it('allows an optional createNodeMock function', () => {
it('allows an optional createNodeMock function', async () => {
const mockDivInstance = {appendChild: () => {}};
const mockInputInstance = {focus: () => {}};
const mockListItemInstance = {click: () => {}};
@@ -351,27 +448,41 @@ describe('ReactTestRenderer', () => {
return {};
}
}
ReactTestRenderer.create(<div ref={r => log.push(r)} />, {createNodeMock});
ReactTestRenderer.create(<input ref={r => log.push(r)} />, {
createNodeMock,
await act(() => {
ReactTestRenderer.create(<div ref={r => log.push(r)} />, {
createNodeMock,
});
});
await act(() => {
ReactTestRenderer.create(<input ref={r => log.push(r)} />, {
createNodeMock,
});
});
await act(() => {
ReactTestRenderer.create(
<div>
<span>
<ul>
<li ref={r => log.push(r)} />
</ul>
<ul>
<li ref={r => log.push(r)} />
<li ref={r => log.push(r)} />
</ul>
</span>
</div>,
{createNodeMock, foobar: true},
);
});
await act(() => {
ReactTestRenderer.create(<Foo />, {createNodeMock});
});
await act(() => {
ReactTestRenderer.create(<div ref={r => log.push(r)} />);
});
await act(() => {
ReactTestRenderer.create(<div ref={r => log.push(r)} />, {});
});
ReactTestRenderer.create(
<div>
<span>
<ul>
<li ref={r => log.push(r)} />
</ul>
<ul>
<li ref={r => log.push(r)} />
<li ref={r => log.push(r)} />
</ul>
</span>
</div>,
{createNodeMock, foobar: true},
);
ReactTestRenderer.create(<Foo />, {createNodeMock});
ReactTestRenderer.create(<div ref={r => log.push(r)} />);
ReactTestRenderer.create(<div ref={r => log.push(r)} />, {});
expect(log).toEqual([
mockDivInstance,
mockInputInstance,
@@ -396,7 +507,7 @@ describe('ReactTestRenderer', () => {
expect(() => inst.unmount()).not.toThrow();
});
it('supports unmounting inner instances', () => {
it('supports unmounting inner instances', async () => {
let count = 0;
class Foo extends React.Component {
componentWillUnmount() {
@@ -406,19 +517,24 @@ describe('ReactTestRenderer', () => {
return <div />;
}
}
const inst = ReactTestRenderer.create(
<div>
<Foo />
</div>,
{
createNodeMock: () => 'foo',
},
);
expect(() => inst.unmount()).not.toThrow();
let inst;
await act(() => {
inst = ReactTestRenderer.create(
<div>
<Foo />
</div>,
{
createNodeMock: () => 'foo',
},
);
});
await act(() => {
inst.unmount();
});
expect(count).toEqual(1);
});
it('supports updates when using refs', () => {
it('supports updates when using refs', async () => {
const log = [];
const createNodeMock = element => {
log.push(element.type);
@@ -433,14 +549,19 @@ describe('ReactTestRenderer', () => {
);
}
}
const inst = ReactTestRenderer.create(<Foo useDiv={true} />, {
createNodeMock,
let inst;
await act(() => {
inst = ReactTestRenderer.create(<Foo useDiv={true} />, {
createNodeMock,
});
});
await act(() => {
inst.update(<Foo useDiv={false} />);
});
inst.update(<Foo useDiv={false} />);
expect(log).toEqual(['div', 'span']);
});
it('supports error boundaries', () => {
it('supports error boundaries', async () => {
const log = [];
class Angry extends React.Component {
render() {
@@ -489,13 +610,20 @@ describe('ReactTestRenderer', () => {
}
}
const renderer = ReactTestRenderer.create(<Boundary />);
let renderer;
await act(() => {
renderer = ReactTestRenderer.create(<Boundary />, {
unstable_isConcurrent: true,
});
});
expect(renderer.toJSON()).toEqual({
type: 'div',
props: {},
children: ['Happy Birthday!'],
});
expect(log).toEqual([
'Boundary render',
'Angry render',
'Boundary render',
'Angry render',
'Boundary componentDidMount',
@@ -504,42 +632,53 @@ describe('ReactTestRenderer', () => {
]);
});
it('can update text nodes', () => {
it('can update text nodes', async () => {
class Component extends React.Component {
render() {
return <div>{this.props.children}</div>;
}
}
const renderer = ReactTestRenderer.create(<Component>Hi</Component>);
let renderer;
await act(() => {
renderer = ReactTestRenderer.create(<Component>Hi</Component>);
});
expect(renderer.toJSON()).toEqual({
type: 'div',
children: ['Hi'],
props: {},
});
renderer.update(<Component>{['Hi', 'Bye']}</Component>);
await act(() => {
renderer.update(<Component>{['Hi', 'Bye']}</Component>);
});
expect(renderer.toJSON()).toEqual({
type: 'div',
children: ['Hi', 'Bye'],
props: {},
});
renderer.update(<Component>Bye</Component>);
await act(() => {
renderer.update(<Component>Bye</Component>);
});
expect(renderer.toJSON()).toEqual({
type: 'div',
children: ['Bye'],
props: {},
});
renderer.update(<Component>{42}</Component>);
await act(() => {
renderer.update(<Component>{42}</Component>);
});
expect(renderer.toJSON()).toEqual({
type: 'div',
children: ['42'],
props: {},
});
renderer.update(
<Component>
<div />
</Component>,
);
await act(() => {
renderer.update(
<Component>
<div />
</Component>,
);
});
expect(renderer.toJSON()).toEqual({
type: 'div',
children: [
@@ -553,10 +692,13 @@ describe('ReactTestRenderer', () => {
});
});
it('toTree() renders simple components returning host components', () => {
it('toTree() renders simple components returning host components', async () => {
const Qoo = () => <span className="Qoo">Hello World!</span>;
const renderer = ReactTestRenderer.create(<Qoo />);
let renderer;
await act(() => {
renderer = ReactTestRenderer.create(<Qoo />);
});
const tree = renderer.toTree();
cleanNodeOrArray(tree);
@@ -578,13 +720,16 @@ describe('ReactTestRenderer', () => {
);
});
it('toTree() handles nested Fragments', () => {
it('toTree() handles nested Fragments', async () => {
const Foo = () => (
<>
<>foo</>
</>
);
const renderer = ReactTestRenderer.create(<Foo />);
let renderer;
await act(() => {
renderer = ReactTestRenderer.create(<Foo />);
});
const tree = renderer.toTree();
cleanNodeOrArray(tree);
@@ -600,14 +745,17 @@ describe('ReactTestRenderer', () => {
);
});
it('toTree() handles null rendering components', () => {
it('toTree() handles null rendering components', async () => {
class Foo extends React.Component {
render() {
return null;
}
}
const renderer = ReactTestRenderer.create(<Foo />);
let renderer;
await act(() => {
renderer = ReactTestRenderer.create(<Foo />);
});
const tree = renderer.toTree();
expect(tree.instance).toBeInstanceOf(Foo);
@@ -623,15 +771,18 @@ describe('ReactTestRenderer', () => {
});
});
it('toTree() handles simple components that return arrays', () => {
it('toTree() handles simple components that return arrays', async () => {
const Foo = ({children}) => children;
const renderer = ReactTestRenderer.create(
<Foo>
<div>One</div>
<div>Two</div>
</Foo>,
);
let renderer;
await act(() => {
renderer = ReactTestRenderer.create(
<Foo>
<div>One</div>
<div>Two</div>
</Foo>,
);
});
const tree = renderer.toTree();
@@ -663,25 +814,28 @@ describe('ReactTestRenderer', () => {
);
});
it('toTree() handles complicated tree of arrays', () => {
it('toTree() handles complicated tree of arrays', async () => {
class Foo extends React.Component {
render() {
return this.props.children;
}
}
const renderer = ReactTestRenderer.create(
<div>
<Foo>
<div>One</div>
<div>Two</div>
let renderer;
await act(() => {
renderer = ReactTestRenderer.create(
<div>
<Foo>
<div>Three</div>
<div>One</div>
<div>Two</div>
<Foo>
<div>Three</div>
</Foo>
</Foo>
</Foo>
<div>Four</div>
</div>,
);
<div>Four</div>
</div>,
);
});
const tree = renderer.toTree();
@@ -741,19 +895,22 @@ describe('ReactTestRenderer', () => {
);
});
it('toTree() handles complicated tree of fragments', () => {
const renderer = ReactTestRenderer.create(
<>
it('toTree() handles complicated tree of fragments', async () => {
let renderer;
await act(() => {
renderer = ReactTestRenderer.create(
<>
<div>One</div>
<div>Two</div>
<>
<div>Three</div>
<div>One</div>
<div>Two</div>
<>
<div>Three</div>
</>
</>
</>
<div>Four</div>
</>,
);
<div>Four</div>
</>,
);
});
const tree = renderer.toTree();
@@ -793,18 +950,22 @@ describe('ReactTestRenderer', () => {
);
});
it('root instance and createNodeMock ref return the same value', () => {
it('root instance and createNodeMock ref return the same value', async () => {
const createNodeMock = ref => ({node: ref});
let refInst = null;
const renderer = ReactTestRenderer.create(
<div ref={ref => (refInst = ref)} />,
{createNodeMock},
);
let renderer;
await act(() => {
renderer = ReactTestRenderer.create(
<div ref={ref => (refInst = ref)} />,
{createNodeMock},
);
});
const root = renderer.getInstance();
expect(root).toEqual(refInst);
});
it('toTree() renders complicated trees of composites and hosts', () => {
it('toTree() renders complicated trees of composites and hosts', async () => {
// SFC returning host. no children props.
const Qoo = () => <span className="Qoo">Hello World!</span>;
@@ -835,7 +996,11 @@ describe('ReactTestRenderer', () => {
}
}
const renderer = ReactTestRenderer.create(<Bam />);
let renderer;
await act(() => {
renderer = ReactTestRenderer.create(<Bam />);
});
const tree = renderer.toTree();
// we test for the presence of instances before nulling them out
@@ -894,30 +1059,45 @@ describe('ReactTestRenderer', () => {
);
});
it('can update text nodes when rendered as root', () => {
const renderer = ReactTestRenderer.create(['Hello', 'world']);
it('can update text nodes when rendered as root', async () => {
let renderer;
await act(() => {
renderer = ReactTestRenderer.create(['Hello', 'world']);
});
expect(renderer.toJSON()).toEqual(['Hello', 'world']);
renderer.update(42);
await act(() => {
renderer.update(42);
});
expect(renderer.toJSON()).toEqual('42');
renderer.update([42, 'world']);
await act(() => {
renderer.update([42, 'world']);
});
expect(renderer.toJSON()).toEqual(['42', 'world']);
});
it('can render and update root fragments', () => {
it('can render and update root fragments', async () => {
const Component = props => props.children;
const renderer = ReactTestRenderer.create([
<Component key="a">Hi</Component>,
<Component key="b">Bye</Component>,
]);
let renderer;
await act(() => {
renderer = ReactTestRenderer.create([
<Component key="a">Hi</Component>,
<Component key="b">Bye</Component>,
]);
});
expect(renderer.toJSON()).toEqual(['Hi', 'Bye']);
renderer.update(<div />);
await act(() => {
renderer.update(<div />);
});
expect(renderer.toJSON()).toEqual({
type: 'div',
children: null,
props: {},
});
renderer.update([<div key="a">goodbye</div>, 'world']);
await act(() => {
renderer.update([<div key="a">goodbye</div>, 'world']);
});
expect(renderer.toJSON()).toEqual([
{
type: 'div',
@@ -928,7 +1108,7 @@ describe('ReactTestRenderer', () => {
]);
});
it('supports context providers and consumers', () => {
it('supports context providers and consumers', async () => {
const {Consumer, Provider} = React.createContext('a');
function Child(props) {
@@ -943,7 +1123,10 @@ describe('ReactTestRenderer', () => {
);
}
const renderer = ReactTestRenderer.create(<App />);
let renderer;
await act(() => {
renderer = ReactTestRenderer.create(<App />);
});
const child = renderer.root.findByType(Child);
expect(child.children).toEqual(['b']);
expect(prettyFormat(renderer.toTree())).toEqual(
@@ -965,7 +1148,7 @@ describe('ReactTestRenderer', () => {
);
});
it('supports modes', () => {
it('supports modes', async () => {
function Child(props) {
return props.value;
}
@@ -978,7 +1161,10 @@ describe('ReactTestRenderer', () => {
);
}
const renderer = ReactTestRenderer.create(<App value="a" />);
let renderer;
await act(() => {
renderer = ReactTestRenderer.create(<App value="a" />);
});
const child = renderer.root.findByType(Child);
expect(child.children).toEqual(['a']);
expect(prettyFormat(renderer.toTree())).toEqual(
@@ -1002,7 +1188,7 @@ describe('ReactTestRenderer', () => {
);
});
it('supports forwardRef', () => {
it('supports forwardRef', async () => {
const InnerRefed = React.forwardRef((props, ref) => (
<div>
<span ref={ref} />
@@ -1020,7 +1206,10 @@ describe('ReactTestRenderer', () => {
}
}
const renderer = ReactTestRenderer.create(<App />);
let renderer;
await act(() => {
renderer = ReactTestRenderer.create(<App />);
});
const tree = renderer.toTree();
cleanNodeOrArray(tree);
@@ -1065,12 +1254,17 @@ describe('ReactTestRenderer', () => {
);
ReactNoop.render(<App />);
await waitForAll([]);
ReactTestRenderer.create(<App />);
await act(() => {
ReactTestRenderer.create(<App />);
});
});
it('calling findByType() with an invalid component will fall back to "Unknown" for component name', () => {
it('calling findByType() with an invalid component will fall back to "Unknown" for component name', async () => {
const App = () => null;
const renderer = ReactTestRenderer.create(<App />);
let renderer;
await act(() => {
renderer = ReactTestRenderer.create(<App />);
});
const NonComponent = {};
expect(() => {

View File

@@ -13,7 +13,7 @@ let ReactDOM;
let React;
let ReactCache;
let ReactTestRenderer;
let waitForAll;
let act;
describe('ReactTestRenderer', () => {
beforeEach(() => {
@@ -26,32 +26,28 @@ describe('ReactTestRenderer', () => {
ReactCache = require('react-cache');
ReactTestRenderer = require('react-test-renderer');
const InternalTestUtils = require('internal-test-utils');
waitForAll = InternalTestUtils.waitForAll;
act = InternalTestUtils.act;
});
it('should warn if used to render a ReactDOM portal', () => {
it('should warn if used to render a ReactDOM portal', async () => {
const container = document.createElement('div');
expect(() => {
let error;
try {
let error;
await expect(async () => {
await act(() => {
ReactTestRenderer.create(ReactDOM.createPortal('foo', container));
} catch (e) {
error = e;
}
// After the update throws, a subsequent render is scheduled to
// unmount the whole tree. This update also causes an error, so React
// throws an AggregateError.
const errors = error.errors;
expect(errors.length).toBe(2);
expect(errors[0].message.includes('indexOf is not a function')).toBe(
true,
);
expect(errors[1].message.includes('indexOf is not a function')).toBe(
true,
);
}).catch(e => (error = e));
}).toErrorDev('An invalid container has been provided.', {
withoutStack: true,
});
// After the update throws, a subsequent render is scheduled to
// unmount the whole tree. This update also causes an error, so React
// throws an AggregateError.
const errors = error.errors;
expect(errors.length).toBe(2);
expect(errors[0].message.includes('indexOf is not a function')).toBe(true);
expect(errors[1].message.includes('indexOf is not a function')).toBe(true);
});
describe('timed out Suspense hidden subtrees should not be observable via toJSON', () => {
@@ -84,16 +80,23 @@ describe('ReactTestRenderer', () => {
);
};
const root = ReactTestRenderer.create(<App text="initial" />);
PendingResources.initial('initial');
await waitForAll([]);
let root;
await act(() => {
root = ReactTestRenderer.create(<App text="initial" />);
});
await act(() => {
PendingResources.initial('initial');
});
expect(root.toJSON()).toEqual('initial');
root.update(<App text="dynamic" />);
await act(() => {
root.update(<App text="dynamic" />);
});
expect(root.toJSON()).toEqual('fallback');
PendingResources.dynamic('dynamic');
await waitForAll([]);
await act(() => {
PendingResources.dynamic('dynamic');
});
expect(root.toJSON()).toEqual('dynamic');
});
@@ -108,16 +111,23 @@ describe('ReactTestRenderer', () => {
);
};
const root = ReactTestRenderer.create(<App text="initial" />);
PendingResources.initial('initial');
await waitForAll([]);
let root;
await act(() => {
root = ReactTestRenderer.create(<App text="initial" />);
});
await act(() => {
PendingResources.initial('initial');
});
expect(root.toJSON().children).toEqual(['initial']);
root.update(<App text="dynamic" />);
await act(() => {
root.update(<App text="dynamic" />);
});
expect(root.toJSON().children).toEqual(['fallback']);
PendingResources.dynamic('dynamic');
await waitForAll([]);
await act(() => {
PendingResources.dynamic('dynamic');
});
expect(root.toJSON().children).toEqual(['dynamic']);
});
});

View File

@@ -16,6 +16,7 @@ describe('ReactTestRenderer.act()', () => {
const InternalTestUtils = require('internal-test-utils');
assertLog = InternalTestUtils.assertLog;
global.IS_REACT_ACT_ENVIRONMENT = true;
});
// @gate __DEV__
@@ -91,7 +92,10 @@ describe('ReactTestRenderer.act()', () => {
});
return step;
}
const root = ReactTestRenderer.create(null);
let root;
await act(() => {
root = ReactTestRenderer.create(null);
});
await act(async () => {
root.update(<App />);
});

View File

@@ -13,6 +13,7 @@
const React = require('react');
let ReactTestRenderer;
let Context;
let act;
const RCTView = 'RCTView';
const View = props => <RCTView {...props} />;
@@ -21,6 +22,7 @@ describe('ReactTestRendererTraversal', () => {
beforeEach(() => {
jest.resetModules();
ReactTestRenderer = require('react-test-renderer');
act = require('internal-test-utils').act;
Context = React.createContext(null);
});
@@ -68,8 +70,11 @@ describe('ReactTestRendererTraversal', () => {
<View {...props} ref={ref} />
));
it('initializes', () => {
const render = ReactTestRenderer.create(<Example />);
it('initializes', async () => {
let render;
await act(() => {
render = ReactTestRenderer.create(<Example />);
});
const hasFooProp = node => node.props.hasOwnProperty('foo');
// assert .props, .type and .parent attributes
@@ -80,8 +85,12 @@ describe('ReactTestRendererTraversal', () => {
expect(foo.children[0].parent).toBe(foo);
});
it('searches via .find() / .findAll()', () => {
const render = ReactTestRenderer.create(<Example />);
it('searches via .find() / .findAll()', async () => {
let render;
await act(() => {
render = ReactTestRenderer.create(<Example />);
});
const hasFooProp = node => node.props.hasOwnProperty('foo');
const hasBarProp = node => node.props.hasOwnProperty('bar');
const hasBazProp = node => node.props.hasOwnProperty('baz');
@@ -135,8 +144,11 @@ describe('ReactTestRendererTraversal', () => {
expect(itself.findAll(hasBazProp)).toHaveLength(2);
});
it('searches via .findByType() / .findAllByType()', () => {
const render = ReactTestRenderer.create(<Example />);
it('searches via .findByType() / .findAllByType()', async () => {
let render;
await act(() => {
render = ReactTestRenderer.create(<Example />);
});
expect(() => render.root.findByType(ExampleFn)).not.toThrow(); // 1 match
expect(() => render.root.findByType(View)).not.toThrow(); // 1 match
@@ -159,8 +171,11 @@ describe('ReactTestRendererTraversal', () => {
expect(fn[0].findAllByType(View)).toHaveLength(1);
});
it('searches via .findByProps() / .findAllByProps()', () => {
const render = ReactTestRenderer.create(<Example />);
it('searches via .findByProps() / .findAllByProps()', async () => {
let render;
await act(() => {
render = ReactTestRenderer.create(<Example />);
});
const foo = 'foo';
const bar = 'bar';
const baz = 'baz';
@@ -182,8 +197,11 @@ describe('ReactTestRendererTraversal', () => {
expect(render.root.findAllByProps({qux})).toHaveLength(3);
});
it('skips special nodes', () => {
const render = ReactTestRenderer.create(<Example />);
it('skips special nodes', async () => {
let render;
await act(() => {
render = ReactTestRenderer.create(<Example />);
});
expect(render.root.findAllByType(React.Fragment)).toHaveLength(0);
expect(render.root.findAllByType(Context.Consumer)).toHaveLength(0);
expect(render.root.findAllByType(Context.Provider)).toHaveLength(0);
@@ -200,47 +218,62 @@ describe('ReactTestRendererTraversal', () => {
expect(nestedViews[2].parent).toBe(expectedParent);
});
it('can have special nodes as roots', () => {
it('can have special nodes as roots', async () => {
const FR = React.forwardRef((props, ref) => <section {...props} />);
expect(
ReactTestRenderer.create(
let render1;
await act(() => {
render1 = ReactTestRenderer.create(
<FR>
<div />
<div />
</FR>,
).root.findAllByType('div').length,
).toBe(2);
expect(
ReactTestRenderer.create(
);
});
expect(render1.root.findAllByType('div').length).toBe(2);
let render2;
await act(() => {
render2 = ReactTestRenderer.create(
<>
<div />
<div />
</>,
).root.findAllByType('div').length,
).toBe(2);
expect(
ReactTestRenderer.create(
);
});
expect(render2.root.findAllByType('div').length).toBe(2);
let render3;
await act(() => {
render3 = ReactTestRenderer.create(
<React.Fragment key="foo">
<div />
<div />
</React.Fragment>,
).root.findAllByType('div').length,
).toBe(2);
expect(
ReactTestRenderer.create(
);
});
expect(render3.root.findAllByType('div').length).toBe(2);
let render4;
await act(() => {
render4 = ReactTestRenderer.create(
<React.StrictMode>
<div />
<div />
</React.StrictMode>,
).root.findAllByType('div').length,
).toBe(2);
expect(
ReactTestRenderer.create(
);
});
expect(render4.root.findAllByType('div').length).toBe(2);
let render5;
await act(() => {
render5 = ReactTestRenderer.create(
<Context.Provider value={null}>
<div />
<div />
</Context.Provider>,
).root.findAllByType('div').length,
).toBe(2);
);
});
expect(render5.root.findAllByType('div').length).toBe(2);
});
});

View File

@@ -19,6 +19,7 @@ describe('ReactProfiler DevTools integration', () => {
let hook;
let waitForAll;
let waitFor;
let act;
beforeEach(() => {
global.__REACT_DEVTOOLS_GLOBAL_HOOK__ = hook = {
@@ -39,6 +40,7 @@ describe('ReactProfiler DevTools integration', () => {
const InternalTestUtils = require('internal-test-utils');
waitForAll = InternalTestUtils.waitForAll;
waitFor = InternalTestUtils.waitFor;
act = InternalTestUtils.act;
AdvanceTime = class extends React.Component {
static defaultProps = {
@@ -56,7 +58,7 @@ describe('ReactProfiler DevTools integration', () => {
};
});
it('should auto-Profile all fibers if the DevTools hook is detected', () => {
it('should auto-Profile all fibers if the DevTools hook is detected', async () => {
const App = ({multiplier}) => {
Scheduler.unstable_advanceTime(2);
return (
@@ -71,7 +73,12 @@ describe('ReactProfiler DevTools integration', () => {
};
const onRender = jest.fn(() => {});
const rendered = ReactTestRenderer.create(<App multiplier={1} />);
let rendered;
await act(() => {
rendered = ReactTestRenderer.create(<App multiplier={1} />, {
unstable_isConcurrent: true,
});
});
expect(hook.onCommitFiberRoot).toHaveBeenCalledTimes(1);
@@ -90,7 +97,9 @@ describe('ReactProfiler DevTools integration', () => {
12,
);
rendered.update(<App multiplier={2} />);
await act(() => {
rendered.update(<App multiplier={2} />);
});
// Measure observable timing using the Profiler component.
// The time spent in App (above the Profiler) won't be included in the durations,
@@ -107,14 +116,18 @@ describe('ReactProfiler DevTools integration', () => {
);
});
it('should reset the fiber stack correctly after an error when profiling host roots', () => {
it('should reset the fiber stack correctly after an error when profiling host roots', async () => {
Scheduler.unstable_advanceTime(20);
const rendered = ReactTestRenderer.create(
<div>
<AdvanceTime byAmount={2} />
</div>,
);
let rendered;
await act(() => {
rendered = ReactTestRenderer.create(
<div>
<AdvanceTime byAmount={2} />
</div>,
{unstable_isConcurrent: true},
);
});
Scheduler.unstable_advanceTime(20);
@@ -122,22 +135,26 @@ describe('ReactProfiler DevTools integration', () => {
throw new Error('Oops!');
}
expect(() => {
rendered.update(
<Throws>
<AdvanceTime byAmount={3} />
</Throws>,
);
}).toThrow('Oops!');
await expect(async () => {
await act(() => {
rendered.update(
<Throws>
<AdvanceTime byAmount={3} />
</Throws>,
);
});
}).rejects.toThrow('Oops!');
Scheduler.unstable_advanceTime(20);
// But this should render correctly, if the profiler's fiber stack has been reset.
rendered.update(
<div>
<AdvanceTime byAmount={7} />
</div>,
);
await act(() => {
// But this should render correctly, if the profiler's fiber stack has been reset.
rendered.update(
<div>
<AdvanceTime byAmount={7} />
</div>,
);
});
// Measure unobservable timing required by the DevTools profiler.
// At this point, the base time should include only the most recent (not failed) render.
@@ -154,7 +171,10 @@ describe('ReactProfiler DevTools integration', () => {
return text;
}
const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true});
let root;
await act(() => {
root = ReactTestRenderer.create(null, {unstable_isConcurrent: true});
});
// Commit something
root.update(<Text text="A" />);