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