421 lines
12 KiB
JavaScript
421 lines
12 KiB
JavaScript
|
/*
|
||
|
Copyright (c) 2008-2015 Pivotal Labs
|
||
|
|
||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||
|
a copy of this software and associated documentation files (the
|
||
|
"Software"), to deal in the Software without restriction, including
|
||
|
without limitation the rights to use, copy, modify, merge, publish,
|
||
|
distribute, sublicense, and/or sell copies of the Software, and to
|
||
|
permit persons to whom the Software is furnished to do so, subject to
|
||
|
the following conditions:
|
||
|
|
||
|
The above copyright notice and this permission notice shall be
|
||
|
included in all copies or substantial portions of the Software.
|
||
|
|
||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||
|
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||
|
*/
|
||
|
|
||
|
/* This is jasmine's implementation of a mock clock, lifted from the depths of
|
||
|
* jasmine-core and exposed as a standalone module. The interface is just the
|
||
|
* same as that of jasmine.clock. For example:
|
||
|
*
|
||
|
* var mock_clock = require("mock-clock").clock();
|
||
|
* mock_clock.install();
|
||
|
* setTimeout(function() {
|
||
|
* timerCallback();
|
||
|
* }, 100);
|
||
|
*
|
||
|
* expect(timerCallback).not.toHaveBeenCalled();
|
||
|
* mock_clock.tick(101);
|
||
|
* expect(timerCallback).toHaveBeenCalled();
|
||
|
*
|
||
|
* mock_clock.uninstall();
|
||
|
*
|
||
|
*
|
||
|
* The reason for C&Ping jasmine's clock here is that jasmine itself is
|
||
|
* difficult to webpack, and we don't really want all of it. Sinon also has a
|
||
|
* mock-clock implementation, but again, it is difficult to webpack.
|
||
|
*/
|
||
|
|
||
|
var j$ = {};
|
||
|
|
||
|
j$.Clock = function () {
|
||
|
function Clock(global, delayedFunctionSchedulerFactory, mockDate) {
|
||
|
var self = this,
|
||
|
realTimingFunctions = {
|
||
|
setTimeout: global.setTimeout,
|
||
|
clearTimeout: global.clearTimeout,
|
||
|
setInterval: global.setInterval,
|
||
|
clearInterval: global.clearInterval
|
||
|
},
|
||
|
fakeTimingFunctions = {
|
||
|
setTimeout: setTimeout,
|
||
|
clearTimeout: clearTimeout,
|
||
|
setInterval: setInterval,
|
||
|
clearInterval: clearInterval
|
||
|
},
|
||
|
installed = false,
|
||
|
delayedFunctionScheduler,
|
||
|
timer;
|
||
|
|
||
|
|
||
|
self.install = function() {
|
||
|
if(!originalTimingFunctionsIntact()) {
|
||
|
throw new Error('Jasmine Clock was unable to install over custom global timer functions. Is the clock already installed?');
|
||
|
}
|
||
|
replace(global, fakeTimingFunctions);
|
||
|
timer = fakeTimingFunctions;
|
||
|
delayedFunctionScheduler = delayedFunctionSchedulerFactory();
|
||
|
installed = true;
|
||
|
|
||
|
return self;
|
||
|
};
|
||
|
|
||
|
self.uninstall = function() {
|
||
|
delayedFunctionScheduler = null;
|
||
|
mockDate.uninstall();
|
||
|
replace(global, realTimingFunctions);
|
||
|
|
||
|
timer = realTimingFunctions;
|
||
|
installed = false;
|
||
|
};
|
||
|
|
||
|
self.withMock = function(closure) {
|
||
|
this.install();
|
||
|
try {
|
||
|
closure();
|
||
|
} finally {
|
||
|
this.uninstall();
|
||
|
}
|
||
|
};
|
||
|
|
||
|
self.mockDate = function(initialDate) {
|
||
|
mockDate.install(initialDate);
|
||
|
};
|
||
|
|
||
|
self.setTimeout = function(fn, delay, params) {
|
||
|
if (legacyIE()) {
|
||
|
if (arguments.length > 2) {
|
||
|
throw new Error('IE < 9 cannot support extra params to setTimeout without a polyfill');
|
||
|
}
|
||
|
return timer.setTimeout(fn, delay);
|
||
|
}
|
||
|
return Function.prototype.apply.apply(timer.setTimeout, [global, arguments]);
|
||
|
};
|
||
|
|
||
|
self.setInterval = function(fn, delay, params) {
|
||
|
if (legacyIE()) {
|
||
|
if (arguments.length > 2) {
|
||
|
throw new Error('IE < 9 cannot support extra params to setInterval without a polyfill');
|
||
|
}
|
||
|
return timer.setInterval(fn, delay);
|
||
|
}
|
||
|
return Function.prototype.apply.apply(timer.setInterval, [global, arguments]);
|
||
|
};
|
||
|
|
||
|
self.clearTimeout = function(id) {
|
||
|
return Function.prototype.call.apply(timer.clearTimeout, [global, id]);
|
||
|
};
|
||
|
|
||
|
self.clearInterval = function(id) {
|
||
|
return Function.prototype.call.apply(timer.clearInterval, [global, id]);
|
||
|
};
|
||
|
|
||
|
self.tick = function(millis) {
|
||
|
if (installed) {
|
||
|
mockDate.tick(millis);
|
||
|
delayedFunctionScheduler.tick(millis);
|
||
|
} else {
|
||
|
throw new Error('Mock clock is not installed, use jasmine.clock().install()');
|
||
|
}
|
||
|
};
|
||
|
|
||
|
return self;
|
||
|
|
||
|
function originalTimingFunctionsIntact() {
|
||
|
return global.setTimeout === realTimingFunctions.setTimeout &&
|
||
|
global.clearTimeout === realTimingFunctions.clearTimeout &&
|
||
|
global.setInterval === realTimingFunctions.setInterval &&
|
||
|
global.clearInterval === realTimingFunctions.clearInterval;
|
||
|
}
|
||
|
|
||
|
function legacyIE() {
|
||
|
//if these methods are polyfilled, apply will be present
|
||
|
return !(realTimingFunctions.setTimeout || realTimingFunctions.setInterval).apply;
|
||
|
}
|
||
|
|
||
|
function replace(dest, source) {
|
||
|
for (var prop in source) {
|
||
|
dest[prop] = source[prop];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function setTimeout(fn, delay) {
|
||
|
return delayedFunctionScheduler.scheduleFunction(fn, delay, argSlice(arguments, 2));
|
||
|
}
|
||
|
|
||
|
function clearTimeout(id) {
|
||
|
return delayedFunctionScheduler.removeFunctionWithId(id);
|
||
|
}
|
||
|
|
||
|
function setInterval(fn, interval) {
|
||
|
return delayedFunctionScheduler.scheduleFunction(fn, interval, argSlice(arguments, 2), true);
|
||
|
}
|
||
|
|
||
|
function clearInterval(id) {
|
||
|
return delayedFunctionScheduler.removeFunctionWithId(id);
|
||
|
}
|
||
|
|
||
|
function argSlice(argsObj, n) {
|
||
|
return Array.prototype.slice.call(argsObj, n);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return Clock;
|
||
|
}();
|
||
|
|
||
|
|
||
|
j$.DelayedFunctionScheduler = function() {
|
||
|
function DelayedFunctionScheduler() {
|
||
|
var self = this;
|
||
|
var scheduledLookup = [];
|
||
|
var scheduledFunctions = {};
|
||
|
var currentTime = 0;
|
||
|
var delayedFnCount = 0;
|
||
|
|
||
|
self.tick = function(millis) {
|
||
|
millis = millis || 0;
|
||
|
var endTime = currentTime + millis;
|
||
|
|
||
|
runScheduledFunctions(endTime);
|
||
|
currentTime = endTime;
|
||
|
};
|
||
|
|
||
|
self.scheduleFunction = function(funcToCall, millis, params, recurring, timeoutKey, runAtMillis) {
|
||
|
var f;
|
||
|
if (typeof(funcToCall) === 'string') {
|
||
|
/* jshint evil: true */
|
||
|
f = function() { return eval(funcToCall); };
|
||
|
/* jshint evil: false */
|
||
|
} else {
|
||
|
f = funcToCall;
|
||
|
}
|
||
|
|
||
|
millis = millis || 0;
|
||
|
timeoutKey = timeoutKey || ++delayedFnCount;
|
||
|
runAtMillis = runAtMillis || (currentTime + millis);
|
||
|
|
||
|
var funcToSchedule = {
|
||
|
runAtMillis: runAtMillis,
|
||
|
funcToCall: f,
|
||
|
recurring: recurring,
|
||
|
params: params,
|
||
|
timeoutKey: timeoutKey,
|
||
|
millis: millis
|
||
|
};
|
||
|
|
||
|
if (runAtMillis in scheduledFunctions) {
|
||
|
scheduledFunctions[runAtMillis].push(funcToSchedule);
|
||
|
} else {
|
||
|
scheduledFunctions[runAtMillis] = [funcToSchedule];
|
||
|
scheduledLookup.push(runAtMillis);
|
||
|
scheduledLookup.sort(function (a, b) {
|
||
|
return a - b;
|
||
|
});
|
||
|
}
|
||
|
|
||
|
return timeoutKey;
|
||
|
};
|
||
|
|
||
|
self.removeFunctionWithId = function(timeoutKey) {
|
||
|
for (var runAtMillis in scheduledFunctions) {
|
||
|
var funcs = scheduledFunctions[runAtMillis];
|
||
|
var i = indexOfFirstToPass(funcs, function (func) {
|
||
|
return func.timeoutKey === timeoutKey;
|
||
|
});
|
||
|
|
||
|
if (i > -1) {
|
||
|
if (funcs.length === 1) {
|
||
|
delete scheduledFunctions[runAtMillis];
|
||
|
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;
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
return self;
|
||
|
|
||
|
function indexOfFirstToPass(array, testFn) {
|
||
|
var index = -1;
|
||
|
|
||
|
for (var i = 0; i < array.length; ++i) {
|
||
|
if (testFn(array[i])) {
|
||
|
index = i;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return index;
|
||
|
}
|
||
|
|
||
|
function deleteFromLookup(key) {
|
||
|
var value = Number(key);
|
||
|
var i = indexOfFirstToPass(scheduledLookup, function (millis) {
|
||
|
return millis === value;
|
||
|
});
|
||
|
|
||
|
if (i > -1) {
|
||
|
scheduledLookup.splice(i, 1);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function reschedule(scheduledFn) {
|
||
|
self.scheduleFunction(scheduledFn.funcToCall,
|
||
|
scheduledFn.millis,
|
||
|
scheduledFn.params,
|
||
|
true,
|
||
|
scheduledFn.timeoutKey,
|
||
|
scheduledFn.runAtMillis + scheduledFn.millis);
|
||
|
}
|
||
|
|
||
|
function forEachFunction(funcsToRun, callback) {
|
||
|
for (var i = 0; i < funcsToRun.length; ++i) {
|
||
|
callback(funcsToRun[i]);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function runScheduledFunctions(endTime) {
|
||
|
if (scheduledLookup.length === 0 || scheduledLookup[0] > endTime) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
do {
|
||
|
currentTime = scheduledLookup.shift();
|
||
|
|
||
|
var funcsToRun = scheduledFunctions[currentTime];
|
||
|
delete scheduledFunctions[currentTime];
|
||
|
|
||
|
forEachFunction(funcsToRun, function(funcToRun) {
|
||
|
if (funcToRun.recurring) {
|
||
|
reschedule(funcToRun);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
forEachFunction(funcsToRun, function(funcToRun) {
|
||
|
funcToRun.funcToCall.apply(null, funcToRun.params || []);
|
||
|
});
|
||
|
} while (scheduledLookup.length > 0 &&
|
||
|
// checking first if we're out of time prevents setTimeout(0)
|
||
|
// scheduled in a funcToRun from forcing an extra iteration
|
||
|
currentTime !== endTime &&
|
||
|
scheduledLookup[0] <= endTime);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return DelayedFunctionScheduler;
|
||
|
}();
|
||
|
|
||
|
|
||
|
j$.MockDate = function() {
|
||
|
function MockDate(global) {
|
||
|
var self = this;
|
||
|
var currentTime = 0;
|
||
|
|
||
|
if (!global || !global.Date) {
|
||
|
self.install = function() {};
|
||
|
self.tick = function() {};
|
||
|
self.uninstall = function() {};
|
||
|
return self;
|
||
|
}
|
||
|
|
||
|
var GlobalDate = global.Date;
|
||
|
|
||
|
self.install = function(mockDate) {
|
||
|
if (mockDate instanceof GlobalDate) {
|
||
|
currentTime = mockDate.getTime();
|
||
|
} else {
|
||
|
currentTime = new GlobalDate().getTime();
|
||
|
}
|
||
|
|
||
|
global.Date = FakeDate;
|
||
|
};
|
||
|
|
||
|
self.tick = function(millis) {
|
||
|
millis = millis || 0;
|
||
|
currentTime = currentTime + millis;
|
||
|
};
|
||
|
|
||
|
self.uninstall = function() {
|
||
|
currentTime = 0;
|
||
|
global.Date = GlobalDate;
|
||
|
};
|
||
|
|
||
|
createDateProperties();
|
||
|
|
||
|
return self;
|
||
|
|
||
|
function FakeDate() {
|
||
|
switch(arguments.length) {
|
||
|
case 0:
|
||
|
return new GlobalDate(currentTime);
|
||
|
case 1:
|
||
|
return new GlobalDate(arguments[0]);
|
||
|
case 2:
|
||
|
return new GlobalDate(arguments[0], arguments[1]);
|
||
|
case 3:
|
||
|
return new GlobalDate(arguments[0], arguments[1], arguments[2]);
|
||
|
case 4:
|
||
|
return new GlobalDate(arguments[0], arguments[1], arguments[2], arguments[3]);
|
||
|
case 5:
|
||
|
return new GlobalDate(arguments[0], arguments[1], arguments[2], arguments[3],
|
||
|
arguments[4]);
|
||
|
case 6:
|
||
|
return new GlobalDate(arguments[0], arguments[1], arguments[2], arguments[3],
|
||
|
arguments[4], arguments[5]);
|
||
|
default:
|
||
|
return new GlobalDate(arguments[0], arguments[1], arguments[2], arguments[3],
|
||
|
arguments[4], arguments[5], arguments[6]);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function createDateProperties() {
|
||
|
FakeDate.prototype = GlobalDate.prototype;
|
||
|
|
||
|
FakeDate.now = function() {
|
||
|
if (GlobalDate.now) {
|
||
|
return currentTime;
|
||
|
} else {
|
||
|
throw new Error('Browser does not support Date.now()');
|
||
|
}
|
||
|
};
|
||
|
|
||
|
FakeDate.toSource = GlobalDate.toSource;
|
||
|
FakeDate.toString = GlobalDate.toString;
|
||
|
FakeDate.parse = GlobalDate.parse;
|
||
|
FakeDate.UTC = GlobalDate.UTC;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return MockDate;
|
||
|
}();
|
||
|
|
||
|
var clock = new j$.Clock(global, function () { return new j$.DelayedFunctionScheduler(); }, new j$.MockDate(global));
|
||
|
|
||
|
module.exports.clock = function() {
|
||
|
return clock;
|
||
|
}
|
||
|
|
||
|
|