@khanacademy/wonder-blocks-timing 5.0.1 → 5.0.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.
@@ -1,489 +0,0 @@
1
- import {renderHook, act} from "@testing-library/react-hooks";
2
- import {SchedulePolicy, ClearPolicy, ActionPolicy} from "../../util/policies";
3
-
4
- import {useTimeout} from "../use-timeout";
5
-
6
- describe("useTimeout", () => {
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(() => useTimeout(null as any, 1000));
20
-
21
- // Assert
22
- expect(result.error).toEqual(Error("Action must be a function"));
23
- });
24
-
25
- it("throws if the period is less than 0", () => {
26
- // Arrange
27
-
28
- // Act
29
- const {result} = renderHook(() => useTimeout(() => {}, -1));
30
-
31
- // Assert
32
- expect(result.error).toEqual(Error("Timeout period must be >= 0"));
33
- });
34
-
35
- it("should return an ITimeout", () => {
36
- // Arrange
37
- const {result} = renderHook(() => useTimeout(() => {}, 1000));
38
-
39
- // Act
40
-
41
- // Assert
42
- expect(result.current).toEqual(
43
- expect.objectContaining({
44
- clear: expect.any(Function),
45
- set: expect.any(Function),
46
- isSet: expect.any(Boolean),
47
- }),
48
- );
49
- });
50
-
51
- it("should default to being immediately set", () => {
52
- // Arrange
53
- const {result} = renderHook(() => useTimeout(() => {}, 1000));
54
-
55
- // Act
56
-
57
- // Assert
58
- expect(result.current.isSet).toBe(true);
59
- });
60
-
61
- it("should call the action before unmounting", () => {
62
- // Arrange
63
- const action = jest.fn();
64
- const {unmount} = renderHook(() =>
65
- useTimeout(action, 1000, {
66
- clearPolicy: ClearPolicy.Resolve,
67
- }),
68
- );
69
-
70
- // Act
71
- act(() => {
72
- unmount();
73
- });
74
-
75
- // Assert
76
- expect(action).toHaveBeenCalled();
77
- });
78
-
79
- it("should call the current action", () => {
80
- // Arrange
81
- const action1 = jest.fn();
82
- const action2 = jest.fn();
83
- const {rerender} = renderHook(
84
- ({action}: any) => useTimeout(action, 500),
85
- {
86
- initialProps: {action: action1},
87
- },
88
- );
89
-
90
- // Act
91
- rerender({action: action2});
92
- jest.advanceTimersByTime(501);
93
-
94
- // Assert
95
- expect(action2).toHaveBeenCalledTimes(1);
96
- });
97
-
98
- it("should only call setTimeout once even if action changes", () => {
99
- // Arrange
100
- const timeoutSpy = jest.spyOn(global, "setTimeout");
101
- const action1 = jest.fn();
102
- const action2 = jest.fn();
103
- const {rerender} = renderHook(
104
- ({action}: any) => useTimeout(action, 500),
105
- {
106
- initialProps: {action: action1},
107
- },
108
- );
109
- // Act
110
- rerender({action: action2});
111
-
112
- // Assert
113
- expect(timeoutSpy).toHaveBeenCalledOnce();
114
- });
115
-
116
- it("should not reset the timeout if the action changes", () => {
117
- // Arrange
118
- const action1 = jest.fn();
119
- const action2 = jest.fn();
120
- const {rerender} = renderHook(
121
- ({action}: any) => useTimeout(action, 500),
122
- {
123
- initialProps: {action: action1},
124
- },
125
- );
126
-
127
- // Act
128
- jest.advanceTimersByTime(250);
129
- rerender({action: action2});
130
- jest.advanceTimersByTime(251);
131
-
132
- // Assert
133
- expect(action1).not.toHaveBeenCalled();
134
- expect(action2).toHaveBeenCalledTimes(1);
135
- });
136
-
137
- it("should reset the timeout if the action changes and the action policy is Reset", () => {
138
- // Arrange
139
- const action1 = jest.fn();
140
- const action2 = jest.fn();
141
- const {rerender} = renderHook(
142
- ({action}: any) =>
143
- useTimeout(action, 500, {actionPolicy: ActionPolicy.Reset}),
144
- {
145
- initialProps: {action: action1},
146
- },
147
- );
148
-
149
- // Act
150
- jest.advanceTimersByTime(250);
151
- rerender({action: action2});
152
- jest.advanceTimersByTime(251);
153
-
154
- // Assert
155
- expect(action1).not.toHaveBeenCalled();
156
- expect(action2).not.toHaveBeenCalled();
157
- });
158
-
159
- it("should use the new timeout duration after changing it", () => {
160
- // Arrange
161
- const action = jest.fn();
162
- const {rerender} = renderHook(
163
- ({timeoutMs}: any) => useTimeout(action, timeoutMs),
164
- {
165
- initialProps: {timeoutMs: 500},
166
- },
167
- );
168
- rerender({timeoutMs: 1000});
169
-
170
- // Act
171
- jest.advanceTimersByTime(1501);
172
-
173
- // Assert
174
- expect(action).toHaveBeenCalledTimes(1);
175
- });
176
-
177
- it("should restart the timeout if intervalMs changes", () => {
178
- // Arrange
179
- const timeoutSpy = jest.spyOn(global, "setTimeout");
180
- const {rerender} = renderHook(
181
- ({timeoutMs}: any) => useTimeout(() => {}, timeoutMs),
182
- {
183
- initialProps: {timeoutMs: 500},
184
- },
185
- );
186
-
187
- // Act
188
- rerender({timeoutMs: 1000});
189
-
190
- // Assert
191
- expect(timeoutSpy).toHaveBeenCalledWith(expect.any(Function), 500);
192
- expect(timeoutSpy).toHaveBeenCalledWith(expect.any(Function), 1000);
193
- });
194
-
195
- describe("SchedulePolicy.Immediately", () => {
196
- it("should call the action after the timeout expires", () => {
197
- // Arrange
198
- const action = jest.fn();
199
- renderHook(() => useTimeout(action, 1000));
200
-
201
- // Act
202
- act(() => {
203
- jest.advanceTimersByTime(1000);
204
- });
205
-
206
- // Assert
207
- expect(action).toHaveBeenCalled();
208
- });
209
-
210
- it("should update isSet to false after the timeout expires", () => {
211
- // Arrange
212
- const action = jest.fn();
213
- const {result} = renderHook(() => useTimeout(action, 1000));
214
-
215
- // Act
216
- act(() => {
217
- jest.advanceTimersByTime(1001);
218
- });
219
-
220
- // Assert
221
- expect(result.current.isSet).toBe(false);
222
- });
223
-
224
- it("should call the action again if 'set' is called after the action was called", () => {
225
- // Arrange
226
- const action = jest.fn();
227
- const {result} = renderHook(() => useTimeout(action, 1000));
228
-
229
- // Act
230
- act(() => {
231
- jest.advanceTimersByTime(1001);
232
- });
233
- act(() => {
234
- result.current.set();
235
- });
236
- act(() => {
237
- jest.advanceTimersByTime(1001);
238
- });
239
-
240
- // Assert
241
- expect(action).toHaveBeenCalledTimes(2);
242
- });
243
-
244
- it("should restart the timeout if timeoutMs gets updated", () => {
245
- // Arrange
246
- const action = jest.fn();
247
- const {rerender} = renderHook(
248
- ({timeoutMs}: any) => useTimeout(action, timeoutMs),
249
- {
250
- initialProps: {timeoutMs: 1000},
251
- },
252
- );
253
-
254
- // Act
255
- act(() => {
256
- jest.advanceTimersByTime(900);
257
- });
258
- rerender({timeoutMs: 500});
259
- act(() => {
260
- jest.advanceTimersByTime(100);
261
- });
262
-
263
- // Assert
264
- expect(action).not.toHaveBeenCalled();
265
- act((): void => jest.advanceTimersByTime(500));
266
- expect(action).toHaveBeenCalled();
267
- });
268
-
269
- it("should should timeout after the new timeoutMs if it gets updated", () => {
270
- // Arrange
271
- const action = jest.fn();
272
- const {rerender} = renderHook(
273
- ({timeoutMs}: any) => useTimeout(action, timeoutMs),
274
- {
275
- initialProps: {timeoutMs: 1000},
276
- },
277
- );
278
-
279
- // Act
280
- rerender({timeoutMs: 500});
281
- act(() => {
282
- jest.advanceTimersByTime(500);
283
- });
284
-
285
- // Assert
286
- expect(action).toHaveBeenCalled();
287
- });
288
-
289
- it("should call the new action after re-rendering with a new action", () => {
290
- // Arrange
291
- const action1 = jest.fn();
292
- const action2 = jest.fn();
293
- const {rerender} = renderHook(
294
- ({action}: any) => useTimeout(action, 1000),
295
- {
296
- initialProps: {action: action1},
297
- },
298
- );
299
-
300
- // Act
301
- rerender({action: action2});
302
- act(() => {
303
- jest.advanceTimersByTime(1000);
304
- });
305
-
306
- // Assert
307
- expect(action2).toHaveBeenCalled();
308
- });
309
-
310
- it("should not call the original action after re-rendering with a new action", () => {
311
- // Arrange
312
- const action1 = jest.fn();
313
- const action2 = jest.fn();
314
- const {rerender} = renderHook(
315
- ({action}: any) => useTimeout(action, 1000),
316
- {
317
- initialProps: {action: action1},
318
- },
319
- );
320
-
321
- // Act
322
- rerender({action: action2});
323
- act(() => {
324
- jest.advanceTimersByTime(1000);
325
- });
326
-
327
- // Assert
328
- expect(action1).not.toHaveBeenCalled();
329
- });
330
-
331
- it("should not call the action if the timeout is cleared", () => {
332
- // Arrange
333
- const action = jest.fn();
334
- const {result} = renderHook(() => useTimeout(action, 1000));
335
-
336
- // Act
337
- act(() => {
338
- result.current.clear();
339
- });
340
- act(() => {
341
- jest.advanceTimersByTime(1000);
342
- });
343
-
344
- // Assert
345
- expect(action).not.toHaveBeenCalled();
346
- });
347
-
348
- it("should call the action when the timeout is cleared when passing ClearPolicy.Resolve to clear()", () => {
349
- // Arrange
350
- const action = jest.fn();
351
- const {result} = renderHook(() => useTimeout(action, 1000));
352
-
353
- // Act
354
- act(() => {
355
- result.current.clear(ClearPolicy.Resolve);
356
- });
357
-
358
- // Assert
359
- expect(action).toHaveBeenCalled();
360
- });
361
-
362
- it("should call the action on unmount when using ClearPolicy.Resolve in options", () => {
363
- // Arrange
364
- const action = jest.fn();
365
- const {unmount} = renderHook(() =>
366
- useTimeout(action, 1000, {
367
- clearPolicy: ClearPolicy.Resolve,
368
- }),
369
- );
370
-
371
- // Act
372
- unmount();
373
-
374
- // Assert
375
- expect(action).toHaveBeenCalled();
376
- });
377
-
378
- it("should not call the action on unmount when using the default options", () => {
379
- // Arrange
380
- const action = jest.fn();
381
- const {unmount} = renderHook(() => useTimeout(action, 1000));
382
-
383
- // Act
384
- unmount();
385
-
386
- // Assert
387
- expect(action).not.toHaveBeenCalled();
388
- });
389
- });
390
-
391
- describe("SchedulePolicy.OnDemand", () => {
392
- it("should not set the timer on creation", () => {
393
- // Arrange
394
- const {result} = renderHook(() =>
395
- useTimeout(() => {}, 1000, {
396
- schedulePolicy: SchedulePolicy.OnDemand,
397
- }),
398
- );
399
-
400
- // Act
401
-
402
- // Assert
403
- expect(result.current.isSet).toBe(false);
404
- });
405
-
406
- it("should not call action after timeoutMs if the timer hasn't been set", () => {
407
- // Arrange
408
- const action = jest.fn();
409
- renderHook(() =>
410
- useTimeout(action, 1000, {
411
- schedulePolicy: SchedulePolicy.OnDemand,
412
- }),
413
- );
414
-
415
- // Act
416
- act(() => {
417
- jest.advanceTimersByTime(1000);
418
- });
419
-
420
- // Assert
421
- expect(action).not.toHaveBeenCalled();
422
- });
423
-
424
- it("should call action after timeoutMs if the timer has been set", () => {
425
- // Arrange
426
- const action = jest.fn();
427
- const {result} = renderHook(() =>
428
- useTimeout(action, 1000, {
429
- schedulePolicy: SchedulePolicy.OnDemand,
430
- }),
431
- );
432
-
433
- // Act
434
- act(() => {
435
- result.current.set();
436
- });
437
- act(() => {
438
- jest.advanceTimersByTime(1000);
439
- });
440
-
441
- // Assert
442
- expect(action).toHaveBeenCalled();
443
- });
444
-
445
- it("should reset the timer after calling set() again", () => {
446
- // Arrange
447
- const action = jest.fn();
448
- const {result} = renderHook(() =>
449
- useTimeout(action, 750, {
450
- schedulePolicy: SchedulePolicy.OnDemand,
451
- }),
452
- );
453
-
454
- // Act
455
- act(() => {
456
- result.current.set();
457
- jest.advanceTimersByTime(501);
458
- result.current.set();
459
- jest.advanceTimersByTime(501);
460
- });
461
-
462
- // Assert
463
- expect(action).not.toHaveBeenCalled();
464
- });
465
-
466
- it("should call the action after calling set() again", () => {
467
- // Arrange
468
- const action = jest.fn();
469
- const {result} = renderHook(() =>
470
- useTimeout(action, 500, {
471
- schedulePolicy: SchedulePolicy.OnDemand,
472
- }),
473
- );
474
-
475
- // Act
476
- act(() => {
477
- result.current.set();
478
- jest.advanceTimersByTime(501);
479
- });
480
- act(() => {
481
- result.current.set();
482
- jest.advanceTimersByTime(501);
483
- });
484
-
485
- // Assert
486
- expect(action).toHaveBeenCalledTimes(2);
487
- });
488
- });
489
- });
@@ -1,101 +0,0 @@
1
- import {useEffect, useMemo, useRef} from "react";
2
- import {ClearPolicy, ActionPolicy} from "../util/policies";
3
-
4
- import type {IInterval, HookOptions} from "../util/types";
5
-
6
- import Interval from "../util/interval";
7
-
8
- /**
9
- * Hook providing access to a scheduled interval.
10
- *
11
- * @param action The action to be invoked each time the interval period has
12
- * passed. By default, this will not cause the interval to restart if it
13
- * changes. This makes it easier to use with inline lambda functions rather than
14
- * requiring consumers to wrap their action in a `useCallback`. To change this
15
- * behavior, see the `actionPolicy` option.
16
- * @param intervalMs The interval period. If this changes, the interval will
17
- * be reset per the `schedulePolicy` option.
18
- * @param options Options for the hook.
19
- * @param options.actionPolicy Determines how the action is handled when it
20
- * changes. By default, the action is replaced but the interval is not reset,
21
- * and the updated action will be invoked when the interval next fires.
22
- * If you want to reset the interval when the action changes, use
23
- * `ActionPolicy.Reset`.
24
- * @param options.clearPolicy Determines how the interval is cleared when the
25
- * component is unmounted or the interval is recreated. By default, the
26
- * interval is cleared immediately. If you want to let the interval run to
27
- * completion, use `ClearPolicy.Resolve`. This is NOT applied if the interval
28
- * is cleared manually via the `clear()` method on the returned API.
29
- * @param options.schedulePolicy Determines when the interval is scheduled.
30
- * By default, the interval is scheduled immediately. If you want to delay
31
- * scheduling the interval, use `SchedulePolicy.OnDemand`.
32
- * @returns An `IInterval` API for interacting with the given interval. This
33
- * API is a no-op if called when not mounted. This means that any calls prior
34
- * to mounting or after unmounting will not have any effect.
35
- */
36
- export function useInterval(
37
- action: () => unknown,
38
- intervalMs: number,
39
- options: HookOptions = {},
40
- ): IInterval {
41
- const {actionPolicy, clearPolicy, schedulePolicy} = options;
42
- const actionProxyRef = useRef<() => unknown>(action);
43
- const intervalRef = useRef<IInterval | null>(null);
44
-
45
- // Since we are passing our proxy function to the interval instance,
46
- // it's check that the action is a function will never fail. So, we have to
47
- // do that check ourselves, and we do it here.
48
- if (typeof action !== "function") {
49
- throw new Error("Action must be a function");
50
- }
51
-
52
- // If we're rendered with an updated action, we want to update the ref
53
- // so the existing interval gets the new action, and then reset the
54
- // interval if our action policy calls for it.
55
- if (action !== actionProxyRef.current) {
56
- actionProxyRef.current = action;
57
- if (actionPolicy === ActionPolicy.Reset) {
58
- intervalRef.current?.set();
59
- }
60
- }
61
-
62
- // This effect updates the interval when the intervalMs, clearPolicy,
63
- // or schedulePolicy changes.
64
- useEffect(() => {
65
- // Make a new interval.
66
- intervalRef.current = new Interval(
67
- () => {
68
- actionProxyRef.current?.();
69
- },
70
- intervalMs,
71
- schedulePolicy,
72
- );
73
-
74
- // Clear the interval when the effect is cleaned up, if necessary,
75
- // making sure to use the clear policy.
76
- return () => {
77
- intervalRef.current?.clear(clearPolicy);
78
- intervalRef.current = null;
79
- };
80
- }, [intervalMs, clearPolicy, schedulePolicy]);
81
-
82
- // This is the API we expose to the consumer. We expose this rather than
83
- // the interval instance itself so that the API we give back is stable
84
- // even if the underlying interval instance changes.
85
- const externalApi = useMemo(
86
- () => ({
87
- set: () => {
88
- intervalRef.current?.set();
89
- },
90
- clear: (policy?: ClearPolicy) => {
91
- intervalRef.current?.clear(policy);
92
- },
93
- get isSet() {
94
- return intervalRef.current?.isSet ?? false;
95
- },
96
- }),
97
- [],
98
- );
99
-
100
- return externalApi;
101
- }
@@ -1,101 +0,0 @@
1
- import {useEffect, useMemo, useRef} from "react";
2
- import {ClearPolicy, ActionPolicy} from "../util/policies";
3
-
4
- import type {ITimeout, HookOptions} from "../util/types";
5
-
6
- import Timeout from "../util/timeout";
7
-
8
- /**
9
- * Hook providing access to a scheduled timeout.
10
- *
11
- * @param action The action to be invoked when the timeout period has
12
- * passed. By default, this will not cause the timeout to restart if it changes.
13
- * This makes it easier to use with inline lambda functions rather than
14
- * requiring consumers to wrap their action in a `useCallback`. To change
15
- * this behavior, see the `actionPolicy` option.
16
- * @param timeoutMs The timeout period. If this changes, the timeout will
17
- * be reset per the `schedulePolicy` option.
18
- * @param options Options for the hook.
19
- * @param options.actionPolicy Determines how the action is handled when it
20
- * changes. By default, the action is replaced but the timeout is not reset,
21
- * and the updated action will be invoked when the timeout next fires.
22
- * If you want to reset the timeout when the action changes, use
23
- * `ActionPolicy.Reset`.
24
- * @param options.clearPolicy Determines how the timeout is cleared when the
25
- * component is unmounted or the timeout is recreated. By default, the
26
- * timeout is cleared immediately. If you want to let the timeout run to
27
- * completion, use `ClearPolicy.Resolve`. This is NOT applied if the timeout
28
- * is cleared manually via the `clear()` method on the returned API.
29
- * @param options.schedulePolicy Determines when the timeout is scheduled.
30
- * By default, the timeout is scheduled immediately. If you want to delay
31
- * scheduling the timeout, use `SchedulePolicy.OnDemand`.
32
- * @returns An `ITimeout` API for interacting with the given timeout. This
33
- * API is a no-op if called when not mounted. This means that any calls prior
34
- * to mounting or after unmounting will not have any effect.
35
- */
36
- export function useTimeout(
37
- action: () => unknown,
38
- timeoutMs: number,
39
- options: HookOptions = {},
40
- ): ITimeout {
41
- const {actionPolicy, clearPolicy, schedulePolicy} = options;
42
- const actionProxyRef = useRef<() => unknown>(action);
43
- const timeoutRef = useRef<ITimeout | null>(null);
44
-
45
- // Since we are passing our proxy function to the timeout instance,
46
- // it's check that the action is a function will never fail. So, we have to
47
- // do that check ourselves, and we do it here.
48
- if (typeof action !== "function") {
49
- throw new Error("Action must be a function");
50
- }
51
-
52
- // If we're rendered with an updated action, we want to update the ref
53
- // so the existing timeout gets the new action, and then reset the
54
- // timeout if our action policy calls for it.
55
- if (action !== actionProxyRef.current) {
56
- actionProxyRef.current = action;
57
- if (actionPolicy === ActionPolicy.Reset) {
58
- timeoutRef.current?.set();
59
- }
60
- }
61
-
62
- // This effect updates the timeout when the timeoutMs, clearPolicy,
63
- // or schedulePolicy changes.
64
- useEffect(() => {
65
- // Make a new timeout.
66
- timeoutRef.current = new Timeout(
67
- () => {
68
- actionProxyRef.current?.();
69
- },
70
- timeoutMs,
71
- schedulePolicy,
72
- );
73
-
74
- // Clear the interval when the effect is cleaned up, if necessary,
75
- // making sure to use the clear policy.
76
- return () => {
77
- timeoutRef.current?.clear(clearPolicy);
78
- timeoutRef.current = null;
79
- };
80
- }, [timeoutMs, clearPolicy, schedulePolicy]);
81
-
82
- // This is the API we expose to the consumer. We expose this rather than
83
- // the interval instance itself so that the API we give back is stable
84
- // even if the underlying interval instance changes.
85
- const externalApi = useMemo(
86
- () => ({
87
- set: () => {
88
- timeoutRef.current?.set();
89
- },
90
- clear: (policy?: ClearPolicy) => {
91
- timeoutRef.current?.clear(policy);
92
- },
93
- get isSet() {
94
- return timeoutRef.current?.isSet ?? false;
95
- },
96
- }),
97
- [],
98
- );
99
-
100
- return externalApi;
101
- }
package/src/index.ts DELETED
@@ -1,17 +0,0 @@
1
- export type {
2
- IAnimationFrame,
3
- IInterval,
4
- IScheduleActions,
5
- ITimeout,
6
- WithActionScheduler,
7
- WithActionSchedulerProps,
8
- WithoutActionScheduler,
9
- HookOptions,
10
- Options,
11
- } from "./util/types";
12
-
13
- export {SchedulePolicy, ClearPolicy, ActionPolicy} from "./util/policies";
14
- export {default as ActionSchedulerProvider} from "./components/action-scheduler-provider";
15
- export {default as withActionScheduler} from "./components/with-action-scheduler";
16
- export {useInterval} from "./hooks/use-interval";
17
- export {useTimeout} from "./hooks/use-timeout";