/**
* @license
* Copyright The Closure Library Authors.
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @fileoverview A utility for wrapping a JSTD test object so that any test
* methods are receive a queue that is compatible with JSTD but supports the
* JsUnit async API of returning a promise in the test method.
*
* To convert a JSTD object call convertToAsyncTestObj on it and run with the
* JsUnit test runner.
*/
goog.provide('goog.testing.JsTdAsyncWrapper');
goog.require('goog.Promise');
/**
* @param {Function|string} callback
* @param {number=} opt_delay
* @param {...*} var_args
* @return {number}
* @private
*/
goog.testing.JsTdAsyncWrapper.REAL_SET_TIMEOUT_FN_ = goog.global.setTimeout;
/**
* Calls a function after a specified timeout. This uses the original setTimeout
* to be resilient to tests that override it.
* @param {Function} fn The function to call.
* @param {number} timeout Timeout time in ms.
* @private
*/
goog.testing.JsTdAsyncWrapper.REAL_SET_TIMEOUT_ = function(fn, timeout) {
'use strict';
// Setting timeout into a variable is necessary to invoke the function in the
// default global context. Inlining breaks chrome since it requires setTimeout
// to be called with the global context, and IE8 doesn't support the call
// method on setTimeout.
var setTimeoutFn = goog.testing.JsTdAsyncWrapper.REAL_SET_TIMEOUT_FN_;
setTimeoutFn(fn, timeout);
};
/**
* Wraps an object's methods by passing in a Queue that is based on the JSTD
* async API. The queue exposes a promise that resolves when the queue
* completes. This promise can be used in JsUnit tests.
*
* @template T
* @param {T} original The original JSTD test object. The object should
* contain methods such as testXyz or setUp.
* @return {T} A object that has all test methods wrapped in a fake
* testing queue.
*/
goog.testing.JsTdAsyncWrapper.convertToAsyncTestObj = function(original) {
'use strict';
// Wraps a call to a test function and passes an instance of a fake queue
// into the test function.
var queueWrapperFn = function(fn) {
'use strict';
return function() {
'use strict';
var self = /** @type {?} */ (this); // T this is expected
var queue = new goog.testing.JsTdAsyncWrapper.Queue(self);
fn.call(self, queue);
return queue.startExecuting();
};
};
var newTestObj = {};
for (var prop in original) {
// If this is a test or tearDown/setUp method wrap the method with a queue
if (prop.indexOf('test') == 0 || prop == 'setUp' || prop == 'tearDown') {
newTestObj[prop] = queueWrapperFn(original[prop]);
} else {
newTestObj[prop] = original[prop];
}
}
return newTestObj;
};
/**
* A queue that mirrors the JSTD Async Queue api but exposes a promise that
* resolves once the queue is complete for compatibility with JsUnit.
* @param {!Object} testObj The test object containing all test methods. This
* object is passed into queue callbacks as the "this" object.
* @constructor
* @final
*/
goog.testing.JsTdAsyncWrapper.Queue = function(testObj) {
'use strict';
/**
* The queue steps.
* @private {!Array<!goog.testing.JsTdAsyncWrapper.Step_>}
*/
this.steps_ = [];
/**
* A delegate that is used within a defer call.
* @private {?goog.testing.JsTdAsyncWrapper.Queue}
*/
this.delegate_ = null;
/**
* thisArg that should be used by default for addCallback function calls.
* @private {!Object}
*/
this.testObj_ = testObj;
};
/**
* @param {string|function(!goog.testing.JsTdAsyncWrapper.Pool_=)} stepName
* The name of the current testing step, or the fn parameter if
* no stepName is desired.
* @param {function(!goog.testing.JsTdAsyncWrapper.Pool_=)=} opt_fn A function
* that will be called.
*/
goog.testing.JsTdAsyncWrapper.Queue.prototype.defer = function(
stepName, opt_fn) {
'use strict';
var fn = opt_fn;
if (!opt_fn && typeof stepName == 'function') {
fn = stepName;
stepName = '(Not named)';
}
// If another queue.defer is called within a pool callback it should be
// executed after the current one. Any defer that is called within a defer
// will be passed to a delegate and the current defer waits till all delegate
// defer are resolved.
if (this.delegate_) {
this.delegate_.defer(stepName, fn);
return;
}
this.steps_.push(new goog.testing.JsTdAsyncWrapper.Step_(
/** @type {string} */ (stepName),
/** @type {function(!goog.testing.JsTdAsyncWrapper.Pool_=)} */ (fn)));
};
/**
* Starts the execution.
* @return {!goog.Promise<void>}
*/
goog.testing.JsTdAsyncWrapper.Queue.prototype.startExecuting = function() {
'use strict';
return new goog.Promise(goog.bind(function(resolve, reject) {
'use strict';
this.executeNextStep_(resolve, reject);
}, this));
};
/**
* Executes the next step on the queue waiting for all pool callbacks and then
* starts executing any delegate queues before it finishes.
* @param {function()} callback
* @param {function(*)} errback
* @private
*/
goog.testing.JsTdAsyncWrapper.Queue.prototype.executeNextStep_ = function(
callback, errback) {
'use strict';
// Note: From this point on, we can no longer use goog.Promise (which uses
// the goog.async.run queue) because it conflicts with MockClock, and we can't
// use the native Promise because it is not supported on IE. So we revert to
// using callbacks and setTimeout.
if (!this.steps_.length) {
callback();
return;
}
var step = this.steps_.shift();
this.delegate_ = new goog.testing.JsTdAsyncWrapper.Queue(this.testObj_);
var pool = new goog.testing.JsTdAsyncWrapper.Pool_(
this.testObj_, goog.bind(function() {
'use strict';
goog.testing.JsTdAsyncWrapper.REAL_SET_TIMEOUT_(goog.bind(function() {
'use strict';
this.executeDelegate_(callback, errback);
}, this), 0);
}, this), goog.bind(function(reason) {
'use strict';
this.handleError_(errback, reason, step.name);
}, this));
try {
step.fn.call(this.testObj_, pool);
} catch (e) {
this.handleError_(errback, e, step.name);
}
pool.maybeComplete();
};
/**
* Execute the delegate queue.
* @param {function()} callback
* @param {function(*)} errback
* @private
*/
goog.testing.JsTdAsyncWrapper.Queue.prototype.executeDelegate_ = function(
callback, errback) {
'use strict';
// Wait till the delegate queue completes before moving on to the
// next step.
if (!this.delegate_) {
this.executeNextStep_(callback, errback);
return;
}
this.delegate_.executeNextStep_(goog.bind(function() {
'use strict';
this.delegate_ = null;
goog.testing.JsTdAsyncWrapper.REAL_SET_TIMEOUT_(goog.bind(function() {
'use strict';
this.executeNextStep_(callback, errback);
}, this), 0);
}, this), errback);
};
/**
* @param {function(*)} errback
* @param {*} reason
* @param {string} stepName
* @private
*/
goog.testing.JsTdAsyncWrapper.Queue.prototype.handleError_ = function(
errback, reason, stepName) {
'use strict';
var error = reason instanceof Error ? reason : Error(reason);
error.message = 'In step ' + stepName + ', error: ' + error.message;
errback(reason);
};
/**
* A step to be executed.
* @param {string} name
* @param {function(!goog.testing.JsTdAsyncWrapper.Pool_=)} fn
* @constructor
* @private
*/
goog.testing.JsTdAsyncWrapper.Step_ = function(name, fn) {
'use strict';
/** @final {string} */
this.name = name;
/** @final {function(!goog.testing.JsTdAsyncWrapper.Pool_=)} */
this.fn = fn;
};
/**
* A fake pool that mimics the JSTD AsyncTestCase's pool object.
* @param {!Object} testObj The test object containing all test methods. This
* object is passed into queue callbacks as the "this" object.
* @param {function()} callback
* @param {function(*)} errback
* @constructor
* @private
* @final
*/
goog.testing.JsTdAsyncWrapper.Pool_ = function(testObj, callback, errback) {
'use strict';
/** @private {number} */
this.outstandingCallbacks_ = 0;
/** @private {function()} */
this.callback_ = callback;
/** @private {function(*)} */
this.errback_ = errback;
/**
* thisArg that should be used by default for defer function calls.
* @private {!Object}
*/
this.testObj_ = testObj;
/** @private {boolean} */
this.callbackCalled_ = false;
};
/**
* @return {function()}
*/
goog.testing.JsTdAsyncWrapper.Pool_.prototype.noop = function() {
'use strict';
return this.addCallback(function() {});
};
/**
* @param {function(...*):*} fn The function to add to the pool.
* @param {?number=} opt_n The number of permitted uses of the given callback;
* defaults to one.
* @param {?number=} opt_timeout The timeout in milliseconds.
* This is not supported in the adapter for now. Specifying this argument
* will result in a test failure.
* @param {?string=} opt_description The callback description.
* @return {function()}
*/
goog.testing.JsTdAsyncWrapper.Pool_.prototype.addCallback = function(
fn, opt_n, opt_timeout, opt_description) {
'use strict';
// TODO(mtragut): This could be fixed if required by test cases.
if (opt_timeout || opt_description) {
throw new Error(
'Setting timeout or description in a pool callback is not supported.');
}
var numCallbacks = opt_n || 1;
this.outstandingCallbacks_ = this.outstandingCallbacks_ + numCallbacks;
return goog.bind(function() {
'use strict';
try {
fn.apply(this.testObj_, arguments);
} catch (e) {
if (opt_description) {
e.message = opt_description + e.message;
}
this.errback_(e);
}
this.outstandingCallbacks_ = this.outstandingCallbacks_ - 1;
this.maybeComplete();
}, this);
};
/**
* @param {function(...*):*} fn The function to add to the pool.
* @param {?number=} opt_n The number of permitted uses of the given callback;
* defaults to one.
* @param {?number=} opt_timeout The timeout in milliseconds.
* This is not supported in the adapter for now. Specifying this argument
* will result in a test failure.
* @param {?string=} opt_description The callback description.
* @return {function()}
*/
goog.testing.JsTdAsyncWrapper.Pool_.prototype.add =
goog.testing.JsTdAsyncWrapper.Pool_.prototype.addCallback;
/**
* @param {string} msg The message to print if the error callback gets called.
* @return {function()}
*/
goog.testing.JsTdAsyncWrapper.Pool_.prototype.addErrback = function(msg) {
'use strict';
return goog.bind(function() {
'use strict';
var errorMsg = msg;
if (arguments.length) {
errorMsg += ' - Error callback called with params: ( ';
for (var i = 0; i < arguments.length; i++) {
var arg = arguments[i];
errorMsg += arg + ' ';
if (arg instanceof Error) {
errorMsg += '\n' + arg.stack + '\n';
}
}
errorMsg += ')';
}
this.errback_(errorMsg);
}, this);
};
/**
* Completes the pool if there are no outstanding callbacks.
*/
goog.testing.JsTdAsyncWrapper.Pool_.prototype.maybeComplete = function() {
'use strict';
if (this.outstandingCallbacks_ == 0 && !this.callbackCalled_) {
this.callbackCalled_ = true;
this.callback_();
}
};