@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,4 +1,5 @@
1
- import type { ITimeout, SchedulePolicy, ClearPolicy } from "./types";
1
+ import { SchedulePolicy, ClearPolicy } from "./policies";
2
+ import type { ITimeout } from "./types";
2
3
  /**
3
4
  * Encapsulates everything associated with calling setTimeout/clearTimeout, and
4
5
  * managing the lifecycle of that timer, including the ability to resolve or
@@ -16,10 +17,10 @@ export default class Timeout implements ITimeout {
16
17
  * Creates a timeout that will invoke the given action after
17
18
  * the given period. The timeout does not start until set is called.
18
19
  *
19
- * @param {() => mixed} action The action to be invoked when the timeout
20
+ * @param action The action to be invoked when the timeout
20
21
  * period has passed.
21
- * @param {number} timeoutMs The timeout period.
22
- * @param {SchedulePolicy} [schedulePolicy] When SchedulePolicy.Immediately,
22
+ * @param timeoutMs The timeout period.
23
+ * @param [schedulePolicy] When SchedulePolicy.Immediately,
23
24
  * the timer is set immediately on instantiation; otherwise, `set` must be
24
25
  * called to set the timeout.
25
26
  * Defaults to `SchedulePolicy.Immediately`.
@@ -29,7 +30,7 @@ export default class Timeout implements ITimeout {
29
30
  /**
30
31
  * Determine if the timeout is set or not.
31
32
  *
32
- * @returns {boolean} true if the timeout is set (aka pending), otherwise
33
+ * @returns true if the timeout is set (aka pending), otherwise
33
34
  * false.
34
35
  * @memberof Timeout
35
36
  */
@@ -50,7 +51,7 @@ export default class Timeout implements ITimeout {
50
51
  * If the timeout is pending, this cancels that pending timeout without
51
52
  * invoking the action. If no timeout is pending, this does nothing.
52
53
  *
53
- * @param {ClearPolicy} [policy] When ClearPolicy.Resolve, if the request
54
+ * @param [policy] When ClearPolicy.Resolve, if the request
54
55
  * was set when called, the request action is invoked after cancelling
55
56
  * the request; otherwise, the pending action is cancelled.
56
57
  * Defaults to `ClearPolicy.Cancel`.
@@ -1,5 +1,4 @@
1
- export type SchedulePolicy = "schedule-immediately" | "schedule-on-demand";
2
- export type ClearPolicy = "resolve-on-clear" | "cancel-on-clear";
1
+ import * as Policies from "./policies";
3
2
  /**
4
3
  * Encapsulates everything associated with calling setTimeout/clearTimeout, and
5
4
  * managing the lifecycle of that timer, including the ability to resolve or
@@ -33,14 +32,14 @@ export interface ITimeout {
33
32
  * If the timeout is pending, this cancels that pending timeout. If no
34
33
  * timeout is pending, this does nothing.
35
34
  *
36
- * @param {ClearPolicy} [policy] When ClearPolicy.Resolve, if the request
35
+ * @param [policy] When ClearPolicy.Resolve, if the request
37
36
  * was set when called, the request action is invoked after cancelling
38
37
  * the request; otherwise, the pending action is cancelled.
39
38
  * Defaults to `ClearPolicy.Cancel`.
40
39
  *
41
40
  * @memberof ITimeout
42
41
  */
43
- clear(policy?: ClearPolicy): void;
42
+ clear(policy?: Policies.ClearPolicy): void;
44
43
  }
45
44
  /**
46
45
  * Encapsulates everything associated with calling setInterval/clearInterval,
@@ -73,14 +72,14 @@ export interface IInterval {
73
72
  * If the interval is active, this cancels that interval. If the interval
74
73
  * is not active, this does nothing.
75
74
  *
76
- * @param {ClearPolicy} [policy] When ClearPolicy.Resolve, if the request
75
+ * @param [policy] When ClearPolicy.Resolve, if the request
77
76
  * was set when called, the request action is invoked after cancelling
78
77
  * the request; otherwise, the pending action is cancelled.
79
78
  * Defaults to `ClearPolicy.Cancel`.
80
79
  *
81
80
  * @memberof IInterval
82
81
  */
83
- clear(policy?: ClearPolicy): void;
82
+ clear(policy?: Policies.ClearPolicy): void;
84
83
  }
85
84
  /**
86
85
  * Encapsulates everything associated with calling requestAnimationFrame/
@@ -115,18 +114,27 @@ export interface IAnimationFrame {
115
114
  * If the request is pending, this cancels that pending request. If no
116
115
  * request is pending, this does nothing.
117
116
  *
118
- * @param {ClearPolicy} [policy] When ClearPolicy.Resolve, if the request
117
+ * @param [policy] When ClearPolicy.Resolve, if the request
119
118
  * was set when called, the request action is invoked after cancelling
120
119
  * the request; otherwise, the pending action is cancelled.
121
120
  * Defaults to `ClearPolicy.Cancel`.
122
121
  *
123
122
  * @memberof IAnimationFrame
124
123
  */
125
- clear(policy?: ClearPolicy): void;
124
+ clear(policy?: Policies.ClearPolicy): void;
126
125
  }
126
+ /**
127
+ * Options for the scheduling APIs.
128
+ */
127
129
  export type Options = {
128
- schedulePolicy?: SchedulePolicy;
129
- clearPolicy?: ClearPolicy;
130
+ schedulePolicy?: Policies.SchedulePolicy;
131
+ clearPolicy?: Policies.ClearPolicy;
132
+ };
133
+ /**
134
+ * Options for the hook variants of our scheduling APIs.
135
+ */
136
+ export type HookOptions = Options & {
137
+ actionPolicy?: Policies.ActionPolicy;
130
138
  };
131
139
  /**
132
140
  * Provides means to request timeouts, intervals, and animation frames, with
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@khanacademy/wonder-blocks-timing",
3
3
  "private": false,
4
- "version": "4.0.2",
4
+ "version": "5.0.0",
5
5
  "design": "v1",
6
6
  "publishConfig": {
7
7
  "access": "public"
@@ -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
  });