main
js 331 lines 8.73 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 */
9
10 'use strict';
11
12 let React;
13 let ReactDOMClient;
14 let act;
15 let assertConsoleErrorDev;
16
17 describe('ReactIdentity', () => {
18 beforeEach(() => {
19 jest.resetModules();
20 React = require('react');
21 ReactDOMClient = require('react-dom/client');
22 act = require('internal-test-utils').act;
23 assertConsoleErrorDev =
24 require('internal-test-utils').assertConsoleErrorDev;
25 });
26
27 it('should allow key property to express identity', async () => {
28 let node;
29 const Component = props => (
30 <div ref={c => (node = c)}>
31 <div key={props.swap ? 'banana' : 'apple'} />
32 <div key={props.swap ? 'apple' : 'banana'} />
33 </div>
34 );
35
36 const container = document.createElement('div');
37 const root = ReactDOMClient.createRoot(container);
38 await act(async () => {
39 root.render(<Component />);
40 });
41 const origChildren = Array.from(node.childNodes);
42 await act(async () => {
43 root.render(<Component swap={true} />);
44 });
45 const newChildren = Array.from(node.childNodes);
46 expect(origChildren[0]).toBe(newChildren[1]);
47 expect(origChildren[1]).toBe(newChildren[0]);
48 });
49
50 it('should use composite identity', async () => {
51 class Wrapper extends React.Component {
52 render() {
53 return <a>{this.props.children}</a>;
54 }
55 }
56
57 const container = document.createElement('div');
58 const root = ReactDOMClient.createRoot(container);
59 let node1;
60 let node2;
61 await act(async () => {
62 root.render(
63 <Wrapper key="wrap1">
64 <span ref={c => (node1 = c)} />
65 </Wrapper>,
66 );
67 });
68 await act(async () => {
69 root.render(
70 <Wrapper key="wrap2">
71 <span ref={c => (node2 = c)} />
72 </Wrapper>,
73 );
74 });
75 expect(node1).not.toBe(node2);
76 });
77
78 async function renderAComponentWithKeyIntoContainer(key, root) {
79 class Wrapper extends React.Component {
80 spanRef = React.createRef();
81 render() {
82 return (
83 <div>
84 <span ref={this.spanRef} key={key} />
85 </div>
86 );
87 }
88 }
89 const wrapperRef = React.createRef();
90 await act(async () => {
91 root.render(<Wrapper ref={wrapperRef} />);
92 });
93 const span = wrapperRef.current.spanRef.current;
94 expect(span).not.toBe(null);
95 }
96
97 it('should allow any character as a key, in a detached parent', async () => {
98 const detachedContainer = document.createElement('div');
99 const root = ReactDOMClient.createRoot(detachedContainer);
100 await renderAComponentWithKeyIntoContainer("<'WEIRD/&\\key'>", root);
101 });
102
103 it('should allow any character as a key, in an attached parent', async () => {
104 // This test exists to protect against implementation details that
105 // incorrectly query escaped IDs using DOM tools like getElementById.
106 const attachedContainer = document.createElement('div');
107 const root = ReactDOMClient.createRoot(attachedContainer);
108 document.body.appendChild(attachedContainer);
109
110 await renderAComponentWithKeyIntoContainer("<'WEIRD/&\\key'>", root);
111
112 document.body.removeChild(attachedContainer);
113 });
114
115 it('should not allow scripts in keys to execute', async () => {
116 const h4x0rKey =
117 '"><script>window[\'YOUVEBEENH4X0RED\']=true;</script><div id="';
118
119 const attachedContainer = document.createElement('div');
120 const root = ReactDOMClient.createRoot(attachedContainer);
121 document.body.appendChild(attachedContainer);
122
123 await renderAComponentWithKeyIntoContainer(h4x0rKey, root);
124
125 document.body.removeChild(attachedContainer);
126
127 // If we get this far, make sure we haven't executed the code
128 expect(window.YOUVEBEENH4X0RED).toBe(undefined);
129 });
130
131 it('should let restructured components retain their uniqueness', async () => {
132 const instance0 = <span />;
133 const instance1 = <span />;
134 const instance2 = <span />;
135
136 class TestComponent extends React.Component {
137 render() {
138 return (
139 <div>
140 {instance2}
141 {this.props.children[0]}
142 {this.props.children[1]}
143 </div>
144 );
145 }
146 }
147
148 class TestContainer extends React.Component {
149 render() {
150 return (
151 <TestComponent>
152 {instance0}
153 {instance1}
154 </TestComponent>
155 );
156 }
157 }
158
159 const container = document.createElement('div');
160 const root = ReactDOMClient.createRoot(container);
161 await expect(
162 act(() => {
163 root.render(<TestContainer />);
164 }),
165 ).resolves.not.toThrow();
166 });
167
168 it('should let nested restructures retain their uniqueness', async () => {
169 const instance0 = <span />;
170 const instance1 = <span />;
171 const instance2 = <span />;
172
173 class TestComponent extends React.Component {
174 render() {
175 return (
176 <div>
177 {instance2}
178 {this.props.children[0]}
179 {this.props.children[1]}
180 </div>
181 );
182 }
183 }
184
185 class TestContainer extends React.Component {
186 render() {
187 return (
188 <div>
189 <TestComponent>
190 {instance0}
191 {instance1}
192 </TestComponent>
193 </div>
194 );
195 }
196 }
197
198 const container = document.createElement('div');
199 const root = ReactDOMClient.createRoot(container);
200 await expect(
201 act(() => {
202 root.render(<TestContainer />);
203 }),
204 ).resolves.not.toThrow();
205 });
206
207 it('should let text nodes retain their uniqueness', async () => {
208 class TestComponent extends React.Component {
209 render() {
210 return (
211 <div>
212 {this.props.children}
213 <span />
214 </div>
215 );
216 }
217 }
218
219 class TestContainer extends React.Component {
220 render() {
221 return (
222 <TestComponent>
223 <div />
224 {'second'}
225 </TestComponent>
226 );
227 }
228 }
229
230 const container = document.createElement('div');
231 const root = ReactDOMClient.createRoot(container);
232 await expect(
233 act(() => {
234 root.render(<TestContainer />);
235 }),
236 ).resolves.not.toThrow();
237 });
238
239 it('should retain key during updates in composite components', async () => {
240 class TestComponent extends React.Component {
241 render() {
242 return <div>{this.props.children}</div>;
243 }
244 }
245
246 class TestContainer extends React.Component {
247 state = {swapped: false};
248
249 swap = () => {
250 this.setState({swapped: true});
251 };
252
253 render() {
254 return (
255 <TestComponent>
256 {this.state.swapped ? this.props.second : this.props.first}
257 {this.state.swapped ? this.props.first : this.props.second}
258 </TestComponent>
259 );
260 }
261 }
262
263 const instance0 = <span key="A" />;
264 const instance1 = <span key="B" />;
265
266 const container = document.createElement('div');
267 const root = ReactDOMClient.createRoot(container);
268 const wrappedRef = React.createRef();
269 await act(async () => {
270 root.render(
271 <TestContainer first={instance0} second={instance1} ref={wrappedRef} />,
272 );
273 });
274 const div = container.firstChild;
275
276 const beforeA = div.firstChild;
277 const beforeB = div.lastChild;
278 await act(async () => {
279 wrappedRef.current.swap();
280 });
281 const afterA = div.lastChild;
282 const afterB = div.firstChild;
283
284 expect(beforeA).toBe(afterA);
285 expect(beforeB).toBe(afterB);
286 });
287
288 it('should not allow implicit and explicit keys to collide', async () => {
289 const component = (
290 <div>
291 <span />
292 <span key="0" />
293 </div>
294 );
295
296 const container = document.createElement('div');
297 const root = ReactDOMClient.createRoot(container);
298 await expect(
299 act(() => {
300 root.render(component);
301 }),
302 ).resolves.not.toThrow();
303 });
304
305 it('should throw if key is a Temporal-like object', async () => {
306 class TemporalLike {
307 valueOf() {
308 // Throwing here is the behavior of ECMAScript "Temporal" date/time API.
309 // See https://tc39.es/proposal-temporal/docs/plaindate.html#valueOf
310 throw new TypeError('prod message');
311 }
312 toString() {
313 return '2020-01-01';
314 }
315 }
316
317 const el = document.createElement('div');
318 const root = ReactDOMClient.createRoot(el);
319 await expect(() => {
320 root.render(
321 <div>
322 <span key={new TemporalLike()} />
323 </div>,
324 );
325 }).toThrow(new TypeError('prod message'));
326 assertConsoleErrorDev([
327 'The provided key is an unsupported type TemporalLike.' +
328 ' This value must be coerced to a string before using it here.',
329 ]);
330 });
331 });