@khanacademy/wonder-blocks-timing 1.2.3 → 2.0.3

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,11 +1,16 @@
1
1
  // @flow
2
2
  import Interval from "../interval.js";
3
+ import {SchedulePolicy, ClearPolicy} from "../policies.js";
3
4
 
4
5
  describe("Interval", () => {
5
6
  beforeEach(() => {
6
7
  jest.useFakeTimers();
7
8
  });
8
9
 
10
+ afterEach(() => {
11
+ jest.restoreAllMocks();
12
+ });
13
+
9
14
  describe("constructor", () => {
10
15
  it("creates instance", () => {
11
16
  // Arrange
@@ -41,22 +46,27 @@ describe("Interval", () => {
41
46
  );
42
47
  });
43
48
 
44
- it("sets an interval when autoSchedule is true", () => {
49
+ it("sets an interval when schedule policy is SchedulePolicy.Immediately", () => {
45
50
  // Arrange
51
+ const intervalSpy = jest.spyOn(global, "setInterval");
46
52
 
47
53
  // Act
48
54
  // eslint-disable-next-line no-new
49
- new Interval(() => {}, 1000, true);
55
+ new Interval(() => {}, 1000, SchedulePolicy.Immediately);
50
56
 
51
57
  // Assert
52
- expect(setInterval).toHaveBeenCalledTimes(1);
58
+ expect(intervalSpy).toHaveBeenCalledTimes(1);
53
59
  });
54
60
  });
55
61
 
56
62
  describe("isSet", () => {
57
63
  it("is false when the interval has not been set", () => {
58
64
  // Arrange
59
- const interval = new Interval(() => {}, 1000, false);
65
+ const interval = new Interval(
66
+ () => {},
67
+ 1000,
68
+ SchedulePolicy.OnDemand,
69
+ );
60
70
 
61
71
  // Act
62
72
  const result = interval.isSet;
@@ -94,13 +104,18 @@ describe("Interval", () => {
94
104
  describe("#set", () => {
95
105
  it("should call setInterval", () => {
96
106
  // Arrange
97
- const interval = new Interval(() => {}, 500);
107
+ const intervalSpy = jest.spyOn(global, "setInterval");
108
+ const interval = new Interval(
109
+ () => {},
110
+ 500,
111
+ SchedulePolicy.OnDemand,
112
+ );
98
113
 
99
114
  // Act
100
115
  interval.set();
101
116
 
102
117
  // Assert
103
- expect(setInterval).toHaveBeenNthCalledWith(
118
+ expect(intervalSpy).toHaveBeenNthCalledWith(
104
119
  1,
105
120
  expect.any(Function),
106
121
  500,
@@ -109,12 +124,15 @@ describe("Interval", () => {
109
124
 
110
125
  it("should invoke setInterval to call the given action", () => {
111
126
  // Arrange
127
+ const intervalSpy = jest.spyOn(global, "setInterval");
112
128
  const action = jest.fn();
113
- const interval = new Interval(() => action(), 500);
129
+ const interval = new Interval(
130
+ () => action(),
131
+ 500,
132
+ SchedulePolicy.OnDemand,
133
+ );
114
134
  interval.set();
115
- // Flow doesn't know we added jest mocks to this
116
- // $FlowFixMe[prop-missing]
117
- const scheduledAction = setInterval.mock.calls[0][0];
135
+ const scheduledAction = intervalSpy.mock.calls[0][0];
118
136
 
119
137
  // Act
120
138
  scheduledAction();
@@ -126,12 +144,16 @@ describe("Interval", () => {
126
144
  it("should clear the active interval", () => {
127
145
  // Arrange
128
146
  const action = jest.fn();
129
- const interval = new Interval(() => action(), 500);
147
+ const interval = new Interval(
148
+ () => action(),
149
+ 500,
150
+ SchedulePolicy.OnDemand,
151
+ );
130
152
  interval.set();
131
153
 
132
154
  // Act
133
155
  interval.set();
134
- jest.runTimersToTime(501);
156
+ jest.advanceTimersByTime(501);
135
157
 
136
158
  // Assert
137
159
  expect(action).toHaveBeenCalledTimes(1);
@@ -144,7 +166,7 @@ describe("Interval", () => {
144
166
  interval.set();
145
167
 
146
168
  // Act
147
- jest.runTimersToTime(1501);
169
+ jest.advanceTimersByTime(1501);
148
170
 
149
171
  // Assert
150
172
  expect(action).toHaveBeenCalledTimes(3);
@@ -160,47 +182,51 @@ describe("Interval", () => {
160
182
 
161
183
  // Act
162
184
  interval.clear();
163
- jest.runTimersToTime(501);
185
+ jest.advanceTimersByTime(501);
164
186
 
165
187
  // Assert
166
188
  expect(action).not.toHaveBeenCalled();
167
189
  });
168
190
 
169
- it("should invoke the action if resolve is true", () => {
191
+ it("should invoke the action if clear policy is ClearPolicy.Resolve", () => {
170
192
  // Arrange
171
193
  const action = jest.fn();
172
194
  const interval = new Interval(action, 500);
173
195
  interval.set();
174
196
 
175
197
  // Act
176
- interval.clear(true);
177
- jest.runTimersToTime(501);
198
+ interval.clear(ClearPolicy.Resolve);
199
+ jest.advanceTimersByTime(501);
178
200
 
179
201
  // Assert
180
202
  expect(action).toHaveBeenCalledTimes(1);
181
203
  });
182
204
 
183
- it("should not invoke the action if resolve is false", () => {
205
+ it("should not invoke the action if clear policy is ClearPolicy.Cancel", () => {
184
206
  // Arrange
185
207
  const action = jest.fn();
186
- const interval = new Interval(action, 500, true);
208
+ const interval = new Interval(
209
+ action,
210
+ 500,
211
+ SchedulePolicy.Immediately,
212
+ );
187
213
 
188
214
  // Act
189
- interval.clear(false);
190
- jest.runTimersToTime(501);
215
+ interval.clear(ClearPolicy.Cancel);
216
+ jest.advanceTimersByTime(501);
191
217
 
192
218
  // Assert
193
219
  expect(action).not.toHaveBeenCalled();
194
220
  });
195
221
 
196
- it("should not invoke the action if interval is inactive", () => {
222
+ it("should not invoke the action if interval is inactive and clear policy is ClearPolicy.Resolve", () => {
197
223
  // Arrange
198
224
  const action = jest.fn();
199
- const interval = new Interval(action, 500, false);
225
+ const interval = new Interval(action, 500, SchedulePolicy.OnDemand);
200
226
 
201
227
  // Act
202
- interval.clear(true);
203
- jest.runTimersToTime(501);
228
+ interval.clear(ClearPolicy.Resolve);
229
+ jest.advanceTimersByTime(501);
204
230
 
205
231
  // Assert
206
232
  expect(action).not.toHaveBeenCalled();
@@ -1,11 +1,16 @@
1
1
  // @flow
2
2
  import Timeout from "../timeout.js";
3
+ import {SchedulePolicy, ClearPolicy} from "../policies.js";
3
4
 
4
5
  describe("Timeout", () => {
5
6
  beforeEach(() => {
6
7
  jest.useFakeTimers();
7
8
  });
8
9
 
10
+ afterEach(() => {
11
+ jest.restoreAllMocks();
12
+ });
13
+
9
14
  describe("constructor", () => {
10
15
  it("creates instance", () => {
11
16
  // Arrange
@@ -41,22 +46,23 @@ describe("Timeout", () => {
41
46
  );
42
47
  });
43
48
 
44
- it("sets a timeout when autoSchedule is true", () => {
49
+ it("sets a timeout when schedule policy is SchedulePolicy.Immediately", () => {
45
50
  // Arrange
51
+ const timeoutSpy = jest.spyOn(global, "setTimeout");
46
52
 
47
53
  // Act
48
54
  // eslint-disable-next-line no-new
49
- new Timeout(() => {}, 0, true);
55
+ new Timeout(() => {}, 0, SchedulePolicy.Immediately);
50
56
 
51
57
  // Assert
52
- expect(setTimeout).toHaveBeenCalledTimes(1);
58
+ expect(timeoutSpy).toHaveBeenCalledTimes(1);
53
59
  });
54
60
  });
55
61
 
56
62
  describe("isSet", () => {
57
63
  it("is false when the timeout has not been set", () => {
58
64
  // Arrange
59
- const timeout = new Timeout(() => {}, 0, false);
65
+ const timeout = new Timeout(() => {}, 0, SchedulePolicy.OnDemand);
60
66
 
61
67
  // Act
62
68
  const result = timeout.isSet;
@@ -94,13 +100,14 @@ describe("Timeout", () => {
94
100
  describe("#set", () => {
95
101
  it("should call setTimeout", () => {
96
102
  // Arrange
97
- const timeout = new Timeout(() => {}, 500, false);
103
+ const timeoutSpy = jest.spyOn(global, "setTimeout");
104
+ const timeout = new Timeout(() => {}, 500, SchedulePolicy.OnDemand);
98
105
 
99
106
  // Act
100
107
  timeout.set();
101
108
 
102
109
  // Assert
103
- expect(setTimeout).toHaveBeenNthCalledWith(
110
+ expect(timeoutSpy).toHaveBeenNthCalledWith(
104
111
  1,
105
112
  expect.any(Function),
106
113
  500,
@@ -109,12 +116,15 @@ describe("Timeout", () => {
109
116
 
110
117
  it("should invoke setTimeout to call the given action", () => {
111
118
  // Arrange
119
+ const timeoutSpy = jest.spyOn(global, "setTimeout");
112
120
  const action = jest.fn();
113
- const timeout = new Timeout(() => action(), 500);
121
+ const timeout = new Timeout(
122
+ () => action(),
123
+ 500,
124
+ SchedulePolicy.OnDemand,
125
+ );
114
126
  timeout.set();
115
- // Flow doesn't know we added jest mocks to this
116
- // $FlowFixMe[prop-missing]
117
- const scheduledAction = setTimeout.mock.calls[0][0];
127
+ const scheduledAction = timeoutSpy.mock.calls[0][0];
118
128
 
119
129
  // Act
120
130
  scheduledAction();
@@ -139,16 +149,17 @@ describe("Timeout", () => {
139
149
 
140
150
  it("should set the timeout again if it has already executed", () => {
141
151
  // Arrange
152
+ const timeoutSpy = jest.spyOn(global, "setTimeout");
142
153
  const action = jest.fn();
143
- const timeout = new Timeout(action, 500, false);
154
+ const timeout = new Timeout(action, 500, SchedulePolicy.OnDemand);
144
155
  timeout.set();
145
- jest.runOnlyPendingTimers();
156
+ jest.runAllTimers();
146
157
 
147
158
  // Act
148
159
  timeout.set();
149
160
 
150
161
  // Assert
151
- expect(setTimeout).toHaveBeenCalledTimes(2);
162
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
152
163
  });
153
164
  });
154
165
 
@@ -167,27 +178,31 @@ describe("Timeout", () => {
167
178
  expect(action).not.toHaveBeenCalled();
168
179
  });
169
180
 
170
- it("should invoke the action if resolve is true", () => {
181
+ it("should invoke the action if clear policy is ClearPolicy.Resolve", () => {
171
182
  // Arrange
172
183
  const action = jest.fn();
173
184
  const timeout = new Timeout(action, 500);
174
185
  timeout.set();
175
186
 
176
187
  // Act
177
- timeout.clear(true);
188
+ timeout.clear(ClearPolicy.Resolve);
178
189
  jest.runOnlyPendingTimers();
179
190
 
180
191
  // Assert
181
192
  expect(action).toHaveBeenCalledTimes(1);
182
193
  });
183
194
 
184
- it("should not invoke the action if resolve is false", () => {
195
+ it("should not invoke the action if clear policy is ClearPolicy.Cancel", () => {
185
196
  // Arrange
186
197
  const action = jest.fn();
187
- const timeout = new Timeout(action, 500, true);
198
+ const timeout = new Timeout(
199
+ action,
200
+ 500,
201
+ SchedulePolicy.Immediately,
202
+ );
188
203
 
189
204
  // Act
190
- timeout.clear(false);
205
+ timeout.clear(ClearPolicy.Cancel);
191
206
  jest.runOnlyPendingTimers();
192
207
 
193
208
  // Assert
@@ -197,10 +212,10 @@ describe("Timeout", () => {
197
212
  it("should not invoke the action if timeout is not pending", () => {
198
213
  // Arrange
199
214
  const action = jest.fn();
200
- const timeout = new Timeout(action, 500, false);
215
+ const timeout = new Timeout(action, 500, SchedulePolicy.OnDemand);
201
216
 
202
217
  // Act
203
- timeout.clear(true);
218
+ timeout.clear(ClearPolicy.Resolve);
204
219
  jest.runOnlyPendingTimers();
205
220
 
206
221
  // Assert
@@ -8,6 +8,7 @@ import type {
8
8
  IInterval,
9
9
  ITimeout,
10
10
  IScheduleActions,
11
+ Options,
11
12
  } from "./types.js";
12
13
 
13
14
  /**
@@ -17,38 +18,53 @@ import type {
17
18
  * `IScheduleActions` instance.
18
19
  */
19
20
  export default class ActionScheduler implements IScheduleActions {
21
+ _disabled: boolean = false;
20
22
  _registeredActions: Array<() => void> = [];
23
+ static +NoopAction: ITimeout & IAnimationFrame & IInterval = {
24
+ set: () => {},
25
+ get isSet() {
26
+ return false;
27
+ },
28
+ clear: () => {},
29
+ };
21
30
 
22
- timeout(
23
- action: () => mixed,
24
- period: number,
25
- autoSchedule?: boolean,
26
- resolveOnClear?: boolean,
27
- ): ITimeout {
28
- const timeout = new Timeout(action, period, autoSchedule);
29
- this._registeredActions.push(() => timeout.clear(resolveOnClear));
31
+ timeout(action: () => mixed, period: number, options?: Options): ITimeout {
32
+ if (this._disabled) {
33
+ return ActionScheduler.NoopAction;
34
+ }
35
+ const timeout = new Timeout(action, period, options?.schedulePolicy);
36
+ this._registeredActions.push(() => timeout.clear(options?.clearPolicy));
30
37
  return timeout;
31
38
  }
32
39
 
33
40
  interval(
34
41
  action: () => mixed,
35
42
  period: number,
36
- autoSchedule?: boolean,
37
- resolveOnClear?: boolean,
43
+ options?: Options,
38
44
  ): IInterval {
39
- const interval = new Interval(action, period, autoSchedule);
40
- this._registeredActions.push(() => interval.clear(resolveOnClear));
45
+ if (this._disabled) {
46
+ return ActionScheduler.NoopAction;
47
+ }
48
+ const interval = new Interval(action, period, options?.schedulePolicy);
49
+ this._registeredActions.push(() =>
50
+ interval.clear(options?.clearPolicy),
51
+ );
41
52
  return interval;
42
53
  }
43
54
 
44
55
  animationFrame(
45
56
  action: (DOMHighResTimeStamp) => void,
46
- autoSchedule?: boolean,
47
- resolveOnClear?: boolean,
57
+ options?: Options,
48
58
  ): IAnimationFrame {
49
- const animationFrame = new AnimationFrame(action, autoSchedule);
59
+ if (this._disabled) {
60
+ return ActionScheduler.NoopAction;
61
+ }
62
+ const animationFrame = new AnimationFrame(
63
+ action,
64
+ options?.schedulePolicy,
65
+ );
50
66
  this._registeredActions.push(() =>
51
- animationFrame.clear(resolveOnClear),
67
+ animationFrame.clear(options?.clearPolicy),
52
68
  );
53
69
  return animationFrame;
54
70
  }
@@ -58,4 +74,13 @@ export default class ActionScheduler implements IScheduleActions {
58
74
  this._registeredActions = [];
59
75
  registered.forEach((clearFn) => clearFn());
60
76
  }
77
+
78
+ /**
79
+ * Prevents this scheduler from creating any additional actions.
80
+ * This also clears any pending actions.
81
+ */
82
+ disable(): void {
83
+ this._disabled = true;
84
+ this.clearAll();
85
+ }
61
86
  }
@@ -1,5 +1,10 @@
1
1
  // @flow
2
- import type {IAnimationFrame} from "./types.js";
2
+ import {
3
+ SchedulePolicy as SchedulePolicies,
4
+ ClearPolicy as ClearPolicies,
5
+ } from "./policies.js";
6
+
7
+ import type {IAnimationFrame, SchedulePolicy, ClearPolicy} from "./types.js";
3
8
 
4
9
  /**
5
10
  * Encapsulates everything associated with calling requestAnimationFrame/
@@ -19,14 +24,15 @@ export default class AnimationFrame implements IAnimationFrame {
19
24
  * The request is not made until set is called.
20
25
  *
21
26
  * @param {DOMHighResTimeStamp => mixed} action The action to be invoked.
22
- * @param {boolean} [autoSchedule] When true, the request is made immediately on
23
- * instanstiation; otherwise, `set` must be called to make the request.
24
- * Defaults to `true`.
27
+ * @param {SchedulePolicy} [schedulePolicy] When SchedulePolicy.Immediately,
28
+ * the interval is set immediately on instantiation; otherwise, `set` must be
29
+ * called to set the interval.
30
+ * Defaults to `SchedulePolicy.Immediately`.
25
31
  * @memberof AnimationFrame
26
32
  */
27
33
  constructor(
28
34
  action: (DOMHighResTimeStamp) => mixed,
29
- autoSchedule?: boolean,
35
+ schedulePolicy: SchedulePolicy = SchedulePolicies.Immediately,
30
36
  ) {
31
37
  if (typeof action !== "function") {
32
38
  throw new Error("Action must be a function");
@@ -34,7 +40,7 @@ export default class AnimationFrame implements IAnimationFrame {
34
40
 
35
41
  this._action = action;
36
42
 
37
- if (autoSchedule || autoSchedule == null) {
43
+ if (schedulePolicy === SchedulePolicies.Immediately) {
38
44
  this.set();
39
45
  }
40
46
  }
@@ -61,10 +67,10 @@ export default class AnimationFrame implements IAnimationFrame {
61
67
  */
62
68
  set(): void {
63
69
  if (this.isSet) {
64
- this.clear(false);
70
+ this.clear(ClearPolicies.Cancel);
65
71
  }
66
72
  this._animationFrameId = requestAnimationFrame((time) =>
67
- this.clear(true, time),
73
+ this.clear(ClearPolicies.Resolve, time),
68
74
  );
69
75
  }
70
76
 
@@ -74,21 +80,28 @@ export default class AnimationFrame implements IAnimationFrame {
74
80
  * If the request is pending, this cancels that pending request without
75
81
  * invoking the action. If no request is pending, this does nothing.
76
82
  *
77
- * @param {boolean} [resolve] When true, if the request was pending when
78
- * called, the request action is invoked after cancelling the timeout.
79
- * Defaults to false.
83
+ * @param {ClearPolicy} [policy] When ClearPolicy.Resolve, if the request
84
+ * was set when called, the request action is invoked after cancelling
85
+ * the request; otherwise, the pending action is cancelled.
86
+ * Defaults to `ClearPolicy.Cancel`.
87
+ * @param {DOMHighResTimeStamp} [time] Timestamp to pass to the action when
88
+ * ClearPolicy.Resolve is specified. Ignored when ClearPolicy.Cancel is
89
+ * specified.
80
90
  *
81
91
  * @returns {void}
82
92
  * @memberof AnimationFrame
83
93
  */
84
- clear(resolve?: boolean, time?: DOMHighResTimeStamp): void {
94
+ clear(
95
+ policy: ClearPolicy = ClearPolicies.Cancel,
96
+ time?: DOMHighResTimeStamp,
97
+ ): void {
85
98
  const animationFrameId = this._animationFrameId;
86
99
  this._animationFrameId = null;
87
100
  if (animationFrameId == null) {
88
101
  return;
89
102
  }
90
103
  cancelAnimationFrame(animationFrameId);
91
- if (resolve) {
104
+ if (policy === ClearPolicies.Resolve) {
92
105
  this._action(time || performance.now());
93
106
  }
94
107
  }
@@ -1,5 +1,10 @@
1
1
  // @flow
2
- import type {IInterval} from "./types.js";
2
+ import {
3
+ SchedulePolicy as SchedulePolicies,
4
+ ClearPolicy as ClearPolicies,
5
+ } from "./policies.js";
6
+
7
+ import type {IInterval, SchedulePolicy, ClearPolicy} from "./types.js";
3
8
 
4
9
  /**
5
10
  * Encapsulates everything associated with calling setInterval/clearInterval,
@@ -22,16 +27,16 @@ export default class Interval implements IInterval {
22
27
  * @param {() => mixed} action The action to be invoked each time the
23
28
  * interval period has passed.
24
29
  * @param {number} intervalMs The interval period.
25
- * @param {boolean} [autoSchedule] When true, the interval is activated
26
- * immediately on instanstiation; otherwise, `set` must be called to
27
- * activate the interval.
28
- * Defaults to `true`.
30
+ * @param {SchedulePolicy} [schedulePolicy] When SchedulePolicy.Immediately,
31
+ * the interval is set immediately on instantiation; otherwise, `set` must be
32
+ * called to set the interval.
33
+ * Defaults to `SchedulePolicy.Immediately`.
29
34
  * @memberof Interval
30
35
  */
31
36
  constructor(
32
37
  action: () => mixed,
33
38
  intervalMs: number,
34
- autoSchedule?: boolean,
39
+ schedulePolicy: SchedulePolicy = SchedulePolicies.Immediately,
35
40
  ) {
36
41
  if (typeof action !== "function") {
37
42
  throw new Error("Action must be a function");
@@ -44,7 +49,7 @@ export default class Interval implements IInterval {
44
49
  this._action = action;
45
50
  this._intervalMs = intervalMs;
46
51
 
47
- if (autoSchedule || autoSchedule == null) {
52
+ if (schedulePolicy === SchedulePolicies.Immediately) {
48
53
  this.set();
49
54
  }
50
55
  }
@@ -69,7 +74,7 @@ export default class Interval implements IInterval {
69
74
  */
70
75
  set(): void {
71
76
  if (this.isSet) {
72
- this.clear(false);
77
+ this.clear(ClearPolicies.Cancel);
73
78
  }
74
79
  this._intervalId = setInterval(() => this._action(), this._intervalMs);
75
80
  }
@@ -80,21 +85,22 @@ export default class Interval implements IInterval {
80
85
  * If the interval is active, this cancels that interval. If no interval is
81
86
  * pending, this does nothing.
82
87
  *
83
- * @param {boolean} [resolve] When true, if the interval was active when
84
- * called, the interval action is invoked after cancellation. Defaults to
85
- * false.
88
+ * @param {ClearPolicy} [policy] When ClearPolicy.Resolve, if the request
89
+ * was set when called, the request action is invoked after cancelling
90
+ * the request; otherwise, the pending action is cancelled.
91
+ * Defaults to `ClearPolicy.Cancel`.
86
92
  *
87
93
  * @returns {void}
88
94
  * @memberof Interval
89
95
  */
90
- clear(resolve?: boolean): void {
96
+ clear(policy: ClearPolicy = ClearPolicies.Cancel): void {
91
97
  const intervalId = this._intervalId;
92
98
  this._intervalId = null;
93
99
  if (intervalId == null) {
94
100
  return;
95
101
  }
96
102
  clearInterval(intervalId);
97
- if (resolve) {
103
+ if (policy === ClearPolicies.Resolve) {
98
104
  this._action();
99
105
  }
100
106
  }
@@ -0,0 +1,10 @@
1
+ // @flow
2
+ export const SchedulePolicy = {
3
+ Immediately: "schedule-immediately",
4
+ OnDemand: "schedule-on-demand",
5
+ };
6
+
7
+ export const ClearPolicy = {
8
+ Resolve: "resolve-on-clear",
9
+ Cancel: "cancel-on-clear",
10
+ };