main
js 183 lines 5.35 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @emails react-core
8 * @jest-environment node
9 */
10
11 'use strict';
12
13 let React;
14 let ReactNoop;
15 let Scheduler;
16 let waitFor;
17 let waitForAll;
18
19 describe('ReactIncrementalReflection', () => {
20 beforeEach(() => {
21 jest.resetModules();
22
23 React = require('react');
24 ReactNoop = require('react-noop-renderer');
25 Scheduler = require('scheduler');
26
27 const InternalTestUtils = require('internal-test-utils');
28 waitFor = InternalTestUtils.waitFor;
29 waitForAll = InternalTestUtils.waitForAll;
30 });
31
32 function div(...children) {
33 children = children.map(c =>
34 typeof c === 'string' ? {text: c, hidden: false} : c,
35 );
36 return {type: 'div', children, prop: undefined, hidden: false};
37 }
38
39 function span(prop) {
40 return {type: 'span', children: [], prop, hidden: false};
41 }
42
43 it('finds no node before insertion and correct node before deletion', async () => {
44 let classInstance = null;
45
46 function findInstance(inst) {
47 // We ignore warnings fired by findInstance because we are testing
48 // that the actual behavior still works as expected even though it
49 // is deprecated.
50 const oldConsoleError = console.error;
51 console.error = jest.fn();
52 try {
53 return ReactNoop.findInstance(inst);
54 } finally {
55 console.error = oldConsoleError;
56 }
57 }
58
59 class Component extends React.Component {
60 UNSAFE_componentWillMount() {
61 classInstance = this;
62 Scheduler.log(['componentWillMount', findInstance(this)]);
63 }
64 componentDidMount() {
65 Scheduler.log(['componentDidMount', findInstance(this)]);
66 }
67 UNSAFE_componentWillUpdate() {
68 Scheduler.log(['componentWillUpdate', findInstance(this)]);
69 }
70 componentDidUpdate() {
71 Scheduler.log(['componentDidUpdate', findInstance(this)]);
72 }
73 componentWillUnmount() {
74 Scheduler.log(['componentWillUnmount', findInstance(this)]);
75 }
76 render() {
77 Scheduler.log('render');
78 return this.props.step < 2 ? (
79 <span ref={ref => (this.span = ref)} />
80 ) : this.props.step === 2 ? (
81 <div ref={ref => (this.div = ref)} />
82 ) : this.props.step === 3 ? null : this.props.step === 4 ? (
83 <div ref={ref => (this.span = ref)} />
84 ) : null;
85 }
86 }
87
88 function Sibling() {
89 // Sibling is used to assert that we've rendered past the first component.
90 Scheduler.log('render sibling');
91 return <span />;
92 }
93
94 function Foo(props) {
95 return [<Component key="a" step={props.step} />, <Sibling key="b" />];
96 }
97
98 React.startTransition(() => {
99 ReactNoop.render(<Foo step={0} />);
100 });
101 // Flush past Component but don't complete rendering everything yet.
102 await waitFor([['componentWillMount', null], 'render', 'render sibling']);
103
104 expect(classInstance).toBeDefined();
105 // The instance has been complete but is still not committed so it should
106 // not find any host nodes in it.
107 expect(findInstance(classInstance)).toBe(null);
108
109 await waitForAll([['componentDidMount', span()]]);
110
111 const hostSpan = classInstance.span;
112 expect(hostSpan).toBeDefined();
113
114 expect(findInstance(classInstance)).toBe(hostSpan);
115
116 // Flush next step which will cause an update but not yet render a new host
117 // node.
118 ReactNoop.render(<Foo step={1} />);
119 await waitForAll([
120 ['componentWillUpdate', hostSpan],
121 'render',
122 'render sibling',
123 ['componentDidUpdate', hostSpan],
124 ]);
125
126 expect(ReactNoop.findInstance(classInstance)).toBe(hostSpan);
127
128 // The next step will render a new host node but won't get committed yet.
129 // We expect this to mutate the original Fiber.
130 React.startTransition(() => {
131 ReactNoop.render(<Foo step={2} />);
132 });
133 await waitFor([
134 ['componentWillUpdate', hostSpan],
135 'render',
136 'render sibling',
137 ]);
138
139 // This should still be the host span.
140 expect(ReactNoop.findInstance(classInstance)).toBe(hostSpan);
141
142 // When we finally flush the tree it will get committed.
143 await waitForAll([['componentDidUpdate', div()]]);
144
145 const hostDiv = classInstance.div;
146 expect(hostDiv).toBeDefined();
147 expect(hostSpan).not.toBe(hostDiv);
148
149 // We should now find the new host node.
150 expect(ReactNoop.findInstance(classInstance)).toBe(hostDiv);
151
152 // Render to null but don't commit it yet.
153 React.startTransition(() => {
154 ReactNoop.render(<Foo step={3} />);
155 });
156 await waitFor([
157 ['componentWillUpdate', hostDiv],
158 'render',
159 'render sibling',
160 ]);
161
162 // This should still be the host div since the deletion is not committed.
163 expect(ReactNoop.findInstance(classInstance)).toBe(hostDiv);
164
165 await waitForAll([['componentDidUpdate', null]]);
166
167 // This should still be the host div since the deletion is not committed.
168 expect(ReactNoop.findInstance(classInstance)).toBe(null);
169
170 // Render a div again
171 ReactNoop.render(<Foo step={4} />);
172 await waitForAll([
173 ['componentWillUpdate', null],
174 'render',
175 'render sibling',
176 ['componentDidUpdate', div()],
177 ]);
178
179 // Unmount the component.
180 ReactNoop.render([]);
181 await waitForAll([['componentWillUnmount', hostDiv]]);
182 });
183 });