@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.
- package/LICENSE +21 -0
- package/dist/es/index.js +712 -0
- package/dist/index.js +1056 -0
- package/dist/index.js.flow +2 -0
- package/docs.md +7 -0
- package/package.json +32 -0
- package/src/__tests__/__snapshots__/generated-snapshot.test.js.snap +426 -0
- package/src/__tests__/generated-snapshot.test.js +176 -0
- package/src/components/__tests__/clickable-behavior.test.js +1313 -0
- package/src/components/__tests__/clickable.test.js +500 -0
- package/src/components/clickable-behavior.js +646 -0
- package/src/components/clickable.js +388 -0
- package/src/components/clickable.md +196 -0
- package/src/components/clickable.stories.js +129 -0
- package/src/index.js +15 -0
- package/src/util/__tests__/get-clickable-behavior.test.js +105 -0
- package/src/util/__tests__/is-client-side-url.js.test.js +50 -0
- package/src/util/get-clickable-behavior.js +43 -0
- package/src/util/is-client-side-url.js +16 -0
|
@@ -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
|
+
});
|