Source: br-test/src/br/test/TimeUtility.js

'use strict';

/**
 * @module br/test/TimeUtility
 */

var Errors = require('br/Errors');

/**
 * @private
 * @class
 * @alias module:br/test/TimeUtility
 *
 * @classdesc
 * Utility class containing static methods that can be useful for controlling time in tests.
 */
var TimeUtility = {};

/** @private */
TimeUtility.TIMER_ID = 0;
/** @private */
TimeUtility.MANUAL_TIME_MODE = 'Manual';
/** @private */
TimeUtility.NEXT_STEP_TIME_MODE = 'NextStep';

/** @private */
TimeUtility.timeMode = TimeUtility.NEXT_STEP_TIME_MODE;
/** @private */
TimeUtility.pCapturedTimerFunctionArgs = [];
/** @private */
TimeUtility._bHasReplacedTimerFunctions = false;
/** @private */
TimeUtility.bCaptureTimeoutAndIntervals = true;

/** @private */
TimeUtility.fCapturedTimersSort = function(firstFunction, secondFunction) {
	return firstFunction[1] - secondFunction[1];
};

/**
 * Reset this TimeUtility to its original state. Useful for testing.
 * @private
 */
TimeUtility.reset = function() {
	this.timeMode = this.NEXT_STEP_TIME_MODE;
	this.bCaptureTimeoutAndIntervals = true;

	this.clearCapturedFunctions();
	this.releaseTimerFunctions();
};

/**
 * Overrides the default <code>setTimeout</code> and <code>setInterval</code> methods. This allows the storing of all
 *  functions passed in to those methods.
 */
TimeUtility.captureTimerFunctions = function() {
	if (this.bCaptureTimeoutAndIntervals && this._bHasReplacedTimerFunctions === false) {
		this.ORIGINAL_SETTIMEOUT_FUNCTION = window.setTimeout;
		this.ORIGINAL_SETINTERVAL_FUNCTION = window.setInterval;
		this.ORIGINAL_CLEARTIMEOUT_FUNCTION = window.clearTimeout;
		this.ORIGINAL_CLEARINTERVAL_FUNCTION = window.clearInterval;

		window.setTimeout = TimeUtility.fCaptureArguments;
		window.setInterval = TimeUtility.fCaptureArguments;
		window.clearTimeout = TimeUtility.fClearTimer;
		window.clearInterval = TimeUtility.fClearTimer;

		this._bHasReplacedTimerFunctions = true;
	}
};

/**
 * @return {Array} A list of <code>argument</code> objects that were passed into <code>setTimeout</code> and
 *  <code>setInterval</code>.
 */
TimeUtility.getCapturedFunctions = function() {
	var capturedFunctions = this.pCapturedTimerFunctionArgs.slice();

	return capturedFunctions.sort(this.fCapturedTimersSort);
};

/** @private */
TimeUtility.clearCapturedFunctions = function() {
	this.pCapturedTimerFunctionArgs.length = 0;
};

/**
 * Execute all captured functions that are set to be triggered within the passed in millisecond time value. If no value
 *  is passed, this will execute all captured functions.
 */
TimeUtility.executeCapturedFunctions = function(nMsToExecuteTo) {
	var capturedFunction, innerCapturedFunction;

	this.pCapturedTimerFunctionArgs.sort(this.fCapturedTimersSort);

	for (var idx = 0; idx < this.pCapturedTimerFunctionArgs.length; idx++) {
		capturedFunction = this.pCapturedTimerFunctionArgs[idx];

		if (nMsToExecuteTo == null || capturedFunction[1] <= nMsToExecuteTo) {
			var timerArgsLength = this.pCapturedTimerFunctionArgs.length;
			capturedFunction[0]();
			
			for (var idy = timerArgsLength; idy < this.pCapturedTimerFunctionArgs.length; idy++) {
				innerCapturedFunction = this.pCapturedTimerFunctionArgs[idy];
				innerCapturedFunction[1] += capturedFunction[1];
			}
			
			this.pCapturedTimerFunctionArgs.splice(idx, 1);
			idx--;
		} else {
			capturedFunction[1] -= nMsToExecuteTo;
		}
	}
};

/**
 * Execute all captured functions if we are in NEXT_STEP_TIME_MODE. All functions will be cleared even if an error is
 *  thrown, although not all functions will be executed. Returns false if we are not in NEXT_STEP_TIME_MODE

 */
TimeUtility.nextStep = function() {
	if (this.timeMode === this.NEXT_STEP_TIME_MODE) {
		try {
			this.executeCapturedFunctions();
		} finally {
			this.clearCapturedFunctions();
		}
		return true;
	} else {
		return false;
	}
};

/**
 * Sets the timer mode which controls when captured timeouts and intervals run.
 */
TimeUtility.setTimeMode = function(timeMode) {
	if (timeMode === this.MANUAL_TIME_MODE || timeMode === this.NEXT_STEP_TIME_MODE) {
		this.timeMode = timeMode;
	} else {
		throw new Errors.InvalidTestError('Incorrect time mode (' + timeMode + ') set on TimeUtility.');
	}
};

/** @private */
TimeUtility.releaseTimerFunctions = function() {
	if (this._bHasReplacedTimerFunctions) {
		window.setTimeout = this.ORIGINAL_SETTIMEOUT_FUNCTION;
		window.setInterval = this.ORIGINAL_SETINTERVAL_FUNCTION;
		window.clearTimeout = this.ORIGINAL_CLEARTIMEOUT_FUNCTION;
		window.clearInterval = this.ORIGINAL_CLEARINTERVAL_FUNCTION;

		this._bHasReplacedTimerFunctions = false;

		delete this.ORIGINAL_SETTIMEOUT_FUNCTION;
		delete this.ORIGINAL_SETINTERVAL_FUNCTION;
		delete this.ORIGINAL_CLEARTIMEOUT_FUNCTION;
		delete this.ORIGINAL_CLEARINTERVAL_FUNCTION;
	}
};

/** @private */
TimeUtility.fCaptureArguments = function() {
	arguments.nTimerId = TimeUtility.TIMER_ID++;

	TimeUtility.pCapturedTimerFunctionArgs.push(arguments);

	return arguments.nTimerId;
};

/** @private */
TimeUtility.fClearTimer = function(timerId) {
	var capturedFunctions = TimeUtility.pCapturedTimerFunctionArgs;

	capturedFunctions = capturedFunctions.filter(function(capturedFunction){
		return capturedFunction.nTimerId !== timerId;
	});

	TimeUtility.pCapturedTimerFunctionArgs = capturedFunctions;
};

module.exports = TimeUtility;