// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/** Overrides timeout and interval callbacks to mock timing behavior. */
interface Timer {
callback: Function;
delay: number;
key: number;
repeats: boolean;
}
interface ScheduledTask {
when: number;
key: number;
}
type WindowObject = Window&{[key: string]: any};
export class MockTimer {
/** Default versions of the timing functions. */
private originals_: {[key: string]: Function} = {};
/**
* Key to assign on the next creation of a scheduled timer. Each call to
* setTimeout or setInterval returns a unique key that can be used for
* clearing the timer.
*/
private nextTimerKey_: number = 1;
/** Details for active timers. */
private timers_: Array<(Timer | undefined)> = [];
/** List of scheduled tasks. */
private schedule_: ScheduledTask[] = [];
/** Virtual elapsed time in milliseconds. */
private now_: number = 0;
/**
* Used to control when scheduled callbacks fire. Calling the 'tick' method
* inflates this parameter and triggers callbacks.
*/
private until_: number = 0;
/**
* Replaces built-in functions for scheduled callbacks.
*/
install() {
this.replace_('setTimeout', this.setTimeout_.bind(this));
this.replace_('clearTimeout', this.clearTimeout_.bind(this));
this.replace_('setInterval', this.setInterval_.bind(this));
this.replace_('clearInterval', this.clearInterval_.bind(this));
}
/**
* Restores default behavior for scheduling callbacks.
*/
uninstall() {
if (this.originals_) {
for (const key in this.originals_) {
(window as WindowObject)[key] = this.originals_[key];
}
}
}
/**
* Overrides a global function.
* @param functionName The name of the function.
* @param replacementFunction The function override.
*/
private replace_(functionName: string, replacementFunction: Function) {
this.originals_[functionName] = (window as WindowObject)[functionName];
(window as WindowObject)[functionName] = replacementFunction;
}
/**
* Creates a virtual timer.
* @param callback The callback function.
* @param delayInMs The virtual delay in milliseconds.
* @param repeats Indicates if the timer repeats.
* @return Identifier for the timer.
*/
private createTimer_(callback: Function, delayInMs: number, repeats: boolean):
number {
const key = this.nextTimerKey_++;
const task =
{callback: callback, delay: delayInMs, key: key, repeats: repeats};
this.timers_[key] = task;
this.scheduleTask_(task);
return key;
}
/**
* Schedules a callback for execution after a virtual time delay. The tasks
* are sorted in descending order of time delay such that the next callback
* to fire is at the end of the list.
* @param details The timer details.
*/
private scheduleTask_(details: Timer) {
const key = details.key;
const when = this.now_ + details.delay;
let index = this.schedule_.length;
while (index > 0 && this.schedule_[index - 1]!.when < when) {
index--;
}
this.schedule_.splice(index, 0, {when: when, key: key});
}
/**
* Override of window.setInterval.
* @param callback The callback function.
* @param intervalInMs The repeat interval.
*/
private setInterval_(callback: Function, intervalInMs: number) {
return this.createTimer_(callback, intervalInMs, true);
}
/**
* Override of window.clearInterval.
* @param key The ID of the interval timer returned from setInterval.
*/
private clearInterval_(key: number) {
this.timers_[key] = undefined;
}
/**
* Override of window.setTimeout.
* @param callback The callback function.
* @param delayInMs The scheduled delay.
*/
private setTimeout_(callback: Function, delayInMs: number) {
return this.createTimer_(callback, delayInMs, false);
}
/**
* Override of window.clearTimeout.
* @param key The ID of the schedule timeout callback returned
* from setTimeout.
*/
private clearTimeout_(key: number) {
this.timers_[key] = undefined;
}
/**
* Simulates passage of time, triggering any scheduled callbacks whose timer
* has elapsed.
* @param elapsedMs The simulated elapsed time in milliseconds.
*/
tick(elapsedMs: number) {
this.until_ += elapsedMs;
this.fireElapsedCallbacks_();
}
/**
* Triggers any callbacks that should have fired based in the simulated
* timing.
*/
private fireElapsedCallbacks_() {
while (this.schedule_.length > 0) {
const when = this.schedule_[this.schedule_.length - 1]!.when;
if (when > this.until_) {
break;
}
const task = this.schedule_.pop();
const details = this.timers_[task!.key];
if (!details) {
continue;
} // Cancelled task.
this.now_ = when;
details.callback.apply(window);
if (details.repeats) {
this.scheduleTask_(details);
} else {
this.clearTimeout_(details.key);
}
}
this.now_ = this.until_;
}
}