| Index: remoting/webapp/unittests/spy_promise.js
|
| diff --git a/remoting/webapp/unittests/spy_promise.js b/remoting/webapp/unittests/spy_promise.js
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..fb0894ac0b6ef0a3c0c3ec9bd9b15fbf6ca17ae4
|
| --- /dev/null
|
| +++ b/remoting/webapp/unittests/spy_promise.js
|
| @@ -0,0 +1,294 @@
|
| +// Copyright 2015 The Chromium Authors. All rights reserved.
|
| +// Use of this source code is governed by a BSD-style license that can be
|
| +// found in the LICENSE file.
|
| +
|
| +'use strict';
|
| +
|
| +/** @suppress {duplicate} */
|
| +var base = base || {};
|
| +
|
| +(function() {
|
| +/**
|
| + * A wrapper around a Promise object that keeps track of all
|
| + * outstanding promises. This function is written to serve as a
|
| + * drop-in replacement for the native Promise constructor. To create
|
| + * a SpyPromise from an existing native Promise, use
|
| + * SpyPromise.resolve.
|
| + *
|
| + * Note that this is a pseudo-constructor that actually returns a
|
| + * regular promise with appropriate handlers attached. This detail
|
| + * should be transparent when SpyPromise.activate has been called.
|
| + *
|
| + * The normal way to use this class is within a call to
|
| + * SpyPromise.run, for example:
|
| + *
|
| + * base.SpyPromise.run(function() {
|
| + * myCodeThatUsesPromises();
|
| + * });
|
| + * base.SpyPromise.settleAll().then(function() {
|
| + * console.log('All promises have been settled!');
|
| + * });
|
| + *
|
| + * @constructor
|
| + * @extends {Promise}
|
| + * @param {function(function(?):?, function(*):?):?} func A function
|
| + * of the same type used as an argument to the native Promise
|
| + * constructor, in other words, a function which is called
|
| + * immediately, and whose arguments are a resolve function and a
|
| + * reject function.
|
| + */
|
| +base.SpyPromise = function(func) {
|
| + var unsettled = new RealPromise(func);
|
| + var unsettledId = remember(unsettled);
|
| + return unsettled.then(function(/** * */value) {
|
| + forget(unsettledId);
|
| + return value;
|
| + }, function(error) {
|
| + forget(unsettledId);
|
| + throw error;
|
| + });
|
| +};
|
| +
|
| +/**
|
| + * The real promise constructor. Needed because it is normally hidden
|
| + * by SpyPromise.activate or SpyPromise.run.
|
| + * @const
|
| + */
|
| +var RealPromise = Promise;
|
| +
|
| +/**
|
| + * The real window.setTimeout method. Needed because some test
|
| + * frameworks like to replace this method with a fake implementation.
|
| + * @const
|
| + */
|
| +var realSetTimeout = window.setTimeout.bind(window);
|
| +
|
| +/**
|
| + * The number of unsettled promises.
|
| + * @type {number}
|
| + */
|
| +base.SpyPromise.unsettledCount; // initialized by reset()
|
| +
|
| +/**
|
| + * A collection of all unsettled promises.
|
| + * @type {!Object<number,!Promise>}
|
| + */
|
| +var unsettled; // initialized by reset()
|
| +
|
| +/**
|
| + * A counter used to assign ID numbers to new SpyPromise objects.
|
| + * @type {number}
|
| + */
|
| +var nextPromiseId; // initialized by reset()
|
| +
|
| +/**
|
| + * A promise returned by SpyPromise.settleAll.
|
| + * @type {Promise<null>}
|
| + */
|
| +var settleAllPromise; // initialized by reset()
|
| +
|
| +/**
|
| + * Records an unsettled promise.
|
| + *
|
| + * @param {!Promise} unsettledPromise
|
| + * @return {number} The ID number to be passed to forget_.
|
| + */
|
| +function remember(unsettledPromise) {
|
| + var id = nextPromiseId++;
|
| + if (unsettled[id] != null) {
|
| + throw Error('Duplicate ID: ' + id);
|
| + }
|
| + base.SpyPromise.unsettledCount++;
|
| + unsettled[id] = unsettledPromise;
|
| + return id;
|
| +};
|
| +
|
| +/**
|
| + * Forgets a promise. Called after the promise has been settled.
|
| + *
|
| + * @param {number} id
|
| + * @private
|
| + */
|
| +function forget(id) {
|
| + console.assert(unsettled[id] != null, 'No such Promise: ' + id + '.');
|
| + base.SpyPromise.unsettledCount--;
|
| + delete unsettled[id];
|
| +};
|
| +
|
| +/**
|
| + * Forgets about all unsettled promises.
|
| + */
|
| +base.SpyPromise.reset = function() {
|
| + base.SpyPromise.unsettledCount = 0;
|
| + unsettled = {};
|
| + nextPromiseId = 0;
|
| + settleAllPromise = null;
|
| +};
|
| +
|
| +// Initialize static variables.
|
| +base.SpyPromise.reset();
|
| +
|
| +/**
|
| + * Tries to wait until all promises has been settled.
|
| + *
|
| + * @param {number=} opt_maxTimeMs The maximum number of milliseconds
|
| + * (approximately) to wait (default: 1000).
|
| + * @return {!Promise<null>} A real promise that is resolved when all
|
| + * SpyPromises have been settled, or rejected after opt_maxTimeMs
|
| + * milliseconds have elapsed.
|
| + */
|
| +base.SpyPromise.settleAll = function(opt_maxTimeMs) {
|
| + if (settleAllPromise) {
|
| + return settleAllPromise;
|
| + }
|
| +
|
| + var maxDelay = opt_maxTimeMs == null ? 1000 : opt_maxTimeMs;
|
| +
|
| + /**
|
| + * @param {number} count
|
| + * @param {number} totalDelay
|
| + * @return {!Promise<null>}
|
| + */
|
| + function loop(count, totalDelay) {
|
| + return new RealPromise(function(resolve, reject) {
|
| + if (base.SpyPromise.unsettledCount == 0) {
|
| + settleAllPromise = null;
|
| + resolve(null);
|
| + } else if (totalDelay > maxDelay) {
|
| + settleAllPromise = null;
|
| + base.SpyPromise.reset();
|
| + reject(new Error('base.SpyPromise.settleAll timed out'));
|
| + } else {
|
| + // This implements quadratic backoff according to Euler's
|
| + // triangular number formula.
|
| + var delay = count;
|
| +
|
| + // Must jump through crazy hoops to get a real timer in a unit test.
|
| + realSetTimeout(function() {
|
| + resolve(loop(
|
| + count + 1,
|
| + delay + totalDelay));
|
| + }, delay);
|
| + }
|
| + });
|
| + };
|
| +
|
| + // An extra promise needed here to prevent the loop function from
|
| + // finishing before settleAllPromise is set. If that happens,
|
| + // settleAllPromise will never be reset to null.
|
| + settleAllPromise = RealPromise.resolve().then(function() {
|
| + return loop(0, 0);
|
| + });
|
| + return settleAllPromise;
|
| +};
|
| +
|
| +/**
|
| + * Only for testing this class. Do not use.
|
| + * @returns {boolean} True if settleAll is executing.
|
| + */
|
| +base.SpyPromise.isSettleAllRunning = function() {
|
| + return settleAllPromise != null;
|
| +};
|
| +
|
| +/**
|
| + * Wrapper for Promise.resolve.
|
| + *
|
| + * @param {*} value
|
| + * @return {!base.SpyPromise}
|
| + */
|
| +base.SpyPromise.resolve = function(value) {
|
| + return new base.SpyPromise(function(resolve, reject) {
|
| + resolve(value);
|
| + });
|
| +};
|
| +
|
| +/**
|
| + * Wrapper for Promise.reject.
|
| + *
|
| + * @param {*} value
|
| + * @return {!base.SpyPromise}
|
| + */
|
| +base.SpyPromise.reject = function(value) {
|
| + return new base.SpyPromise(function(resolve, reject) {
|
| + reject(value);
|
| + });
|
| +};
|
| +
|
| +/**
|
| + * Wrapper for Promise.all.
|
| + *
|
| + * @param {!Array<Promise>} promises
|
| + * @return {!base.SpyPromise}
|
| + */
|
| +base.SpyPromise.all = function(promises) {
|
| + return base.SpyPromise.resolve(RealPromise.all(promises));
|
| +};
|
| +
|
| +/**
|
| + * Wrapper for Promise.race.
|
| + *
|
| + * @param {!Array<Promise>} promises
|
| + * @return {!base.SpyPromise}
|
| + */
|
| +base.SpyPromise.race = function(promises) {
|
| + return base.SpyPromise.resolve(RealPromise.race(promises));
|
| +};
|
| +
|
| +/**
|
| + * Sets Promise = base.SpyPromise. Must not be called more than once
|
| + * without an intervening call to restore().
|
| + */
|
| +base.SpyPromise.activate = function() {
|
| + if (settleAllPromise) {
|
| + throw Error('called base.SpyPromise.activate while settleAll is running');
|
| + }
|
| + if (Promise === base.SpyPromise) {
|
| + throw Error('base.SpyPromise is already active');
|
| + }
|
| + Promise = /** @type {function(new:Promise)} */(base.SpyPromise);
|
| +};
|
| +
|
| +/**
|
| + * Restores the original value of Promise.
|
| + */
|
| +base.SpyPromise.restore = function() {
|
| + if (settleAllPromise) {
|
| + throw Error('called base.SpyPromise.restore while settleAll is running');
|
| + }
|
| + if (Promise === base.SpyPromise) {
|
| + Promise = RealPromise;
|
| + } else if (Promise === RealPromise) {
|
| + throw new Error('base.SpyPromise is not active.');
|
| + } else {
|
| + throw new Error('Something fishy is going on.');
|
| + }
|
| +};
|
| +
|
| +/**
|
| + * Calls func with Promise equal to base.SpyPromise.
|
| + *
|
| + * @param {function():void} func A function which is expected to
|
| + * create one or more promises.
|
| + * @param {number=} opt_timeoutMs An optional timeout specifying how
|
| + * long to wait for promise chains started in func to be settled.
|
| + * (default: 1000 ms)
|
| + * @return {!Promise<null>} A promise that is resolved after every
|
| + * promise chain started in func is fully settled, or rejected
|
| + * after a opt_timeoutMs. In any case, the original value of the
|
| + * Promise constructor is restored before this promise is settled.
|
| + */
|
| +base.SpyPromise.run = function(func, opt_timeoutMs) {
|
| + base.SpyPromise.activate();
|
| + try {
|
| + func();
|
| + } finally {
|
| + return base.SpyPromise.settleAll(opt_timeoutMs).then(function() {
|
| + base.SpyPromise.restore();
|
| + return null;
|
| + }, function(error) {
|
| + base.SpyPromise.restore();
|
| + throw error;
|
| + });
|
| + }
|
| +};
|
| +})();
|
|
|