@khanacademy/wonder-blocks-timing 4.0.1 → 5.0.0

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.
Files changed (50) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/components/with-action-scheduler.d.ts +4 -1
  3. package/dist/es/index.js +93 -105
  4. package/dist/hooks/use-interval.d.ts +27 -5
  5. package/dist/hooks/use-timeout.d.ts +27 -5
  6. package/dist/index.d.ts +2 -5
  7. package/dist/index.js +92 -105
  8. package/dist/util/animation-frame.d.ts +6 -6
  9. package/dist/util/interval.d.ts +8 -8
  10. package/dist/util/policies.d.ts +12 -8
  11. package/dist/util/timeout.d.ts +8 -7
  12. package/dist/util/types.d.ts +18 -10
  13. package/package.json +3 -3
  14. package/src/components/with-action-scheduler.tsx +9 -1
  15. package/src/hooks/__tests__/use-interval.test.ts +453 -56
  16. package/src/hooks/__tests__/use-timeout.test.ts +412 -58
  17. package/src/hooks/use-interval.ts +90 -25
  18. package/src/hooks/use-timeout.ts +90 -25
  19. package/src/index.ts +4 -14
  20. package/src/util/__tests__/animation-frame.test.ts +9 -10
  21. package/src/util/animation-frame.ts +12 -16
  22. package/src/util/interval.ts +13 -18
  23. package/src/util/policies.ts +13 -8
  24. package/src/util/timeout.ts +14 -18
  25. package/src/util/types.ts +19 -11
  26. package/tsconfig-build.json +2 -4
  27. package/tsconfig-build.tsbuildinfo +1 -1
  28. package/dist/components/action-scheduler-provider.js.flow +0 -33
  29. package/dist/components/with-action-scheduler.js.flow +0 -22
  30. package/dist/hooks/internal/use-updating-ref.d.ts +0 -13
  31. package/dist/hooks/internal/use-updating-ref.js.flow +0 -20
  32. package/dist/hooks/use-interval.js.flow +0 -17
  33. package/dist/hooks/use-scheduled-interval.d.ts +0 -2
  34. package/dist/hooks/use-scheduled-interval.js.flow +0 -12
  35. package/dist/hooks/use-scheduled-timeout.d.ts +0 -2
  36. package/dist/hooks/use-scheduled-timeout.js.flow +0 -12
  37. package/dist/hooks/use-timeout.js.flow +0 -17
  38. package/dist/index.js.flow +0 -31
  39. package/dist/util/action-scheduler.js.flow +0 -38
  40. package/dist/util/animation-frame.js.flow +0 -70
  41. package/dist/util/interval.js.flow +0 -69
  42. package/dist/util/policies.js.flow +0 -14
  43. package/dist/util/timeout.js.flow +0 -71
  44. package/dist/util/types.js.flow +0 -232
  45. package/dist/util/types.typestest.js.flow +0 -6
  46. package/src/hooks/__tests__/use-scheduled-interval.test.ts +0 -460
  47. package/src/hooks/__tests__/use-scheduled-timeout.test.ts +0 -478
  48. package/src/hooks/internal/use-updating-ref.ts +0 -23
  49. package/src/hooks/use-scheduled-interval.ts +0 -72
  50. package/src/hooks/use-scheduled-timeout.ts +0 -79
@@ -1,123 +1,520 @@
1
- import {renderHook} from "@testing-library/react-hooks";
1
+ import {renderHook, act} from "@testing-library/react-hooks";
2
+ import {SchedulePolicy, ClearPolicy, ActionPolicy} from "../../util/policies";
2
3
 
3
4
  import {useInterval} from "../use-interval";
4
5
 
5
- describe("useTimeout", () => {
6
+ describe("useInterval", () => {
6
7
  beforeEach(() => {
7
8
  jest.useFakeTimers();
8
9
  });
9
10
 
10
- it("should not fire if 'active' is false", () => {
11
+ afterEach(() => {
12
+ jest.restoreAllMocks();
13
+ });
14
+
15
+ it("throws if the action is not a function", () => {
11
16
  // Arrange
12
- const action = jest.fn();
13
17
 
14
18
  // Act
15
- renderHook(() => useInterval(action, 500, false));
16
- jest.advanceTimersByTime(501);
19
+ const {result} = renderHook(() => useInterval(null as any, 1000));
17
20
 
18
21
  // Assert
19
- expect(action).not.toHaveBeenCalled();
22
+ expect(result.error).toEqual(Error("Action must be a function"));
20
23
  });
21
24
 
22
- it("should fire the action multiple times based on 'intervalMs'", () => {
25
+ it("throws if the period is less than 1", () => {
23
26
  // Arrange
24
- const action = jest.fn();
25
27
 
26
28
  // Act
27
- renderHook(() => useInterval(action, 500, true));
28
- jest.advanceTimersByTime(1001);
29
+ const {result} = renderHook(() => useInterval(() => {}, 0));
29
30
 
30
31
  // Assert
31
- expect(action).toHaveBeenCalledTimes(2);
32
+ expect(result.error).toEqual(Error("Interval period must be >= 1"));
32
33
  });
33
34
 
34
- it("should fire after time after 'active' changes to true", () => {
35
+ it("sets an interval when schedule policy is SchedulePolicy.Immediately", () => {
35
36
  // Arrange
36
- const action = jest.fn();
37
+ const intervalSpy = jest.spyOn(global, "setInterval");
37
38
 
38
39
  // Act
39
- const {rerender} = renderHook(
40
- ({active}: any) => useInterval(action, 500, active),
41
- {initialProps: {active: false}},
42
- );
43
- rerender({active: true});
44
- jest.advanceTimersByTime(501);
40
+ renderHook(() => useInterval(() => {}, 1000));
45
41
 
46
42
  // Assert
43
+ expect(intervalSpy).toHaveBeenCalledTimes(1);
44
+ });
45
+
46
+ it("should call the action before unmounting when clear policy is Resolve", () => {
47
+ const action = jest.fn();
48
+ const {unmount} = renderHook(() =>
49
+ useInterval(action, 1000, {
50
+ clearPolicy: ClearPolicy.Resolve,
51
+ }),
52
+ );
53
+
54
+ act(() => {
55
+ unmount();
56
+ });
57
+
47
58
  expect(action).toHaveBeenCalled();
48
59
  });
49
60
 
50
- it("should not fire after 'intervalMs' if 'active' changes to false", () => {
61
+ it("should call the current action", () => {
51
62
  // Arrange
52
- const action = jest.fn();
63
+ const action1 = jest.fn();
64
+ const action2 = jest.fn();
53
65
  const {rerender} = renderHook(
54
- ({active}: any) => useInterval(action, 500, active),
55
- {initialProps: {active: true}},
66
+ ({action}: any) => useInterval(action, 500),
67
+ {
68
+ initialProps: {action: action1},
69
+ },
56
70
  );
57
71
 
58
72
  // Act
59
- rerender({active: false});
73
+ rerender({action: action2});
60
74
  jest.advanceTimersByTime(501);
61
75
 
62
76
  // Assert
63
- expect(action).not.toHaveBeenCalled();
77
+ expect(action2).toHaveBeenCalledTimes(1);
64
78
  });
65
79
 
66
- it("should reset the interval if 'intervalMs' is changes", () => {
80
+ it("should only call setInterval once even if action changes", () => {
67
81
  // Arrange
68
- const action = jest.fn();
69
-
70
- // Act
82
+ const intervalSpy = jest.spyOn(global, "setInterval");
83
+ const action1 = jest.fn();
84
+ const action2 = jest.fn();
71
85
  const {rerender} = renderHook(
72
- ({timeoutMs}: any) => useInterval(action, timeoutMs, true),
73
- {initialProps: {timeoutMs: 500}},
86
+ ({action}: any) => useInterval(action, 500),
87
+ {
88
+ initialProps: {action: action1},
89
+ },
74
90
  );
75
- rerender({timeoutMs: 1000});
76
- jest.advanceTimersByTime(501);
91
+
92
+ // Act
93
+ rerender({action: action2});
77
94
 
78
95
  // Assert
79
- expect(action).not.toHaveBeenCalled();
80
- jest.advanceTimersByTime(1001);
81
- expect(action).toHaveBeenCalled();
96
+ expect(intervalSpy).toHaveBeenCalledTimes(1);
82
97
  });
83
98
 
84
- it("should not reset the interval if 'action' changes", () => {
99
+ it("should not reset the interval if the action changes", () => {
85
100
  // Arrange
86
101
  const action1 = jest.fn();
87
102
  const action2 = jest.fn();
88
- const timeoutSpy = jest.spyOn(window, "setTimeout");
89
-
90
- // Act
91
103
  const {rerender} = renderHook(
92
- ({action}: any) => useInterval(action, 500, true),
93
- {initialProps: {action: action1}},
104
+ ({action}: any) => useInterval(action, 500),
105
+ {
106
+ initialProps: {action: action1},
107
+ },
94
108
  );
95
- // NOTE: For some reason setTimeout is called twice by the time we get
96
- // here. I've verified that it only gets called once inside the hook
97
- // so something else must be calling it.
98
- const callCount = timeoutSpy.mock.calls.length;
109
+
110
+ // Act
111
+ jest.advanceTimersByTime(250);
99
112
  rerender({action: action2});
100
- jest.advanceTimersByTime(501);
113
+ jest.advanceTimersByTime(751);
101
114
 
102
115
  // Assert
103
- expect(timeoutSpy).toHaveBeenCalledTimes(callCount);
116
+ expect(action1).not.toHaveBeenCalled();
117
+ expect(action2).toHaveBeenCalledTimes(2);
104
118
  });
105
119
 
106
- it("should fire the current action if 'action' changes", () => {
120
+ it("should reset the interval if the action changes and the action policy is Reset", () => {
107
121
  // Arrange
108
122
  const action1 = jest.fn();
109
123
  const action2 = jest.fn();
124
+ const {rerender} = renderHook(
125
+ ({action}: any) =>
126
+ useInterval(action, 500, {actionPolicy: ActionPolicy.Reset}),
127
+ {
128
+ initialProps: {action: action1},
129
+ },
130
+ );
131
+
132
+ // Act
133
+ jest.advanceTimersByTime(250);
134
+ rerender({action: action2});
135
+ jest.advanceTimersByTime(751);
136
+
137
+ // Assert
138
+ expect(action1).not.toHaveBeenCalled();
139
+ expect(action2).toHaveBeenCalledTimes(1);
140
+ });
141
+
142
+ it("should use the new interval period after changing it", () => {
143
+ // Arrange
144
+ const action = jest.fn();
145
+ const {rerender} = renderHook(
146
+ ({intervalMs}: any) => useInterval(action, intervalMs),
147
+ {
148
+ initialProps: {intervalMs: 500},
149
+ },
150
+ );
151
+ rerender({intervalMs: 1000});
110
152
 
111
153
  // Act
154
+ jest.advanceTimersByTime(1501);
155
+
156
+ // Assert
157
+ expect(action).toHaveBeenCalledTimes(1);
158
+ });
159
+
160
+ it("should restart the interval if intervalMs changes", () => {
161
+ // Arrange
162
+ const intervalSpy = jest.spyOn(global, "setInterval");
112
163
  const {rerender} = renderHook(
113
- ({action}: any) => useInterval(action, 500, true),
114
- {initialProps: {action: action1}},
164
+ ({intervalMs}: any) => useInterval(() => {}, intervalMs),
165
+ {
166
+ initialProps: {intervalMs: 500},
167
+ },
115
168
  );
116
- rerender({action: action2});
117
- jest.advanceTimersByTime(501);
169
+
170
+ // Act
171
+ rerender({intervalMs: 1000});
118
172
 
119
173
  // Assert
120
- expect(action1).not.toHaveBeenCalledWith();
121
- expect(action2).toHaveBeenCalledWith();
174
+ expect(intervalSpy).toHaveBeenCalledWith(expect.any(Function), 500);
175
+ expect(intervalSpy).toHaveBeenCalledWith(expect.any(Function), 1000);
176
+ });
177
+
178
+ describe("isSet", () => {
179
+ it("is false when the interval has not been set [SchedulePolicy.OnDemand]", () => {
180
+ // Arrange
181
+ const {result} = renderHook(() =>
182
+ useInterval(() => {}, 1000, {
183
+ schedulePolicy: SchedulePolicy.OnDemand,
184
+ }),
185
+ );
186
+
187
+ // Act
188
+ const isSet = result.current.isSet;
189
+
190
+ // Assert
191
+ expect(isSet).toBeFalsy();
192
+ });
193
+
194
+ it("is true when the interval is active", () => {
195
+ // Arrange
196
+ const {result} = renderHook(() => useInterval(() => {}, 1000));
197
+ act(() => {
198
+ result.current.set();
199
+ });
200
+
201
+ // Act
202
+ const isSet = result.current.isSet;
203
+
204
+ // Assert
205
+ expect(isSet).toBeTruthy();
206
+ });
207
+
208
+ it("is false when the interval is cleared", () => {
209
+ // Arrange
210
+ const {result} = renderHook(() => useInterval(() => {}, 1000));
211
+ act(() => {
212
+ result.current.set();
213
+ result.current.clear();
214
+ });
215
+
216
+ // Act
217
+ const isSet = result.current.isSet;
218
+
219
+ // Assert
220
+ expect(isSet).toBeFalsy();
221
+ });
222
+ });
223
+
224
+ describe("#set", () => {
225
+ it("should call setInterval", () => {
226
+ // Arrange
227
+ const intervalSpy = jest.spyOn(global, "setInterval");
228
+ const {result} = renderHook(() =>
229
+ useInterval(() => {}, 500, {
230
+ schedulePolicy: SchedulePolicy.OnDemand,
231
+ }),
232
+ );
233
+
234
+ // Act
235
+ act(() => {
236
+ result.current.set();
237
+ });
238
+
239
+ // Assert
240
+ expect(intervalSpy).toHaveBeenNthCalledWith(
241
+ 1,
242
+ expect.any(Function),
243
+ 500,
244
+ );
245
+ });
246
+
247
+ it("should invoke setInterval to call the given action", () => {
248
+ // Arrange
249
+ const intervalSpy = jest.spyOn(global, "setInterval");
250
+ const action = jest.fn();
251
+ const {result} = renderHook(() =>
252
+ useInterval(action, 500, {
253
+ schedulePolicy: SchedulePolicy.OnDemand,
254
+ }),
255
+ );
256
+
257
+ act(() => {
258
+ result.current.set();
259
+ });
260
+ const scheduledAction = intervalSpy.mock.calls[0][0];
261
+
262
+ // Act
263
+ scheduledAction();
264
+
265
+ // Assert
266
+ expect(action).toHaveBeenCalledTimes(1);
267
+ });
268
+
269
+ it("should clear the active interval", () => {
270
+ // Arrange
271
+ const action = jest.fn();
272
+ const {result} = renderHook(() =>
273
+ useInterval(action, 500, {
274
+ schedulePolicy: SchedulePolicy.OnDemand,
275
+ }),
276
+ );
277
+ act(() => {
278
+ result.current.set();
279
+ });
280
+
281
+ // Act
282
+ act(() => {
283
+ result.current.set();
284
+ jest.advanceTimersByTime(501);
285
+ });
286
+
287
+ // Assert
288
+ expect(action).toHaveBeenCalledTimes(1);
289
+ });
290
+
291
+ it("should set an interval that stays active while not cleared", () => {
292
+ // Arrange
293
+ const action = jest.fn();
294
+ const {result} = renderHook(() =>
295
+ useInterval(action, 500, {
296
+ schedulePolicy: SchedulePolicy.OnDemand,
297
+ }),
298
+ );
299
+ act(() => {
300
+ result.current.set();
301
+ });
302
+
303
+ // Act
304
+ act(() => {
305
+ jest.advanceTimersByTime(1501);
306
+ });
307
+
308
+ // Assert
309
+ expect(action).toHaveBeenCalledTimes(3);
310
+ });
311
+
312
+ it("should continue to be set after calling it multiple times", () => {
313
+ // Arrange
314
+ const action = jest.fn();
315
+ const {result} = renderHook(() =>
316
+ useInterval(action, 500, {
317
+ schedulePolicy: SchedulePolicy.OnDemand,
318
+ }),
319
+ );
320
+ act(() => {
321
+ result.current.set();
322
+ });
323
+
324
+ // Act
325
+ act(() => {
326
+ result.current.set();
327
+ });
328
+ act(() => {
329
+ jest.advanceTimersByTime(501);
330
+ });
331
+
332
+ // Assert
333
+ expect(action).toHaveBeenCalled();
334
+ });
335
+
336
+ it("should set the interval after clearing it", () => {
337
+ // Arrange
338
+ const action = jest.fn();
339
+ const {result} = renderHook(() =>
340
+ useInterval(action, 500, {
341
+ schedulePolicy: SchedulePolicy.OnDemand,
342
+ }),
343
+ );
344
+ act(() => {
345
+ result.current.clear();
346
+ });
347
+
348
+ // Act
349
+ act(() => {
350
+ result.current.set();
351
+ });
352
+ act(() => {
353
+ jest.advanceTimersByTime(501);
354
+ });
355
+
356
+ // Assert
357
+ expect(action).toHaveBeenCalled();
358
+ });
359
+
360
+ it("should reset the inteval after calling set() again", () => {
361
+ // Arrange
362
+ const action = jest.fn();
363
+ const {result} = renderHook(() =>
364
+ useInterval(action, 750, {
365
+ schedulePolicy: SchedulePolicy.OnDemand,
366
+ }),
367
+ );
368
+
369
+ // Act
370
+ act(() => {
371
+ result.current.set();
372
+ jest.advanceTimersByTime(501);
373
+ result.current.set();
374
+ jest.advanceTimersByTime(501);
375
+ });
376
+
377
+ // Assert
378
+ expect(action).not.toHaveBeenCalled();
379
+ });
380
+
381
+ it("shouldn't throw an error if called after the component unmounted", () => {
382
+ const action = jest.fn();
383
+ const {result, unmount} = renderHook(() =>
384
+ useInterval(action, 500),
385
+ );
386
+ act(() => {
387
+ unmount();
388
+ });
389
+
390
+ // Act
391
+ const underTest = () => result.current.set();
392
+
393
+ // Assert
394
+ expect(underTest).not.toThrow();
395
+ });
396
+ });
397
+
398
+ describe("#clear", () => {
399
+ it("should clear an active interval", () => {
400
+ // Arrange
401
+ const action = jest.fn();
402
+ const {result} = renderHook(() => useInterval(action, 500));
403
+ act(() => {
404
+ result.current.set();
405
+ });
406
+
407
+ // Act
408
+ act(() => {
409
+ result.current.clear();
410
+ });
411
+ act(() => {
412
+ jest.advanceTimersByTime(501);
413
+ });
414
+
415
+ // Assert
416
+ expect(action).not.toHaveBeenCalled();
417
+ });
418
+
419
+ it("should invoke the action if clear policy is ClearPolicy.Resolve", () => {
420
+ // Arrange
421
+ const action = jest.fn();
422
+ const {result} = renderHook(() => useInterval(action, 500));
423
+ act(() => {
424
+ result.current.set();
425
+ });
426
+
427
+ // Act
428
+ act(() => {
429
+ result.current.clear(ClearPolicy.Resolve);
430
+ });
431
+ act(() => {
432
+ jest.advanceTimersByTime(501);
433
+ });
434
+
435
+ // Assert
436
+ expect(action).toHaveBeenCalledTimes(1);
437
+ });
438
+
439
+ it("should not invoke the action if clear policy is ClearPolicy.Cancel", () => {
440
+ // Arrange
441
+ const action = jest.fn();
442
+ const {result} = renderHook(() =>
443
+ useInterval(action, 500, {
444
+ schedulePolicy: SchedulePolicy.Immediately,
445
+ }),
446
+ );
447
+ act(() => {
448
+ result.current.set();
449
+ });
450
+
451
+ // Act
452
+ act(() => {
453
+ result.current.clear(ClearPolicy.Cancel);
454
+ });
455
+ act(() => {
456
+ jest.advanceTimersByTime(501);
457
+ });
458
+
459
+ // Assert
460
+ expect(action).not.toHaveBeenCalled();
461
+ });
462
+
463
+ it("should not invoke the action if interval is inactive and clear policy is ClearPolicy.Resolve", () => {
464
+ // Arrange
465
+ const action = jest.fn();
466
+ const {result} = renderHook(() =>
467
+ useInterval(action, 500, {
468
+ schedulePolicy: SchedulePolicy.OnDemand,
469
+ }),
470
+ );
471
+
472
+ // Act
473
+ act(() => {
474
+ result.current.clear(ClearPolicy.Resolve);
475
+ jest.advanceTimersByTime(501);
476
+ });
477
+
478
+ // Assert
479
+ expect(action).not.toHaveBeenCalled();
480
+ });
481
+
482
+ it("should not call the action on unmount if the interval is not running when the clearPolicy is ClearPolicy.Resolve", async () => {
483
+ // Arrange
484
+ const action = jest.fn();
485
+ const {result, unmount} = renderHook(() =>
486
+ useInterval(action, 500, {
487
+ clearPolicy: ClearPolicy.Resolve,
488
+ }),
489
+ );
490
+
491
+ // Act
492
+ act(() => {
493
+ result.current.clear();
494
+ });
495
+ act(() => {
496
+ unmount();
497
+ });
498
+
499
+ // Assert
500
+ expect(action).not.toHaveBeenCalled();
501
+ });
502
+
503
+ it("should not error if calling clear() after unmounting", () => {
504
+ // Arrange
505
+ const action = jest.fn();
506
+ const {result, unmount} = renderHook(() =>
507
+ useInterval(action, 500),
508
+ );
509
+ act(() => {
510
+ unmount();
511
+ });
512
+
513
+ // Act
514
+ const underTest = () => result.current.clear();
515
+
516
+ // Assert
517
+ expect(underTest).not.toThrow();
518
+ });
122
519
  });
123
520
  });