main
js 1,511 lines 38.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 react-core
8 */
9
10 'use strict';
11
12 describe('ReactDOMTestSelectors', () => {
13 let React;
14 let createRoot;
15 let act;
16 let createComponentSelector;
17 let createHasPseudoClassSelector;
18 let createRoleSelector;
19 let createTextSelector;
20 let createTestNameSelector;
21 let findAllNodes;
22 let findBoundingRects;
23 let focusWithin;
24 let getFindAllNodesFailureDescription;
25 let observeVisibleRects;
26 let mockIntersectionObserver;
27 let simulateIntersection;
28 let setBoundingClientRect;
29
30 let container;
31
32 beforeEach(() => {
33 jest.resetModules();
34
35 React = require('react');
36
37 act = require('internal-test-utils').act;
38
39 if (__EXPERIMENTAL__ || global.__WWW__) {
40 const ReactDOM = require('react-dom/unstable_testing');
41 createComponentSelector = ReactDOM.createComponentSelector;
42 createHasPseudoClassSelector = ReactDOM.createHasPseudoClassSelector;
43 createRoleSelector = ReactDOM.createRoleSelector;
44 createTextSelector = ReactDOM.createTextSelector;
45 createTestNameSelector = ReactDOM.createTestNameSelector;
46 findAllNodes = ReactDOM.findAllNodes;
47 findBoundingRects = ReactDOM.findBoundingRects;
48 focusWithin = ReactDOM.focusWithin;
49 getFindAllNodesFailureDescription =
50 ReactDOM.getFindAllNodesFailureDescription;
51 observeVisibleRects = ReactDOM.observeVisibleRects;
52 createRoot = ReactDOM.createRoot;
53 }
54
55 container = document.createElement('div');
56 document.body.appendChild(container);
57 const IntersectionMocks = require('./utils/IntersectionMocks');
58 mockIntersectionObserver = IntersectionMocks.mockIntersectionObserver;
59 simulateIntersection = IntersectionMocks.simulateIntersection;
60 setBoundingClientRect = IntersectionMocks.setBoundingClientRect;
61 });
62
63 afterEach(() => {
64 document.body.removeChild(container);
65 });
66
67 describe('findAllNodes', () => {
68 // @gate www || experimental
69 it('should support searching from the document root', async () => {
70 function Example() {
71 return (
72 <div>
73 <div data-testname="match" id="match" />
74 </div>
75 );
76 }
77
78 const root = createRoot(container);
79 await act(() => {
80 root.render(<Example />);
81 });
82
83 const matches = findAllNodes(document.body, [
84 createComponentSelector(Example),
85 createTestNameSelector('match'),
86 ]);
87 expect(matches).toHaveLength(1);
88 expect(matches[0].id).toBe('match');
89 });
90
91 // @gate www || experimental
92 it('should support searching from the container', async () => {
93 function Example() {
94 return (
95 <div>
96 <div data-testname="match" id="match" />
97 </div>
98 );
99 }
100
101 const root = createRoot(container);
102 await act(() => {
103 root.render(<Example />);
104 });
105
106 const matches = findAllNodes(container, [
107 createComponentSelector(Example),
108 createTestNameSelector('match'),
109 ]);
110 expect(matches).toHaveLength(1);
111 expect(matches[0].id).toBe('match');
112 });
113
114 // @gate www || experimental
115 it('should support searching from a previous match if the match had a data-testname', async () => {
116 function Outer() {
117 return (
118 <div data-testname="outer" id="outer">
119 <Inner />
120 </div>
121 );
122 }
123
124 function Inner() {
125 return <div data-testname="inner" id="inner" />;
126 }
127
128 const root = createRoot(container);
129 await act(() => {
130 root.render(<Outer />);
131 });
132
133 let matches = findAllNodes(container, [
134 createComponentSelector(Outer),
135 createTestNameSelector('outer'),
136 ]);
137 expect(matches).toHaveLength(1);
138 expect(matches[0].id).toBe('outer');
139
140 matches = findAllNodes(matches[0], [
141 createComponentSelector(Inner),
142 createTestNameSelector('inner'),
143 ]);
144 expect(matches).toHaveLength(1);
145 expect(matches[0].id).toBe('inner');
146 });
147
148 // @gate www || experimental
149 it('should not support searching from a previous match if the match did not have a data-testname', async () => {
150 function Outer() {
151 return (
152 <div id="outer">
153 <Inner />
154 </div>
155 );
156 }
157
158 function Inner() {
159 return <div id="inner" />;
160 }
161
162 const root = createRoot(container);
163 await act(() => {
164 root.render(<Outer />);
165 });
166
167 const matches = findAllNodes(container, [createComponentSelector(Outer)]);
168 expect(matches).toHaveLength(1);
169 expect(matches[0].id).toBe('outer');
170
171 expect(() => {
172 findAllNodes(matches[0], [
173 createComponentSelector(Inner),
174 createTestNameSelector('inner'),
175 ]);
176 }).toThrow(
177 'Invalid host root specified. Should be either a React container or a node with a testname attribute.',
178 );
179 });
180
181 // @gate www || experimental
182 it('should support an multiple component types in the selector array', async () => {
183 function Outer() {
184 return (
185 <>
186 <div data-testname="match" id="match1" />
187 <Middle />
188 </>
189 );
190 }
191 function Middle() {
192 return (
193 <>
194 <div data-testname="match" id="match2" />
195 <Inner />
196 </>
197 );
198 }
199 function Inner() {
200 return (
201 <>
202 <div data-testname="match" id="match3" />
203 </>
204 );
205 }
206
207 const root = createRoot(container);
208 await act(() => {
209 root.render(<Outer />);
210 });
211
212 let matches = findAllNodes(document.body, [
213 createComponentSelector(Outer),
214 createComponentSelector(Middle),
215 createTestNameSelector('match'),
216 ]);
217 expect(matches).toHaveLength(2);
218 expect(matches.map(m => m.id).sort()).toEqual(['match2', 'match3']);
219
220 matches = findAllNodes(document.body, [
221 createComponentSelector(Outer),
222 createComponentSelector(Middle),
223 createComponentSelector(Inner),
224 createTestNameSelector('match'),
225 ]);
226 expect(matches).toHaveLength(1);
227 expect(matches[0].id).toBe('match3');
228
229 matches = findAllNodes(document.body, [
230 createComponentSelector(Outer),
231 createComponentSelector(Inner),
232 createTestNameSelector('match'),
233 ]);
234 expect(matches).toHaveLength(1);
235 expect(matches[0].id).toBe('match3');
236 });
237
238 // @gate www || experimental
239 it('should find multiple matches', async () => {
240 function Example1() {
241 return (
242 <div>
243 <div data-testname="match" id="match1" />
244 </div>
245 );
246 }
247
248 function Example2() {
249 return (
250 <div>
251 <div data-testname="match" id="match2" />
252 <div data-testname="match" id="match3" />
253 </div>
254 );
255 }
256
257 const root = createRoot(container);
258 await act(() => {
259 root.render(
260 <>
261 <Example1 />
262 <Example2 />
263 </>,
264 );
265 });
266
267 const matches = findAllNodes(document.body, [
268 createTestNameSelector('match'),
269 ]);
270 expect(matches).toHaveLength(3);
271 expect(matches.map(m => m.id).sort()).toEqual([
272 'match1',
273 'match2',
274 'match3',
275 ]);
276 });
277
278 // @gate www || experimental
279 it('should ignore nested matches', async () => {
280 function Example() {
281 return (
282 <div data-testname="match" id="match1">
283 <div data-testname="match" id="match2" />
284 </div>
285 );
286 }
287
288 const root = createRoot(container);
289 await act(() => {
290 root.render(<Example />);
291 });
292
293 const matches = findAllNodes(document.body, [
294 createComponentSelector(Example),
295 createTestNameSelector('match'),
296 ]);
297 expect(matches).toHaveLength(1);
298 expect(matches[0].id).toEqual('match1');
299 });
300
301 // @gate www || experimental
302 it('should enforce the specific order of selectors', async () => {
303 function Outer() {
304 return (
305 <>
306 <div data-testname="match" id="match1" />
307 <Inner />
308 </>
309 );
310 }
311 function Inner() {
312 return <div data-testname="match" id="match1" />;
313 }
314
315 const root = createRoot(container);
316 await act(() => {
317 root.render(<Outer />);
318 });
319
320 expect(
321 findAllNodes(document.body, [
322 createComponentSelector(Inner),
323 createComponentSelector(Outer),
324 createTestNameSelector('match'),
325 ]),
326 ).toHaveLength(0);
327 });
328
329 // @gate www || experimental
330 it('should not search within hidden subtrees', async () => {
331 const ref1 = React.createRef(null);
332 const ref2 = React.createRef(null);
333
334 function Outer() {
335 return (
336 <>
337 <div hidden={true}>
338 <div ref={ref1} data-testname="match" />
339 </div>
340 <Inner />
341 </>
342 );
343 }
344 function Inner() {
345 return <div ref={ref2} data-testname="match" />;
346 }
347
348 const root = createRoot(container);
349 await act(() => {
350 root.render(<Outer />);
351 });
352
353 const matches = findAllNodes(document.body, [
354 createComponentSelector(Outer),
355 createTestNameSelector('match'),
356 ]);
357
358 expect(matches).toHaveLength(1);
359 expect(matches[0]).toBe(ref2.current);
360 });
361
362 // @gate www || experimental
363 it('should support filtering by display text', async () => {
364 function Example() {
365 return (
366 <div>
367 <div>foo</div>
368 <div>
369 <div id="match">bar</div>
370 </div>
371 </div>
372 );
373 }
374
375 const root = createRoot(container);
376 await act(() => {
377 root.render(<Example />);
378 });
379
380 const matches = findAllNodes(document.body, [
381 createComponentSelector(Example),
382 createTextSelector('bar'),
383 ]);
384 expect(matches).toHaveLength(1);
385 expect(matches[0].id).toBe('match');
386 });
387
388 // @gate www || experimental
389 it('should support filtering by explicit accessibiliy role', async () => {
390 function Example() {
391 return (
392 <div>
393 <div>foo</div>
394 <div>
395 <div role="button" id="match">
396 bar
397 </div>
398 </div>
399 </div>
400 );
401 }
402
403 const root = createRoot(container);
404 await act(() => {
405 root.render(<Example />);
406 });
407
408 const matches = findAllNodes(document.body, [
409 createComponentSelector(Example),
410 createRoleSelector('button'),
411 ]);
412 expect(matches).toHaveLength(1);
413 expect(matches[0].id).toBe('match');
414 });
415
416 // @gate www || experimental
417 it('should support filtering by explicit secondary accessibiliy role', async () => {
418 const ref = React.createRef();
419
420 function Example() {
421 return (
422 <div>
423 <div>foo</div>
424 <div>
425 <div ref={ref} role="meter progressbar" />
426 </div>
427 </div>
428 );
429 }
430
431 const root = createRoot(container);
432 await act(() => {
433 root.render(<Example />);
434 });
435
436 const matches = findAllNodes(document.body, [
437 createComponentSelector(Example),
438 createRoleSelector('progressbar'),
439 ]);
440 expect(matches).toHaveLength(1);
441 expect(matches[0]).toBe(ref.current);
442 });
443
444 // @gate www || experimental
445 it('should support filtering by implicit accessibiliy role', async () => {
446 function Example() {
447 return (
448 <div>
449 <div>foo</div>
450 <div>
451 <button id="match">bar</button>
452 </div>
453 </div>
454 );
455 }
456
457 const root = createRoot(container);
458 await act(() => {
459 root.render(<Example />);
460 });
461
462 const matches = findAllNodes(document.body, [
463 createComponentSelector(Example),
464 createRoleSelector('button'),
465 ]);
466 expect(matches).toHaveLength(1);
467 expect(matches[0].id).toBe('match');
468 });
469
470 // @gate www || experimental
471 it('should support filtering by implicit accessibiliy role with attributes qualifications', async () => {
472 function Example() {
473 return (
474 <div>
475 <div>foo</div>
476 <div>
477 <input type="checkbox" id="match" value="bar" />
478 </div>
479 </div>
480 );
481 }
482
483 const root = createRoot(container);
484 await act(() => {
485 root.render(<Example />);
486 });
487
488 const matches = findAllNodes(document.body, [
489 createComponentSelector(Example),
490 createRoleSelector('checkbox'),
491 ]);
492 expect(matches).toHaveLength(1);
493 expect(matches[0].id).toBe('match');
494 });
495
496 // @gate www || experimental
497 it('should support searching ahead with the has() selector', async () => {
498 function Example() {
499 return (
500 <div>
501 <article>
502 <h1>Should match</h1>
503 <p>
504 <button id="match">Like</button>
505 </p>
506 </article>
507 <article>
508 <h1>Should not match</h1>
509 <p>
510 <button>Like</button>
511 </p>
512 </article>
513 </div>
514 );
515 }
516
517 const root = createRoot(container);
518 await act(() => {
519 root.render(<Example />);
520 });
521
522 const matches = findAllNodes(document.body, [
523 createComponentSelector(Example),
524 createRoleSelector('article'),
525 createHasPseudoClassSelector([
526 createRoleSelector('heading'),
527 createTextSelector('Should match'),
528 ]),
529 createRoleSelector('button'),
530 ]);
531 expect(matches).toHaveLength(1);
532 expect(matches[0].id).toBe('match');
533 });
534
535 // @gate www || experimental
536 it('should throw if no container can be found', () => {
537 expect(() => findAllNodes(document.body, [])).toThrow(
538 'Could not find React container within specified host subtree.',
539 );
540 });
541
542 // @gate www || experimental
543 it('should throw if an invalid host root is specified', async () => {
544 const ref = React.createRef();
545 function Example() {
546 return <div ref={ref} />;
547 }
548
549 const root = createRoot(container);
550 await act(() => {
551 root.render(<Example />);
552 });
553
554 expect(() => findAllNodes(ref.current, [])).toThrow(
555 'Invalid host root specified. Should be either a React container or a node with a testname attribute.',
556 );
557 });
558 });
559
560 describe('getFindAllNodesFailureDescription', () => {
561 // @gate www || experimental
562 it('should describe findAllNodes failures caused by the component type selector', async () => {
563 function Outer() {
564 return <Middle />;
565 }
566 function Middle() {
567 return <div />;
568 }
569 function NotRendered() {
570 return <div data-testname="match" />;
571 }
572
573 const root = createRoot(container);
574 await act(() => {
575 root.render(<Outer />);
576 });
577
578 const description = getFindAllNodesFailureDescription(document.body, [
579 createComponentSelector(Outer),
580 createComponentSelector(Middle),
581 createComponentSelector(NotRendered),
582 createTestNameSelector('match'),
583 ]);
584
585 expect(description).toEqual(
586 `findAllNodes was able to match part of the selector:
587 <Outer> > <Middle>
588
589 No matching component was found for:
590 <NotRendered> > [data-testname="match"]`,
591 );
592 });
593
594 // @gate www || experimental
595 it('should return null if findAllNodes was able to find a match', async () => {
596 function Example() {
597 return (
598 <div>
599 <div data-testname="match" id="match" />
600 </div>
601 );
602 }
603
604 const root = createRoot(container);
605 await act(() => {
606 root.render(<Example />);
607 });
608
609 const description = getFindAllNodesFailureDescription(document.body, [
610 createComponentSelector(Example),
611 ]);
612
613 expect(description).toBe(null);
614 });
615 });
616
617 describe('findBoundingRects', () => {
618 // @gate www || experimental
619 it('should return a single rect for a component that returns a single root host element', async () => {
620 const ref = React.createRef();
621
622 function Example() {
623 return (
624 <div ref={ref}>
625 <div />
626 <div />
627 </div>
628 );
629 }
630
631 const root = createRoot(container);
632 await act(() => {
633 root.render(<Example />);
634 });
635
636 setBoundingClientRect(ref.current, {
637 x: 10,
638 y: 20,
639 width: 200,
640 height: 100,
641 });
642
643 const rects = findBoundingRects(document.body, [
644 createComponentSelector(Example),
645 ]);
646 expect(rects).toHaveLength(1);
647 expect(rects).toContainEqual({
648 x: 10,
649 y: 20,
650 width: 200,
651 height: 100,
652 });
653 });
654
655 // @gate www || experimental
656 it('should return a multiple rects for multiple matches', async () => {
657 const outerRef = React.createRef();
658 const innerRef = React.createRef();
659
660 function Outer() {
661 return (
662 <>
663 <div ref={outerRef} />
664 <Inner />
665 </>
666 );
667 }
668 function Inner() {
669 return <div ref={innerRef} />;
670 }
671
672 const root = createRoot(container);
673 await act(() => {
674 root.render(<Outer />);
675 });
676
677 setBoundingClientRect(outerRef.current, {
678 x: 10,
679 y: 20,
680 width: 200,
681 height: 100,
682 });
683 setBoundingClientRect(innerRef.current, {
684 x: 110,
685 y: 120,
686 width: 250,
687 height: 150,
688 });
689
690 const rects = findBoundingRects(document.body, [
691 createComponentSelector(Outer),
692 ]);
693 expect(rects).toHaveLength(2);
694 expect(rects).toContainEqual({
695 x: 10,
696 y: 20,
697 width: 200,
698 height: 100,
699 });
700 expect(rects).toContainEqual({
701 x: 110,
702 y: 120,
703 width: 250,
704 height: 150,
705 });
706 });
707
708 // @gate www || experimental
709 it('should return a multiple rects for single match that returns a fragment', async () => {
710 const refA = React.createRef();
711 const refB = React.createRef();
712
713 function Example() {
714 return (
715 <>
716 <div ref={refA}>
717 <div />
718 <div />
719 </div>
720 <div ref={refB} />
721 </>
722 );
723 }
724
725 const root = createRoot(container);
726 await act(() => {
727 root.render(<Example />);
728 });
729
730 setBoundingClientRect(refA.current, {
731 x: 10,
732 y: 20,
733 width: 200,
734 height: 100,
735 });
736 setBoundingClientRect(refB.current, {
737 x: 110,
738 y: 120,
739 width: 250,
740 height: 150,
741 });
742
743 const rects = findBoundingRects(document.body, [
744 createComponentSelector(Example),
745 ]);
746 expect(rects).toHaveLength(2);
747 expect(rects).toContainEqual({
748 x: 10,
749 y: 20,
750 width: 200,
751 height: 100,
752 });
753 expect(rects).toContainEqual({
754 x: 110,
755 y: 120,
756 width: 250,
757 height: 150,
758 });
759 });
760
761 // @gate www || experimental
762 it('should merge overlapping rects', async () => {
763 const refA = React.createRef();
764 const refB = React.createRef();
765 const refC = React.createRef();
766
767 function Example() {
768 return (
769 <>
770 <div ref={refA} />
771 <div ref={refB} />
772 <div ref={refC} />
773 </>
774 );
775 }
776
777 const root = createRoot(container);
778 await act(() => {
779 root.render(<Example />);
780 });
781
782 setBoundingClientRect(refA.current, {
783 x: 10,
784 y: 10,
785 width: 50,
786 height: 25,
787 });
788 setBoundingClientRect(refB.current, {
789 x: 10,
790 y: 10,
791 width: 20,
792 height: 10,
793 });
794 setBoundingClientRect(refC.current, {
795 x: 100,
796 y: 10,
797 width: 50,
798 height: 25,
799 });
800
801 const rects = findBoundingRects(document.body, [
802 createComponentSelector(Example),
803 ]);
804 expect(rects).toHaveLength(2);
805 expect(rects).toContainEqual({
806 x: 10,
807 y: 10,
808 width: 50,
809 height: 25,
810 });
811 expect(rects).toContainEqual({
812 x: 100,
813 y: 10,
814 width: 50,
815 height: 25,
816 });
817 });
818
819 // @gate www || experimental
820 it('should merge some types of adjacent rects (if they are the same in one dimension)', async () => {
821 const refA = React.createRef();
822 const refB = React.createRef();
823 const refC = React.createRef();
824 const refD = React.createRef();
825 const refE = React.createRef();
826 const refF = React.createRef();
827 const refG = React.createRef();
828
829 function Example() {
830 return (
831 <>
832 <div ref={refA} data-debug="A" />
833 <div ref={refB} data-debug="B" />
834 <div ref={refC} data-debug="C" />
835 <div ref={refD} data-debug="D" />
836 <div ref={refE} data-debug="E" />
837 <div ref={refF} data-debug="F" />
838 <div ref={refG} data-debug="G" />
839 </>
840 );
841 }
842
843 const root = createRoot(container);
844 await act(() => {
845 root.render(<Example />);
846 });
847
848 // A, B, and C are all adjacent and/or overlapping, with the same height.
849 setBoundingClientRect(refA.current, {
850 x: 30,
851 y: 0,
852 width: 40,
853 height: 25,
854 });
855 setBoundingClientRect(refB.current, {
856 x: 0,
857 y: 0,
858 width: 50,
859 height: 25,
860 });
861 setBoundingClientRect(refC.current, {
862 x: 70,
863 y: 0,
864 width: 20,
865 height: 25,
866 });
867
868 // D is partially overlapping with A and B, but is too tall to be merged.
869 setBoundingClientRect(refD.current, {
870 x: 20,
871 y: 0,
872 width: 20,
873 height: 30,
874 });
875
876 // Same thing but for a vertical group.
877 // Some of them could intersect with the horizontal group,
878 // except they're too far to the right.
879 setBoundingClientRect(refE.current, {
880 x: 100,
881 y: 25,
882 width: 25,
883 height: 50,
884 });
885 setBoundingClientRect(refF.current, {
886 x: 100,
887 y: 0,
888 width: 25,
889 height: 25,
890 });
891 setBoundingClientRect(refG.current, {
892 x: 100,
893 y: 75,
894 width: 25,
895 height: 10,
896 });
897
898 const rects = findBoundingRects(document.body, [
899 createComponentSelector(Example),
900 ]);
901 expect(rects).toHaveLength(3);
902 expect(rects).toContainEqual({
903 x: 0,
904 y: 0,
905 width: 90,
906 height: 25,
907 });
908 expect(rects).toContainEqual({
909 x: 20,
910 y: 0,
911 width: 20,
912 height: 30,
913 });
914 expect(rects).toContainEqual({
915 x: 100,
916 y: 0,
917 width: 25,
918 height: 85,
919 });
920 });
921
922 // @gate www || experimental
923 it('should not search within hidden subtrees', async () => {
924 const refA = React.createRef();
925 const refB = React.createRef();
926 const refC = React.createRef();
927
928 function Example() {
929 return (
930 <>
931 <div ref={refA} />
932 <div hidden={true} ref={refB} />
933 <div ref={refC} />
934 </>
935 );
936 }
937
938 const root = createRoot(container);
939 await act(() => {
940 root.render(<Example />);
941 });
942
943 setBoundingClientRect(refA.current, {
944 x: 10,
945 y: 10,
946 width: 50,
947 height: 25,
948 });
949 setBoundingClientRect(refB.current, {
950 x: 100,
951 y: 10,
952 width: 20,
953 height: 10,
954 });
955 setBoundingClientRect(refC.current, {
956 x: 200,
957 y: 10,
958 width: 50,
959 height: 25,
960 });
961
962 const rects = findBoundingRects(document.body, [
963 createComponentSelector(Example),
964 ]);
965 expect(rects).toHaveLength(2);
966 expect(rects).toContainEqual({
967 x: 10,
968 y: 10,
969 width: 50,
970 height: 25,
971 });
972 expect(rects).toContainEqual({
973 x: 200,
974 y: 10,
975 width: 50,
976 height: 25,
977 });
978 });
979 });
980
981 describe('focusWithin', () => {
982 // @gate www || experimental
983 it('should return false if the specified component path has no matches', async () => {
984 function Example() {
985 return <Child />;
986 }
987 function Child() {
988 return null;
989 }
990 function NotUsed() {
991 return null;
992 }
993
994 const root = createRoot(container);
995 await act(() => {
996 root.render(<Example />);
997 });
998
999 const didFocus = focusWithin(document.body, [
1000 createComponentSelector(Example),
1001 createComponentSelector(NotUsed),
1002 ]);
1003 expect(didFocus).toBe(false);
1004 });
1005
1006 // @gate www || experimental
1007 it('should return false if there are no focusable elements within the matched subtree', async () => {
1008 function Example() {
1009 return <Child />;
1010 }
1011 function Child() {
1012 return 'not focusable';
1013 }
1014
1015 const root = createRoot(container);
1016 await act(() => {
1017 root.render(<Example />);
1018 });
1019
1020 const didFocus = focusWithin(document.body, [
1021 createComponentSelector(Example),
1022 createComponentSelector(Child),
1023 ]);
1024 expect(didFocus).toBe(false);
1025 });
1026
1027 // @gate www || experimental
1028 it('should return false if the only focusable elements are disabled', async () => {
1029 function Example() {
1030 return (
1031 <button disabled={true} style={{width: 10, height: 10}}>
1032 not clickable
1033 </button>
1034 );
1035 }
1036
1037 const root = createRoot(container);
1038 await act(() => {
1039 root.render(<Example />);
1040 });
1041
1042 const didFocus = focusWithin(document.body, [
1043 createComponentSelector(Example),
1044 ]);
1045 expect(didFocus).toBe(false);
1046 });
1047
1048 // @gate www || experimental
1049 it('should return false if the only focusable elements are hidden', async () => {
1050 function Example() {
1051 return <button hidden={true}>not clickable</button>;
1052 }
1053
1054 const root = createRoot(container);
1055 await act(() => {
1056 root.render(<Example />);
1057 });
1058
1059 const didFocus = focusWithin(document.body, [
1060 createComponentSelector(Example),
1061 ]);
1062 expect(didFocus).toBe(false);
1063 });
1064
1065 // @gate www || experimental
1066 it('should successfully focus the first focusable element within the tree', async () => {
1067 const secondRef = React.createRef(null);
1068
1069 const handleFirstFocus = jest.fn();
1070 const handleSecondFocus = jest.fn();
1071 const handleThirdFocus = jest.fn();
1072
1073 function Example() {
1074 return (
1075 <>
1076 <FirstChild />
1077 <SecondChild />
1078 <ThirdChild />
1079 </>
1080 );
1081 }
1082 function FirstChild() {
1083 return (
1084 <button hidden={true} onFocus={handleFirstFocus}>
1085 not clickable
1086 </button>
1087 );
1088 }
1089 function SecondChild() {
1090 return (
1091 <button
1092 ref={secondRef}
1093 style={{width: 10, height: 10}}
1094 onFocus={handleSecondFocus}>
1095 clickable
1096 </button>
1097 );
1098 }
1099 function ThirdChild() {
1100 return (
1101 <button style={{width: 10, height: 10}} onFocus={handleThirdFocus}>
1102 clickable
1103 </button>
1104 );
1105 }
1106
1107 const root = createRoot(container);
1108 await act(() => {
1109 root.render(<Example />);
1110 });
1111
1112 const didFocus = focusWithin(document.body, [
1113 createComponentSelector(Example),
1114 ]);
1115 expect(didFocus).toBe(true);
1116 expect(document.activeElement).not.toBeNull();
1117 expect(document.activeElement).toBe(secondRef.current);
1118 expect(handleFirstFocus).not.toHaveBeenCalled();
1119 expect(handleSecondFocus).toHaveBeenCalledTimes(1);
1120 expect(handleThirdFocus).not.toHaveBeenCalled();
1121 });
1122
1123 // @gate www || experimental
1124 it('should successfully focus the first focusable element even if application logic interferes', async () => {
1125 const ref = React.createRef(null);
1126
1127 const handleFocus = jest.fn(event => {
1128 event.target.blur();
1129 });
1130
1131 function Example() {
1132 return (
1133 <button
1134 ref={ref}
1135 style={{width: 10, height: 10}}
1136 onFocus={handleFocus}>
1137 clickable
1138 </button>
1139 );
1140 }
1141
1142 const root = createRoot(container);
1143 await act(() => {
1144 root.render(<Example />);
1145 });
1146
1147 const didFocus = focusWithin(document.body, [
1148 createComponentSelector(Example),
1149 ]);
1150 expect(didFocus).toBe(true);
1151 expect(ref.current).not.toBeNull();
1152 expect(ref.current).not.toBe(document.activeElement);
1153 expect(handleFocus).toHaveBeenCalledTimes(1);
1154 });
1155
1156 // @gate www || experimental
1157 it('should not focus within hidden subtrees', async () => {
1158 const secondRef = React.createRef(null);
1159
1160 const handleFirstFocus = jest.fn();
1161 const handleSecondFocus = jest.fn();
1162 const handleThirdFocus = jest.fn();
1163
1164 function Example() {
1165 return (
1166 <>
1167 <FirstChild />
1168 <SecondChild />
1169 <ThirdChild />
1170 </>
1171 );
1172 }
1173 function FirstChild() {
1174 return (
1175 <div hidden={true}>
1176 <button style={{width: 10, height: 10}} onFocus={handleFirstFocus}>
1177 hidden
1178 </button>
1179 </div>
1180 );
1181 }
1182 function SecondChild() {
1183 return (
1184 <button
1185 ref={secondRef}
1186 style={{width: 10, height: 10}}
1187 onFocus={handleSecondFocus}>
1188 clickable
1189 </button>
1190 );
1191 }
1192 function ThirdChild() {
1193 return (
1194 <button style={{width: 10, height: 10}} onFocus={handleThirdFocus}>
1195 clickable
1196 </button>
1197 );
1198 }
1199
1200 const root = createRoot(container);
1201 await act(() => {
1202 root.render(<Example />);
1203 });
1204
1205 const didFocus = focusWithin(document.body, [
1206 createComponentSelector(Example),
1207 ]);
1208 expect(didFocus).toBe(true);
1209 expect(document.activeElement).not.toBeNull();
1210 expect(document.activeElement).toBe(secondRef.current);
1211 expect(handleFirstFocus).not.toHaveBeenCalled();
1212 expect(handleSecondFocus).toHaveBeenCalledTimes(1);
1213 expect(handleThirdFocus).not.toHaveBeenCalled();
1214 });
1215 });
1216
1217 describe('observeVisibleRects', () => {
1218 let observerMock;
1219
1220 beforeEach(() => {
1221 observerMock = mockIntersectionObserver();
1222 });
1223
1224 // @gate www || experimental
1225 it('should notify a listener when the underlying instance intersection changes', async () => {
1226 const ref = React.createRef(null);
1227
1228 function Example() {
1229 return <div ref={ref} />;
1230 }
1231
1232 const root = createRoot(container);
1233 await act(() => {
1234 root.render(<Example />);
1235 });
1236
1237 // Stub out the size of the element this test will be observing.
1238 const rect = {
1239 x: 10,
1240 y: 20,
1241 width: 200,
1242 height: 100,
1243 };
1244 setBoundingClientRect(ref.current, rect);
1245
1246 const handleVisibilityChange = jest.fn();
1247 observeVisibleRects(
1248 document.body,
1249 [createComponentSelector(Example)],
1250 handleVisibilityChange,
1251 );
1252
1253 expect(observerMock.callback).not.toBeNull();
1254 expect(observerMock.observedTargets).toHaveLength(1);
1255 expect(handleVisibilityChange).not.toHaveBeenCalled();
1256
1257 // Simulate IntersectionObserver notification.
1258 simulateIntersection([ref.current, rect, 0.5]);
1259
1260 expect(handleVisibilityChange).toHaveBeenCalledTimes(1);
1261 expect(handleVisibilityChange).toHaveBeenCalledWith([{rect, ratio: 0.5}]);
1262 });
1263
1264 // @gate www || experimental
1265 it('should notify a listener of multiple targets when the underlying instance intersection changes', async () => {
1266 const ref1 = React.createRef(null);
1267 const ref2 = React.createRef(null);
1268
1269 function Example() {
1270 return (
1271 <>
1272 <div ref={ref1} />
1273 <div ref={ref2} />
1274 </>
1275 );
1276 }
1277
1278 const root = createRoot(container);
1279 await act(() => {
1280 root.render(<Example />);
1281 });
1282
1283 // Stub out the size of the element this test will be observing.
1284 const rect1 = {
1285 x: 10,
1286 y: 20,
1287 width: 200,
1288 height: 100,
1289 };
1290 let rect2 = {
1291 x: 210,
1292 y: 20,
1293 width: 200,
1294 height: 100,
1295 };
1296 setBoundingClientRect(ref1.current, rect1);
1297 setBoundingClientRect(ref2.current, rect2);
1298
1299 const handleVisibilityChange = jest.fn();
1300 observeVisibleRects(
1301 document.body,
1302 [createComponentSelector(Example)],
1303 handleVisibilityChange,
1304 );
1305
1306 expect(observerMock.callback).not.toBeNull();
1307 expect(observerMock.observedTargets).toHaveLength(2);
1308 expect(handleVisibilityChange).not.toHaveBeenCalled();
1309
1310 // Simulate IntersectionObserver notification.
1311 simulateIntersection([ref1.current, rect1, 0.5]);
1312
1313 // Even though only one of the rects changed intersection,
1314 // the test selector should describe the current state of both.
1315 expect(handleVisibilityChange).toHaveBeenCalledTimes(1);
1316 expect(handleVisibilityChange).toHaveBeenCalledWith([
1317 {rect: rect1, ratio: 0.5},
1318 {rect: rect2, ratio: 0},
1319 ]);
1320
1321 handleVisibilityChange.mockClear();
1322
1323 rect2 = {
1324 x: 210,
1325 y: 20,
1326 width: 200,
1327 height: 200,
1328 };
1329
1330 // Simulate another IntersectionObserver notification.
1331 simulateIntersection(
1332 [ref1.current, rect1, 1],
1333 [ref2.current, rect2, 0.25],
1334 );
1335
1336 // The newly changed display rect should also be provided for the second target.
1337 expect(handleVisibilityChange).toHaveBeenCalledTimes(1);
1338 expect(handleVisibilityChange).toHaveBeenCalledWith([
1339 {rect: rect1, ratio: 1},
1340 {rect: rect2, ratio: 0.25},
1341 ]);
1342 });
1343
1344 // @gate www || experimental
1345 it('should stop listening when its disconnected', async () => {
1346 const ref = React.createRef(null);
1347
1348 function Example() {
1349 return <div ref={ref} />;
1350 }
1351
1352 const root = createRoot(container);
1353 await act(() => {
1354 root.render(<Example />);
1355 });
1356
1357 // Stub out the size of the element this test will be observing.
1358 const rect = {
1359 x: 10,
1360 y: 20,
1361 width: 200,
1362 height: 100,
1363 };
1364 setBoundingClientRect(ref.current, rect);
1365
1366 const handleVisibilityChange = jest.fn();
1367 const {disconnect} = observeVisibleRects(
1368 document.body,
1369 [createComponentSelector(Example)],
1370 handleVisibilityChange,
1371 );
1372
1373 expect(observerMock.callback).not.toBeNull();
1374 expect(observerMock.observedTargets).toHaveLength(1);
1375 expect(handleVisibilityChange).not.toHaveBeenCalled();
1376
1377 disconnect();
1378 expect(observerMock.callback).toBeNull();
1379 });
1380
1381 // This test reuires gating because it relies on the __DEV__ only commit hook to work.
1382 // @gate www || experimental && __DEV__
1383 it('should update which targets its listening to after a commit', async () => {
1384 const ref1 = React.createRef(null);
1385 const ref2 = React.createRef(null);
1386
1387 let increment;
1388
1389 function Example() {
1390 const [count, setCount] = React.useState(0);
1391 increment = () => setCount(count + 1);
1392 return (
1393 <>
1394 {count < 2 && <div ref={ref1} />}
1395 {count > 0 && <div ref={ref2} />}
1396 </>
1397 );
1398 }
1399
1400 const root = createRoot(container);
1401 await act(() => {
1402 root.render(<Example />);
1403 });
1404
1405 // Stub out the size of the element this test will be observing.
1406 const rect1 = {
1407 x: 10,
1408 y: 20,
1409 width: 200,
1410 height: 100,
1411 };
1412 setBoundingClientRect(ref1.current, rect1);
1413
1414 const handleVisibilityChange = jest.fn();
1415 observeVisibleRects(
1416 document.body,
1417 [createComponentSelector(Example)],
1418 handleVisibilityChange,
1419 );
1420
1421 // Simulate IntersectionObserver notification.
1422 simulateIntersection([ref1.current, rect1, 1]);
1423
1424 expect(handleVisibilityChange).toHaveBeenCalledTimes(1);
1425 expect(handleVisibilityChange).toHaveBeenCalledWith([
1426 {rect: rect1, ratio: 1},
1427 ]);
1428
1429 await act(() => increment());
1430
1431 const rect2 = {
1432 x: 110,
1433 y: 20,
1434 width: 200,
1435 height: 100,
1436 };
1437 setBoundingClientRect(ref2.current, rect2);
1438
1439 handleVisibilityChange.mockClear();
1440
1441 simulateIntersection(
1442 [ref1.current, rect1, 0.5],
1443 [ref2.current, rect2, 0.25],
1444 );
1445
1446 expect(handleVisibilityChange).toHaveBeenCalledTimes(1);
1447 expect(handleVisibilityChange).toHaveBeenCalledWith([
1448 {rect: rect1, ratio: 0.5},
1449 {rect: rect2, ratio: 0.25},
1450 ]);
1451
1452 await act(() => increment());
1453
1454 handleVisibilityChange.mockClear();
1455
1456 simulateIntersection([ref2.current, rect2, 0.75]);
1457
1458 expect(handleVisibilityChange).toHaveBeenCalledTimes(1);
1459 expect(handleVisibilityChange).toHaveBeenCalledWith([
1460 {rect: rect2, ratio: 0.75},
1461 ]);
1462 });
1463
1464 // @gate www || experimental
1465 it('should not observe components within hidden subtrees', async () => {
1466 const ref1 = React.createRef(null);
1467 const ref2 = React.createRef(null);
1468
1469 function Example() {
1470 return (
1471 <>
1472 <div ref={ref1} />
1473 <div hidden={true} ref={ref2} />
1474 </>
1475 );
1476 }
1477
1478 const root = createRoot(container);
1479 await act(() => {
1480 root.render(<Example />);
1481 });
1482
1483 // Stub out the size of the element this test will be observing.
1484 const rect1 = {
1485 x: 10,
1486 y: 20,
1487 width: 200,
1488 height: 100,
1489 };
1490 const rect2 = {
1491 x: 210,
1492 y: 20,
1493 width: 200,
1494 height: 100,
1495 };
1496 setBoundingClientRect(ref1.current, rect1);
1497 setBoundingClientRect(ref2.current, rect2);
1498
1499 const handleVisibilityChange = jest.fn();
1500 observeVisibleRects(
1501 document.body,
1502 [createComponentSelector(Example)],
1503 handleVisibilityChange,
1504 );
1505
1506 expect(observerMock.callback).not.toBeNull();
1507 expect(observerMock.observedTargets).toHaveLength(1);
1508 expect(observerMock.observedTargets[0]).toBe(ref1.current);
1509 });
1510 });
1511 });