main
js 402 lines 12.5 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 reactcore
8 * @jest-environment node
9 */
10
11 'use strict';
12
13 let JSDOM;
14 let React;
15 let ReactDOMClient;
16 let act;
17 let document;
18 let Fragment;
19 let Node;
20
21 describe('FragmentRefs', () => {
22 beforeEach(() => {
23 jest.resetModules();
24 JSDOM = require('jsdom');
25 React = require('react');
26 Fragment = React.Fragment;
27 ReactDOMClient = require('react-dom/client');
28 act = require('internal-test-utils').act;
29
30 const jsdom = new JSDOM.JSDOM('');
31 document = jsdom.window.document;
32 Node = jsdom.window.Node;
33 global.window = jsdom.window;
34 global.document = global.window.document;
35 global.navigator = global.window.navigator;
36 global.Event = global.window.Event;
37 global.MouseEvent = global.window.MouseEvent;
38 global.Node = Node;
39 });
40
41 describe('focus methods', () => {
42 describe('blur()', () => {
43 // @gate enableFragmentRefs
44 it('throws when the nearest host parent is a Document container', async () => {
45 const fragmentRef = React.createRef();
46 const root = ReactDOMClient.createRoot(document);
47
48 await act(() => {
49 root.render(
50 <Fragment ref={fragmentRef}>
51 <html>
52 <body>
53 <a id="child-a" href="/">
54 A
55 </a>
56 </body>
57 </html>
58 </Fragment>,
59 );
60 });
61
62 await act(() => {
63 // focus() would stop at <body>, which is a child of the fragment
64 // and usually already the activeElement.
65 document.getElementById('child-a').focus();
66 });
67 expect(document.activeElement.id).toEqual('child-a');
68
69 await act(() => {
70 fragmentRef.current.blur();
71 });
72 expect(document.activeElement).toEqual(document.body);
73 });
74 });
75 });
76
77 describe('events', () => {
78 describe('dispatchEvent()', () => {
79 // @gate enableFragmentRefs
80 it('fires events when the fragment is a child of a HostSingleton in a document root', async () => {
81 const fragmentRef = React.createRef();
82 const bodyRef = React.createRef();
83 const root = ReactDOMClient.createRoot(document);
84
85 await act(() => {
86 root.render(
87 <html>
88 <body ref={bodyRef}>
89 <Fragment ref={fragmentRef} />
90 </body>
91 </html>,
92 );
93 });
94
95 const fragmentListener = jest.fn();
96 fragmentRef.current.addEventListener('custom', fragmentListener);
97 const bodyListener = jest.fn();
98 bodyRef.current.addEventListener('custom', bodyListener);
99
100 // The <body> is the fragment's host parent, so the
101 // temporary event target is appended there.
102 fragmentRef.current.dispatchEvent(new Event('custom', {bubbles: true}));
103
104 expect(fragmentListener).toHaveBeenCalledTimes(1);
105 expect(bodyListener).toHaveBeenCalledTimes(1);
106 });
107
108 // @gate enableFragmentRefs
109 it('dispatches to its own listeners when the container is a Document', async () => {
110 const fragmentRef = React.createRef();
111 const root = ReactDOMClient.createRoot(document);
112 const logs = [];
113
114 await act(() => {
115 root.render(
116 <>
117 <Fragment ref={fragmentRef} />
118 <html>
119 <body>
120 <div id="child" />
121 </body>
122 </html>
123 </>,
124 );
125 });
126
127 fragmentRef.current.addEventListener('click', () => {
128 logs.push('fragment');
129 });
130 document.addEventListener('click', () => {
131 logs.push('document');
132 });
133
134 const isCancelable = !fragmentRef.current.dispatchEvent(
135 new MouseEvent('click', {bubbles: true}),
136 );
137
138 expect(logs).toEqual(['fragment', 'document']);
139 expect(isCancelable).toBe(false);
140 });
141
142 // @gate enableFragmentRefs
143 it('does not propagate through its own children when wrapping documentElement', async () => {
144 const fragmentRef = React.createRef();
145 const root = ReactDOMClient.createRoot(document);
146 const logs = [];
147
148 await act(() => {
149 root.render(
150 <Fragment ref={fragmentRef}>
151 <html>
152 <body>
153 <div id="child" />
154 </body>
155 </html>
156 </Fragment>,
157 );
158 });
159
160 // This also registers the listener on the <html> child. Because the
161 // fragment's position is a sibling of <html>, the event must not
162 // propagate through it and fire the listener a second time.
163 fragmentRef.current.addEventListener('click', () => {
164 logs.push('fragment');
165 });
166 document.addEventListener('click', () => {
167 logs.push('document');
168 });
169
170 fragmentRef.current.dispatchEvent(
171 new MouseEvent('click', {bubbles: true}),
172 );
173
174 expect(logs).toEqual(['fragment', 'document']);
175 });
176
177 // @gate enableFragmentRefs
178 it('dispatches non-bubbling events when the container is a Document', async () => {
179 const fragmentRef = React.createRef();
180 const root = ReactDOMClient.createRoot(document);
181 const logs = [];
182
183 await act(() => {
184 root.render(
185 <>
186 <Fragment ref={fragmentRef} />
187 <html>
188 <body>
189 <div id="child" />
190 </body>
191 </html>
192 </>,
193 );
194 });
195
196 document.addEventListener('click', () => {
197 logs.push('document');
198 });
199
200 const isCancelable = !fragmentRef.current.dispatchEvent(
201 new MouseEvent('click', {bubbles: false}),
202 );
203
204 expect(logs).toEqual([]);
205 expect(isCancelable).toBe(false);
206 });
207 });
208
209 describe('addEventListener()', () => {
210 // @gate enableFragmentRefs
211 it('attaches listeners to the host children inside singletons', async () => {
212 const fragmentRef = React.createRef();
213 const childRef = React.createRef();
214 const root = ReactDOMClient.createRoot(document);
215
216 await act(() => {
217 root.render(
218 <Fragment ref={fragmentRef}>
219 <html>
220 <body>
221 <div ref={childRef} id="child" />
222 </body>
223 </html>
224 </Fragment>,
225 );
226 });
227
228 const currentTargets = [];
229 fragmentRef.current.addEventListener('click', event => {
230 currentTargets.push(event.currentTarget);
231 });
232
233 childRef.current.dispatchEvent(new Event('click', {bubbles: true}));
234
235 // The <html> singleton is the fragment's child, so the listener is
236 // attached there and receives the bubbling event.
237 expect(currentTargets).toEqual([document.documentElement]);
238 });
239
240 // @gate enableFragmentRefs
241 it('attaches listeners to a singleton mounted into the fragment, but not to its content', async () => {
242 const fragmentRef = React.createRef();
243 const childRef = React.createRef();
244 const root = ReactDOMClient.createRoot(document);
245
246 function Test({showShell}) {
247 return (
248 <Fragment ref={fragmentRef}>
249 {showShell && (
250 <html>
251 <body>
252 <div ref={childRef} id="child" />
253 </body>
254 </html>
255 )}
256 </Fragment>
257 );
258 }
259
260 await act(() => {
261 root.render(<Test showShell={false} />);
262 });
263
264 const currentTargets = [];
265 fragmentRef.current.addEventListener('click', event => {
266 currentTargets.push(event.currentTarget);
267 });
268
269 await act(() => {
270 root.render(<Test showShell={true} />);
271 });
272
273 childRef.current.dispatchEvent(new Event('click', {bubbles: true}));
274
275 // The placed <html> singleton receives the fragment's listener as a
276 // new child. Its content is not attributed to the fragment, so the
277 // event only fires once when it bubbles to <html>.
278 expect(currentTargets).toEqual([document.documentElement]);
279 });
280
281 // @gate enableFragmentRefs
282 it('attributes new children inside a singleton to fragments below it, not above it', async () => {
283 const outerFragmentRef = React.createRef();
284 const innerFragmentRef = React.createRef();
285 const lateChildRef = React.createRef();
286 const root = ReactDOMClient.createRoot(document);
287
288 function Test({showLateChild}) {
289 return (
290 <Fragment ref={outerFragmentRef}>
291 <html>
292 <body>
293 <Fragment ref={innerFragmentRef}>
294 <div id="child" />
295 {showLateChild && <span ref={lateChildRef} id="late" />}
296 </Fragment>
297 </body>
298 </html>
299 </Fragment>
300 );
301 }
302
303 await act(() => {
304 root.render(<Test showLateChild={false} />);
305 });
306
307 const outerCurrentTargets = [];
308 outerFragmentRef.current.addEventListener('click', event => {
309 outerCurrentTargets.push(event.currentTarget);
310 });
311 const innerCurrentTargets = [];
312 innerFragmentRef.current.addEventListener('click', event => {
313 innerCurrentTargets.push(event.currentTarget);
314 });
315
316 await act(() => {
317 root.render(<Test showLateChild={true} />);
318 });
319
320 lateChildRef.current.dispatchEvent(new Event('click', {bubbles: true}));
321
322 // The inner fragment owns the new child directly and attaches its
323 // listener on insertion. The outer fragment's child is the <html>
324 // singleton, so the new child inside <body> is not attributed to it
325 // and its listener only fires once via bubbling.
326 expect(innerCurrentTargets).toEqual([lateChildRef.current]);
327 expect(outerCurrentTargets).toEqual([document.documentElement]);
328 });
329 });
330 });
331
332 describe('getClientRects()', () => {
333 // @gate enableFragmentRefs
334 it('measures the host children inside singletons', async () => {
335 const fragmentRef = React.createRef();
336 const childRef = React.createRef();
337 const root = ReactDOMClient.createRoot(document);
338
339 await act(() => {
340 root.render(
341 <Fragment ref={fragmentRef}>
342 <html>
343 <body>
344 <div ref={childRef} id="child" />
345 </body>
346 </html>
347 </Fragment>,
348 );
349 });
350
351 childRef.current.getClientRects = jest.fn(() => ['child-rect']);
352 document.documentElement.getClientRects = jest.fn(() => ['html-rect']);
353
354 // The <html> singleton is the fragment's child, so it is measured
355 // instead of the elements inside it
356 expect(fragmentRef.current.getClientRects()).toEqual(['html-rect']);
357 });
358 });
359
360 describe('compareDocumentPosition', () => {
361 function expectPosition(position, spec) {
362 const positionResult = {
363 following: (position & Node.DOCUMENT_POSITION_FOLLOWING) !== 0,
364 preceding: (position & Node.DOCUMENT_POSITION_PRECEDING) !== 0,
365 contains: (position & Node.DOCUMENT_POSITION_CONTAINS) !== 0,
366 containedBy: (position & Node.DOCUMENT_POSITION_CONTAINED_BY) !== 0,
367 disconnected: (position & Node.DOCUMENT_POSITION_DISCONNECTED) !== 0,
368 implementationSpecific:
369 (position & Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC) !== 0,
370 };
371 expect(positionResult).toEqual(spec);
372 }
373
374 // @gate enableFragmentRefs
375 it('treats documentElement as containing the fragment', async () => {
376 const fragmentRef = React.createRef();
377 const container = document.createElement('div');
378 document.body.appendChild(container);
379 const root = ReactDOMClient.createRoot(container);
380
381 await act(() => {
382 root.render(
383 <Fragment ref={fragmentRef}>
384 <div id="child" />
385 </Fragment>,
386 );
387 });
388
389 expectPosition(
390 fragmentRef.current.compareDocumentPosition(document.documentElement),
391 {
392 preceding: true,
393 following: false,
394 contains: true,
395 containedBy: false,
396 disconnected: false,
397 implementationSpecific: false,
398 },
399 );
400 });
401 });
402 });