@khanacademy/wonder-blocks-link 6.1.6 → 6.1.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,79 +0,0 @@
1
- import * as React from "react";
2
-
3
- import {render} from "@testing-library/react";
4
- import LinkCore from "../components/link-core";
5
- import Link from "../components/link";
6
-
7
- const defaultHandlers = {
8
- onClick: () => void 0,
9
- onMouseEnter: () => void 0,
10
- onMouseLeave: () => void 0,
11
- onMouseDown: () => void 0,
12
- onMouseUp: () => void 0,
13
- onTouchStart: () => void 0,
14
- onTouchEnd: () => void 0,
15
- onTouchCancel: () => void 0,
16
- onKeyDown: () => void 0,
17
- onKeyUp: () => void 0,
18
- onFocus: () => void 0,
19
- onBlur: () => void 0,
20
- tabIndex: 0,
21
- } as const;
22
-
23
- describe("Link", () => {
24
- test.each`
25
- tabIndex
26
- ${-1}
27
- ${0}
28
- ${1}
29
- `("<Link tabIndex={$tabIndex}>", ({tabIndex}: any) => {
30
- const {container} = render(
31
- <Link href="#" tabIndex={tabIndex}>
32
- Click me
33
- </Link>,
34
- );
35
- expect(container).toMatchSnapshot();
36
- });
37
- });
38
-
39
- describe("LinkCore", () => {
40
- for (const kind of ["primary", "secondary"] as const) {
41
- for (const href of ["#", "#non-existent-link"]) {
42
- for (const light of kind === "primary" ? [true, false] : [false]) {
43
- for (const visitable of kind === "primary"
44
- ? [true, false]
45
- : [false]) {
46
- for (const inline of [true, false]) {
47
- for (const state of ["focused", "hovered", "pressed"]) {
48
- const stateProps = {
49
- focused: state === "focused",
50
- hovered: state === "hovered",
51
- pressed: state === "pressed",
52
- waiting: false,
53
- } as const;
54
- test(`kind:${kind} href:${href} light:${String(
55
- light,
56
- )} visitable:${String(visitable)} ${state}`, () => {
57
- const {container} = render(
58
- <LinkCore
59
- href="#"
60
- inline={inline}
61
- kind={kind}
62
- light={light}
63
- visitable={visitable}
64
- {...stateProps}
65
- {...defaultHandlers}
66
- >
67
- Click me
68
- </LinkCore>,
69
- );
70
-
71
- expect(container).toMatchSnapshot();
72
- });
73
- }
74
- }
75
- }
76
- }
77
- }
78
- }
79
- });
@@ -1,559 +0,0 @@
1
- import * as React from "react";
2
- import {MemoryRouter, Route, Switch} from "react-router-dom";
3
- import {fireEvent, render, screen, waitFor} from "@testing-library/react";
4
- import {userEvent} from "@testing-library/user-event";
5
-
6
- import {PhosphorIcon} from "@khanacademy/wonder-blocks-icon";
7
- import plusIcon from "@phosphor-icons/core/bold/plus-bold.svg";
8
-
9
- import Link from "../link";
10
-
11
- describe("Link", () => {
12
- beforeEach(() => {
13
- // Note: window.location.assign needs a mock function in the testing
14
- // environment.
15
- // @ts-expect-error [FEI-5019] - TS2790 - The operand of a 'delete' operator must be optional.
16
- delete window.location;
17
- // @ts-expect-error [FEI-5019] - TS2740 - Type '{ assign: Mock<any, any, any>; }' is missing the following properties from type 'Location': ancestorOrigins, hash, host, hostname, and 8 more.
18
- window.location = {assign: jest.fn(), origin: "http://localhost"};
19
- });
20
-
21
- afterEach(() => {
22
- // @ts-expect-error [FEI-5019] - TS2339 - Property 'mockClear' does not exist on type '(url: string | URL) => void'.
23
- window.location.assign.mockClear();
24
- jest.clearAllMocks();
25
- });
26
-
27
- describe("client-side navigation", () => {
28
- test("works for known URLs", async () => {
29
- // Arrange
30
- render(
31
- <MemoryRouter>
32
- <div>
33
- <Link href="/foo">Click me!</Link>
34
- <Switch>
35
- <Route path="/foo">
36
- <div id="foo">Hello, world!</div>
37
- </Route>
38
- </Switch>
39
- </div>
40
- </MemoryRouter>,
41
- );
42
-
43
- // Act
44
- const link = await screen.findByText("Click me!");
45
- await userEvent.click(link);
46
-
47
- // Assert
48
- expect(
49
- await screen.findByText("Hello, world!"),
50
- ).toBeInTheDocument();
51
- });
52
-
53
- test("navigation to without route does not render", async () => {
54
- // Arrange
55
- render(
56
- <MemoryRouter>
57
- <div>
58
- <Link href="/">Click me!</Link>
59
- <Switch>
60
- <Route path="/foo">
61
- <div id="foo">Hello, world!</div>
62
- </Route>
63
- </Switch>
64
- </div>
65
- </MemoryRouter>,
66
- );
67
-
68
- // Act
69
- const link = await screen.findByText("Click me!");
70
- await userEvent.click(link);
71
-
72
- // Assert
73
- expect(screen.queryByText("Hello, world!")).not.toBeInTheDocument();
74
- });
75
-
76
- // NOTE(john): This fails after upgrading to user-event v14.
77
- // I believe that the act() call is resolving the promise, so this
78
- // test is no longer doing what it expects.
79
- test.skip("waits until beforeNav resolves before navigating", async () => {
80
- // Arrange
81
- render(
82
- <MemoryRouter>
83
- <div>
84
- <Link href="/foo" beforeNav={() => Promise.resolve()}>
85
- Click me!
86
- </Link>
87
- <Switch>
88
- <Route path="/foo">
89
- <div id="foo">Hello, world!</div>
90
- </Route>
91
- </Switch>
92
- </div>
93
- </MemoryRouter>,
94
- );
95
-
96
- // Act
97
- const link = await screen.findByText("Click me!");
98
- await userEvent.click(link);
99
-
100
- // Assert
101
- expect(screen.queryByText("Hello, world!")).not.toBeInTheDocument();
102
- });
103
-
104
- // NOTE(john): This fails after upgrading to user-event v14.
105
- // I believe that the act() call is resolving the promise, so this
106
- // test is no longer doing what it expects.
107
- test.skip("doesn't navigate before beforeNav resolves", async () => {
108
- // Arrange
109
- render(
110
- <MemoryRouter>
111
- <div>
112
- <Link href="/foo" beforeNav={() => Promise.resolve()}>
113
- Click me!
114
- </Link>
115
- <Switch>
116
- <Route path="/foo">
117
- <div id="foo">Hello, world!</div>
118
- </Route>
119
- </Switch>
120
- </div>
121
- </MemoryRouter>,
122
- );
123
-
124
- // Act
125
- const link = await screen.findByText("Click me!");
126
- await userEvent.click(link);
127
-
128
- // Assert
129
- expect(screen.queryByText("Hello, world!")).not.toBeInTheDocument();
130
- });
131
-
132
- test("does not navigate if beforeNav rejects", async () => {
133
- // Arrange
134
- render(
135
- <MemoryRouter>
136
- <div>
137
- <Link href="/foo" beforeNav={() => Promise.reject()}>
138
- Click me!
139
- </Link>
140
- <Switch>
141
- <Route path="/foo">
142
- <div id="foo">Hello, world!</div>
143
- </Route>
144
- </Switch>
145
- </div>
146
- </MemoryRouter>,
147
- );
148
-
149
- // Act
150
- const link = await screen.findByText("Click me!");
151
- await userEvent.click(link);
152
-
153
- // Assert
154
- expect(screen.queryByText("Hello, world!")).not.toBeInTheDocument();
155
- });
156
-
157
- test("runs safeWithNav if set", async () => {
158
- // Arrange
159
- const safeWithNavMock = jest.fn();
160
- render(
161
- <MemoryRouter>
162
- <div>
163
- <Link
164
- href="/foo"
165
- beforeNav={() => Promise.resolve()}
166
- safeWithNav={safeWithNavMock}
167
- >
168
- Click me!
169
- </Link>
170
- <Switch>
171
- <Route path="/foo">
172
- <div id="foo">Hello, world!</div>
173
- </Route>
174
- </Switch>
175
- </div>
176
- </MemoryRouter>,
177
- );
178
-
179
- // Act
180
- const link = await screen.findByText("Click me!");
181
- await userEvent.click(link);
182
-
183
- // Assert
184
- await waitFor(() => {
185
- expect(safeWithNavMock).toHaveBeenCalled();
186
- });
187
- });
188
-
189
- // NOTE(john): This fails after upgrading to user-event v14.
190
- // I believe that the act() call is resolving the promise, so this
191
- // test is no longer doing what it expects.
192
- test.skip("doesn't run safeWithNav until beforeNav resolves", async () => {
193
- // Arrange
194
- const safeWithNavMock = jest.fn();
195
- render(
196
- <MemoryRouter>
197
- <div>
198
- <Link
199
- href="/foo"
200
- beforeNav={() => Promise.resolve()}
201
- safeWithNav={safeWithNavMock}
202
- >
203
- Click me!
204
- </Link>
205
- <Switch>
206
- <Route path="/foo">
207
- <div id="foo">Hello, world!</div>
208
- </Route>
209
- </Switch>
210
- </div>
211
- </MemoryRouter>,
212
- );
213
-
214
- // Act
215
- const link = await screen.findByText("Click me!");
216
- await userEvent.click(link);
217
-
218
- // Assert
219
- expect(safeWithNavMock).not.toHaveBeenCalled();
220
- });
221
- });
222
-
223
- describe("full page load navigation", () => {
224
- // NOTE(john): This fails after upgrading to user-event v14.
225
- // I believe that the act() call is resolving the promise, so this
226
- // test is no longer doing what it expects.
227
- test.skip("doesn't redirect if safeWithNav hasn't resolved yet when skipClientNav=true", async () => {
228
- // Arrange
229
- jest.spyOn(window.location, "assign").mockImplementation(() => {});
230
- render(
231
- <Link
232
- href="/foo"
233
- safeWithNav={() => Promise.resolve()}
234
- skipClientNav={true}
235
- >
236
- Click me!
237
- </Link>,
238
- );
239
-
240
- // Act
241
- const link = await screen.findByText("Click me!");
242
- await userEvent.click(link);
243
-
244
- // Assert
245
- expect(window.location.assign).not.toHaveBeenCalled();
246
- });
247
-
248
- test("redirects after safeWithNav resolves when skipClientNav=true", async () => {
249
- // Arrange
250
- jest.spyOn(window.location, "assign").mockImplementation(() => {});
251
- render(
252
- <Link
253
- href="/foo"
254
- safeWithNav={() => Promise.resolve()}
255
- skipClientNav={true}
256
- >
257
- Click me!
258
- </Link>,
259
- );
260
-
261
- // Act
262
- const link = await screen.findByText("Click me!");
263
- await userEvent.click(link);
264
-
265
- // Assert
266
- await waitFor(() => {
267
- expect(window.location.assign).toHaveBeenCalledWith("/foo");
268
- });
269
- });
270
-
271
- test("redirects after beforeNav and safeWithNav resolve when skipClientNav=true", async () => {
272
- // Arrange
273
- jest.spyOn(window.location, "assign").mockImplementation(() => {});
274
- render(
275
- <Link
276
- href="/foo"
277
- beforeNav={() => Promise.resolve()}
278
- safeWithNav={() => Promise.resolve()}
279
- skipClientNav={true}
280
- >
281
- Click me!
282
- </Link>,
283
- );
284
-
285
- // Act
286
- const link = await screen.findByText("Click me!");
287
- await userEvent.click(link);
288
-
289
- // Assert
290
- await waitFor(() => {
291
- expect(window.location.assign).toHaveBeenCalledWith("/foo");
292
- });
293
- });
294
-
295
- // NOTE(john): This fails after upgrading to user-event v14.
296
- // I believe that the act() call is resolving the promise, so this
297
- // test is no longer doing what it expects.
298
- test.skip("doesn't redirect before beforeNav resolves when skipClientNav=true", async () => {
299
- // Arrange
300
- jest.spyOn(window.location, "assign").mockImplementation(() => {});
301
- render(
302
- <Link
303
- href="/foo"
304
- beforeNav={() => Promise.resolve()}
305
- skipClientNav={true}
306
- >
307
- Click me!
308
- </Link>,
309
- );
310
-
311
- // Act
312
- const link = await screen.findByText("Click me!");
313
- await userEvent.click(link);
314
-
315
- // Assert
316
- expect(window.location.assign).not.toHaveBeenCalled();
317
- });
318
- });
319
-
320
- describe("raw events", () => {
321
- test("onKeyDown", async () => {
322
- // Arrange
323
- let keyCode: any;
324
- render(
325
- <Link href="/foo" onKeyDown={(e: any) => (keyCode = e.keyCode)}>
326
- Click me!
327
- </Link>,
328
- );
329
-
330
- // Act
331
- const link = await screen.findByText("Click me!");
332
- // eslint-disable-next-line testing-library/prefer-user-event
333
- fireEvent.keyDown(link, {keyCode: 32});
334
-
335
- // Assert
336
- expect(keyCode).toEqual(32);
337
- });
338
-
339
- test("onKeyUp", async () => {
340
- // Arrange
341
- let keyCode: any;
342
- render(
343
- <Link href="/foo" onKeyUp={(e: any) => (keyCode = e.keyCode)}>
344
- Click me!
345
- </Link>,
346
- );
347
-
348
- // Act
349
- const link = await screen.findByText("Click me!");
350
- // eslint-disable-next-line testing-library/prefer-user-event
351
- fireEvent.keyUp(link, {keyCode: 32});
352
-
353
- // Assert
354
- expect(keyCode).toEqual(32);
355
- });
356
- });
357
-
358
- describe("external link that opens in a new tab", () => {
359
- test("target attribute passed down correctly", async () => {
360
- // Arrange
361
- render(
362
- <Link href="https://www.google.com/" target="_blank">
363
- Click me!
364
- </Link>,
365
- );
366
-
367
- // Act
368
- const link = await screen.findByRole("link");
369
-
370
- // Assert
371
- expect(link).toHaveAttribute("target", "_blank");
372
- });
373
-
374
- test("render external icon when `target=_blank` and link is external", async () => {
375
- // Arrange
376
- render(
377
- <Link href="https://www.google.com/" target="_blank">
378
- Click me!
379
- </Link>,
380
- );
381
-
382
- // Act
383
- const icon = await screen.findByTestId("external-icon");
384
-
385
- // Assert
386
- expect(icon).toHaveStyle({
387
- maskImage: "url(arrow-square-out-bold.svg)",
388
- });
389
- });
390
-
391
- test("does not render external icon when `target=_blank` and link is relative", async () => {
392
- // Arrange
393
- render(
394
- <Link href="/" target="_blank">
395
- Click me!
396
- </Link>,
397
- );
398
-
399
- // Act
400
- const icon = screen.queryByTestId("external-icon");
401
-
402
- // Assert
403
- expect(icon).not.toBeInTheDocument();
404
- });
405
-
406
- test("does not render external icon when there is no target and link is external", async () => {
407
- // Arrange
408
- render(<Link href="https://www.google.com/">Click me!</Link>);
409
-
410
- // Act
411
- const icon = screen.queryByTestId("external-icon");
412
-
413
- // Assert
414
- expect(icon).not.toBeInTheDocument();
415
- });
416
-
417
- test("does not render external icon when there is no target and link is relative", async () => {
418
- // Arrange
419
- render(<Link href="/">Click me!</Link>);
420
-
421
- // Act
422
- const icon = screen.queryByTestId("external-icon");
423
-
424
- // Assert
425
- expect(icon).not.toBeInTheDocument();
426
- });
427
- });
428
-
429
- describe("start and end icons", () => {
430
- test("render icon with link when startIcon prop is passed in", async () => {
431
- // Arrange
432
- render(
433
- <Link
434
- href="https://www.khanacademy.org/"
435
- startIcon={<PhosphorIcon icon={plusIcon} />}
436
- >
437
- Add new item
438
- </Link>,
439
- );
440
-
441
- // Act
442
- const icon = await screen.findByTestId("start-icon");
443
-
444
- // Assert
445
- expect(icon).toHaveStyle({maskImage: "url(plus-bold.svg)"});
446
- });
447
-
448
- test("does not render icon when startIcon prop is not passed in", async () => {
449
- // Arrange
450
- render(<Link href="https://www.khanacademy.org/">Click me!</Link>);
451
-
452
- // Act
453
- const icon = screen.queryByTestId("start-icon");
454
-
455
- // Assert
456
- expect(icon).not.toBeInTheDocument();
457
- });
458
-
459
- test("startIcon prop passed down correctly", async () => {
460
- // Arrange
461
- render(
462
- <Link
463
- href="https://www.khanacademy.org/"
464
- startIcon={<PhosphorIcon icon={plusIcon} />}
465
- >
466
- Add new item
467
- </Link>,
468
- );
469
-
470
- // Act
471
- const icon = await screen.findByTestId("start-icon");
472
-
473
- // Assert
474
- expect(icon).toHaveStyle({maskImage: "url(plus-bold.svg)"});
475
- });
476
-
477
- test("render icon with link when endIcon prop is passed in", async () => {
478
- // Arrange
479
- render(
480
- <Link
481
- href="https://www.khanacademy.org/"
482
- endIcon={<PhosphorIcon icon={plusIcon} />}
483
- >
484
- Click to go back
485
- </Link>,
486
- );
487
-
488
- // Act
489
- const icon = await screen.findByTestId("end-icon");
490
-
491
- // Assert
492
- expect(icon).toHaveStyle({maskImage: "url(plus-bold.svg)"});
493
- });
494
-
495
- test("does not render icon when endIcon prop is not passed in", async () => {
496
- // Arrange
497
- render(<Link href="https://www.khanacademy.org/">Click me!</Link>);
498
-
499
- // Act
500
- const icon = screen.queryByTestId("end-icon");
501
-
502
- // Assert
503
- expect(icon).not.toBeInTheDocument();
504
- });
505
-
506
- test("does not render externalIcon when endIcon is passed in and `target='_blank'`", async () => {
507
- // Arrange
508
- render(
509
- <Link
510
- href="https://www.google.com/"
511
- endIcon={<PhosphorIcon icon={plusIcon} />}
512
- target="_blank"
513
- >
514
- Open a new tab
515
- </Link>,
516
- );
517
-
518
- // Act
519
- const externalIcon = screen.queryByTestId("external-icon");
520
-
521
- // Assert
522
- expect(externalIcon).not.toBeInTheDocument();
523
- });
524
-
525
- test("render endIcon instead of default externalIcon when `target='_blank' and link is external`", async () => {
526
- // Arrange
527
- render(
528
- <Link
529
- href="https://www.google.com/"
530
- endIcon={<PhosphorIcon icon={plusIcon} />}
531
- target="_blank"
532
- >
533
- Open a new tab
534
- </Link>,
535
- );
536
-
537
- // Act
538
- const icon = await screen.findByTestId("end-icon");
539
-
540
- // Assert
541
- expect(icon).toHaveStyle({maskImage: "url(plus-bold.svg)"});
542
- });
543
-
544
- test("endIcon prop passed down correctly", async () => {
545
- // Arrange
546
- render(
547
- <Link href="/" endIcon={<PhosphorIcon icon={plusIcon} />}>
548
- Click to go back
549
- </Link>,
550
- );
551
-
552
- // Act
553
- const icon = await screen.findByTestId("end-icon");
554
-
555
- // Assert
556
- expect(icon).toHaveStyle({maskImage: "url(plus-bold.svg)"});
557
- });
558
- });
559
- });
@@ -1,40 +0,0 @@
1
- import * as React from "react";
2
-
3
- import Link from "../link";
4
-
5
- // @ts-expect-error - href must be used with safeWithNav
6
- <Link beforeNav={() => Promise.resolve()}>Hello, world!</Link>;
7
-
8
- // @ts-expect-error - href must be used with safeWithNav
9
- <Link safeWithNav={() => Promise.resolve()}>Hello, world!</Link>;
10
-
11
- // It's okay to use onClick with href
12
- <Link href="/foo" onClick={() => {}}>
13
- Hello, world!
14
- </Link>;
15
-
16
- <Link href="/foo" beforeNav={() => Promise.resolve()}>
17
- Hello, world!
18
- </Link>;
19
-
20
- <Link href="/foo" safeWithNav={() => Promise.resolve()}>
21
- Hello, world!
22
- </Link>;
23
-
24
- // @ts-expect-error - `target="_blank"` cannot beused with `beforeNav`
25
- <Link href="/foo" target="_blank" beforeNav={() => Promise.resolve()}>
26
- Hello, world!
27
- </Link>;
28
-
29
- // All three of these props can be used together
30
- <Link
31
- href="/foo"
32
- beforeNav={() => Promise.resolve()}
33
- safeWithNav={() => Promise.resolve()}
34
- onClick={() => {}}
35
- >
36
- Hello, world!
37
- </Link>;
38
-
39
- // It's also fine to use href by itself
40
- <Link href="/foo">Hello, world!</Link>;