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