@khanacademy/wonder-blocks-dropdown 2.6.7 → 2.6.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,42 +1,515 @@
1
1
  // @flow
2
2
  import * as React from "react";
3
- import * as ReactDOM from "react-dom";
4
- import {mount} from "enzyme";
5
- import "jest-enzyme";
3
+ import {fireEvent, render, screen} from "@testing-library/react";
4
+ import userEvent from "@testing-library/user-event";
6
5
 
7
6
  import OptionItem from "../option-item.js";
8
7
  import SearchTextInput from "../search-text-input.js";
9
8
  import DropdownCore from "../dropdown-core.js";
10
- import {keyCodes} from "../../util/constants.js";
11
9
 
12
- import {unmountAll} from "../../../../../utils/testing/enzyme-shim.js";
13
-
14
- jest.mock("../dropdown-core-virtualized.js");
15
-
16
- const elementAtIndex = (wrapper, index) =>
17
- wrapper.find(`[data-test-id="item-${index}"]`).first().getDOMNode();
10
+ const items = [
11
+ {
12
+ component: (
13
+ <OptionItem testId="item-0" label="item 0" value="0" key="0" />
14
+ ),
15
+ focusable: true,
16
+ populatedProps: {},
17
+ },
18
+ {
19
+ component: (
20
+ <OptionItem testId="item-1" label="item 1" value="1" key="1" />
21
+ ),
22
+ focusable: true,
23
+ populatedProps: {},
24
+ },
25
+ {
26
+ component: (
27
+ <OptionItem testId="item-2" label="item 2" value="2" key="2" />
28
+ ),
29
+ focusable: true,
30
+ populatedProps: {},
31
+ },
32
+ ];
18
33
 
19
34
  describe("DropdownCore", () => {
20
- window.getComputedStyle = jest.fn();
35
+ it("should throw for invalid role", () => {
36
+ // Arrange
37
+ // Passing an invalid role will throw an error.
38
+ jest.spyOn(console, "error").mockImplementation(() => {});
21
39
 
22
- let dropdown;
40
+ // Act
41
+ const underTest = () =>
42
+ render(
43
+ <div>
44
+ <button data-test-id="external-button" />
45
+ <DropdownCore
46
+ initialFocusedIndex={0}
47
+ // mock the items
48
+ items={[]}
49
+ role={("invalid": any)}
50
+ open={true}
51
+ // mock the opener elements
52
+ opener={<button />}
53
+ openerElement={null}
54
+ onOpenChanged={jest.fn()}
55
+ />
56
+ </div>,
57
+ );
23
58
 
24
- beforeEach(() => {
25
- jest.useFakeTimers();
59
+ // Assert
60
+ expect(underTest).toThrow();
61
+ });
26
62
 
27
- // Jest doesn't fake out the animation frame API, so we're going to do
28
- // it here and map it to timeouts, that way we can use the fake timer
29
- // API to test our animation frame things.
30
- jest.spyOn(global, "requestAnimationFrame").mockImplementation((fn) =>
31
- setTimeout(fn, 0),
32
- );
33
- jest.spyOn(global, "cancelAnimationFrame").mockImplementation((id) =>
34
- clearTimeout(id),
63
+ it("focus on the correct option", () => {
64
+ // Arrange
65
+ render(
66
+ <DropdownCore
67
+ initialFocusedIndex={0}
68
+ // mock the items
69
+ items={items}
70
+ role="listbox"
71
+ open={true}
72
+ // mock the opener elements
73
+ opener={<button />}
74
+ openerElement={null}
75
+ onOpenChanged={jest.fn()}
76
+ />,
35
77
  );
36
78
 
79
+ // Act
80
+ const item = screen.getByTestId("item-0");
81
+
82
+ // Assert
83
+ expect(item).toHaveFocus();
84
+ });
85
+
86
+ it("handles basic keyboard navigation as expected", () => {
87
+ // Arrange
37
88
  const dummyOpener = <button />;
38
89
  const openChanged = jest.fn();
39
- dropdown = mount(
90
+
91
+ render(
92
+ <DropdownCore
93
+ initialFocusedIndex={0}
94
+ // mock the items
95
+ items={items}
96
+ role="listbox"
97
+ open={true}
98
+ // mock the opener elements
99
+ opener={dummyOpener}
100
+ openerElement={null}
101
+ onOpenChanged={openChanged}
102
+ />,
103
+ );
104
+
105
+ // Act
106
+ // navigate down two times
107
+ userEvent.keyboard("{arrowdown}"); // 0 -> 1
108
+ userEvent.keyboard("{arrowdown}"); // 1 -> 2
109
+
110
+ // Assert
111
+ expect(screen.queryByTestId("item-2")).toHaveFocus();
112
+ });
113
+
114
+ it("keyboard works backwards as expected", () => {
115
+ // Arrange
116
+ render(
117
+ <DropdownCore
118
+ initialFocusedIndex={0}
119
+ // mock the items
120
+ items={items}
121
+ role="listbox"
122
+ open={true}
123
+ // mock the opener elements
124
+ opener={<button />}
125
+ openerElement={null}
126
+ onOpenChanged={jest.fn()}
127
+ />,
128
+ );
129
+
130
+ // Act
131
+ // navigate down tree times
132
+ userEvent.keyboard("{arrowdown}"); // 0 -> 1
133
+ userEvent.keyboard("{arrowdown}"); // 1 -> 2
134
+ userEvent.keyboard("{arrowdown}"); // 2 -> 0
135
+
136
+ // navigate up back two times
137
+ userEvent.keyboard("{arrowup}"); // 0 -> 2
138
+ userEvent.keyboard("{arrowup}"); // 2 -> 1
139
+
140
+ // Assert
141
+ expect(screen.getByTestId("item-1")).toHaveFocus();
142
+ });
143
+
144
+ it("closes on tab as expected", () => {
145
+ // Arrange
146
+ const handleOpenChangedMock = jest.fn();
147
+
148
+ render(
149
+ <DropdownCore
150
+ initialFocusedIndex={0}
151
+ // mock the items
152
+ items={items}
153
+ role="listbox"
154
+ open={true}
155
+ // mock the opener elements
156
+ opener={<button />}
157
+ openerElement={null}
158
+ onOpenChanged={handleOpenChangedMock}
159
+ />,
160
+ );
161
+
162
+ // Act
163
+ // close the dropdown by tabbing out
164
+ userEvent.tab();
165
+
166
+ // Assert
167
+ expect(handleOpenChangedMock).toHaveBeenNthCalledWith(1, false);
168
+ });
169
+
170
+ it("closes on escape as expected", () => {
171
+ // Arrange
172
+ const handleOpenChangedMock = jest.fn();
173
+
174
+ render(
175
+ <DropdownCore
176
+ initialFocusedIndex={0}
177
+ // mock the items
178
+ items={items}
179
+ role="listbox"
180
+ open={true}
181
+ // mock the opener elements
182
+ opener={<button />}
183
+ openerElement={null}
184
+ onOpenChanged={handleOpenChangedMock}
185
+ />,
186
+ );
187
+
188
+ // Act
189
+ // close the dropdown by pressing "Escape"
190
+ userEvent.keyboard("{escape}");
191
+
192
+ // Assert
193
+ expect(handleOpenChangedMock).toHaveBeenNthCalledWith(1, false);
194
+ });
195
+
196
+ it("closes on external mouse click", () => {
197
+ // Arrange
198
+ const handleOpenChangedMock = jest.fn();
199
+
200
+ const {container} = render(
201
+ <div>
202
+ <button data-test-id="external-button" />
203
+ <DropdownCore
204
+ initialFocusedIndex={0}
205
+ // mock the items
206
+ items={items}
207
+ role="listbox"
208
+ open={true}
209
+ // mock the opener elements
210
+ opener={<button />}
211
+ openerElement={null}
212
+ onOpenChanged={handleOpenChangedMock}
213
+ />
214
+ </div>,
215
+ );
216
+
217
+ // Act
218
+ // close the dropdown by clicking outside the dropdown
219
+ userEvent.click(container);
220
+
221
+ // Assert
222
+ expect(handleOpenChangedMock).toHaveBeenNthCalledWith(1, false);
223
+ });
224
+
225
+ it("doesn't close on external mouse click if already closed", () => {
226
+ // Arrange
227
+ const handleOpenChangedMock = jest.fn();
228
+
229
+ render(
230
+ <DropdownCore
231
+ initialFocusedIndex={0}
232
+ // mock the items
233
+ items={items}
234
+ role="listbox"
235
+ open={false}
236
+ // mock the opener elements
237
+ opener={<button />}
238
+ openerElement={null}
239
+ onOpenChanged={handleOpenChangedMock}
240
+ />,
241
+ );
242
+
243
+ // Act
244
+ // click outside the dropdown
245
+ userEvent.click(document.body);
246
+
247
+ // Assert
248
+ expect(handleOpenChangedMock).toHaveBeenCalledTimes(0);
249
+ });
250
+
251
+ it("opens on down key as expected", () => {
252
+ // Arrange
253
+ const handleOpenChangedMock = jest.fn();
254
+ const opener = <button data-test-id="opener" />;
255
+
256
+ render(
257
+ <DropdownCore
258
+ initialFocusedIndex={0}
259
+ // mock the items
260
+ items={items}
261
+ role="listbox"
262
+ open={false}
263
+ // mock the opener elements
264
+ opener={opener}
265
+ openerElement={null}
266
+ onOpenChanged={handleOpenChangedMock}
267
+ />,
268
+ );
269
+
270
+ const openerElement = screen.getByTestId("opener");
271
+ openerElement.focus();
272
+
273
+ // Act
274
+ userEvent.keyboard("{arrowdown}");
275
+
276
+ // Assert
277
+ expect(handleOpenChangedMock).toHaveBeenNthCalledWith(1, true);
278
+ });
279
+
280
+ it("selects correct item when starting off at an undefined index", () => {
281
+ // Arrange
282
+ render(
283
+ <DropdownCore
284
+ initialFocusedIndex={undefined}
285
+ // mock the items
286
+ items={items}
287
+ role="listbox"
288
+ open={true}
289
+ // mock the opener elements
290
+ opener={<button />}
291
+ openerElement={null}
292
+ onOpenChanged={jest.fn()}
293
+ />,
294
+ );
295
+
296
+ // Assert
297
+ expect(screen.queryByTestId("item-0")).toHaveFocus();
298
+ });
299
+
300
+ it("selects correct item when starting off at an undefined index and a searchbox", () => {
301
+ // Arrange
302
+ render(
303
+ <DropdownCore
304
+ initialFocusedIndex={undefined}
305
+ searchText=""
306
+ // mock the items
307
+ items={[
308
+ {
309
+ component: (
310
+ <SearchTextInput
311
+ testId="item-0"
312
+ key="search-text-input"
313
+ onChange={jest.fn()}
314
+ searchText={""}
315
+ />
316
+ ),
317
+ focusable: true,
318
+ populatedProps: {},
319
+ },
320
+ ]}
321
+ role="listbox"
322
+ open={true}
323
+ // mock the opener elements
324
+ opener={<button />}
325
+ openerElement={null}
326
+ onOpenChanged={jest.fn()}
327
+ />,
328
+ );
329
+
330
+ // Act
331
+ const firstItem = screen.queryByTestId("item-0");
332
+
333
+ // Assert
334
+ expect(firstItem).toHaveFocus();
335
+ });
336
+
337
+ it("selects correct item when starting off at a different index and a searchbox", () => {
338
+ // Arrange
339
+ render(
340
+ <DropdownCore
341
+ initialFocusedIndex={1}
342
+ searchText=""
343
+ // mock the items
344
+ items={[
345
+ {
346
+ component: (
347
+ <SearchTextInput
348
+ testId="search"
349
+ key="search-text-input"
350
+ onChange={jest.fn()}
351
+ searchText={""}
352
+ />
353
+ ),
354
+ focusable: true,
355
+ populatedProps: {},
356
+ },
357
+ {
358
+ component: (
359
+ <OptionItem
360
+ testId="item-0"
361
+ label="item 1"
362
+ value="1"
363
+ key="1"
364
+ />
365
+ ),
366
+ focusable: true,
367
+ populatedProps: {},
368
+ },
369
+ {
370
+ component: (
371
+ <OptionItem
372
+ testId="item-1"
373
+ label="item 2"
374
+ value="2"
375
+ key="2"
376
+ />
377
+ ),
378
+ focusable: true,
379
+ populatedProps: {},
380
+ },
381
+ ]}
382
+ role="listbox"
383
+ open={true}
384
+ // mock the opener elements
385
+ opener={<button />}
386
+ openerElement={null}
387
+ onOpenChanged={jest.fn()}
388
+ />,
389
+ );
390
+
391
+ // Act
392
+ const firstItem = screen.queryByTestId("item-0");
393
+
394
+ // Assert
395
+ expect(firstItem).toHaveFocus();
396
+ });
397
+
398
+ it("selects correct item when starting off at a different index", () => {
399
+ // Arrange
400
+ render(
401
+ <DropdownCore
402
+ initialFocusedIndex={2}
403
+ // mock the items
404
+ items={items}
405
+ role="listbox"
406
+ open={true}
407
+ // mock the opener elements
408
+ opener={<button />}
409
+ openerElement={null}
410
+ onOpenChanged={jest.fn()}
411
+ />,
412
+ );
413
+
414
+ // Act
415
+ // navigate down
416
+ userEvent.keyboard("{arrowdown}"); // 2 -> 0
417
+
418
+ // Assert
419
+ expect(screen.getByTestId("item-0")).toHaveFocus();
420
+ });
421
+
422
+ it("focuses correct item with clicking/pressing with initial focused of not 0", () => {
423
+ // Arrange
424
+ render(
425
+ <DropdownCore
426
+ initialFocusedIndex={2}
427
+ // mock the items
428
+ items={items}
429
+ role="listbox"
430
+ open={true}
431
+ // mock the opener elements
432
+ opener={<button />}
433
+ openerElement={null}
434
+ onOpenChanged={jest.fn()}
435
+ />,
436
+ );
437
+
438
+ // Act
439
+ userEvent.click(screen.getByTestId("item-1"));
440
+ // navigate down
441
+ userEvent.keyboard("{arrowdown}"); // 1 -> 2
442
+
443
+ // Assert
444
+ expect(screen.getByTestId("item-2")).toHaveFocus();
445
+ });
446
+
447
+ it("focuses correct item with a disabled item", () => {
448
+ // Arrange
449
+ render(
450
+ <DropdownCore
451
+ initialFocusedIndex={0}
452
+ // mock the items
453
+ items={[
454
+ {
455
+ component: (
456
+ <OptionItem
457
+ testId="item-0"
458
+ label="item 0"
459
+ value="0"
460
+ key="0"
461
+ />
462
+ ),
463
+ focusable: true,
464
+ populatedProps: {},
465
+ },
466
+ {
467
+ component: (
468
+ <OptionItem
469
+ testId="item-1"
470
+ label="item 1"
471
+ value="1"
472
+ key="1"
473
+ disabled={true}
474
+ />
475
+ ),
476
+ focusable: false,
477
+ populatedProps: {},
478
+ },
479
+ {
480
+ component: (
481
+ <OptionItem
482
+ testId="item-2"
483
+ label="item 2"
484
+ value="2"
485
+ key="2"
486
+ />
487
+ ),
488
+ focusable: true,
489
+ populatedProps: {},
490
+ },
491
+ ]}
492
+ role="listbox"
493
+ open={true}
494
+ // mock the opener elements
495
+ opener={<button data-test-id="opener" />}
496
+ openerElement={null}
497
+ onOpenChanged={jest.fn()}
498
+ />,
499
+ );
500
+
501
+ // Act
502
+ // navigate down
503
+ userEvent.keyboard("{arrowdown}"); // 0 -> 2 (1 is disabled)
504
+
505
+ // Assert
506
+ expect(screen.getByTestId("item-2")).toHaveFocus();
507
+ });
508
+
509
+ it("calls correct onclick for an option item", () => {
510
+ // Arrange
511
+ const onClick1 = jest.fn();
512
+ render(
40
513
  <DropdownCore
41
514
  initialFocusedIndex={0}
42
515
  // mock the items
@@ -44,10 +517,10 @@ describe("DropdownCore", () => {
44
517
  {
45
518
  component: (
46
519
  <OptionItem
47
- testId="item-0"
48
520
  label="item 0"
49
521
  value="0"
50
522
  key="0"
523
+ testId="item-0"
51
524
  />
52
525
  ),
53
526
  focusable: true,
@@ -56,10 +529,11 @@ describe("DropdownCore", () => {
56
529
  {
57
530
  component: (
58
531
  <OptionItem
59
- testId="item-1"
60
532
  label="item 1"
61
533
  value="1"
62
534
  key="1"
535
+ testId="item-1"
536
+ onClick={onClick1}
63
537
  />
64
538
  ),
65
539
  focusable: true,
@@ -68,8 +542,8 @@ describe("DropdownCore", () => {
68
542
  {
69
543
  component: (
70
544
  <OptionItem
71
- testId="item-2"
72
545
  label="item 2"
546
+ testId="item-2"
73
547
  value="2"
74
548
  key="2"
75
549
  />
@@ -79,682 +553,280 @@ describe("DropdownCore", () => {
79
553
  },
80
554
  ]}
81
555
  role="listbox"
82
- light={false}
83
- open={false}
556
+ open={true}
84
557
  // mock the opener elements
85
- opener={dummyOpener}
558
+ opener={<button />}
86
559
  openerElement={null}
87
- onOpenChanged={openChanged}
560
+ onOpenChanged={jest.fn()}
88
561
  />,
89
562
  );
90
- });
91
563
 
92
- afterEach(() => {
93
- // We have to explicitly unmount before clearing mocks, otherwise jest
94
- // timers will throw because we'll try to clear an animation frame that
95
- // we set with a setTimeout but are clearing with clearAnimationFrame
96
- // because we restored the clearAnimationFrame mock (and we won't
97
- // have cleared the timeout we actually set!)
98
- unmountAll();
99
- jest.restoreAllMocks();
564
+ // Act
565
+ userEvent.click(screen.getByTestId("item-1"));
566
+
567
+ // Assert
568
+ expect(onClick1).toHaveBeenCalledTimes(1);
100
569
  });
101
570
 
102
- it("handles basic keyboard navigation as expected", () => {
103
- const handleOpen = jest.fn();
104
- dropdown.setProps({
105
- initialFocusedIndex: 0,
106
- onOpenChanged: (open) => handleOpen(open),
107
- open: true,
108
- });
571
+ it("Displays no results when no items are left with filter", () => {
572
+ // Arrange
573
+ const handleSearchTextChanged = jest.fn();
109
574
 
110
- jest.runAllTimers();
111
- expect(document.activeElement).toBe(elementAtIndex(dropdown, 0));
575
+ // Act
576
+ render(
577
+ <DropdownCore
578
+ onSearchTextChanged={handleSearchTextChanged}
579
+ searchText="ab"
580
+ items={[
581
+ {
582
+ component: (
583
+ <SearchTextInput
584
+ key="search-text-input"
585
+ onChange={handleSearchTextChanged}
586
+ searchText={""}
587
+ />
588
+ ),
589
+ focusable: true,
590
+ populatedProps: {},
591
+ },
592
+ ]}
593
+ role="listbox"
594
+ open={true}
595
+ // mock the opener elements
596
+ opener={<button />}
597
+ openerElement={null}
598
+ onOpenChanged={jest.fn()}
599
+ />,
600
+ );
112
601
 
113
- // navigate down three times
114
- dropdown.simulate("keydown", {keyCode: keyCodes.down});
115
- dropdown.simulate("keyup", {keyCode: keyCodes.down});
116
- jest.runAllTimers();
117
- expect(document.activeElement).toBe(elementAtIndex(dropdown, 1));
602
+ // Assert
603
+ expect(
604
+ screen.getByTestId("dropdown-core-no-results"),
605
+ ).toBeInTheDocument();
606
+ });
118
607
 
119
- dropdown.simulate("keydown", {keyCode: keyCodes.down});
120
- dropdown.simulate("keyup", {keyCode: keyCodes.down});
121
- jest.runAllTimers();
122
- expect(document.activeElement).toBe(elementAtIndex(dropdown, 2));
608
+ it("SearchTextInput should be focused when opened", () => {
609
+ // Arrange
610
+ const handleSearchTextChanged = jest.fn();
611
+ const handleOpen = jest.fn();
123
612
 
124
- dropdown.simulate("keydown", {keyCode: keyCodes.down});
125
- dropdown.simulate("keyup", {keyCode: keyCodes.down});
126
- jest.runAllTimers();
127
- expect(document.activeElement).toBe(elementAtIndex(dropdown, 0));
613
+ // Act
614
+ render(
615
+ <DropdownCore
616
+ initialFocusedIndex={0}
617
+ onOpenChanged={handleOpen}
618
+ onSearchTextChanged={handleSearchTextChanged}
619
+ searchText="ab"
620
+ items={[
621
+ {
622
+ component: (
623
+ <SearchTextInput
624
+ testId="item-0"
625
+ key="search-text-input"
626
+ onChange={handleSearchTextChanged}
627
+ searchText={""}
628
+ />
629
+ ),
630
+ focusable: true,
631
+ populatedProps: {},
632
+ },
633
+ ]}
634
+ role="listbox"
635
+ open={true}
636
+ // mock the opener elements
637
+ opener={<button />}
638
+ openerElement={null}
639
+ />,
640
+ );
128
641
 
129
- // navigate up back two times
130
- // to last item
131
- dropdown.simulate("keydown", {keyCode: keyCodes.up});
132
- dropdown.simulate("keyup", {keyCode: keyCodes.up});
133
- jest.runAllTimers();
134
- expect(document.activeElement).toBe(elementAtIndex(dropdown, 2));
135
- // to the previous one
136
- dropdown.simulate("keydown", {keyCode: keyCodes.up});
137
- dropdown.simulate("keyup", {keyCode: keyCodes.up});
138
- jest.runAllTimers();
139
- expect(document.activeElement).toBe(elementAtIndex(dropdown, 1));
642
+ // Assert
643
+ expect(screen.getByPlaceholderText("Filter")).toHaveFocus();
140
644
  });
141
645
 
142
- it("doesn't close on touch interaction with option", () => {
646
+ it("When SearchTextInput has input and focused, tab key should not close the select", () => {
647
+ // Arrange
648
+ const handleSearchTextChanged = jest.fn();
143
649
  const handleOpen = jest.fn();
144
- dropdown.setProps({
145
- onOpenChanged: (open) => handleOpen(open),
146
- open: true,
147
- });
148
650
 
149
- const option0 = dropdown.find("OptionItem").at(0);
150
- // This is the full order of events fired when tapping an element
151
- // on mobile
152
- // https://developer.mozilla.org/en-US/docs/Web/API/Touch_events/Supporting_both_TouchEvent_and_MouseEvent#Event_order
153
- option0.simulate("touchstart");
154
- option0.simulate("touchend");
155
- option0.simulate("mousemove");
156
- option0.simulate("mousedown");
157
- option0.simulate("mouseup");
158
- option0.simulate("click");
651
+ render(
652
+ <DropdownCore
653
+ onOpenChanged={handleOpen}
654
+ onSearchTextChanged={handleSearchTextChanged}
655
+ searchText="ab"
656
+ items={[
657
+ {
658
+ component: (
659
+ <SearchTextInput
660
+ testId="item-0"
661
+ key="search-text-input"
662
+ onChange={handleSearchTextChanged}
663
+ searchText={""}
664
+ />
665
+ ),
666
+ focusable: true,
667
+ populatedProps: {},
668
+ },
669
+ ]}
670
+ role="listbox"
671
+ open={true}
672
+ // mock the opener elements
673
+ opener={<button />}
674
+ openerElement={null}
675
+ />,
676
+ );
677
+
678
+ // Act
679
+ userEvent.keyboard("{tab}");
159
680
 
681
+ // Assert
160
682
  expect(handleOpen).toHaveBeenCalledTimes(0);
683
+ expect(screen.getByTestId("item-0")).toHaveFocus();
161
684
  });
162
685
 
163
- it("closes on tab and escape as expected", () => {
164
- const handleOpen = jest.fn();
165
- dropdown.setProps({
166
- onOpenChanged: (open) => handleOpen(open),
167
- open: true,
168
- });
686
+ it("When SearchTextInput exists and focused, space key pressing should be allowed", () => {
687
+ // Arrange
688
+ const handleSearchTextChanged = jest.fn();
689
+ const preventDefaultMock = jest.fn();
169
690
 
170
- // "close" some menus
171
- dropdown.simulate("keydown", {keyCode: keyCodes.tab});
172
- dropdown.simulate("keyup", {keyCode: keyCodes.tab});
173
- dropdown.simulate("keydown", {keyCode: keyCodes.escape});
174
- dropdown.simulate("keyup", {keyCode: keyCodes.escape});
175
- expect(handleOpen).toHaveBeenCalledTimes(2);
176
- // Test that we pass "false" to handleOpenChanged both times
177
- expect(handleOpen.mock.calls[0][0]).toBe(false);
178
- expect(handleOpen.mock.calls[1][0]).toBe(false);
179
- });
691
+ render(
692
+ <DropdownCore
693
+ onSearchTextChanged={jest.fn()}
694
+ searchText="ab"
695
+ items={[
696
+ {
697
+ component: (
698
+ <SearchTextInput
699
+ testId="item-0"
700
+ key="search-text-input"
701
+ onChange={handleSearchTextChanged}
702
+ searchText={"ab"}
703
+ />
704
+ ),
705
+ focusable: true,
706
+ populatedProps: {},
707
+ },
708
+ ]}
709
+ role="listbox"
710
+ open={true}
711
+ // mock the opener elements
712
+ opener={<button />}
713
+ openerElement={null}
714
+ onOpenChanged={jest.fn()}
715
+ />,
716
+ );
180
717
 
181
- it("closes on external mouse click", () => {
182
- const handleOpen = jest.fn();
183
- dropdown.setProps({
184
- onOpenChanged: (open) => handleOpen(open),
185
- open: true,
718
+ // Act
719
+ const searchInput = screen.getByTestId("item-0");
720
+ // eslint-disable-next-line testing-library/prefer-user-event
721
+ fireEvent.keyDown(searchInput, {
722
+ keyCode: 32,
723
+ preventDefault: preventDefaultMock,
724
+ });
725
+ // eslint-disable-next-line testing-library/prefer-user-event
726
+ fireEvent.keyUp(searchInput, {
727
+ keyCode: 32,
728
+ preventDefault: preventDefaultMock,
186
729
  });
187
730
 
188
- const event = new MouseEvent("mouseup");
189
- document.dispatchEvent(event);
190
-
191
- expect(handleOpen).toHaveBeenCalledTimes(1);
192
- expect(handleOpen.mock.calls[0][0]).toBe(false);
731
+ // Assert
732
+ expect(preventDefaultMock).toHaveBeenCalledTimes(0);
193
733
  });
194
734
 
195
- it("closes on external mouse click on an element inside document.body", () => {
196
- const handleOpen = jest.fn();
197
- const container = document.createElement("container");
198
- if (!document.body) {
199
- throw new Error("No document.body");
200
- }
201
- document.body.appendChild(container);
202
-
203
- /**
204
- * According to https://stackoverflow.com/questions/36803733/jsdom-dispatchevent-addeventlistener-doesnt-seem-to-work
205
- * Enzyme uses renderIntoDocument from React.TestUtils which doesn't actually
206
- * render the component into document.body so testing behavior that relies
207
- * on bubbling won't work. This test works around this limitation by using
208
- * ReactDOM.render() to render our test component into a container that lives
209
- * in document.body.
210
- */
211
-
212
- ReactDOM.render(
213
- <div>
214
- <h1 id="foo">DropdownCore test</h1>
735
+ describe("VirtualizedList", () => {
736
+ const optionItems = new Array(200).fill(null).map((_, i) => ({
737
+ component: (
738
+ <OptionItem
739
+ key={i}
740
+ value={(i + 1).toString()}
741
+ label={`Fruit # ${i + 1}`}
742
+ testId={`item-${i}`}
743
+ />
744
+ ),
745
+ focusable: true,
746
+ populatedProps: {},
747
+ }));
748
+
749
+ it("should render a virtualized list of options and focus on the search field", async () => {
750
+ // Arrange
751
+ render(
215
752
  <DropdownCore
216
- initialFocusedIndex={0}
753
+ initialFocusedIndex={undefined}
754
+ onSearchTextChanged={jest.fn()}
755
+ searchText=""
217
756
  // mock the items
218
757
  items={[
219
758
  {
220
759
  component: (
221
- <OptionItem label="item 0" value="0" key="0" />
222
- ),
223
- focusable: true,
224
- populatedProps: {},
225
- },
226
- {
227
- component: (
228
- <OptionItem label="item 1" value="1" key="1" />
760
+ <SearchTextInput
761
+ testId="search-text-input"
762
+ key="search-text-input"
763
+ onChange={jest.fn()}
764
+ searchText={""}
765
+ />
229
766
  ),
230
767
  focusable: true,
231
768
  populatedProps: {},
232
769
  },
770
+ ...optionItems,
771
+ ]}
772
+ role="listbox"
773
+ open={true}
774
+ // mock the opener elements
775
+ opener={<button />}
776
+ openerElement={null}
777
+ onOpenChanged={jest.fn()}
778
+ />,
779
+ );
780
+
781
+ await screen.findByRole("listbox");
782
+
783
+ // Act
784
+ const searchField = await screen.findByPlaceholderText("Filter");
785
+
786
+ // Assert
787
+ expect(searchField).toHaveFocus();
788
+ });
789
+
790
+ it("should focus on the item after clicking on it", async () => {
791
+ // Arrange
792
+ render(
793
+ <DropdownCore
794
+ initialFocusedIndex={undefined}
795
+ onSearchTextChanged={jest.fn()}
796
+ searchText=""
797
+ // mock the items
798
+ items={[
233
799
  {
234
800
  component: (
235
- <OptionItem label="item 2" value="2" key="2" />
801
+ <SearchTextInput
802
+ testId="search-text-input"
803
+ key="search-text-input"
804
+ onChange={jest.fn()}
805
+ searchText={""}
806
+ />
236
807
  ),
237
808
  focusable: true,
238
809
  populatedProps: {},
239
810
  },
811
+ ...optionItems,
240
812
  ]}
241
813
  role="listbox"
242
- light={false}
243
814
  open={true}
244
815
  // mock the opener elements
245
816
  opener={<button />}
246
817
  openerElement={null}
247
- onOpenChanged={(open) => handleOpen(open)}
248
- />
249
- </div>,
250
- container,
251
- );
252
-
253
- const title = document.querySelector("#foo");
254
- if (!title) {
255
- throw new Error("Couldn't find title");
256
- }
257
- const event = new MouseEvent("mouseup", {bubbles: true});
258
- title.dispatchEvent(event);
259
-
260
- expect(handleOpen).toHaveBeenCalledTimes(1);
261
- expect(handleOpen.mock.calls[0][0]).toBe(false);
262
-
263
- // cleanup
264
- ReactDOM.unmountComponentAtNode(container);
265
- if (!document.body) {
266
- throw new Error("No document.body");
267
- }
268
- document.body.removeChild(container);
269
- });
270
-
271
- it("doesn't close on external mouse click if already closed", () => {
272
- const handleOpen = jest.fn();
273
- dropdown.setProps({
274
- onOpenChanged: (open) => handleOpen(open),
275
- open: false,
276
- });
277
-
278
- const event = new MouseEvent("mouseup");
279
- document.dispatchEvent(event);
280
-
281
- expect(handleOpen).toHaveBeenCalledTimes(0);
282
- });
283
-
284
- it("opens on down key as expected", () => {
285
- const handleOpen = jest.fn();
286
- dropdown.setProps({
287
- onOpenChanged: (open) => handleOpen(open),
288
- });
289
-
290
- dropdown.simulate("keydown", {keyCode: keyCodes.down});
291
- dropdown.simulate("keyup", {keyCode: keyCodes.down});
292
-
293
- expect(handleOpen).toHaveBeenCalledTimes(1);
294
- expect(handleOpen.mock.calls[0][0]).toBe(true);
295
- });
296
-
297
- it("selects correct item when starting off at an undefined index", () => {
298
- const handleOpen = jest.fn();
299
- dropdown.setProps({
300
- initialFocusedIndex: undefined,
301
- onOpenChanged: (open) => handleOpen(open),
302
- open: true,
303
- });
304
-
305
- jest.runAllTimers();
306
- expect(document.activeElement).toBe(elementAtIndex(dropdown, 0));
307
- });
308
-
309
- it("selects correct item when starting off at an undefined index and a searchbox", () => {
310
- const handleOpen = jest.fn();
311
- const handleSearchTextChanged = jest.fn();
312
- dropdown.setProps({
313
- initialFocusedIndex: undefined,
314
- onOpenChanged: (open) => handleOpen(open),
315
- searchText: "",
316
- items: [
317
- {
318
- component: (
319
- <SearchTextInput
320
- testId="item-0"
321
- key="search-text-input"
322
- onChange={handleSearchTextChanged}
323
- searchText={""}
324
- />
325
- ),
326
- focusable: true,
327
- populatedProps: {},
328
- },
329
- ],
330
- open: true,
331
- });
332
- jest.runAllTimers();
333
- expect(document.activeElement).toBe(elementAtIndex(dropdown, 0));
334
- });
335
-
336
- it("selects correct item when starting off at a different index and a searchbox", () => {
337
- const handleOpen = jest.fn();
338
- const handleSearchTextChanged = jest.fn();
339
- dropdown.setProps({
340
- initialFocusedIndex: 1,
341
- onOpenChanged: (open) => handleOpen(open),
342
- searchText: "",
343
- items: [
344
- {
345
- component: (
346
- <SearchTextInput
347
- testId="search"
348
- key="search-text-input"
349
- onChange={handleSearchTextChanged}
350
- searchText={""}
351
- />
352
- ),
353
- focusable: true,
354
- populatedProps: {},
355
- },
356
- {
357
- component: (
358
- <OptionItem
359
- testId="item-0"
360
- label="item 1"
361
- value="1"
362
- key="1"
363
- />
364
- ),
365
- focusable: false,
366
- populatedProps: {},
367
- },
368
- {
369
- component: (
370
- <OptionItem
371
- testId="item-1"
372
- label="item 2"
373
- value="2"
374
- key="2"
375
- />
376
- ),
377
- focusable: true,
378
- populatedProps: {},
379
- },
380
- ],
381
- open: true,
382
- });
383
- jest.runAllTimers();
384
- expect(document.activeElement).toBe(elementAtIndex(dropdown, 1));
385
- });
386
-
387
- it("selects correct item when starting off at a different index", () => {
388
- const handleOpen = jest.fn();
389
- dropdown.setProps({
390
- initialFocusedIndex: 2,
391
- onOpenChanged: (open) => handleOpen(open),
392
- open: true,
393
- });
394
-
395
- jest.runAllTimers();
396
- expect(document.activeElement).toBe(elementAtIndex(dropdown, 2));
397
-
398
- // navigate down
399
- dropdown.simulate("keydown", {keyCode: keyCodes.down});
400
- dropdown.simulate("keyup", {keyCode: keyCodes.down});
401
- jest.runAllTimers();
402
- expect(document.activeElement).toBe(elementAtIndex(dropdown, 0));
403
- });
404
-
405
- it("focuses correct item with clicking/pressing with initial focused of not 0", () => {
406
- // Same as the previous test, expect initialFocusedIndex is 2 now
407
- dropdown.setProps({
408
- initialFocusedIndex: 2,
409
- open: true,
410
- });
411
-
412
- const option1 = dropdown.find("OptionItem").at(1);
413
-
414
- // Click on item at index 1
415
- option1.simulate("click");
416
-
417
- // should move to next item
418
- dropdown.simulate("keydown", {keyCode: keyCodes.down});
419
- dropdown.simulate("keyup", {keyCode: keyCodes.down});
420
- jest.runAllTimers();
421
- expect(document.activeElement).toBe(elementAtIndex(dropdown, 2));
422
- });
423
-
424
- it("focuses correct item with a disabled item", () => {
425
- dropdown.setProps({
426
- initialFocusedIndex: 0,
427
- items: [
428
- {
429
- component: (
430
- <OptionItem
431
- testId="item-0"
432
- label="item 0"
433
- value="0"
434
- key="0"
435
- />
436
- ),
437
- focusable: true,
438
- populatedProps: {},
439
- },
440
- {
441
- component: (
442
- <OptionItem
443
- testId="item-1"
444
- label="item 1"
445
- value="1"
446
- key="1"
447
- />
448
- ),
449
- focusable: false,
450
- populatedProps: {},
451
- },
452
- {
453
- component: (
454
- <OptionItem
455
- testId="item-2"
456
- label="item 2"
457
- value="2"
458
- key="2"
459
- />
460
- ),
461
- focusable: true,
462
- populatedProps: {},
463
- },
464
- ],
465
- open: true,
466
- });
467
-
468
- jest.runAllTimers();
469
- expect(document.activeElement).toBe(elementAtIndex(dropdown, 0));
470
-
471
- // Should select option2
472
- dropdown.simulate("keydown", {keyCode: keyCodes.down});
473
- dropdown.simulate("keyup", {keyCode: keyCodes.down});
474
- jest.runAllTimers();
475
- expect(document.activeElement).toBe(elementAtIndex(dropdown, 2));
476
-
477
- // Should select option0
478
- dropdown.simulate("keydown", {keyCode: keyCodes.down});
479
- dropdown.simulate("keyup", {keyCode: keyCodes.down});
480
- jest.runAllTimers();
481
- expect(document.activeElement).toBe(elementAtIndex(dropdown, 0));
482
- });
483
-
484
- it("focuses correct item after different items become focusable", () => {
485
- dropdown.setProps({
486
- initialFocusedIndex: 0,
487
- open: true,
488
- });
489
-
490
- jest.runAllTimers();
491
- expect(document.activeElement).toBe(elementAtIndex(dropdown, 0));
492
-
493
- // Should select option1
494
- dropdown.simulate("keydown", {keyCode: keyCodes.down});
495
- dropdown.simulate("keyup", {keyCode: keyCodes.down});
496
- jest.runAllTimers();
497
- expect(document.activeElement).toBe(elementAtIndex(dropdown, 1));
498
-
499
- dropdown.setProps({
500
- items: [
501
- {
502
- component: (
503
- <OptionItem
504
- testId="item-0"
505
- label="item 0"
506
- value="0"
507
- key="0"
508
- />
509
- ),
510
- focusable: false,
511
- populatedProps: {},
512
- },
513
- {
514
- component: (
515
- <OptionItem
516
- testId="item-1"
517
- label="item 1"
518
- value="1"
519
- key="1"
520
- />
521
- ),
522
- focusable: true,
523
- populatedProps: {},
524
- },
525
- {
526
- component: (
527
- <OptionItem
528
- testId="item-2"
529
- label="item 2"
530
- value="2"
531
- key="2"
532
- />
533
- ),
534
- focusable: true,
535
- populatedProps: {},
536
- },
537
- ],
538
- });
539
-
540
- // Should figure out that option1, which is now at index 0, is selected
541
- jest.runAllTimers();
542
- expect(document.activeElement).toBe(elementAtIndex(dropdown, 1));
543
- });
544
-
545
- it("focuses first item after currently focused item is no longer focusable", () => {
546
- dropdown.setProps({
547
- initialFocusedIndex: 0,
548
- open: true,
549
- });
550
-
551
- jest.runAllTimers();
552
- expect(document.activeElement).toBe(elementAtIndex(dropdown, 0));
553
-
554
- const option0 = dropdown.find("OptionItem").at(0);
555
- option0.simulate("click");
556
- jest.runAllTimers();
557
- expect(document.activeElement).toBe(elementAtIndex(dropdown, 0));
558
-
559
- dropdown.setProps({
560
- items: [
561
- {
562
- component: (
563
- <OptionItem
564
- testId="item-0"
565
- label="item 0"
566
- value="0"
567
- key="0"
568
- />
569
- ),
570
- focusable: false,
571
- populatedProps: {},
572
- },
573
- {
574
- component: (
575
- <OptionItem
576
- testId="item-1"
577
- label="item 1"
578
- value="1"
579
- key="1"
580
- />
581
- ),
582
- focusable: true,
583
- populatedProps: {},
584
- },
585
- {
586
- component: (
587
- <OptionItem
588
- testId="item-2"
589
- label="item 2"
590
- value="2"
591
- key="2"
592
- />
593
- ),
594
- focusable: true,
595
- populatedProps: {},
596
- },
597
- ],
598
- });
599
-
600
- // Should figure out that option1 is now selected
601
- jest.runAllTimers();
602
- expect(document.activeElement).toBe(elementAtIndex(dropdown, 1));
603
- });
604
-
605
- it("calls correct onclick for an option item", () => {
606
- const onClick0 = jest.fn();
607
- const onClick1 = jest.fn();
608
- dropdown.setProps({
609
- items: [
610
- {
611
- component: (
612
- <OptionItem
613
- label="item 0"
614
- value="0"
615
- key="0"
616
- onClick={onClick0}
617
- />
618
- ),
619
- focusable: true,
620
- populatedProps: {},
621
- },
622
- {
623
- component: (
624
- <OptionItem
625
- label="item 1"
626
- value="1"
627
- key="1"
628
- onClick={onClick1}
629
- />
630
- ),
631
- focusable: true,
632
- populatedProps: {},
633
- },
634
- {
635
- component: <OptionItem label="item 2" value="2" key="2" />,
636
- focusable: true,
637
- populatedProps: {},
638
- },
639
- ],
640
- open: true,
641
- });
642
-
643
- const option1 = dropdown.find("OptionItem").at(1);
644
-
645
- option1.simulate("click");
646
- expect(onClick1).toHaveBeenCalledTimes(1);
647
- });
648
-
649
- it("Displays no results when no items are left with filter", () => {
650
- // Arrange
651
- const handleSearchTextChanged = jest.fn();
818
+ onOpenChanged={jest.fn()}
819
+ />,
820
+ );
652
821
 
653
- // Act
654
- dropdown.setProps({
655
- onSearchTextChanged: (text) => handleSearchTextChanged(text),
656
- searchText: "ab",
657
- items: [
658
- {
659
- component: (
660
- <SearchTextInput
661
- key="search-text-input"
662
- onChange={handleSearchTextChanged}
663
- searchText={""}
664
- />
665
- ),
666
- focusable: true,
667
- populatedProps: {},
668
- },
669
- ],
670
- open: true,
671
- });
672
-
673
- // Assert
674
- expect(dropdown).toContainMatchingElement(
675
- `[data-test-id="dropdown-core-no-results"]`,
676
- );
677
- });
678
-
679
- it("When SearchTextInput has input and focused, tab key should not close the select", () => {
680
- // Arrange
681
- const handleSearchTextChanged = jest.fn();
682
- const handleOpen = jest.fn();
683
-
684
- dropdown.setProps({
685
- onOpenChanged: (open) => handleOpen(open),
686
- onSearchTextChanged: (text) => handleSearchTextChanged(text),
687
- searchText: "ab",
688
- open: true,
689
- items: [
690
- {
691
- component: (
692
- <SearchTextInput
693
- testId="item-0"
694
- key="search-text-input"
695
- onChange={handleSearchTextChanged}
696
- searchText={""}
697
- />
698
- ),
699
- focusable: true,
700
- populatedProps: {},
701
- },
702
- ],
703
- });
704
- // SearchTextInput should be focused
705
- const searchInput = dropdown.find(SearchTextInput);
706
- jest.runAllTimers();
707
-
708
- expect(searchInput.state("focused")).toBe(true);
709
-
710
- // Act
711
- dropdown.simulate("keydown", {keyCode: keyCodes.tab});
712
- jest.runAllTimers();
822
+ await screen.findByRole("listbox");
713
823
 
714
- // Assert
715
- expect(handleOpen).toHaveBeenCalledTimes(0);
716
- expect(document.activeElement).toBe(elementAtIndex(dropdown, 0));
717
- });
718
-
719
- it("When SearchTextInput exists and focused, space key pressing should be allowed", () => {
720
- // Arrange
721
- const handleSearchTextChanged = jest.fn();
722
- const preventDefaultMock = jest.fn();
723
- dropdown.setProps({
724
- onSearchTextChanged: (text) => handleSearchTextChanged(text),
725
- searchText: "",
726
- items: [
727
- {
728
- component: (
729
- <SearchTextInput
730
- testId="item-0"
731
- key="search-text-input"
732
- onChange={handleSearchTextChanged}
733
- searchText={""}
734
- />
735
- ),
736
- focusable: true,
737
- populatedProps: {},
738
- },
739
- ],
740
- open: true,
741
- });
742
- // SearchTextInput should be focused
743
- const searchInput = dropdown.find(SearchTextInput).find("input");
744
- jest.runAllTimers();
745
- expect(document.activeElement).toBe(elementAtIndex(dropdown, 0));
824
+ // Act
825
+ const item = await screen.findByTestId("item-1");
826
+ userEvent.click(item);
746
827
 
747
- // Act
748
- searchInput.simulate("keydown", {
749
- keyCode: keyCodes.space,
750
- preventDefault: preventDefaultMock,
751
- });
752
- searchInput.simulate("keyup", {
753
- keyCode: keyCodes.space,
754
- preventDefault: preventDefaultMock,
828
+ // Assert
829
+ expect(item).toHaveFocus();
755
830
  });
756
-
757
- // Assert
758
- expect(preventDefaultMock).toHaveBeenCalledTimes(0);
759
831
  });
760
832
  });