@khanacademy/wonder-blocks-timing 4.0.2 → 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.
@@ -1,478 +0,0 @@
1
- import {renderHook, act} from "@testing-library/react-hooks";
2
- import {SchedulePolicy, ClearPolicy} from "../../util/policies";
3
-
4
- import {useScheduledTimeout} from "../use-scheduled-timeout";
5
-
6
- describe("useScheduledTimeout", () => {
7
- beforeEach(() => {
8
- jest.useFakeTimers();
9
- });
10
-
11
- afterEach(() => {
12
- jest.restoreAllMocks();
13
- });
14
-
15
- it("throws if the action is not a function", () => {
16
- // Arrange
17
-
18
- // Act
19
- const {result} = renderHook(() =>
20
- useScheduledTimeout(null as any, 1000),
21
- );
22
-
23
- // Assert
24
- expect(result.error).toEqual(Error("Action must be a function"));
25
- });
26
-
27
- it("throws if the period is less than 0", () => {
28
- // Arrange
29
-
30
- // Act
31
- const {result} = renderHook(() => useScheduledTimeout(() => {}, -1));
32
-
33
- // Assert
34
- expect(result.error).toEqual(Error("Timeout period must be >= 0"));
35
- });
36
-
37
- it("should return an ITimeout", () => {
38
- // Arrange
39
- const {result} = renderHook(() => useScheduledTimeout(() => {}, 1000));
40
-
41
- // Act
42
-
43
- // Assert
44
- expect(result.current).toEqual(
45
- expect.objectContaining({
46
- clear: expect.any(Function),
47
- set: expect.any(Function),
48
- isSet: expect.any(Boolean),
49
- }),
50
- );
51
- });
52
-
53
- it("should default to being immediately set", () => {
54
- // Arrange
55
- const {result} = renderHook(() => useScheduledTimeout(() => {}, 1000));
56
-
57
- // Act
58
-
59
- // Assert
60
- expect(result.current.isSet).toBe(true);
61
- });
62
-
63
- it("should call the action before unmounting", () => {
64
- const action = jest.fn();
65
- const {unmount} = renderHook(() =>
66
- useScheduledTimeout(action, 1000, {
67
- clearPolicy: ClearPolicy.Resolve,
68
- }),
69
- );
70
-
71
- act(() => {
72
- unmount();
73
- });
74
-
75
- expect(action).toHaveBeenCalled();
76
- });
77
-
78
- it("should call the current action", () => {
79
- // Arrange
80
- const action1 = jest.fn();
81
- const action2 = jest.fn();
82
- const {rerender} = renderHook(
83
- ({action}: any) => useScheduledTimeout(action, 500),
84
- {
85
- initialProps: {action: action1},
86
- },
87
- );
88
-
89
- // Act
90
- rerender({action: action2});
91
- jest.advanceTimersByTime(501);
92
-
93
- // Assert
94
- expect(action2).toHaveBeenCalledTimes(1);
95
- });
96
-
97
- it("should only call setTimeout once even if action changes", () => {
98
- // Arrange
99
- const timeoutSpy = jest.spyOn(global, "setTimeout");
100
- const action1 = jest.fn();
101
- const action2 = jest.fn();
102
- const {rerender} = renderHook(
103
- ({action}: any) => useScheduledTimeout(action, 500),
104
- {
105
- initialProps: {action: action1},
106
- },
107
- );
108
- // NOTE: For some reason setTimeout is called twice by the time we get
109
- // here. I've verified that it only gets called once inside the hook
110
- // so something else must be calling it.
111
- const callCount = timeoutSpy.mock.calls.length;
112
-
113
- // Act
114
- rerender({action: action2});
115
-
116
- // Assert
117
- expect(timeoutSpy).toHaveBeenCalledTimes(callCount);
118
- });
119
-
120
- it("should use the new timeout duration after changing it", () => {
121
- // Arrange
122
- const action = jest.fn();
123
- const {rerender} = renderHook(
124
- ({timeoutMs}: any) => useScheduledTimeout(action, timeoutMs),
125
- {
126
- initialProps: {timeoutMs: 500},
127
- },
128
- );
129
- rerender({timeoutMs: 1000});
130
-
131
- // Act
132
- jest.advanceTimersByTime(1501);
133
-
134
- // Assert
135
- expect(action).toHaveBeenCalledTimes(1);
136
- });
137
-
138
- it("should restart the timeout if intervalMs changes", () => {
139
- // Arrange
140
- const timeoutSpy = jest.spyOn(global, "setTimeout");
141
- const {rerender} = renderHook(
142
- ({timeoutMs}: any) => useScheduledTimeout(() => {}, timeoutMs),
143
- {
144
- initialProps: {timeoutMs: 500},
145
- },
146
- );
147
-
148
- // Act
149
- rerender({timeoutMs: 1000});
150
-
151
- // Assert
152
- expect(timeoutSpy).toHaveBeenCalledWith(expect.any(Function), 500);
153
- expect(timeoutSpy).toHaveBeenCalledWith(expect.any(Function), 1000);
154
- });
155
-
156
- describe("SchedulePolicies.Immediately", () => {
157
- it("should call the action after the timeout expires", () => {
158
- // Arrange
159
- const action = jest.fn();
160
- renderHook(() => useScheduledTimeout(action, 1000));
161
-
162
- // Act
163
- act(() => {
164
- jest.advanceTimersByTime(1000);
165
- });
166
-
167
- // Assert
168
- expect(action).toHaveBeenCalled();
169
- });
170
-
171
- it("should update isSet to false after the timeout expires", () => {
172
- // Arrange
173
- const action = jest.fn();
174
- const {result} = renderHook(() =>
175
- useScheduledTimeout(action, 1000),
176
- );
177
-
178
- // Act
179
- act(() => {
180
- jest.advanceTimersByTime(1001);
181
- });
182
-
183
- // Assert
184
- expect(result.current.isSet).toBe(false);
185
- });
186
-
187
- it("should call the action again if 'set' is called after the action was called", () => {
188
- // Arrange
189
- const action = jest.fn();
190
- const {result} = renderHook(() =>
191
- useScheduledTimeout(action, 1000),
192
- );
193
-
194
- // Act
195
- act(() => {
196
- jest.advanceTimersByTime(1001);
197
- });
198
- act(() => {
199
- result.current.set();
200
- });
201
- act(() => {
202
- jest.advanceTimersByTime(1001);
203
- });
204
-
205
- // Assert
206
- expect(action).toHaveBeenCalledTimes(2);
207
- });
208
-
209
- it("should restart the timeout if timeoutMs gets updated", () => {
210
- // Arrange
211
- const action = jest.fn();
212
- const {rerender} = renderHook(
213
- ({timeoutMs}: any) => useScheduledTimeout(action, timeoutMs),
214
- {
215
- initialProps: {timeoutMs: 1000},
216
- },
217
- );
218
-
219
- // Act
220
- act(() => {
221
- jest.advanceTimersByTime(900);
222
- });
223
- rerender({timeoutMs: 500});
224
- act(() => {
225
- jest.advanceTimersByTime(100);
226
- });
227
-
228
- // Assert
229
- expect(action).not.toHaveBeenCalled();
230
- act((): void => jest.advanceTimersByTime(500));
231
- expect(action).toHaveBeenCalled();
232
- });
233
-
234
- it("should should timeout after the new timeoutMs if it gets updated", () => {
235
- // Arrange
236
- const action = jest.fn();
237
- const {rerender} = renderHook(
238
- ({timeoutMs}: any) => useScheduledTimeout(action, timeoutMs),
239
- {
240
- initialProps: {timeoutMs: 1000},
241
- },
242
- );
243
-
244
- // Act
245
- rerender({timeoutMs: 500});
246
- act(() => {
247
- jest.advanceTimersByTime(500);
248
- });
249
-
250
- // Assert
251
- expect(action).toHaveBeenCalled();
252
- });
253
-
254
- it("should call the new action after re-rendering with a new action", () => {
255
- // Arrange
256
- const action1 = jest.fn();
257
- const action2 = jest.fn();
258
- const {rerender} = renderHook(
259
- ({action}: any) => useScheduledTimeout(action, 1000),
260
- {
261
- initialProps: {action: action1},
262
- },
263
- );
264
-
265
- // Act
266
- rerender({action: action2});
267
- act(() => {
268
- jest.advanceTimersByTime(1000);
269
- });
270
-
271
- // Assert
272
- expect(action2).toHaveBeenCalled();
273
- });
274
-
275
- it("should not call the original action after re-rendering with a new action", () => {
276
- // Arrange
277
- const action1 = jest.fn();
278
- const action2 = jest.fn();
279
- const {rerender} = renderHook(
280
- ({action}: any) => useScheduledTimeout(action, 1000),
281
- {
282
- initialProps: {action: action1},
283
- },
284
- );
285
-
286
- // Act
287
- rerender({action: action2});
288
- act(() => {
289
- jest.advanceTimersByTime(1000);
290
- });
291
-
292
- // Assert
293
- expect(action1).not.toHaveBeenCalled();
294
- });
295
-
296
- it("should not call the action if the timeout is cleared", () => {
297
- // Arrange
298
- const action = jest.fn();
299
- const {result} = renderHook(() =>
300
- useScheduledTimeout(action, 1000),
301
- );
302
-
303
- // Act
304
- act(() => {
305
- result.current.clear();
306
- });
307
- act(() => {
308
- jest.advanceTimersByTime(1000);
309
- });
310
-
311
- // Assert
312
- expect(action).not.toHaveBeenCalled();
313
- });
314
-
315
- it("should call the action when the timeout is cleared when passing ClearPolicies.Resolve to clear()", () => {
316
- // Arrange
317
- const action = jest.fn();
318
- const {result} = renderHook(() =>
319
- useScheduledTimeout(action, 1000),
320
- );
321
-
322
- // Act
323
- act(() => {
324
- result.current.clear(ClearPolicy.Resolve);
325
- });
326
-
327
- // Assert
328
- expect(action).toHaveBeenCalled();
329
- });
330
-
331
- it("should call the action when the timeout is cleared when using ClearPolicies.Resolve in options", () => {
332
- // Arrange
333
- const action = jest.fn();
334
- const {result} = renderHook(() =>
335
- useScheduledTimeout(action, 1000, {
336
- clearPolicy: ClearPolicy.Resolve,
337
- }),
338
- );
339
-
340
- // Act
341
- act(() => {
342
- result.current.clear();
343
- });
344
-
345
- // Assert
346
- expect(action).toHaveBeenCalled();
347
- });
348
-
349
- it("should call the action on unmount when using ClearPolicies.Resolve in options", () => {
350
- // Arrange
351
- const action = jest.fn();
352
- const {unmount} = renderHook(() =>
353
- useScheduledTimeout(action, 1000, {
354
- clearPolicy: ClearPolicy.Resolve,
355
- }),
356
- );
357
-
358
- // Act
359
- unmount();
360
-
361
- // Assert
362
- expect(action).toHaveBeenCalled();
363
- });
364
-
365
- it("should not call the action on unmount when using the default options", () => {
366
- // Arrange
367
- const action = jest.fn();
368
- const {unmount} = renderHook(() =>
369
- useScheduledTimeout(action, 1000),
370
- );
371
-
372
- // Act
373
- unmount();
374
-
375
- // Assert
376
- expect(action).not.toHaveBeenCalled();
377
- });
378
- });
379
-
380
- describe("SchedulePolicies.OnDemand", () => {
381
- it("should not set the timer on creation", () => {
382
- // Arrange
383
- const {result} = renderHook(() =>
384
- useScheduledTimeout(() => {}, 1000, {
385
- schedulePolicy: SchedulePolicy.OnDemand,
386
- }),
387
- );
388
-
389
- // Act
390
-
391
- // Assert
392
- expect(result.current.isSet).toBe(false);
393
- });
394
-
395
- it("should not call action after timeoutMs if the timer hasn't been set", () => {
396
- // Arrange
397
- const action = jest.fn();
398
- renderHook(() =>
399
- useScheduledTimeout(action, 1000, {
400
- schedulePolicy: SchedulePolicy.OnDemand,
401
- }),
402
- );
403
-
404
- // Act
405
- act(() => {
406
- jest.advanceTimersByTime(1000);
407
- });
408
-
409
- // Assert
410
- expect(action).not.toHaveBeenCalled();
411
- });
412
-
413
- it("should call action after timeoutMs if the timer has been set", () => {
414
- // Arrange
415
- const action = jest.fn();
416
- const {result} = renderHook(() =>
417
- useScheduledTimeout(action, 1000, {
418
- schedulePolicy: SchedulePolicy.OnDemand,
419
- }),
420
- );
421
-
422
- // Act
423
- act(() => {
424
- result.current.set();
425
- });
426
- act(() => {
427
- jest.advanceTimersByTime(1000);
428
- });
429
-
430
- // Assert
431
- expect(action).toHaveBeenCalled();
432
- });
433
-
434
- it("should reset the timer after calling set() again", () => {
435
- // Arrange
436
- const action = jest.fn();
437
- const {result} = renderHook(() =>
438
- useScheduledTimeout(action, 1000, {
439
- schedulePolicy: SchedulePolicy.OnDemand,
440
- }),
441
- );
442
-
443
- // Act
444
- act(() => {
445
- result.current.set();
446
- jest.advanceTimersByTime(500);
447
- result.current.set();
448
- jest.advanceTimersByTime(500);
449
- });
450
-
451
- // Assert
452
- expect(action).not.toHaveBeenCalled();
453
- });
454
-
455
- it("should call the action after calling set() again", () => {
456
- // Arrange
457
- const action = jest.fn();
458
- const {result} = renderHook(() =>
459
- useScheduledTimeout(action, 1000, {
460
- schedulePolicy: SchedulePolicy.OnDemand,
461
- }),
462
- );
463
-
464
- // Act
465
- act(() => {
466
- result.current.set();
467
- jest.advanceTimersByTime(500);
468
- });
469
- act(() => {
470
- result.current.set();
471
- jest.advanceTimersByTime(1000);
472
- });
473
-
474
- // Assert
475
- expect(action).toHaveBeenCalled();
476
- });
477
- });
478
- });
@@ -1,23 +0,0 @@
1
- import {useEffect, useRef} from "react";
2
-
3
- /**
4
- * Returns a ref whose .current value is updated whenever
5
- * the `value` passed to this hook changes.
6
- *
7
- * this is great for values that you want to reference from
8
- * within a useCallback or useEffect event listener, without
9
- * re-triggering the effect when the value changes
10
- *
11
- * @returns {{current: T}}
12
- */
13
- export const useUpdatingRef = <T>(
14
- value: T,
15
- ): {
16
- current: T;
17
- } => {
18
- const ref = useRef<T>(value);
19
- useEffect(() => {
20
- ref.current = value;
21
- }, [value]);
22
- return ref;
23
- };
@@ -1,72 +0,0 @@
1
- import {useEffect, useState, useCallback} from "react";
2
-
3
- import {
4
- SchedulePolicy as SchedulePolicies,
5
- ClearPolicy as ClearPolicies,
6
- } from "../util/policies";
7
- import type {IInterval, ClearPolicy, Options} from "../util/types";
8
-
9
- import {useUpdatingRef} from "./internal/use-updating-ref";
10
- import {useInterval} from "./use-interval";
11
-
12
- export function useScheduledInterval(
13
- action: () => unknown,
14
- intervalMs: number,
15
- options?: Options,
16
- ): IInterval {
17
- if (typeof action !== "function") {
18
- throw new Error("Action must be a function");
19
- }
20
-
21
- if (intervalMs < 1) {
22
- throw new Error("Interval period must be >= 1");
23
- }
24
-
25
- const schedulePolicy =
26
- options?.schedulePolicy ?? SchedulePolicies.Immediately;
27
-
28
- const [isSet, setIsSet] = useState(
29
- schedulePolicy === SchedulePolicies.Immediately,
30
- );
31
-
32
- const set = useCallback(() => setIsSet(true), []);
33
-
34
- const actionRef = useUpdatingRef(action);
35
-
36
- const clear = useCallback(
37
- (policy?: ClearPolicy) => {
38
- policy = policy ?? options?.clearPolicy;
39
- if (isSet && policy === ClearPolicies.Resolve) {
40
- actionRef.current();
41
- }
42
- setIsSet(false);
43
- },
44
- // react-hooks/exhaustive-deps doesn't require refs to be
45
- // listed in the deps array. Unfortunately, in this situation
46
- // it doesn't recognized actionRef as a ref.
47
- [actionRef, isSet, options?.clearPolicy],
48
- );
49
-
50
- const runOnUnmountRef = useUpdatingRef(
51
- isSet && options?.clearPolicy === ClearPolicies.Resolve,
52
- );
53
-
54
- useEffect(() => {
55
- return () => {
56
- // This code will only run with the component using this
57
- // hook is unmounted.
58
- // eslint-disable-next-line react-hooks/exhaustive-deps
59
- if (runOnUnmountRef.current) {
60
- // eslint-disable-next-line react-hooks/exhaustive-deps
61
- actionRef.current();
62
- }
63
- };
64
- // This eslint rule doesn't realize actionRef and runOnUnmountRef
65
- // a both refs and thus do not have to be listed as deps.
66
- // eslint-disable-next-line react-hooks/exhaustive-deps
67
- }, []);
68
-
69
- useInterval(action, intervalMs, isSet);
70
-
71
- return {isSet, set, clear};
72
- }
@@ -1,79 +0,0 @@
1
- import {useEffect, useState, useCallback} from "react";
2
-
3
- import {
4
- SchedulePolicy as SchedulePolicies,
5
- ClearPolicy as ClearPolicies,
6
- } from "../util/policies";
7
- import type {ITimeout, ClearPolicy, Options} from "../util/types";
8
-
9
- import {useUpdatingRef} from "./internal/use-updating-ref";
10
- import {useTimeout} from "./use-timeout";
11
-
12
- export function useScheduledTimeout(
13
- action: () => unknown,
14
- timeoutMs: number,
15
- options?: Options,
16
- ): ITimeout {
17
- if (typeof action !== "function") {
18
- throw new Error("Action must be a function");
19
- }
20
-
21
- if (timeoutMs < 0) {
22
- throw new Error("Timeout period must be >= 0");
23
- }
24
-
25
- const schedulePolicy =
26
- options?.schedulePolicy ?? SchedulePolicies.Immediately;
27
-
28
- const [isSet, setIsSet] = useState(
29
- schedulePolicy === SchedulePolicies.Immediately,
30
- );
31
-
32
- const set = useCallback(() => setIsSet(true), []);
33
-
34
- // This wrapper isn't present in useScheduledInterval because we
35
- // don't need to update `isSet` in that situations.
36
- const wrappedAction = useCallback(() => {
37
- setIsSet(false);
38
- action();
39
- }, [action]);
40
-
41
- const actionRef = useUpdatingRef(wrappedAction);
42
-
43
- const clear = useCallback(
44
- (policy?: ClearPolicy) => {
45
- policy = policy ?? options?.clearPolicy;
46
- if (isSet && policy === ClearPolicies.Resolve) {
47
- actionRef.current();
48
- }
49
- setIsSet(false);
50
- },
51
- // react-hooks/exhaustive-deps doesn't require refs to be
52
- // listed in the deps array. Unfortunately, in this situation
53
- // it doesn't recognized actionRef as a ref.
54
- [actionRef, isSet, options?.clearPolicy],
55
- );
56
-
57
- const runOnUnmountRef = useUpdatingRef(
58
- isSet && options?.clearPolicy === ClearPolicies.Resolve,
59
- );
60
-
61
- useEffect(() => {
62
- return () => {
63
- // This code will only run with the component using this
64
- // hook is unmounted.
65
- // eslint-disable-next-line react-hooks/exhaustive-deps
66
- if (runOnUnmountRef.current) {
67
- // eslint-disable-next-line react-hooks/exhaustive-deps
68
- actionRef.current();
69
- }
70
- };
71
- // This eslint rule doesn't realize actionRef and runOnUnmountRef
72
- // a both refs and thus do not have to be listed as deps.
73
- // eslint-disable-next-line react-hooks/exhaustive-deps
74
- }, []);
75
-
76
- useTimeout(wrappedAction, timeoutMs, isSet);
77
-
78
- return {isSet, set, clear};
79
- }