/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
* @jest-environment node
*/
'use strict';
let React;
let ReactNoop;
let Scheduler;
let waitFor;
let waitForAll;
describe('ReactIncrementalReflection', () => {
beforeEach(() => {
jest.resetModules();
React = require('react');
ReactNoop = require('react-noop-renderer');
Scheduler = require('scheduler');
const InternalTestUtils = require('internal-test-utils');
waitFor = InternalTestUtils.waitFor;
waitForAll = InternalTestUtils.waitForAll;
});
function div(...children) {
children = children.map(c =>
typeof c === 'string' ? {text: c, hidden: false} : c,
);
return {type: 'div', children, prop: undefined, hidden: false};
}
function span(prop) {
return {type: 'span', children: [], prop, hidden: false};
}
it('handles isMounted even when the initial render is deferred', async () => {
const instances = [];
class Component extends React.Component {
_isMounted() {
// No longer a public API, but we can test that it works internally by
// reaching into the updater.
return this.updater.isMounted(this);
}
UNSAFE_componentWillMount() {
instances.push(this);
Scheduler.log('componentWillMount: ' + this._isMounted());
}
componentDidMount() {
Scheduler.log('componentDidMount: ' + this._isMounted());
}
render() {
return <span />;
}
}
function Foo() {
return <Component />;
}
React.startTransition(() => {
ReactNoop.render(<Foo />);
});
// Render part way through but don't yet commit the updates.
await waitFor(['componentWillMount: false']);
expect(instances[0]._isMounted()).toBe(false);
// Render the rest and commit the updates.
await waitForAll(['componentDidMount: true']);
expect(instances[0]._isMounted()).toBe(true);
});
it('handles isMounted when an unmount is deferred', async () => {
const instances = [];
class Component extends React.Component {
_isMounted() {
return this.updater.isMounted(this);
}
UNSAFE_componentWillMount() {
instances.push(this);
}
componentWillUnmount() {
Scheduler.log('componentWillUnmount: ' + this._isMounted());
}
render() {
Scheduler.log('Component');
return <span />;
}
}
function Other() {
Scheduler.log('Other');
return <span />;
}
function Foo(props) {
return props.mount ? <Component /> : <Other />;
}
ReactNoop.render(<Foo mount={true} />);
await waitForAll(['Component']);
expect(instances[0]._isMounted()).toBe(true);
React.startTransition(() => {
ReactNoop.render(<Foo mount={false} />);
});
// Render part way through but don't yet commit the updates so it is not
// fully unmounted yet.
await waitFor(['Other']);
expect(instances[0]._isMounted()).toBe(true);
// Finish flushing the unmount.
await waitForAll(['componentWillUnmount: true']);
expect(instances[0]._isMounted()).toBe(false);
});
it('finds no node before insertion and correct node before deletion', async () => {
let classInstance = null;
function findInstance(inst) {
// We ignore warnings fired by findInstance because we are testing
// that the actual behavior still works as expected even though it
// is deprecated.
const oldConsoleError = console.error;
console.error = jest.fn();
try {
return ReactNoop.findInstance(inst);
} finally {
console.error = oldConsoleError;
}
}
class Component extends React.Component {
UNSAFE_componentWillMount() {
classInstance = this;
Scheduler.log(['componentWillMount', findInstance(this)]);
}
componentDidMount() {
Scheduler.log(['componentDidMount', findInstance(this)]);
}
UNSAFE_componentWillUpdate() {
Scheduler.log(['componentWillUpdate', findInstance(this)]);
}
componentDidUpdate() {
Scheduler.log(['componentDidUpdate', findInstance(this)]);
}
componentWillUnmount() {
Scheduler.log(['componentWillUnmount', findInstance(this)]);
}
render() {
Scheduler.log('render');
return this.props.step < 2 ? (
<span ref={ref => (this.span = ref)} />
) : this.props.step === 2 ? (
<div ref={ref => (this.div = ref)} />
) : this.props.step === 3 ? null : this.props.step === 4 ? (
<div ref={ref => (this.span = ref)} />
) : null;
}
}
function Sibling() {
// Sibling is used to assert that we've rendered past the first component.
Scheduler.log('render sibling');
return <span />;
}
function Foo(props) {
return [<Component key="a" step={props.step} />, <Sibling key="b" />];
}
React.startTransition(() => {
ReactNoop.render(<Foo step={0} />);
});
// Flush past Component but don't complete rendering everything yet.
await waitFor([['componentWillMount', null], 'render', 'render sibling']);
expect(classInstance).toBeDefined();
// The instance has been complete but is still not committed so it should
// not find any host nodes in it.
expect(findInstance(classInstance)).toBe(null);
await waitForAll([['componentDidMount', span()]]);
const hostSpan = classInstance.span;
expect(hostSpan).toBeDefined();
expect(findInstance(classInstance)).toBe(hostSpan);
// Flush next step which will cause an update but not yet render a new host
// node.
ReactNoop.render(<Foo step={1} />);
await waitForAll([
['componentWillUpdate', hostSpan],
'render',
'render sibling',
['componentDidUpdate', hostSpan],
]);
expect(ReactNoop.findInstance(classInstance)).toBe(hostSpan);
// The next step will render a new host node but won't get committed yet.
// We expect this to mutate the original Fiber.
React.startTransition(() => {
ReactNoop.render(<Foo step={2} />);
});
await waitFor([
['componentWillUpdate', hostSpan],
'render',
'render sibling',
]);
// This should still be the host span.
expect(ReactNoop.findInstance(classInstance)).toBe(hostSpan);
// When we finally flush the tree it will get committed.
await waitForAll([['componentDidUpdate', div()]]);
const hostDiv = classInstance.div;
expect(hostDiv).toBeDefined();
expect(hostSpan).not.toBe(hostDiv);
// We should now find the new host node.
expect(ReactNoop.findInstance(classInstance)).toBe(hostDiv);
// Render to null but don't commit it yet.
React.startTransition(() => {
ReactNoop.render(<Foo step={3} />);
});
await waitFor([
['componentWillUpdate', hostDiv],
'render',
'render sibling',
]);
// This should still be the host div since the deletion is not committed.
expect(ReactNoop.findInstance(classInstance)).toBe(hostDiv);
await waitForAll([['componentDidUpdate', null]]);
// This should still be the host div since the deletion is not committed.
expect(ReactNoop.findInstance(classInstance)).toBe(null);
// Render a div again
ReactNoop.render(<Foo step={4} />);
await waitForAll([
['componentWillUpdate', null],
'render',
'render sibling',
['componentDidUpdate', div()],
]);
// Unmount the component.
ReactNoop.render([]);
await waitForAll([['componentWillUnmount', hostDiv]]);
});
});