chromium/chrome/test/data/webui/mock_timer.ts

// 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_;
  }
}