mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2024-12-23 06:23:37 -05:00
Add clearTimeout
to Timer and MockTimer
This commit is contained in:
parent
46a1ae83a0
commit
5a17bb1451
5 changed files with 155 additions and 69 deletions
|
@ -95,9 +95,19 @@ class Timer {
|
||||||
* Call a handler function after a specified amount of time has elapsed.
|
* Call a handler function after a specified amount of time has elapsed.
|
||||||
* @param {function} handler - function to call after the timeout
|
* @param {function} handler - function to call after the timeout
|
||||||
* @param {number} timeout - number of milliseconds to delay before calling the handler
|
* @param {number} timeout - number of milliseconds to delay before calling the handler
|
||||||
|
* @returns {number} - the ID of the new timeout
|
||||||
*/
|
*/
|
||||||
setTimeout (handler, timeout) {
|
setTimeout (handler, timeout) {
|
||||||
global.setTimeout(handler, timeout);
|
return global.setTimeout(handler, timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear a timeout from the pending timeout pool.
|
||||||
|
* @param {number} timeoutId - the ID returned by `setTimeout()`
|
||||||
|
* @memberof Timer
|
||||||
|
*/
|
||||||
|
clearTimeout (timeoutId) {
|
||||||
|
global.clearTimeout(timeoutId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
51
test/fixtures/mock-timer.js
vendored
51
test/fixtures/mock-timer.js
vendored
|
@ -28,10 +28,18 @@ class MockTimer {
|
||||||
this.startTime = 0;
|
this.startTime = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Array of pending timeout callbacks
|
* The ID to use the next time `setTimeout` is called.
|
||||||
* @type {Array.<Object>}
|
* @type {number}
|
||||||
*/
|
*/
|
||||||
this._timeouts = [];
|
this._nextTimeoutId = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map of timeout ID to pending timeout callback info.
|
||||||
|
* @type {Map.<Object>}
|
||||||
|
* @property {number} time - the time at/after which this handler should run
|
||||||
|
* @property {Function} handler - the handler to call when the time comes
|
||||||
|
*/
|
||||||
|
this._timeouts = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -85,14 +93,35 @@ class MockTimer {
|
||||||
* Guaranteed to happen in between "ticks" of JavaScript.
|
* Guaranteed to happen in between "ticks" of JavaScript.
|
||||||
* @param {function} handler - function to call after the timeout
|
* @param {function} handler - function to call after the timeout
|
||||||
* @param {number} timeout - number of milliseconds to delay before calling the handler
|
* @param {number} timeout - number of milliseconds to delay before calling the handler
|
||||||
|
* @returns {number} - the ID of the new timeout.
|
||||||
* @memberof MockTimer
|
* @memberof MockTimer
|
||||||
*/
|
*/
|
||||||
setTimeout (handler, timeout) {
|
setTimeout (handler, timeout) {
|
||||||
this._timeouts.push({
|
const timeoutId = this._nextTimeoutId++;
|
||||||
|
this._timeouts.set(timeoutId, {
|
||||||
time: this._mockTime + timeout,
|
time: this._mockTime + timeout,
|
||||||
handler
|
handler
|
||||||
});
|
});
|
||||||
this._runTimeouts();
|
this._runTimeouts();
|
||||||
|
return timeoutId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear a particular timeout from the pending timeout pool.
|
||||||
|
* @param {number} timeoutId - the value returned from `setTimeout()`
|
||||||
|
* @memberof MockTimer
|
||||||
|
*/
|
||||||
|
clearTimeout (timeoutId) {
|
||||||
|
this._timeouts.delete(timeoutId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WARNING: this method has no equivalent in `Timer`. Do not use this method outside of tests!
|
||||||
|
* @returns {boolean} - true if there are any pending timeouts, false otherwise.
|
||||||
|
* @memberof MockTimer
|
||||||
|
*/
|
||||||
|
hasTimeouts () {
|
||||||
|
return this._timeouts.size > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -101,15 +130,17 @@ class MockTimer {
|
||||||
*/
|
*/
|
||||||
_runTimeouts () {
|
_runTimeouts () {
|
||||||
const ready = [];
|
const ready = [];
|
||||||
const waiting = [];
|
|
||||||
|
|
||||||
// partition timeout records by whether or not they're ready to call
|
this._timeouts.forEach((timeoutRecord, timeoutId) => {
|
||||||
this._timeouts.forEach(o => {
|
const isReady = timeoutRecord.time <= this._mockTime;
|
||||||
const isReady = o.time <= this._mockTime;
|
if (isReady) {
|
||||||
(isReady ? ready : waiting).push(o);
|
ready.push(timeoutRecord);
|
||||||
|
this._timeouts.delete(timeoutId);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this._timeouts = waiting;
|
// sort so that earlier timeouts run before later timeouts
|
||||||
|
ready.sort((a, b) => a.time < b.time);
|
||||||
|
|
||||||
// next tick, call everything that's ready
|
// next tick, call everything that's ready
|
||||||
global.setTimeout(() => {
|
global.setTimeout(() => {
|
||||||
|
|
89
test/unit/mock-timer.js
Normal file
89
test/unit/mock-timer.js
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
const test = require('tap').test;
|
||||||
|
const MockTimer = require('../fixtures/mock-timer');
|
||||||
|
|
||||||
|
test('spec', t => {
|
||||||
|
const timer = new MockTimer();
|
||||||
|
|
||||||
|
t.type(MockTimer, 'function');
|
||||||
|
t.type(timer, 'object');
|
||||||
|
|
||||||
|
// Most members of MockTimer mimic members of Timer.
|
||||||
|
t.type(timer.startTime, 'number');
|
||||||
|
t.type(timer.time, 'function');
|
||||||
|
t.type(timer.start, 'function');
|
||||||
|
t.type(timer.timeElapsed, 'function');
|
||||||
|
t.type(timer.setTimeout, 'function');
|
||||||
|
t.type(timer.clearTimeout, 'function');
|
||||||
|
|
||||||
|
// A few members of MockTimer have no Timer equivalent and should only be used in tests.
|
||||||
|
t.type(timer.hasTimeouts, 'function');
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('time', t => {
|
||||||
|
const timer = new MockTimer();
|
||||||
|
const delta = 1;
|
||||||
|
|
||||||
|
const time1 = timer.time();
|
||||||
|
const time2 = timer.time();
|
||||||
|
timer.advanceMockTime(delta);
|
||||||
|
const time3 = timer.time();
|
||||||
|
|
||||||
|
t.equal(time1, time2);
|
||||||
|
t.equal(time2 + delta, time3);
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('start / timeElapsed', t => new Promise(resolve => {
|
||||||
|
const timer = new MockTimer();
|
||||||
|
const halfDelay = 1;
|
||||||
|
const fullDelay = halfDelay + halfDelay;
|
||||||
|
|
||||||
|
timer.start();
|
||||||
|
|
||||||
|
let timeoutCalled = 0;
|
||||||
|
|
||||||
|
// Wait and measure timer
|
||||||
|
timer.setTimeout(() => {
|
||||||
|
t.equal(timeoutCalled, 0);
|
||||||
|
++timeoutCalled;
|
||||||
|
|
||||||
|
const timeElapsed = timer.timeElapsed();
|
||||||
|
t.equal(timeElapsed, fullDelay);
|
||||||
|
t.end();
|
||||||
|
|
||||||
|
resolve();
|
||||||
|
}, fullDelay);
|
||||||
|
|
||||||
|
// this should not trigger the callback
|
||||||
|
timer.advanceMockTime(halfDelay);
|
||||||
|
|
||||||
|
// give the mock timer a chance to run tasks
|
||||||
|
global.setTimeout(() => {
|
||||||
|
// we've only mock-waited for half the delay so it should not have run yet
|
||||||
|
t.equal(timeoutCalled, 0);
|
||||||
|
|
||||||
|
// this should trigger the callback
|
||||||
|
timer.advanceMockTime(halfDelay);
|
||||||
|
}, 0);
|
||||||
|
}));
|
||||||
|
|
||||||
|
test('clearTimeout / hasTimeouts', t => new Promise((resolve, reject) => {
|
||||||
|
const timer = new MockTimer();
|
||||||
|
|
||||||
|
const timeoutId = timer.setTimeout(() => {
|
||||||
|
reject(new Error('Canceled task ran'));
|
||||||
|
}, 1);
|
||||||
|
|
||||||
|
timer.setTimeout(() => {
|
||||||
|
resolve('Non-canceled task ran');
|
||||||
|
t.end();
|
||||||
|
}, 2);
|
||||||
|
|
||||||
|
timer.clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
while (timer.hasTimeouts()) {
|
||||||
|
timer.advanceMockTime(1);
|
||||||
|
}
|
||||||
|
}));
|
|
@ -1,58 +0,0 @@
|
||||||
const test = require('tap').test;
|
|
||||||
const MockTimer = require('../fixtures/mock-timer');
|
|
||||||
|
|
||||||
test('spec', t => {
|
|
||||||
const timer = new MockTimer();
|
|
||||||
|
|
||||||
t.type(MockTimer, 'function');
|
|
||||||
t.type(timer, 'object');
|
|
||||||
|
|
||||||
t.type(timer.startTime, 'number');
|
|
||||||
t.type(timer.time, 'function');
|
|
||||||
t.type(timer.start, 'function');
|
|
||||||
t.type(timer.timeElapsed, 'function');
|
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('time', t => {
|
|
||||||
const timer = new MockTimer();
|
|
||||||
|
|
||||||
const time1 = timer.time();
|
|
||||||
const time2 = timer.time();
|
|
||||||
timer.advanceMockTime(1);
|
|
||||||
const time3 = timer.time();
|
|
||||||
|
|
||||||
t.ok(time1 === time2);
|
|
||||||
t.ok(time2 < time3);
|
|
||||||
t.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('start / timeElapsed', t => {
|
|
||||||
const timer = new MockTimer();
|
|
||||||
const halfDelay = 50;
|
|
||||||
const fullDelay = halfDelay + halfDelay;
|
|
||||||
|
|
||||||
timer.start();
|
|
||||||
|
|
||||||
let timeoutCalled = 0;
|
|
||||||
|
|
||||||
// Wait and measure timer
|
|
||||||
timer.setTimeout(() => {
|
|
||||||
t.ok(timeoutCalled === 0);
|
|
||||||
++timeoutCalled;
|
|
||||||
|
|
||||||
const timeElapsed = timer.timeElapsed();
|
|
||||||
t.ok(timeElapsed === fullDelay);
|
|
||||||
|
|
||||||
t.end();
|
|
||||||
}, fullDelay);
|
|
||||||
|
|
||||||
// this should not call the callback
|
|
||||||
timer.advanceMockTime(halfDelay);
|
|
||||||
|
|
||||||
t.ok(timeoutCalled === 0);
|
|
||||||
|
|
||||||
// this should call the callback
|
|
||||||
timer.advanceMockTime(halfDelay);
|
|
||||||
});
|
|
|
@ -11,6 +11,8 @@ test('spec', t => {
|
||||||
t.type(timer.time, 'function');
|
t.type(timer.time, 'function');
|
||||||
t.type(timer.start, 'function');
|
t.type(timer.start, 'function');
|
||||||
t.type(timer.timeElapsed, 'function');
|
t.type(timer.timeElapsed, 'function');
|
||||||
|
t.type(timer.setTimeout, 'function');
|
||||||
|
t.type(timer.clearTimeout, 'function');
|
||||||
|
|
||||||
t.end();
|
t.end();
|
||||||
});
|
});
|
||||||
|
@ -40,3 +42,15 @@ test('start / timeElapsed', t => {
|
||||||
t.end();
|
t.end();
|
||||||
}, delay);
|
}, delay);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('setTimeout / clearTimeout', t => new Promise((resolve, reject) => {
|
||||||
|
const timer = new Timer();
|
||||||
|
const cancelId = timer.setTimeout(() => {
|
||||||
|
reject(new Error('Canceled task ran'));
|
||||||
|
}, 1);
|
||||||
|
timer.setTimeout(() => {
|
||||||
|
resolve('Non-canceled task ran');
|
||||||
|
t.end();
|
||||||
|
}, 2);
|
||||||
|
timer.clearTimeout(cancelId);
|
||||||
|
}));
|
||||||
|
|
Loading…
Reference in a new issue