This commit attempts to ensure that the timers created by jasmine mock clock do not conflict with the native timers. This also retains pre-existing behavior whereby a native scheduled function cannot be cleared if it was created prior to the mock clock being installed (unless the mock clock is uninstalled first). Prior to this commit, attempting to clear a native timer would result in clearing a mocked scheduled function instead, in some scenarios where the IDs conflicted. fixes #2068
218 lines
6.2 KiB
JavaScript
218 lines
6.2 KiB
JavaScript
// Warning: don't add "use strict" to this file. Doing so potentially changes
|
|
// the behavior of user code that does things like setTimeout("var x = 1;")
|
|
// while the mock clock is installed.
|
|
getJasmineRequireObj().DelayedFunctionScheduler = function(j$) {
|
|
function DelayedFunctionScheduler() {
|
|
this.scheduledLookup_ = [];
|
|
this.scheduledFunctions_ = {};
|
|
this.currentTime_ = 0;
|
|
this.delayedFnStartCount_ = 1e12; // arbitrarily large number to avoid collisions with native timer IDs;
|
|
this.deletedKeys_ = [];
|
|
|
|
this.tick = function(millis, tickDate) {
|
|
millis = millis || 0;
|
|
const endTime = this.currentTime_ + millis;
|
|
|
|
this.runScheduledFunctions_(endTime, tickDate);
|
|
};
|
|
|
|
this.scheduleFunction = function(
|
|
funcToCall,
|
|
millis,
|
|
params,
|
|
recurring,
|
|
timeoutKey,
|
|
runAtMillis
|
|
) {
|
|
let f;
|
|
if (typeof funcToCall === 'string') {
|
|
// setTimeout("some code") and setInterval("some code") are legal, if
|
|
// not recommended. We don't do that ourselves, but user code might.
|
|
// This allows such code to work when the mock clock is installed.
|
|
f = function() {
|
|
// eslint-disable-next-line no-eval
|
|
return eval(funcToCall);
|
|
};
|
|
} else {
|
|
f = funcToCall;
|
|
}
|
|
|
|
millis = millis || 0;
|
|
timeoutKey = timeoutKey || ++this.delayedFnStartCount_;
|
|
runAtMillis = runAtMillis || this.currentTime_ + millis;
|
|
|
|
const funcToSchedule = {
|
|
runAtMillis: runAtMillis,
|
|
funcToCall: f,
|
|
recurring: recurring,
|
|
params: params,
|
|
timeoutKey: timeoutKey,
|
|
millis: millis
|
|
};
|
|
|
|
if (runAtMillis in this.scheduledFunctions_) {
|
|
this.scheduledFunctions_[runAtMillis].push(funcToSchedule);
|
|
} else {
|
|
this.scheduledFunctions_[runAtMillis] = [funcToSchedule];
|
|
this.scheduledLookup_.push(runAtMillis);
|
|
this.scheduledLookup_.sort(function(a, b) {
|
|
return a - b;
|
|
});
|
|
}
|
|
|
|
return timeoutKey;
|
|
};
|
|
|
|
this.removeFunctionWithId = function(timeoutKey) {
|
|
this.deletedKeys_.push(timeoutKey);
|
|
|
|
for (const runAtMillis in this.scheduledFunctions_) {
|
|
const funcs = this.scheduledFunctions_[runAtMillis];
|
|
const i = indexOfFirstToPass(funcs, function(func) {
|
|
return func.timeoutKey === timeoutKey;
|
|
});
|
|
|
|
if (i > -1) {
|
|
if (funcs.length === 1) {
|
|
delete this.scheduledFunctions_[runAtMillis];
|
|
this.deleteFromLookup_(runAtMillis);
|
|
} else {
|
|
funcs.splice(i, 1);
|
|
}
|
|
|
|
// intervals get rescheduled when executed, so there's never more
|
|
// than a single scheduled function with a given timeoutKey
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
// Returns whether there are any scheduled functions.
|
|
// Returns true if there are any scheduled functions, otherwise false.
|
|
this.isEmpty = function() {
|
|
return this.scheduledFunctions_.length === 0;
|
|
};
|
|
|
|
// Runs the next timeout in the queue, advancing the clock.
|
|
this.runNextQueuedFunction = function(tickDate) {
|
|
if (this.scheduledLookup_.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const newCurrentTime = this.scheduledLookup_[0];
|
|
if (newCurrentTime >= this.currentTime_) {
|
|
tickDate(newCurrentTime - this.currentTime_);
|
|
this.currentTime_ = newCurrentTime;
|
|
}
|
|
|
|
const funcsAtTime = this.scheduledFunctions_[this.currentTime_];
|
|
const fn = funcsAtTime.shift();
|
|
if (funcsAtTime.length === 0) {
|
|
delete this.scheduledFunctions_[this.currentTime_];
|
|
this.scheduledLookup_.splice(0, 1);
|
|
}
|
|
|
|
if (fn.recurring) {
|
|
this.reschedule_(fn);
|
|
}
|
|
fn.funcToCall.apply(null, fn.params || []);
|
|
};
|
|
|
|
return this;
|
|
}
|
|
|
|
DelayedFunctionScheduler.prototype.runScheduledFunctions_ = function(
|
|
endTime,
|
|
tickDate
|
|
) {
|
|
tickDate = tickDate || function() {};
|
|
if (
|
|
this.scheduledLookup_.length === 0 ||
|
|
this.scheduledLookup_[0] > endTime
|
|
) {
|
|
if (endTime >= this.currentTime_) {
|
|
tickDate(endTime - this.currentTime_);
|
|
this.currentTime_ = endTime;
|
|
}
|
|
return;
|
|
}
|
|
|
|
do {
|
|
this.deletedKeys_ = [];
|
|
const newCurrentTime = this.scheduledLookup_.shift();
|
|
if (newCurrentTime >= this.currentTime_) {
|
|
tickDate(newCurrentTime - this.currentTime_);
|
|
this.currentTime_ = newCurrentTime;
|
|
}
|
|
|
|
const funcsToRun = this.scheduledFunctions_[this.currentTime_];
|
|
|
|
delete this.scheduledFunctions_[this.currentTime_];
|
|
|
|
for (const fn of funcsToRun) {
|
|
if (fn.recurring) {
|
|
this.reschedule_(fn);
|
|
}
|
|
}
|
|
|
|
for (const fn of funcsToRun) {
|
|
if (this.deletedKeys_.includes(fn.timeoutKey)) {
|
|
// skip a timeoutKey deleted whilst we were running
|
|
return;
|
|
}
|
|
fn.funcToCall.apply(null, fn.params || []);
|
|
}
|
|
this.deletedKeys_ = [];
|
|
} while (
|
|
this.scheduledLookup_.length > 0 &&
|
|
// checking first if we're out of time prevents setTimeout(0)
|
|
// scheduled in a funcToRun from forcing an extra iteration
|
|
this.currentTime_ !== endTime &&
|
|
this.scheduledLookup_[0] <= endTime
|
|
);
|
|
|
|
// ran out of functions to call, but still time left on the clock
|
|
if (endTime >= this.currentTime_) {
|
|
tickDate(endTime - this.currentTime_);
|
|
this.currentTime_ = endTime;
|
|
}
|
|
};
|
|
|
|
DelayedFunctionScheduler.prototype.reschedule_ = function(scheduledFn) {
|
|
this.scheduleFunction(
|
|
scheduledFn.funcToCall,
|
|
scheduledFn.millis,
|
|
scheduledFn.params,
|
|
true,
|
|
scheduledFn.timeoutKey,
|
|
scheduledFn.runAtMillis + scheduledFn.millis
|
|
);
|
|
};
|
|
|
|
DelayedFunctionScheduler.prototype.deleteFromLookup_ = function(key) {
|
|
const value = Number(key);
|
|
const i = indexOfFirstToPass(this.scheduledLookup_, function(millis) {
|
|
return millis === value;
|
|
});
|
|
|
|
if (i > -1) {
|
|
this.scheduledLookup_.splice(i, 1);
|
|
}
|
|
};
|
|
|
|
function indexOfFirstToPass(array, testFn) {
|
|
let index = -1;
|
|
|
|
for (let i = 0; i < array.length; ++i) {
|
|
if (testFn(array[i])) {
|
|
index = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return index;
|
|
}
|
|
|
|
return DelayedFunctionScheduler;
|
|
};
|