@khanacademy/wonder-blocks-clickable 2.1.2

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.
@@ -0,0 +1,1313 @@
1
+ /* eslint-disable max-lines */
2
+ // @flow
3
+ import * as React from "react";
4
+ import {MemoryRouter, Switch, Route} from "react-router-dom";
5
+ import {mount, shallow} from "enzyme";
6
+
7
+ import getClickableBehavior from "../../util/get-clickable-behavior.js";
8
+ import ClickableBehavior from "../clickable-behavior.js";
9
+
10
+ const keyCodes = {
11
+ tab: 9,
12
+ enter: 13,
13
+ space: 32,
14
+ };
15
+
16
+ const wait = (delay: number = 0) =>
17
+ new Promise((resolve, reject) => {
18
+ // eslint-disable-next-line no-restricted-syntax
19
+ return setTimeout(resolve, delay);
20
+ });
21
+
22
+ describe("ClickableBehavior", () => {
23
+ beforeEach(() => {
24
+ // Note: window.location.assign and window.open need mock functions in
25
+ // the testing environment.
26
+ window.location.assign = jest.fn();
27
+ window.open = jest.fn();
28
+ });
29
+
30
+ afterEach(() => {
31
+ window.location.assign.mockClear();
32
+ window.open.mockClear();
33
+ });
34
+
35
+ it("renders a label", () => {
36
+ const onClick = jest.fn();
37
+ const button = shallow(
38
+ <ClickableBehavior disabled={false} onClick={(e) => onClick(e)}>
39
+ {(state, childrenProps) => {
40
+ return <button {...childrenProps}>Label</button>;
41
+ }}
42
+ </ClickableBehavior>,
43
+ );
44
+ expect(onClick).not.toHaveBeenCalled();
45
+ button.simulate("click", {preventDefault: jest.fn()});
46
+ expect(onClick).toHaveBeenCalled();
47
+ });
48
+
49
+ it("changes only hovered state on mouse enter/leave", () => {
50
+ const onClick = jest.fn();
51
+ const button = shallow(
52
+ <ClickableBehavior disabled={false} onClick={(e) => onClick(e)}>
53
+ {(state, childrenProps) => {
54
+ return <button {...childrenProps}>Label</button>;
55
+ }}
56
+ </ClickableBehavior>,
57
+ );
58
+ expect(button.state("hovered")).toEqual(false);
59
+ button.simulate("mouseenter", {
60
+ buttons: 0,
61
+ });
62
+ expect(button.state("hovered")).toEqual(true);
63
+ button.simulate("mouseleave");
64
+ expect(button.state("hovered")).toEqual(false);
65
+ });
66
+
67
+ it("changes only pressed state on mouse enter/leave while dragging", () => {
68
+ const onClick = jest.fn();
69
+ const button = shallow(
70
+ <ClickableBehavior disabled={false} onClick={(e) => onClick(e)}>
71
+ {(state, childrenProps) => {
72
+ return <button {...childrenProps}>Label</button>;
73
+ }}
74
+ </ClickableBehavior>,
75
+ );
76
+ expect(button.state("pressed")).toEqual(false);
77
+
78
+ button.simulate("mousedown");
79
+ button.simulate("dragstart", {preventDefault: jest.fn()});
80
+ expect(button.state("pressed")).toEqual(true);
81
+
82
+ button.simulate("mouseleave");
83
+ expect(button.state("pressed")).toEqual(false);
84
+
85
+ button.simulate("mouseenter", {
86
+ buttons: 1,
87
+ });
88
+ expect(button.state("pressed")).toEqual(true);
89
+ });
90
+
91
+ it("changes pressed state on mouse down/up", () => {
92
+ const onClick = jest.fn();
93
+ const button = shallow(
94
+ <ClickableBehavior disabled={false} onClick={(e) => onClick(e)}>
95
+ {(state, childrenProps) => {
96
+ return <button {...childrenProps}>Label</button>;
97
+ }}
98
+ </ClickableBehavior>,
99
+ );
100
+ expect(button.state("pressed")).toEqual(false);
101
+ button.simulate("mousedown");
102
+ expect(button.state("pressed")).toEqual(true);
103
+ button.simulate("mouseup");
104
+ expect(button.state("pressed")).toEqual(false);
105
+ });
106
+
107
+ it("changes pressed state on touch start/end/cancel", () => {
108
+ const onClick = jest.fn();
109
+ const button = shallow(
110
+ <ClickableBehavior disabled={false} onClick={(e) => onClick(e)}>
111
+ {(state, childrenProps) => {
112
+ return <button {...childrenProps}>Label</button>;
113
+ }}
114
+ </ClickableBehavior>,
115
+ );
116
+ expect(button.state("pressed")).toEqual(false);
117
+ button.simulate("touchstart");
118
+ expect(button.state("pressed")).toEqual(true);
119
+ button.simulate("touchend");
120
+ expect(button.state("pressed")).toEqual(false);
121
+
122
+ expect(button.state("pressed")).toEqual(false);
123
+ button.simulate("touchstart");
124
+ expect(button.state("pressed")).toEqual(true);
125
+ button.simulate("touchcancel");
126
+ expect(button.state("pressed")).toEqual(false);
127
+ });
128
+
129
+ it("enters focused state on key press after click", () => {
130
+ const onClick = jest.fn();
131
+ const button = shallow(
132
+ <ClickableBehavior disabled={false} onClick={(e) => onClick(e)}>
133
+ {(state, childrenProps) => {
134
+ return <button {...childrenProps}>Label</button>;
135
+ }}
136
+ </ClickableBehavior>,
137
+ );
138
+ expect(button.state("focused")).toEqual(false);
139
+ button.simulate("keydown", {
140
+ keyCode: keyCodes.space,
141
+ preventDefault: jest.fn(),
142
+ });
143
+ button.simulate("keyup", {
144
+ keyCode: keyCodes.space,
145
+ preventDefault: jest.fn(),
146
+ });
147
+ button.simulate("click", {preventDefault: jest.fn()});
148
+ expect(button.state("focused")).toEqual(true);
149
+ });
150
+
151
+ it("exits focused state on click after key press", () => {
152
+ const onClick = jest.fn();
153
+
154
+ const button = shallow(
155
+ <ClickableBehavior disabled={false} onClick={(e) => onClick(e)}>
156
+ {(state, childrenProps) => {
157
+ return <button {...childrenProps}>Label</button>;
158
+ }}
159
+ </ClickableBehavior>,
160
+ );
161
+ expect(button.state("focused")).toEqual(false);
162
+ button.simulate("keydown", {
163
+ keyCode: keyCodes.space,
164
+ preventDefault: jest.fn(),
165
+ });
166
+ button.simulate("keyup", {
167
+ keyCode: keyCodes.space,
168
+ preventDefault: jest.fn(),
169
+ });
170
+ button.simulate("click", {preventDefault: jest.fn()});
171
+ expect(button.state("focused")).toEqual(true);
172
+ button.simulate("mousedown");
173
+ button.simulate("mouseup");
174
+ button.simulate("click", {preventDefault: jest.fn()});
175
+ expect(button.state("focused")).toEqual(false);
176
+ });
177
+
178
+ it("changes pressed state on space/enter key down/up if <button>", () => {
179
+ const onClick = jest.fn();
180
+ const button = shallow(
181
+ <ClickableBehavior disabled={false} onClick={(e) => onClick(e)}>
182
+ {(state, childrenProps) => {
183
+ return <button {...childrenProps}>Label</button>;
184
+ }}
185
+ </ClickableBehavior>,
186
+ );
187
+ expect(button.state("pressed")).toEqual(false);
188
+ button.simulate("keydown", {
189
+ keyCode: keyCodes.space,
190
+ preventDefault: jest.fn(),
191
+ });
192
+ expect(button.state("pressed")).toEqual(true);
193
+ button.simulate("keyup", {
194
+ keyCode: keyCodes.space,
195
+ preventDefault: jest.fn(),
196
+ });
197
+ expect(button.state("pressed")).toEqual(false);
198
+
199
+ button.simulate("keydown", {
200
+ keyCode: keyCodes.enter,
201
+ preventDefault: jest.fn(),
202
+ });
203
+ expect(button.state("pressed")).toEqual(true);
204
+ button.simulate("keyup", {
205
+ preventDefault: jest.fn(),
206
+ keyCode: keyCodes.enter,
207
+ });
208
+ expect(button.state("pressed")).toEqual(false);
209
+ });
210
+
211
+ it("changes pressed state on only enter key down/up for a link", () => {
212
+ const onClick = jest.fn();
213
+ // Use mount instead of a shallow render to trigger event defaults
214
+ const link = mount(
215
+ <ClickableBehavior
216
+ disabled={false}
217
+ onClick={(e) => onClick(e)}
218
+ href="https://www.khanacademy.org"
219
+ role="link"
220
+ >
221
+ {(state, childrenProps) => {
222
+ return (
223
+ <a
224
+ href="https://www.khanacademy.org"
225
+ {...childrenProps}
226
+ >
227
+ Label
228
+ </a>
229
+ );
230
+ }}
231
+ </ClickableBehavior>,
232
+ );
233
+ expect(link.state("pressed")).toEqual(false);
234
+ link.simulate("keydown", {keyCode: keyCodes.enter});
235
+ expect(link.state("pressed")).toEqual(true);
236
+ link.simulate("keyup", {
237
+ preventDefault: jest.fn(),
238
+ keyCode: keyCodes.enter,
239
+ });
240
+ expect(link.state("pressed")).toEqual(false);
241
+
242
+ link.simulate("keydown", {keyCode: keyCodes.space});
243
+ expect(link.state("pressed")).toEqual(false);
244
+ link.simulate("keyup", {
245
+ preventDefault: jest.fn(),
246
+ keyCode: keyCodes.space,
247
+ });
248
+ expect(link.state("pressed")).toEqual(false);
249
+ });
250
+
251
+ it("gains focused state on focus event", () => {
252
+ const onClick = jest.fn();
253
+ const button = shallow(
254
+ <ClickableBehavior disabled={false} onClick={(e) => onClick(e)}>
255
+ {(state, childrenProps) => {
256
+ return <button {...childrenProps}>Label</button>;
257
+ }}
258
+ </ClickableBehavior>,
259
+ );
260
+ button.simulate("focus");
261
+ expect(button.state("focused")).toEqual(true);
262
+ });
263
+
264
+ it("changes focused state on blur", () => {
265
+ const onClick = jest.fn();
266
+ const button = shallow(
267
+ <ClickableBehavior disabled={false} onClick={(e) => onClick(e)}>
268
+ {(state, childrenProps) => {
269
+ return <button {...childrenProps}>Label</button>;
270
+ }}
271
+ </ClickableBehavior>,
272
+ );
273
+ button.simulate("blur");
274
+ expect(button.state("focused")).toEqual(false);
275
+ });
276
+
277
+ it("does not change state if disabled", () => {
278
+ const onClick = jest.fn();
279
+ const button = shallow(
280
+ <ClickableBehavior disabled={true} onClick={(e) => onClick(e)}>
281
+ {(state, childrenProps) => {
282
+ return <button {...childrenProps}>Label</button>;
283
+ }}
284
+ </ClickableBehavior>,
285
+ );
286
+
287
+ expect(onClick).not.toHaveBeenCalled();
288
+ button.simulate("click", {preventDefault: jest.fn()});
289
+ expect(onClick).not.toHaveBeenCalled();
290
+
291
+ expect(button.state("hovered")).toEqual(false);
292
+ button.simulate("mouseenter", {
293
+ buttons: 0,
294
+ });
295
+ expect(button.state("hovered")).toEqual(false);
296
+ button.simulate("mouseleave");
297
+ expect(button.state("hovered")).toEqual(false);
298
+
299
+ expect(button.state("pressed")).toEqual(false);
300
+ button.simulate("mousedown");
301
+ expect(button.state("pressed")).toEqual(false);
302
+ button.simulate("mouseup");
303
+ expect(button.state("pressed")).toEqual(false);
304
+
305
+ expect(button.state("pressed")).toEqual(false);
306
+ button.simulate("touchstart");
307
+ expect(button.state("pressed")).toEqual(false);
308
+ button.simulate("touchend");
309
+ expect(button.state("pressed")).toEqual(false);
310
+
311
+ button.simulate("touchstart");
312
+ button.simulate("touchcancel");
313
+ expect(button.state("pressed")).toEqual(false);
314
+
315
+ expect(button.state("focused")).toEqual(false);
316
+ button.simulate("keyup", {
317
+ preventDefault: jest.fn(),
318
+ keyCode: keyCodes.tab,
319
+ });
320
+ expect(button.state("focused")).toEqual(false);
321
+ button.simulate("keydown", {keyCode: keyCodes.tab});
322
+ expect(button.state("focused")).toEqual(false);
323
+
324
+ expect(button.state("pressed")).toEqual(false);
325
+ button.simulate("keydown", {keyCode: keyCodes.space});
326
+ expect(button.state("pressed")).toEqual(false);
327
+ button.simulate("keyup", {
328
+ preventDefault: jest.fn(),
329
+ keyCode: keyCodes.space,
330
+ });
331
+ expect(button.state("pressed")).toEqual(false);
332
+
333
+ button.simulate("keydown", {keyCode: keyCodes.space});
334
+ button.simulate("blur");
335
+ expect(button.state("pressed")).toEqual(false);
336
+
337
+ button.simulate("focus");
338
+ expect(button.state("focused")).toEqual(false);
339
+
340
+ const anchor = shallow(
341
+ <ClickableBehavior
342
+ disabled={true}
343
+ href="https://www.khanacademy.org"
344
+ >
345
+ {(state, childrenProps) => {
346
+ return (
347
+ <a
348
+ href="https://www.khanacademy.org"
349
+ {...childrenProps}
350
+ >
351
+ Label
352
+ </a>
353
+ );
354
+ }}
355
+ </ClickableBehavior>,
356
+ );
357
+
358
+ expect(anchor.state("pressed")).toEqual(false);
359
+ anchor.simulate("keydown", {keyCode: keyCodes.enter});
360
+ expect(anchor.state("pressed")).toEqual(false);
361
+ anchor.simulate("keyup", {
362
+ preventDefault: jest.fn(),
363
+ keyCode: keyCodes.enter,
364
+ });
365
+ expect(anchor.state("pressed")).toEqual(false);
366
+ });
367
+
368
+ it("has onClick triggered just once per click by various means", () => {
369
+ const onClick = jest.fn();
370
+ const button = shallow(
371
+ <ClickableBehavior disabled={false} onClick={(e) => onClick(e)}>
372
+ {(state, childrenProps) => {
373
+ return <button {...childrenProps}>Label</button>;
374
+ }}
375
+ </ClickableBehavior>,
376
+ );
377
+ expect(onClick).not.toHaveBeenCalled();
378
+
379
+ button.simulate("mousedown");
380
+ button.simulate("mouseup");
381
+ button.simulate("click", {preventDefault: jest.fn()});
382
+ expect(onClick).toHaveBeenCalledTimes(1);
383
+
384
+ button.simulate("keydown", {
385
+ keyCode: keyCodes.space,
386
+ preventDefault: jest.fn(),
387
+ });
388
+ button.simulate("keyup", {
389
+ keyCode: keyCodes.space,
390
+ preventDefault: jest.fn(),
391
+ });
392
+ expect(onClick).toHaveBeenCalledTimes(2);
393
+
394
+ button.simulate("keydown", {
395
+ keyCode: keyCodes.enter,
396
+ preventDefault: jest.fn(),
397
+ });
398
+ button.simulate("keyup", {
399
+ preventDefault: jest.fn(),
400
+ keyCode: keyCodes.enter,
401
+ });
402
+ expect(onClick).toHaveBeenCalledTimes(3);
403
+
404
+ button.simulate("touchstart", {keyCode: keyCodes.space});
405
+ button.simulate("touchend", {keyCode: keyCodes.space});
406
+ button.simulate("click", {preventDefault: jest.fn()});
407
+ expect(onClick).toHaveBeenCalledTimes(4);
408
+ });
409
+
410
+ it("resets state when set to disabled", () => {
411
+ const onClick = jest.fn();
412
+ const button = shallow(
413
+ <ClickableBehavior disabled={false} onClick={(e) => onClick(e)}>
414
+ {(state, childrenProps) => {
415
+ return <button {...childrenProps}>Label</button>;
416
+ }}
417
+ </ClickableBehavior>,
418
+ );
419
+ button.setState({hovered: true, pressed: true, focused: true});
420
+ button.setProps({disabled: true});
421
+
422
+ expect(button.state("hovered")).toEqual(false);
423
+ expect(button.state("pressed")).toEqual(false);
424
+ expect(button.state("focused")).toEqual(false);
425
+ });
426
+
427
+ describe("full page load navigation", () => {
428
+ it("both navigates and calls onClick for an anchor link", () => {
429
+ const onClick = jest.fn();
430
+ // Use mount instead of a shallow render to trigger event defaults
431
+ const link = mount(
432
+ <ClickableBehavior
433
+ href="https://khanacademy.org/"
434
+ onClick={(e) => onClick(e)}
435
+ role="link"
436
+ >
437
+ {(state, childrenProps) => {
438
+ // The base element here doesn't matter in this testing
439
+ // environment, but the simulated events in the test are in
440
+ // line with what browsers do for this element.
441
+ return (
442
+ <a
443
+ href="https://khanacademy.org/"
444
+ {...childrenProps}
445
+ >
446
+ Label
447
+ </a>
448
+ );
449
+ }}
450
+ </ClickableBehavior>,
451
+ );
452
+
453
+ // Space press should not trigger the onClick
454
+ link.simulate("keydown", {keyCode: keyCodes.space});
455
+ link.simulate("keyup", {
456
+ preventDefault: jest.fn(),
457
+ keyCode: keyCodes.space,
458
+ });
459
+ expect(onClick).toHaveBeenCalledTimes(0);
460
+
461
+ // Navigation didn't happen with space
462
+ expect(window.location.assign).toHaveBeenCalledTimes(0);
463
+
464
+ // Enter press should trigger the onClick after keyup
465
+ link.simulate("keydown", {keyCode: keyCodes.enter});
466
+ expect(onClick).toHaveBeenCalledTimes(0);
467
+
468
+ // Navigation doesn't happen until after enter is released
469
+ expect(window.location.assign).toHaveBeenCalledTimes(0);
470
+
471
+ link.simulate("keyup", {
472
+ preventDefault: jest.fn(),
473
+ keyCode: keyCodes.enter,
474
+ });
475
+ expect(onClick).toHaveBeenCalledTimes(1);
476
+
477
+ // Navigation happened after enter click
478
+ expect(window.location.assign).toHaveBeenCalledTimes(1);
479
+ });
480
+
481
+ it("waits for safeWithNav to resolve before navigation", async () => {
482
+ // Arrange
483
+ const link = mount(
484
+ <ClickableBehavior
485
+ href="https://khanacademy.org/"
486
+ safeWithNav={() => Promise.resolve()}
487
+ role="link"
488
+ >
489
+ {(state, childrenProps) => {
490
+ // The base element here doesn't matter in this testing
491
+ // environment, but the simulated events in the test are in
492
+ // line with what browsers do for this element.
493
+ return (
494
+ <a
495
+ href="https://khanacademy.org/"
496
+ {...childrenProps}
497
+ >
498
+ Label
499
+ </a>
500
+ );
501
+ }}
502
+ </ClickableBehavior>,
503
+ );
504
+
505
+ // Act
506
+ link.simulate("click", {preventDefault: jest.fn()});
507
+ await wait(0);
508
+
509
+ // Assert
510
+ expect(window.location.assign).toHaveBeenCalledTimes(1);
511
+ });
512
+
513
+ it("should show waiting UI before safeWithNav resolves", async () => {
514
+ // Arrange
515
+ const link = mount(
516
+ <ClickableBehavior
517
+ href="https://khanacademy.org/"
518
+ safeWithNav={() => Promise.resolve()}
519
+ role="link"
520
+ >
521
+ {(state, childrenProps) => {
522
+ // The base element here doesn't matter in this testing
523
+ // environment, but the simulated events in the test are in
524
+ // line with what browsers do for this element.
525
+ return (
526
+ <a
527
+ href="https://khanacademy.org/"
528
+ {...childrenProps}
529
+ >
530
+ {state.waiting ? "waiting" : "Label"}
531
+ </a>
532
+ );
533
+ }}
534
+ </ClickableBehavior>,
535
+ );
536
+
537
+ // Act
538
+ link.simulate("click", {preventDefault: jest.fn()});
539
+
540
+ // Assert
541
+ expect(link).toIncludeText("waiting");
542
+ });
543
+
544
+ it("If onClick calls e.preventDefault() then we won't navigate", () => {
545
+ // Arrange
546
+ const wrapper = mount(
547
+ <ClickableBehavior
548
+ href="/foo"
549
+ onClick={(e) => e.preventDefault()}
550
+ role="checkbox"
551
+ >
552
+ {(state, childrenProps) => {
553
+ // The base element here doesn't matter in this testing
554
+ // environment, but the simulated events in the test are in
555
+ // line with what browsers do for this element.
556
+ return (
557
+ <button id="test-button" {...childrenProps}>
558
+ label
559
+ </button>
560
+ );
561
+ }}
562
+ </ClickableBehavior>,
563
+ );
564
+
565
+ // Act
566
+ const button = wrapper.find("#test-button").first();
567
+ button.simulate("click", {
568
+ preventDefault() {
569
+ this.defaultPrevented = true;
570
+ },
571
+ });
572
+
573
+ // Assert
574
+ expect(window.location.assign).not.toHaveBeenCalled();
575
+ });
576
+ });
577
+
578
+ it("calls onClick correctly for a component that doesn't respond to enter", () => {
579
+ const onClick = jest.fn();
580
+ // Use mount instead of a shallow render to trigger event defaults
581
+ const checkbox = mount(
582
+ // triggerOnEnter may be false for some elements e.g. checkboxes
583
+ <ClickableBehavior onClick={(e) => onClick(e)} role="checkbox">
584
+ {(state, childrenProps) => {
585
+ // The base element here doesn't matter in this testing
586
+ // environment, but the simulated events in the test are in
587
+ // line with what browsers do for this element.
588
+ return <input type="checkbox" {...childrenProps} />;
589
+ }}
590
+ </ClickableBehavior>,
591
+ );
592
+
593
+ // Enter press should not do anything
594
+ checkbox.simulate("keydown", {keyCode: keyCodes.enter});
595
+ expect(onClick).toHaveBeenCalledTimes(0);
596
+ checkbox.simulate("keyup", {
597
+ preventDefault: jest.fn(),
598
+ keyCode: keyCodes.enter,
599
+ });
600
+ expect(onClick).toHaveBeenCalledTimes(0);
601
+
602
+ // Space press should trigger the onClick
603
+ checkbox.simulate("keydown", {keyCode: keyCodes.space});
604
+ checkbox.simulate("keyup", {
605
+ preventDefault: jest.fn(),
606
+ keyCode: keyCodes.space,
607
+ });
608
+ expect(onClick).toHaveBeenCalledTimes(1);
609
+ });
610
+
611
+ it("calls onClick for a button component on both enter/space", () => {
612
+ const onClick = jest.fn();
613
+ // Use mount instead of a shallow render to trigger event defaults
614
+ const button = mount(
615
+ <ClickableBehavior onClick={(e) => onClick(e)}>
616
+ {(state, childrenProps) => {
617
+ // The base element here doesn't matter in this testing
618
+ // environment, but the simulated events in the test are in
619
+ // line with what browsers do for this element.
620
+ return <button {...childrenProps}>Label</button>;
621
+ }}
622
+ </ClickableBehavior>,
623
+ );
624
+
625
+ // Enter press
626
+ button.simulate("keydown", {keyCode: keyCodes.enter});
627
+ expect(onClick).toHaveBeenCalledTimes(0);
628
+ button.simulate("keyup", {
629
+ preventDefault: jest.fn(),
630
+ keyCode: keyCodes.enter,
631
+ });
632
+ expect(onClick).toHaveBeenCalledTimes(1);
633
+
634
+ // Space press
635
+ button.simulate("keydown", {keyCode: keyCodes.space});
636
+ expect(onClick).toHaveBeenCalledTimes(1);
637
+ button.simulate("keyup", {
638
+ preventDefault: jest.fn(),
639
+ keyCode: keyCodes.space,
640
+ });
641
+ expect(onClick).toHaveBeenCalledTimes(2);
642
+ });
643
+
644
+ // This tests the case where we attach the childrenProps to an element that is
645
+ // not canonically clickable (like a div). The browser doesn't naturally
646
+ // trigger keyboard click events for such an element.
647
+ it("calls onClick listener on space/enter with a non-usually clickable element", () => {
648
+ const onClick = jest.fn();
649
+ // Use mount instead of a shallow render to trigger event defaults
650
+ const div = mount(
651
+ <ClickableBehavior onClick={(e) => onClick(e)}>
652
+ {(state, childrenProps) => {
653
+ // The base element here doesn't matter in this testing
654
+ // environment, but the simulated events in the test are in
655
+ // line with what browsers do for this element.
656
+ return <div {...childrenProps}>Label</div>;
657
+ }}
658
+ </ClickableBehavior>,
659
+ );
660
+
661
+ let expectedNumberTimesCalled = 0;
662
+ const clickableDiv = div.find("div");
663
+
664
+ // Enter press on a div
665
+ clickableDiv.simulate("keydown", {keyCode: keyCodes.enter});
666
+ expect(onClick).toHaveBeenCalledTimes(expectedNumberTimesCalled);
667
+ clickableDiv.simulate("keyup", {
668
+ preventDefault: jest.fn(),
669
+ keyCode: keyCodes.enter,
670
+ });
671
+ expectedNumberTimesCalled += 1;
672
+ expect(onClick).toHaveBeenCalledTimes(expectedNumberTimesCalled);
673
+
674
+ // Simulate a mouse click.
675
+ clickableDiv.simulate("mousedown");
676
+ expect(onClick).toHaveBeenCalledTimes(expectedNumberTimesCalled);
677
+ clickableDiv.simulate("mouseup");
678
+ expect(onClick).toHaveBeenCalledTimes(expectedNumberTimesCalled);
679
+ clickableDiv.simulate("click", {preventDefault: jest.fn()});
680
+ expectedNumberTimesCalled += 1;
681
+ expect(onClick).toHaveBeenCalledTimes(expectedNumberTimesCalled);
682
+
683
+ // Space press on a div
684
+ clickableDiv.simulate("keydown", {keyCode: keyCodes.space});
685
+ expect(onClick).toHaveBeenCalledTimes(expectedNumberTimesCalled);
686
+ clickableDiv.simulate("keyup", {
687
+ preventDefault: jest.fn(),
688
+ keyCode: keyCodes.space,
689
+ });
690
+ expectedNumberTimesCalled += 1;
691
+ expect(onClick).toHaveBeenCalledTimes(expectedNumberTimesCalled);
692
+
693
+ // Simulate another mouse click.
694
+ clickableDiv.simulate("mousedown");
695
+ expect(onClick).toHaveBeenCalledTimes(expectedNumberTimesCalled);
696
+ clickableDiv.simulate("mouseup");
697
+ expect(onClick).toHaveBeenCalledTimes(expectedNumberTimesCalled);
698
+ clickableDiv.simulate("click", {preventDefault: jest.fn()});
699
+ expectedNumberTimesCalled += 1;
700
+ expect(onClick).toHaveBeenCalledTimes(expectedNumberTimesCalled);
701
+ });
702
+
703
+ it("calls onClick on mouseup when the mouse was dragging", () => {
704
+ const onClick = jest.fn();
705
+ const button = shallow(
706
+ <ClickableBehavior disabled={false} onClick={(e) => onClick(e)}>
707
+ {(state, childrenProps) => {
708
+ return <button {...childrenProps}>Label</button>;
709
+ }}
710
+ </ClickableBehavior>,
711
+ );
712
+
713
+ button.simulate("mousedown");
714
+ button.simulate("dragstart", {preventDefault: jest.fn()});
715
+ button.simulate("mouseleave");
716
+ button.simulate("mouseup");
717
+ expect(onClick).toHaveBeenCalledTimes(0);
718
+
719
+ button.simulate("mousedown");
720
+ button.simulate("dragstart", {preventDefault: jest.fn()});
721
+ button.simulate("mouseup", {preventDefault: jest.fn()});
722
+ expect(onClick).toHaveBeenCalledTimes(1);
723
+
724
+ button.simulate("mouseenter", {
725
+ buttons: 1,
726
+ });
727
+ button.simulate("mouseup", {preventDefault: jest.fn()});
728
+ expect(onClick).toHaveBeenCalledTimes(2);
729
+ });
730
+
731
+ it("doesn't trigger enter key when browser doesn't stop the click", () => {
732
+ const onClick = jest.fn();
733
+ // Use mount instead of a shallow render to trigger event defaults
734
+ const checkbox = mount(
735
+ <ClickableBehavior onClick={(e) => onClick(e)} role="checkbox">
736
+ {(state, childrenProps) => {
737
+ // The base element here doesn't matter in this testing
738
+ // environment, but the simulated events in the test are in
739
+ // line with what browsers do for this element.
740
+ return <input type="checkbox" {...childrenProps} />;
741
+ }}
742
+ </ClickableBehavior>,
743
+ );
744
+
745
+ // Enter press should not do anything
746
+ checkbox.simulate("keydown", {keyCode: keyCodes.enter});
747
+ // This element still wants to have a click on enter press
748
+ checkbox.simulate("click", {preventDefault: jest.fn()});
749
+ checkbox.simulate("keyup", {
750
+ preventDefault: jest.fn(),
751
+ keyCode: keyCodes.enter,
752
+ });
753
+ expect(onClick).toHaveBeenCalledTimes(0);
754
+ });
755
+
756
+ describe("client-side navgiation", () => {
757
+ const ClickableBehaviorWithRouter = getClickableBehavior(
758
+ "/foo",
759
+ false,
760
+ true, // router
761
+ );
762
+
763
+ it("handles client-side navigation when there's a router context", () => {
764
+ // Arrange
765
+ const wrapper = mount(
766
+ <MemoryRouter>
767
+ <div>
768
+ <ClickableBehaviorWithRouter
769
+ href="/foo"
770
+ onClick={(e) => {}}
771
+ role="checkbox"
772
+ >
773
+ {(state, childrenProps) => {
774
+ // The base element here doesn't matter in this testing
775
+ // environment, but the simulated events in the test are in
776
+ // line with what browsers do for this element.
777
+ return (
778
+ <button id="test-button" {...childrenProps}>
779
+ label
780
+ </button>
781
+ );
782
+ }}
783
+ </ClickableBehaviorWithRouter>
784
+ <Switch>
785
+ <Route path="/foo">
786
+ <div>Hello, world!</div>
787
+ </Route>
788
+ </Switch>
789
+ </div>
790
+ </MemoryRouter>,
791
+ );
792
+
793
+ // Act
794
+ const button = wrapper.find("#test-button").first();
795
+ button.simulate("click", {preventDefault: jest.fn()});
796
+
797
+ // Assert
798
+ expect(wrapper).toIncludeText("Hello, world!");
799
+ });
800
+
801
+ describe("beforeNav", () => {
802
+ it("waits for beforeNav to resolve before client-side navigating", async () => {
803
+ // Arrange
804
+ const wrapper = mount(
805
+ <MemoryRouter>
806
+ <div>
807
+ <ClickableBehaviorWithRouter
808
+ href="/foo"
809
+ onClick={(e) => {}}
810
+ role="checkbox"
811
+ beforeNav={() => Promise.resolve()}
812
+ >
813
+ {(state, childrenProps) => {
814
+ // The base element here doesn't matter in this testing
815
+ // environment, but the simulated events in the test are in
816
+ // line with what browsers do for this element.
817
+ return (
818
+ <button
819
+ id="test-button"
820
+ {...childrenProps}
821
+ >
822
+ {state.waiting
823
+ ? "waiting"
824
+ : "label"}
825
+ </button>
826
+ );
827
+ }}
828
+ </ClickableBehaviorWithRouter>
829
+ <Switch>
830
+ <Route path="/foo">
831
+ <div>Hello, world!</div>
832
+ </Route>
833
+ </Switch>
834
+ </div>
835
+ </MemoryRouter>,
836
+ );
837
+
838
+ // Act
839
+ const button = wrapper.find("#test-button").first();
840
+ button.simulate("click", {preventDefault: jest.fn()});
841
+ await wait(0);
842
+
843
+ // Assert
844
+ expect(wrapper).toIncludeText("Hello, world!");
845
+ });
846
+
847
+ it("shows waiting state before navigating", async () => {
848
+ // Arrange
849
+ const wrapper = mount(
850
+ <MemoryRouter>
851
+ <div>
852
+ <ClickableBehaviorWithRouter
853
+ href="/foo"
854
+ onClick={(e) => {}}
855
+ role="checkbox"
856
+ beforeNav={() => Promise.resolve()}
857
+ >
858
+ {(state, childrenProps) => {
859
+ // The base element here doesn't matter in this testing
860
+ // environment, but the simulated events in the test are in
861
+ // line with what browsers do for this element.
862
+ return (
863
+ <button
864
+ id="test-button"
865
+ {...childrenProps}
866
+ >
867
+ {state.waiting
868
+ ? "waiting"
869
+ : "label"}
870
+ </button>
871
+ );
872
+ }}
873
+ </ClickableBehaviorWithRouter>
874
+ <Switch>
875
+ <Route path="/foo">
876
+ <div>Hello, world!</div>
877
+ </Route>
878
+ </Switch>
879
+ </div>
880
+ </MemoryRouter>,
881
+ );
882
+
883
+ // Act
884
+ const button = wrapper.find("#test-button").first();
885
+ button.simulate("click", {preventDefault: jest.fn()});
886
+
887
+ // Assert
888
+ expect(wrapper).toIncludeText("waiting");
889
+ });
890
+
891
+ it("does not navigate if beforeNav rejects", async () => {
892
+ // Arrange
893
+ const wrapper = mount(
894
+ <MemoryRouter>
895
+ <div>
896
+ <ClickableBehaviorWithRouter
897
+ href="/foo"
898
+ onClick={(e) => {}}
899
+ role="checkbox"
900
+ beforeNav={() => Promise.reject()}
901
+ >
902
+ {(state, childrenProps) => {
903
+ // The base element here doesn't matter in this testing
904
+ // environment, but the simulated events in the test are in
905
+ // line with what browsers do for this element.
906
+ return (
907
+ <button
908
+ id="test-button"
909
+ {...childrenProps}
910
+ >
911
+ label
912
+ </button>
913
+ );
914
+ }}
915
+ </ClickableBehaviorWithRouter>
916
+ <Switch>
917
+ <Route path="/foo">
918
+ <div>Hello, world!</div>
919
+ </Route>
920
+ </Switch>
921
+ </div>
922
+ </MemoryRouter>,
923
+ );
924
+
925
+ // Act
926
+ const button = wrapper.find("#test-button").first();
927
+ button.simulate("click", {preventDefault: jest.fn()});
928
+ await wait(0);
929
+
930
+ // Assert
931
+ expect(wrapper).not.toIncludeText("Hello, world!");
932
+ });
933
+
934
+ it("calls safeWithNav if provided if beforeNav resolves", async () => {
935
+ // Arrange
936
+ const safeWithNavMock = jest.fn();
937
+ const wrapper = mount(
938
+ <MemoryRouter>
939
+ <div>
940
+ <ClickableBehaviorWithRouter
941
+ href="/foo"
942
+ onClick={(e) => {}}
943
+ role="checkbox"
944
+ beforeNav={() => Promise.resolve()}
945
+ safeWithNav={safeWithNavMock}
946
+ >
947
+ {(state, childrenProps) => {
948
+ // The base element here doesn't matter in this testing
949
+ // environment, but the simulated events in the test are in
950
+ // line with what browsers do for this element.
951
+ return (
952
+ <button
953
+ id="test-button"
954
+ {...childrenProps}
955
+ >
956
+ {state.waiting
957
+ ? "waiting"
958
+ : "label"}
959
+ </button>
960
+ );
961
+ }}
962
+ </ClickableBehaviorWithRouter>
963
+ <Switch>
964
+ <Route path="/foo">
965
+ <div>Hello, world!</div>
966
+ </Route>
967
+ </Switch>
968
+ </div>
969
+ </MemoryRouter>,
970
+ );
971
+
972
+ // Act
973
+ const button = wrapper.find("#test-button").first();
974
+ button.simulate("click", {preventDefault: jest.fn()});
975
+ await wait(0);
976
+
977
+ // Assert
978
+ expect(safeWithNavMock).toHaveBeenCalled();
979
+ });
980
+ });
981
+
982
+ it("doesn't wait for safeWithNav to resolve before client-side navigating", async () => {
983
+ // Arrange
984
+ const wrapper = mount(
985
+ <MemoryRouter>
986
+ <div>
987
+ <ClickableBehaviorWithRouter
988
+ href="/foo"
989
+ onClick={(e) => {}}
990
+ role="checkbox"
991
+ safeWithNav={() => Promise.resolve()}
992
+ >
993
+ {(state, childrenProps) => {
994
+ // The base element here doesn't matter in this testing
995
+ // environment, but the simulated events in the test are in
996
+ // line with what browsers do for this element.
997
+ return (
998
+ <button id="test-button" {...childrenProps}>
999
+ label
1000
+ </button>
1001
+ );
1002
+ }}
1003
+ </ClickableBehaviorWithRouter>
1004
+ <Switch>
1005
+ <Route path="/foo">
1006
+ <div>Hello, world!</div>
1007
+ </Route>
1008
+ </Switch>
1009
+ </div>
1010
+ </MemoryRouter>,
1011
+ );
1012
+
1013
+ // Act
1014
+ const button = wrapper.find("#test-button").first();
1015
+ button.simulate("click", {preventDefault: jest.fn()});
1016
+
1017
+ // Assert
1018
+ expect(wrapper).toIncludeText("Hello, world!");
1019
+ });
1020
+
1021
+ it("If onClick calls e.preventDefault() then we won't navigate", () => {
1022
+ // Arrange
1023
+ const wrapper = mount(
1024
+ <MemoryRouter>
1025
+ <div>
1026
+ <ClickableBehaviorWithRouter
1027
+ href="/foo"
1028
+ onClick={(e) => e.preventDefault()}
1029
+ role="checkbox"
1030
+ >
1031
+ {(state, childrenProps) => {
1032
+ // The base element here doesn't matter in this testing
1033
+ // environment, but the simulated events in the test are in
1034
+ // line with what browsers do for this element.
1035
+ return (
1036
+ <button id="test-button" {...childrenProps}>
1037
+ label
1038
+ </button>
1039
+ );
1040
+ }}
1041
+ </ClickableBehaviorWithRouter>
1042
+ <Switch>
1043
+ <Route path="/foo">
1044
+ <div>Hello, world!</div>
1045
+ </Route>
1046
+ </Switch>
1047
+ </div>
1048
+ </MemoryRouter>,
1049
+ );
1050
+
1051
+ // Act
1052
+ const button = wrapper.find("#test-button").first();
1053
+ button.simulate("click", {
1054
+ preventDefault() {
1055
+ this.defaultPrevented = true;
1056
+ },
1057
+ });
1058
+
1059
+ // Assert
1060
+ expect(wrapper).not.toIncludeText("Hello, world!");
1061
+ });
1062
+ });
1063
+
1064
+ describe("target='_blank'", () => {
1065
+ it("opens a new tab", () => {
1066
+ // Arrange
1067
+ const link = mount(
1068
+ <ClickableBehavior
1069
+ disabled={false}
1070
+ href="https://www.khanacademy.org"
1071
+ role="link"
1072
+ target="_blank"
1073
+ >
1074
+ {(state, childrenProps) => {
1075
+ return (
1076
+ <a
1077
+ href="https://www.khanacademy.org"
1078
+ {...childrenProps}
1079
+ >
1080
+ Label
1081
+ </a>
1082
+ );
1083
+ }}
1084
+ </ClickableBehavior>,
1085
+ );
1086
+
1087
+ // Act
1088
+ link.simulate("click");
1089
+
1090
+ // Assert
1091
+ expect(window.open).toHaveBeenCalledWith(
1092
+ "https://www.khanacademy.org",
1093
+ "_blank",
1094
+ );
1095
+ });
1096
+
1097
+ it("opens a new tab when using 'safeWithNav'", () => {
1098
+ // Arrange
1099
+ const safeWithNavMock = jest.fn().mockResolvedValue();
1100
+ const link = mount(
1101
+ <ClickableBehavior
1102
+ disabled={false}
1103
+ href="https://www.khanacademy.org"
1104
+ role="link"
1105
+ target="_blank"
1106
+ safeWithNav={safeWithNavMock}
1107
+ >
1108
+ {(state, childrenProps) => {
1109
+ return (
1110
+ <a
1111
+ href="https://www.khanacademy.org"
1112
+ {...childrenProps}
1113
+ >
1114
+ Label
1115
+ </a>
1116
+ );
1117
+ }}
1118
+ </ClickableBehavior>,
1119
+ );
1120
+
1121
+ // Act
1122
+ link.simulate("click");
1123
+
1124
+ // Assert
1125
+ expect(window.open).toHaveBeenCalledWith(
1126
+ "https://www.khanacademy.org",
1127
+ "_blank",
1128
+ );
1129
+ });
1130
+
1131
+ it("calls 'safeWithNav'", () => {
1132
+ // Arrange
1133
+ const safeWithNavMock = jest.fn().mockResolvedValue();
1134
+ const link = mount(
1135
+ <ClickableBehavior
1136
+ disabled={false}
1137
+ href="https://www.khanacademy.org"
1138
+ role="link"
1139
+ target="_blank"
1140
+ safeWithNav={safeWithNavMock}
1141
+ >
1142
+ {(state, childrenProps) => {
1143
+ return (
1144
+ <a
1145
+ href="https://www.khanacademy.org"
1146
+ {...childrenProps}
1147
+ >
1148
+ Label
1149
+ </a>
1150
+ );
1151
+ }}
1152
+ </ClickableBehavior>,
1153
+ );
1154
+
1155
+ // Act
1156
+ link.simulate("click");
1157
+
1158
+ // Assert
1159
+ expect(safeWithNavMock).toHaveBeenCalled();
1160
+ });
1161
+
1162
+ it("opens a new tab when inside a router", () => {
1163
+ // Arrange
1164
+ const link = mount(
1165
+ <MemoryRouter initialEntries={["/"]}>
1166
+ <ClickableBehavior
1167
+ disabled={false}
1168
+ href="https://www.khanacademy.org"
1169
+ role="link"
1170
+ target="_blank"
1171
+ >
1172
+ {(state, childrenProps) => {
1173
+ return (
1174
+ <a
1175
+ href="https://www.khanacademy.org"
1176
+ {...childrenProps}
1177
+ >
1178
+ Label
1179
+ </a>
1180
+ );
1181
+ }}
1182
+ </ClickableBehavior>
1183
+ </MemoryRouter>,
1184
+ );
1185
+
1186
+ // Act
1187
+ link.simulate("click");
1188
+
1189
+ // Assert
1190
+ expect(window.open).toHaveBeenCalledWith(
1191
+ "https://www.khanacademy.org",
1192
+ "_blank",
1193
+ );
1194
+ });
1195
+
1196
+ it("opens a new tab when using 'safeWithNav' inside a router", () => {
1197
+ // Arrange
1198
+ const safeWithNavMock = jest.fn().mockResolvedValue();
1199
+ const link = mount(
1200
+ <MemoryRouter initialEntries={["/"]}>
1201
+ <ClickableBehavior
1202
+ disabled={false}
1203
+ href="https://www.khanacademy.org"
1204
+ role="link"
1205
+ target="_blank"
1206
+ safeWithNav={safeWithNavMock}
1207
+ >
1208
+ {(state, childrenProps) => {
1209
+ return (
1210
+ <a
1211
+ href="https://www.khanacademy.org"
1212
+ {...childrenProps}
1213
+ >
1214
+ Label
1215
+ </a>
1216
+ );
1217
+ }}
1218
+ </ClickableBehavior>
1219
+ </MemoryRouter>,
1220
+ );
1221
+
1222
+ // Act
1223
+ link.simulate("click");
1224
+
1225
+ // Assert
1226
+ expect(window.open).toHaveBeenCalledWith(
1227
+ "https://www.khanacademy.org",
1228
+ "_blank",
1229
+ );
1230
+ });
1231
+
1232
+ it("calls 'safeWithNav' inside a router", () => {
1233
+ // Arrange
1234
+ const safeWithNavMock = jest.fn().mockResolvedValue();
1235
+ const link = mount(
1236
+ <MemoryRouter initialEntries={["/"]}>
1237
+ <ClickableBehavior
1238
+ disabled={false}
1239
+ href="https://www.khanacademy.org"
1240
+ role="link"
1241
+ target="_blank"
1242
+ safeWithNav={safeWithNavMock}
1243
+ >
1244
+ {(state, childrenProps) => {
1245
+ return (
1246
+ <a
1247
+ href="https://www.khanacademy.org"
1248
+ {...childrenProps}
1249
+ >
1250
+ Label
1251
+ </a>
1252
+ );
1253
+ }}
1254
+ </ClickableBehavior>
1255
+ </MemoryRouter>,
1256
+ );
1257
+
1258
+ // Act
1259
+ link.simulate("click");
1260
+
1261
+ // Assert
1262
+ expect(safeWithNavMock).toHaveBeenCalled();
1263
+ });
1264
+ });
1265
+
1266
+ describe("rel", () => {
1267
+ it("should use the 'rel' that was passed in", () => {
1268
+ // Arrange
1269
+ const childrenMock = jest.fn().mockImplementation(() => null);
1270
+ mount(
1271
+ <ClickableBehavior
1272
+ href="https://www.khanacademy.org"
1273
+ rel="something_else"
1274
+ target="_blank"
1275
+ >
1276
+ {childrenMock}
1277
+ </ClickableBehavior>,
1278
+ );
1279
+
1280
+ const childrenProps = childrenMock.mock.calls[0][1];
1281
+ expect(childrenProps.rel).toEqual("something_else");
1282
+ });
1283
+
1284
+ it("should use 'noopener noreferrer' as a default when target='_blank'", () => {
1285
+ // Arrange
1286
+ const childrenMock = jest.fn().mockImplementation(() => null);
1287
+ mount(
1288
+ <ClickableBehavior
1289
+ href="https://www.khanacademy.org"
1290
+ target="_blank"
1291
+ >
1292
+ {childrenMock}
1293
+ </ClickableBehavior>,
1294
+ );
1295
+
1296
+ const childrenProps = childrenMock.mock.calls[0][1];
1297
+ expect(childrenProps.rel).toEqual("noopener noreferrer");
1298
+ });
1299
+
1300
+ it("should not use the default if target != '_blank'", () => {
1301
+ // Arrange
1302
+ const childrenMock = jest.fn().mockImplementation(() => null);
1303
+ mount(
1304
+ <ClickableBehavior href="https://www.khanacademy.org">
1305
+ {childrenMock}
1306
+ </ClickableBehavior>,
1307
+ );
1308
+
1309
+ const childrenProps = childrenMock.mock.calls[0][1];
1310
+ expect(childrenProps.rel).toBeUndefined();
1311
+ });
1312
+ });
1313
+ });