├── README.md ├── example_customclock.html ├── example_simple_spoof.html ├── example_slow2x.html └── js ├── MTC.js └── MTCWorker.js /README.md: -------------------------------------------------------------------------------- 1 | # MockTheClock 2 | 3 | MockTheClock (MTC) is a JavaScript library for spoofing time in browser. (Yes, this is for browser, not Node, there is a better way to do this in Node) 4 | 5 | While libraries like this existed before, they didn't support performance.now() or Workers, but since I needed this to slow down some 3D stuff, and those 3D apps were using these features, I had to write my own library. 6 | 7 | What this can do\*: 8 | * Spoof Date(), Date.now(), new Date(), performance.now() 9 | * Influence intervals, timeouts 10 | * Do these things inside (nested) workers! 11 | 12 | What this can't do: 13 | * Guarantee that the application cannot figure out the clock is spoofed and bypass this 14 | * Spoof meta refresh, synchronous timeout XHR's, CSS transition/animation timing events etc 15 | * Spoof time for pages inside iframes 16 | * Pretty much everything else :) 17 | 18 | 19 | _\* well, the software is provided as is (orly? can it be provided as it isn't?), and no warranty and blah blah blah... IOW: the author thinks it can do these things_ 20 | 21 | ## Installation 22 | 23 | Add `MTC.js` and `MTCWorker.js` to your project (they need to be placed in the same folder) and include the `MTC.js` file. 24 | ```html 25 | 26 | ``` 27 | 28 | **Note** that this file must be the first JS file included, because otherwise other files may be able to grab references to original JS timing functions, and in that case it's not possible to override their functionality. 29 | 30 | Another thing you need to do as soon as possible is to choose which functions or Worker URL's you want to remain unaffected: 31 | ```javascript 32 | MTC.excludeTimers = [sendHeartbeat, logMemoryUsage]; 33 | ``` 34 | It does **not** mean that calls to ```Date``` will not be spoofed inside these functions, but it means that if you set timers that call these functions (via `setTimeout` or `setInterval`), those timers will execute like MTC wasn't active. 35 | 36 | If you want some Workers to be unaffected by MTC, you can use `excludeWorkers`: 37 | ```javascript 38 | MTC.excludeWorkers = ['js/sendHeartbeat.js', 'js/logMemoryUsage.js']; 39 | ``` 40 | But unlike timers - calls to `Date` will not be spoofed here, because MTC code won't even be injected into these workers. 41 | 42 | 43 | ## Usage 44 | 45 | ### Turn it on 46 | MTC is off by default, however it still takes care of timers itself. It does it every 10ms, so if you schedule a 5ms timer, it will be executed twice every 10ms.... (brilliant, right?) 47 | 48 | To enable MTC, use `toggleSpoofing`: 49 | ```javascript 50 | MTC.toggleSpoofing(true); 51 | ``` 52 | 53 | And from now on... **the clock is stopped**, and timers are not executed (except for `setTimeout(x, 0)` which has nothing to do with actual timers). 54 | 55 | Functions like `Date.now` will now return the same value until you **manually** forward the clock or stop spoofing time! 56 | 57 | ### Control the clock 58 | 59 | If you want to change the clock, use the `setTime` function: 60 | ```javascript 61 | MTC.setTime(Date.now() + 500); //forward the clock by 500ms 62 | ``` 63 | 64 | If some timers have been scheduled, `setTime` will execute them just like 500ms had passed. 65 | 66 | So for instance if `Date.now()` returns `X` and you schedule an interval: 67 | ```javascript 68 | setInterval(someFunction, 1000) 69 | ``` 70 | 71 | And then do: 72 | ```javascript 73 | MTC.setTime(Date.now() + 10000) 74 | ``` 75 | `someFunction` will be executed 10 times, and if that function tries to read the clock, the first call will get `X + 1000`, the second will get `X + 2000` and so on, just like we'd called `setTime` 10 times. 76 | 77 | 78 | ### Original time functions 79 | 80 | If we didn't have access to the original timing functions in JS, we wouldn't be able to slow down timers or speed them up, since no timer would be executed until `setTime` was called. 81 | 82 | So for that purpose, the original functions are stored inside `MTC.real` object: 83 | ```javascript 84 | MTC.real.setTimeout 85 | MTC.real.clearTimeout 86 | MTC.real.setInterval 87 | MTC.real.clearInterval 88 | MTC.real.Date 89 | MTC.real.dateNow //Date.now() 90 | MTC.real.performanceNow //performance.now() 91 | MTC.real.Worker 92 | ``` 93 | 94 | So for instance, if you wanted to slow timers down 2x, you'd do this: 95 | ```javascript 96 | var lastCallTime; 97 | MTC.real.setInterval(function() { 98 | var delta = lastCallTime ? MTC.real.dateNow() - lastCallTime : 0; 99 | lastCallTime = MTC.real.dateNow(); 100 | MTC.setTime(Date.now() + delta/2); 101 | }, 10); 102 | ```` 103 | 104 | ### Just spoofing the date 105 | 106 | If you don't want to change timers' attitude, and just want to use this library to spoof the date, then you still need to take care of the clock, that is, you need to update it, that's how time actually works :) 107 | 108 | But, this is easy: 109 | ```javascript 110 | MTC.real.setInterval(function() { 111 | MTC.setTime(MTC.real.dateNow()-delta); 112 | }, 10); 113 | ``` 114 | 115 | This is indeed stupid, but unfortunately, JS timing functions provide no other way to change clock, it needs to be... stupid. 116 | 117 | -------------------------------------------------------------------------------- /example_customclock.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | Open JS console for fun :)
PS. make sure you don't run this file from a local filesystem, Workers can't be loaded from a local filesystem 11 | 12 | 20 | 34 | -------------------------------------------------------------------------------- /example_simple_spoof.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | Open JS console for fun :) 11 | 12 | 13 | 41 | -------------------------------------------------------------------------------- /example_slow2x.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | Open JS console for fun :) 11 | 12 | 13 | 44 | -------------------------------------------------------------------------------- /js/MTC.js: -------------------------------------------------------------------------------- 1 | if (typeof MTC === 'undefined') { 2 | 3 | (typeof window !== 'undefined' ? window : this).MTC = {}; 4 | MTC.jspath = ''; 5 | if (typeof document === 'object' && document && document.currentScript) { 6 | MTC.jspath = document.currentScript.src.substring(0, document.currentScript.src.lastIndexOf('/')+1) 7 | } 8 | MTC.real = { 9 | Date: Date, 10 | dateNow: Date.now, 11 | performanceNow: this.performance && performance.now.bind(performance) || Date.now, 12 | setTimeout: setTimeout.bind(this), 13 | clearTimeout: clearTimeout.bind(this), 14 | setInterval: setInterval.bind(this), 15 | clearInterval: clearInterval.bind(this), 16 | Worker: this.Worker && Worker.bind(this) 17 | }; 18 | MTC.excludeTimers = []; 19 | MTC.excludeWorkers = []; 20 | MTC.installTimeHook = function() { 21 | var self = this, 22 | spoofOn = false; 23 | 24 | var timeStart = 0, 25 | nowStart, currentTime = 0, 26 | realTimeInterval = null; 27 | var timers = [], 28 | tfirst = 0, 29 | ctimers = [], 30 | workers = []; 31 | 32 | function getTime() { 33 | return spoofOn ? currentTime : self.real.dateNow(); 34 | } 35 | 36 | var fakeDate = function() { 37 | var d = new self.real.Date(); 38 | if (spoofOn) d.setTime(currentTime); 39 | 40 | if (!(this instanceof fakeDate)) 41 | return d.toString(); 42 | 43 | return d; //yeah, legal, it's an object 44 | }; 45 | Date = fakeDate; 46 | Date.prototype = self.real.Date.prototype; //should fake instanceOf 47 | Date.now = getTime; 48 | performance.now = function() { 49 | return spoofOn ? nowStart + getTime() - timeStart : self.real.performanceNow.call(performance); 50 | }; 51 | 52 | var addTimer = function(func, delta, repeat) { 53 | delta = Math.abs(delta); 54 | while (timers[tfirst]) tfirst++; 55 | ctimers.push(tfirst); 56 | 57 | if (!delta || self.excludeTimers && self.excludeTimers.indexOf(func) !== -1) 58 | timers[tfirst] = [self.real[delta ? 'setInterval' : 'setTimeout'](func, delta), Infinity, repeat ? delta : 0]; 59 | else 60 | timers[tfirst] = [func, getTime() + delta, repeat ? delta : 0]; 61 | 62 | return tfirst; 63 | }; 64 | setTimeout = function(func, delta) { 65 | return addTimer(func, delta); 66 | }; 67 | setInterval = function(func, delta) { 68 | return addTimer(func, delta, true); 69 | }; 70 | 71 | clearInterval = clearTimeout = function(id) { 72 | if (id === null || !timers[id]) return; 73 | 74 | if (timers[id][1] === Infinity) 75 | self.real[timers[id][2] ? 'clearInterval' : 'clearTimeout'](timers[id][0]); 76 | timers[id] = null; 77 | tfirst = Math.min(tfirst, id); 78 | ctimers.splice(ctimers.indexOf(id), 1); 79 | }; 80 | 81 | //who doesn't support nested workers? IE? FF? Nope, it's actually chrome. 82 | if (typeof Worker !== 'undefined') { 83 | Worker = function(url) { 84 | if (self.excludeWorkers.indexOf(url) !== -1) return new self.real.Worker(url); 85 | 86 | var worker = new self.real.Worker(MTC.jspath+'MTCWorker.js'); 87 | worker.postMessage({ 88 | MTCInit: true, 89 | spoofOn: spoofOn, 90 | url: url, 91 | currentTime: getTime() 92 | }); 93 | workers.push(worker); 94 | return worker; 95 | }; 96 | //we'll assign the prototype, however we'll overwrite instance things 97 | Worker.prototype = self.real.Worker.prototype; 98 | } 99 | 100 | var changingTime, desiredTime = 0; 101 | 102 | function setTime(newTime) { 103 | if (changingTime) { 104 | desiredTime = Math.max(newTime, desiredTime); 105 | return; 106 | } 107 | 108 | changingTime = true; 109 | desiredTime = newTime; 110 | var minimalTime = 0, 111 | t; 112 | while (minimalTime < desiredTime) { 113 | minimalTime = desiredTime; 114 | 115 | for (t = 0; t < ctimers.length; t++) 116 | if (timers[ctimers[t]][1] < desiredTime) 117 | minimalTime = Math.min(timers[ctimers[t]][1], minimalTime); 118 | 119 | currentTime = minimalTime; 120 | for (t = 0; t < ctimers.length; t++) 121 | if (timers[ctimers[t]][1] === currentTime) { 122 | if (typeof timers[ctimers[t]][0] === 'string') 123 | (1, eval)(timers[ctimers[t]][0]); 124 | else if (typeof timers[ctimers[t]][0] === 'function') 125 | timers[ctimers[t]][0](); 126 | 127 | if (timers[ctimers[t]][2]) 128 | timers[ctimers[t]][1] = currentTime + timers[ctimers[t]][2]; 129 | else 130 | clearTimeout(ctimers[t--]); 131 | } 132 | } 133 | 134 | for (var t = 0; t < workers.length; t++) 135 | workers[t].postMessage({ 136 | MTCcmd: 'setTime', 137 | param: currentTime 138 | }); 139 | 140 | changingTime = false; 141 | } 142 | this.setTime = setTime; 143 | 144 | function syncTime() { 145 | setTime(self.real.dateNow()); 146 | } 147 | 148 | function toggleSpoofing(on) { 149 | if (on) { 150 | currentTime = timeStart = getTime(); 151 | nowStart = performance.now(); 152 | spoofOn = true; 153 | 154 | self.real.clearInterval(realTimeInterval); 155 | realTimeInterval = null; 156 | } else { 157 | spoofOn = false; 158 | if (realTimeInterval === null) 159 | realTimeInterval = self.real.setInterval(syncTime, 10); 160 | } 161 | 162 | for (var t = 0; t < workers.length; t++) 163 | workers[t].postMessage({ 164 | MTCcmd: 'toggleSpoofing', 165 | param: on 166 | }); 167 | 168 | return currentTime; 169 | }; 170 | this.toggleSpoofing = toggleSpoofing; 171 | 172 | toggleSpoofing(spoofOn); 173 | }; 174 | MTC.installTimeHook(); 175 | } -------------------------------------------------------------------------------- /js/MTCWorker.js: -------------------------------------------------------------------------------- 1 | importScripts('MTC.js') 2 | 3 | addEventListener('message', function(e) { 4 | if (typeof e.data === 'object' && e.data !== null && e.data.MTCInit) { 5 | MTC.toggleSpoofing(e.data.spoofOn); 6 | MTC.setTime(e.data.currentTime); 7 | importScripts(e.data.url); 8 | } 9 | 10 | if (typeof e.data === 'object' && e.data !== null && e.data.MTCcmd) 11 | MTC[e.data.MTCcmd](e.data.param); 12 | 13 | if (typeof e.data === 'object' && e.data !== null && (e.data.MTCcmd || e.data.MTCInit)) 14 | e.stopImmediatePropagation(); 15 | }, true); --------------------------------------------------------------------------------