-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathdelorean.js
executable file
·340 lines (314 loc) · 12.9 KB
/
delorean.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
/**
* DeLorean - Flux capacitor for accurately faking time-bound
* JavaScript unit testing, including timeouts, intervals, and dates
*
* version 0.1.2
*
* http://michaelmonteleone.net/projects/delorean
* http://github.com/mmonteleone/delorean
*
* Copyright (c) 2009 Michael Monteleone
* Licensed under terms of the MIT License (README.markdown)
*/
(function() {
var global = this; // capture reference to global scope
var version = '0.1.2';
var globalizedApi = false; // whether or not api has been injected into global scope
var callbacks = {}; // collection of scheduled functions
var advancedMs = 0; // accumulation of total requested ms advancements
var elapsedMs = 0; // accumulation of current time as of each callback
var funcCount = 0; // number of scheduled functions
var currentlyAdvancing = false; // whether or not an advance is in motion
var executionInterrupted = false; // whether or not last advance was interrupted
/**
* Basic extension helper for copying properties of one object to another
* @param {Object} dest object to receive properties
* @param {Object} src object containing properties to copy
*/
var extend = function(dest, src) {
for (var prop in src) {
dest[prop] = src[prop];
}
};
/**
* Captures references to original values of timing functions
*/
var originalClock = {
setTimeout: global.setTimeout,
setInterval: global.setInterval,
clearTimeout: global.clearTimeout,
clearInterval: global.clearInterval,
Date: global.Date
};
/**
* Extension of standard Date using "parasitic inheritance"
* http://www.crockford.com/javascript/inheritance.html
* Intercepts requests to create Date instances of current time
* and offsets them by the faked time advancement
* @param {Number} year year
* @param {Number} month month
* @param {Number} day day of month
* @param {Number} hour hour of day
* @param {Number} minute minute
* @param {Number} second second
* @param {Number} millisecond millisecond
* @returns date
*/
var ShiftedDate = function(year, month, day, hour, minute, second, millisecond) {
var shiftedDate;
if (arguments.length === 0) {
shiftedDate = new originalClock.Date();
shiftedDate.setMilliseconds(shiftedDate.getMilliseconds() + effectiveOffset());
} else if (arguments.length == 1) {
shiftedDate = new originalClock.Date(arguments[0]);
} else {
shiftedDate = new originalClock.Date(
year || null, month || null, day || null, hour || null,
minute || null, second || null, millisecond || null);
}
return shiftedDate;
};
// Keep prototype methods over the facade class
extend(ShiftedDate, {
parse: originalClock.Date.parse,
UTC: originalClock.Date.UTC,
now: originalClock.Date.now
});
/**
* Resets fake time advancement back to 0,
* removing all scheduled functions
*/
var reset = function() {
callbacks = {};
funcCount = 0;
advancedMs = 0;
currentlyAdvancing = false;
executionInterrupted = false;
elapsedMs = 0;
};
/**
* Helper function to return whether a variable is truly numeric
* @param {Object} value value to test
* @returns boolean of whether value was numeric
*/
var isNumeric = function(value) {
return value !== null && !isNaN(value);
};
/**
* Helper function to return the effective current offset of time
* from the perspective of executing callbacks
* @returns milliseconds as Number
*/
var effectiveOffset = function() {
return currentlyAdvancing ? elapsedMs: advancedMs;
};
/**
* Advances fake time by an arbitrary quantity of milliseconds,
* executing all scheduled callbacks that would have occurred within
* advanced range in proper native order and context
* @param {Number} ms quantity of milliseconds to advance fake clock
*/
var advance = function(ms) {
// advance can optionally accept no parameters
// for just returning accumulated advanced offset
if(!!ms) {
if (!isNumeric(ms) || ms < 0) {
throw ("'ms' argument must be a positive number");
}
// scheduled callbacks to be executed within range
var schedule = [];
// build an object to hold time range of this advancement
var range = {
start: advancedMs,
end: advancedMs += ms
};
// register an instance of a callback to occur
// at a particular point in this advance's schedule
var register = function(fn, at) {
schedule.push({
fn: fn,
at: at
});
};
// loop through the scheduleing and execution of callback
// functions since callbacks could possibly schedule more
// callbacks of their own (which would interrupt execution)
do {
executionInterrupted = false;
// collect applicable functions to run
for (var id in callbacks) {
var fn = callbacks[id];
// schedule all non-repeating timeouts that fall within advvanced range
if (!fn.repeats && fn.firstRunAt <= range.end) {
register(fn, fn.firstRunAt);
// schedule repeating inervals that would fall during the range
} else {
// schedule instances of first runs of intervals
if (fn.lastRunAt === null &&
fn.firstRunAt > range.start &&
(fn.lastRunAt || fn.firstRunAt) <= range.end) {
fn.lastRunAt = fn.firstRunAt;
register(fn, fn.lastRunAt);
}
// add as many instances of interval callbacks as would occur within range
while ((fn.lastRunAt || fn.firstRunAt) + fn.ms <= range.end) {
fn.lastRunAt += fn.ms;
register(fn, fn.lastRunAt);
}
}
}
// sort all the scheduled callback instances to
// execute in correct browser order
schedule.sort(function(a, b) {
// ORDER BY
// [execution point] ASC,
// [interval length] DESC,
// [order of addition] ASC
var order = a.at - b.at;
if (order === 0) {
order = b.fn.ms - a.fn.ms;
if (order === 0) {
order = a.fn.id - b.fn.id;
}
}
return order;
});
// run scheduled callback instances
var ran = [];
for (var i = 0; i < schedule.length; ++i) {
var fn = schedule[i].fn;
// only run callbacks that are still in master schedule, since a
// callback could have been cleared by a subsequent run of anther callback
if ( !! callbacks[fn.id]) {
elapsedMs = schedule[i].at;
// run fn surrounded by a state of
// currently advancing
currentlyAdvancing = true;
try {
// run callback function on global context
fn.fn.apply(global);
} finally {
currentlyAdvancing = false;
// record this fn instance as having occurred, and thus trashable
ran.push(i);
// completely trash non-repeating instance
// from ever being scheduled again
if (!fn.repeats) {
removeCallback(fn.id);
}
// execution could have been interrupted if
// a callback had performed some scheduling of its own
if (executionInterrupted) {
break;
}
}
}
}
// remove all run callback instances from schedule
for (var i = ran.length - 1; i >= 0; i--) {
schedule.splice(ran[i], 1);
}
}
while (executionInterrupted);
}
return effectiveOffset();
};
/**
* Adds a callback to the master schedule
* @param {Function} fn callback function
* @param {Number} ms millisecond at which to schedule callback
* @returns unique Number id of scheduled callback
*/
var addCallback = function(fn, ms, repeats) {
// if scheduled fn was old-school string of code
// (yes, js officially allows for this)
if (typeof(fn) == 'string') {
fn = new Function(fn);
}
var at = effectiveOffset();
var id = funcCount++;
callbacks[id] = {
id: id,
fn: fn,
ms: ms,
addedAt: at,
firstRunAt: (at + ms),
lastRunAt: null,
repeats: repeats
};
// stop any currently advancing range of fns
// so that newly scheduled callback can be
// rolled into advance's schedule (if necessary)
if (currentlyAdvancing) {
executionInterrupted = true;
}
return id;
};
/**
* Removes a callback from the master schedule
* @param {Number} id callback identifier
*/
var removeCallback = function(id) {
delete callbacks[id];
};
/**
* Gets (and optinally sets) value of whether
* the native timing functions
* (setInterval, clearInterval, setTimeout, clearTimeout, Date)
* should be overwritten by DeLorean's fakes
* @param {Boolean} shouldOverrideGlobal optional value, when passed, adds or removes the api from global scope
* @returns {Boolean} true if native API is overwritten, false if not
*/
var globalApi = function(shouldOverrideGlobal) {
if (typeof(shouldOverrideGlobal) !== 'undefined') {
globalizedApi = shouldOverrideGlobal;
extend(global, globalizedApi ? api: originalClock);
}
return globalizedApi;
};
/**
* Faked timing API
* These are kept in their own object to allow for easy
* extending and unextending of them from the global scope
*/
var api = {
setTimeout: function(fn, ms) {
// handle exceptional parameters
if (arguments.length === 0) {
throw ("Function setTimeout requires at least 1 parameter");
} else if (arguments.length === 1 && isNumeric(arguments[0])) {
throw ("useless setTimeout call (missing quotes around argument?)");
} else if (arguments.length === 1) {
return addCallback(fn, 0, false);
}
// schedule func
return addCallback(fn, ms, false);
},
setInterval: function(fn, ms) {
// handle exceptional parameters
if (arguments.length === 0) {
throw ("Function setInterval requires at least 1 parameter");
} else if (arguments.length === 1 && isNumeric(arguments[0])) {
throw ("useless setTimeout call (missing quotes around argument?)");
} else if (arguments.length === 1) {
return addCallback(fn, 0, false);
}
// schedule func
return addCallback(fn, ms, true);
},
clearTimeout: removeCallback,
clearInterval: removeCallback,
Date: ShiftedDate
};
// expose a public api containing DeLorean utility methods
global.DeLorean = {
reset: reset,
advance: advance,
globalApi: globalApi,
version: version
};
// extend public API with the timing methods
extend(global.DeLorean, api);
// set the initial state
reset();
})();