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.
 | |
|  */
 | |
| 
 | |
| const j$ = {};
 | |
| 
 | |
| j$.Clock = function() {
 | |
|   function Clock(global, delayedFunctionSchedulerFactory, mockDate) {
 | |
|     let 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 (const 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() {
 | |
|     const self = this;
 | |
|     const scheduledLookup = [];
 | |
|     const scheduledFunctions = {};
 | |
|     let currentTime = 0;
 | |
|     let delayedFnCount = 0;
 | |
| 
 | |
|     self.tick = function(millis) {
 | |
|       millis = millis || 0;
 | |
|       const endTime = currentTime + millis;
 | |
| 
 | |
|       runScheduledFunctions(endTime);
 | |
|       currentTime = endTime;
 | |
|     };
 | |
| 
 | |
|     self.scheduleFunction = function(funcToCall, millis, params, recurring, timeoutKey, runAtMillis) {
 | |
|       let 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);
 | |
| 
 | |
|       const 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 (const runAtMillis in scheduledFunctions) {
 | |
|         const funcs = scheduledFunctions[runAtMillis];
 | |
|         const 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) {
 | |
|       let index = -1;
 | |
| 
 | |
|       for (let i = 0; i < array.length; ++i) {
 | |
|         if (testFn(array[i])) {
 | |
|           index = i;
 | |
|           break;
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       return index;
 | |
|     }
 | |
| 
 | |
|     function deleteFromLookup(key) {
 | |
|       const value = Number(key);
 | |
|       const 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 (let i = 0; i < funcsToRun.length; ++i) {
 | |
|         callback(funcsToRun[i]);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function runScheduledFunctions(endTime) {
 | |
|       if (scheduledLookup.length === 0 || scheduledLookup[0] > endTime) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       do {
 | |
|         currentTime = scheduledLookup.shift();
 | |
| 
 | |
|         const 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) {
 | |
|     const self = this;
 | |
|     let currentTime = 0;
 | |
| 
 | |
|     if (!global || !global.Date) {
 | |
|       self.install = function() {};
 | |
|       self.tick = function() {};
 | |
|       self.uninstall = function() {};
 | |
|       return self;
 | |
|     }
 | |
| 
 | |
|     const 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;
 | |
| }();
 | |
| 
 | |
| const clock = new j$.Clock(global, function() { return new j$.DelayedFunctionScheduler(); }, new j$.MockDate(global));
 | |
| 
 | |
| module.exports.clock = function() {
 | |
|     return clock;
 | |
| };
 | |
| 
 | |
| 
 |