time-enforcer
Version:
A JavaScript implementation of the Ruby Timecop gem https://github.com/travisjeffery/timecop.
199 lines (161 loc) • 5.28 kB
JavaScript
var root = typeof global == 'undefined' ? this : global;
var TimeEnforcer = {
safeMode: false,
stack: [],
timers: {},
root: root,
Date: root.Date,
nextTick: root.process && root.process.nextTick,
setTimeout: root.setTimeout,
setInterval: root.setInterval,
slice: Function.prototype.call.bind(Array.prototype.slice),
toString: Function.prototype.call.bind(Object.prototype.toString),
isFunction: function(object) {
return TimeEnforcer.toString(object) == '[object Function]';
},
freeze: function() {
var args = this.slice(arguments),
scope = this.getScope(args),
date = this.toDate(args);
this.mockDate(date, true, 1);
this.executeScope(scope);
},
travel: function() {
var args = this.slice(arguments),
scope = this.getScope(args),
date = this.toDate(args);
this.mockDate(date, false, 1);
this.executeScope(scope);
},
scale: function(integer, scope) {
this.mockDate(new this.Date(), false, integer);
this.executeScope(scope);
},
restore: function(force) {
if (!force && this.stack.length) {
this.root.Date = this.stack.pop();
} else {
this.stack = [];
this.root.Date = this.Date;
this.isMocking = false;
}
},
generateDate: function(baseDate, start, lock, scale) {
function Date() {
if (!arguments.length) {
if (lock) {
return new TimeEnforcer.Date(baseDate);
}
return TimeEnforcer.calculateDate(baseDate, start, scale);
}
return TimeEnforcer.toDate(TimeEnforcer.slice(arguments));
}
Date.local = function() {
return TimeEnforcer.toDate(arguments);
};
Date.now = function() {
return new Date().getTime();
};
Date.parse = Function.prototype.call.bind(this.Date.parse, null);
Date.UTC = Function.prototype.call.bind(this.Date.UTC, null);
Date.Date = this.Date;
return Date;
},
calculateDate: function(baseDate, start, scale) {
var realDate = this.Date.now(),
difference = (realDate - start) * scale;
return new this.Date(baseDate + difference);
},
mockDate: function(date, lock, scale) {
var root = this.root;
if (this.isMocking) {
this.stack.push(root.Date);
}
root.Date = this.generateDate(Number(date), this.Date.now(), lock, scale);
if (!root.setTimeout._timecop) {
root.setTimeout = this.mockTimer('setTimeout');
}
if (!root.setInterval._timecop) {
root.setInterval = this.mockTimer('setInterval');
}
if (!root.process.nextTick._timecop) {
root.process.nextTick = this.mockTimer('nextTick');
}
this.isMocking = true;
},
toDate: function(args) {
var date;
switch (args.length) {
case 0:
date = new this.Date();
break;
case 1:
if (isFinite(args[0])) {
date = new this.Date(this.Date.now() + Number(args[0]));
}
break;
case 3:
date = new this.Date(args[0], args[1], args[2]);
break;
case 4:
date = new this.Date(args[0], args[1], args[2], args[3]);
break;
case 5:
date = new this.Date(args[0], args[1], args[2], args[3], args[4]);
break;
case 6:
date = new this.Date(args[0], args[1], args[2], args[3], args[4], args[5]);
break;
default:
date = new this.Date(args[0], args[1], args[2], args[3], args[4], args[5], args[6]);
}
if (!date) {
throw new SyntaxError('The wrong information was provided for toDate()');
}
return date;
},
getScope: function(args) {
var lastArgument = args[args.length - 1];
return this.isFunction(lastArgument) ? args.pop() : null;
},
mockTimer: function(name) {
if (!this.timers[name]) {
this.timers[name] = this.mockTimerFunction.bind(this, name, name == 'nextTick' ? root.process : root);
this.timers[name]._timecop = true;
}
return this.timers[name];
},
mockTimerFunction: function(name, context, script, delay) {
var fn = this.isFunction(script) ? script : new root.Function(script),
callback = this.timeoutFunction.bind(context, root.Date, fn, this.slice(arguments, 4));
return this[name].call(context, callback, delay);
},
timeoutFunction: function(mockedDate, fn, args) {
var currentDate = root.Date;
root.Date = mockedDate;
try {
fn.apply(this, args);
} catch(e) {
root.Date = currentDate;
throw e;
}
root.Date = currentDate;
},
executeScope: function(scope) {
if (scope) {
try {
scope();
} catch(e) {
this.restore();
throw e;
}
this.restore();
} else if (this.safeMode) {
throw new this.SafeModeError('Safe mode is enabled, only calls passing a block are allowed.');
}
},
SafeModeError: function() {
Error.apply(this, arguments);
}
};
module.exports = TimeEnforcer;