(function (factory) {
    if (typeof module === "object" && typeof module.exports === "object") {
        var v = factory(require, exports);
        if (v !== undefined) module.exports = v;
    }
    else if (typeof define === "function" && define.amd) {
        define(["require", "exports", "./Deferred", "@dojo/core/async/Task", "@dojo/core/lang"], factory);
    }
})(function (require, exports) {
    "use strict";
    Object.defineProperty(exports, "__esModule", { value: true });
    var Deferred_1 = require("./Deferred");
    var Task_1 = require("@dojo/core/async/Task");
    var lang_1 = require("@dojo/core/lang");
    var Test = (function () {
        function Test(options) {
            var _this = this;
            this._hasPassed = false;
            this._isAsync = false;
            this._usesRemote = false;
            if (!options.name || !options.test) {
                throw new Error('A Test requires a name and a test function');
            }
            ['timeElapsed', 'hasPassed'].forEach(function (name) {
                if (options[name] != null) {
                    _this["_" + name] = options[name];
                }
                delete options[name];
            });
            lang_1.mixin(this, options);
        }
        Object.defineProperty(Test.prototype, "executor", {
            get: function () {
                return this.parent && this.parent.executor;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(Test.prototype, "hasPassed", {
            /**
             * True if the test function completed successfully
             */
            get: function () {
                return this._hasPassed;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(Test.prototype, "id", {
            /**
             * The unique identifier of the test, assuming all combinations of suite + test are unique.
             */
            get: function () {
                var name = [];
                var suiteOrTest = this;
                do {
                    suiteOrTest.name != null && name.unshift(suiteOrTest.name);
                } while ((suiteOrTest = suiteOrTest.parent));
                return name.join(' - ');
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(Test.prototype, "isAsync", {
            /**
             * If true, this Test's test function is async
             */
            get: function () {
                return this._isAsync;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(Test.prototype, "parentId", {
            /**
             * The unique identifier of the test's parent.
             */
            get: function () {
                return this.parent.id;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(Test.prototype, "remote", {
            /**
             * The WebDriver interface for driving a remote environment.
             * @see Suite#remote
             */
            get: function () {
                this._usesRemote = true;
                return this.parent.remote;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(Test.prototype, "sessionId", {
            /**
             * An identifier for the test session this Test is running in.
             */
            get: function () {
                return this.parent.sessionId;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(Test.prototype, "timeElapsed", {
            /**
             * Return the number of milliseconds required for the test function to complete
             */
            get: function () {
                return this._timeElapsed;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(Test.prototype, "timeout", {
            get: function () {
                if (this._timeout != null) {
                    return this._timeout;
                }
                if (this.parent && this.parent.timeout != null) {
                    return this.parent.timeout;
                }
                return 30000;
            },
            set: function (value) {
                this._timeout = value;
            },
            enumerable: true,
            configurable: true
        });
        /**
         * A convenience function that generates and returns a special Deferred that can be used for asynchronous
         * testing.
         * Once called, a test is assumed to be asynchronous no matter its return value (the generated Deferred's
         * promise will always be used as the implied return value if a promise is not returned by the test function).
         *
         * @param timeout If provided, the amount of time to wait before rejecting the test with a timeout error, in milliseconds.
         * @param numCallsUntilResolution The number of times that resolve needs to be called before the Deferred is actually resolved.
         */
        Test.prototype.async = function (timeout, numCallsUntilResolution) {
            this._isAsync = true;
            if (timeout != null) {
                this.timeout = timeout;
            }
            var remainingCalls = numCallsUntilResolution || 1;
            var dfd = new Deferred_1.default();
            var oldResolve = dfd.resolve;
            /**
             * Eventually resolves the deferred, once `resolve` has been called as many times as specified by the
             * `numCallsUntilResolution` parameter of the original `async` call.
             */
            dfd.resolve = function () {
                --remainingCalls;
                if (remainingCalls === 0) {
                    oldResolve.apply(this, arguments);
                }
                else if (remainingCalls < 0) {
                    throw new Error('resolve called too many times');
                }
            };
            // A test may call this function multiple times and should always get the same Deferred
            this.async = function () {
                return dfd;
            };
            return dfd;
        };
        /**
         * During an asynchronous test run, restarts the timeout timer.
         */
        Test.prototype.restartTimeout = function (timeout) {
            var _this = this;
            if (timeout != null) {
                this.timeout = timeout;
            }
            if (this._runTask) {
                if (this._timer) {
                    clearTimeout(this._timer);
                }
                this._timer = setTimeout(function () {
                    _this._timer = undefined;
                    if (_this._runTask) {
                        var error = new Error("Timeout reached on " + _this.id + "#");
                        error.name = 'TimeoutError';
                        _this.error = error;
                        _this._runTask.cancel();
                    }
                }, this.timeout);
            }
        };
        /**
         * Runs the test.
         */
        Test.prototype.run = function () {
            var _this = this;
            var startTime;
            // Cancel any currently running test
            if (this._runTask && this._runTask.state === 1 /* Pending */) {
                this._runTask.cancel();
                this._runTask = undefined;
            }
            if (this._timer) {
                clearTimeout(this._timer);
                this._timer = undefined;
            }
            // Reset some state in case someone tries to re-run the same test
            this._usesRemote = false;
            this._hasPassed = false;
            this._isAsync = false;
            this._timeElapsed = 0;
            this._runTask = undefined;
            this.async = Object.getPrototypeOf(this).async;
            this.error = undefined;
            this.skipped = undefined;
            return this.executor.emit('testStart', this)
                .then(function () { startTime = Date.now(); })
                .then(function () {
                var result = _this.test(_this);
                // Someone called `this.async`, so this test is async; we have to prefer one or the other, so
                // prefer the promise returned from the test function if it exists, otherwise get the one that was
                // generated by `Test#async`
                if (_this.isAsync) {
                    if (!Task_1.isThenable(result)) {
                        result = _this.async().promise;
                    }
                    else {
                        // If the user called this.async and returned a thenable, wait for the first one to resolve or
                        // reject.
                        result = Task_1.default.race([_this.async().promise, result]);
                    }
                }
                if (Task_1.isThenable(result)) {
                    // Even if a user did not call `this.async`, we still mark this test as asynchronous if a promise
                    // was returned
                    _this._isAsync = true;
                    // Wrap the runTask in another Task so that a canceled test can be treated like a skip.
                    return new Task_1.default(function (resolve, reject) {
                        _this._runTask = new Task_1.default(function (resolve, reject) {
                            if (Task_1.isThenable(result)) {
                                result.then(function () { resolve(); }, reject);
                            }
                            // Most promise implementations that allow cancellation don't signal that a promise was
                            // canceled. In order to ensure that a timed out test is never accidentally resolved, reject
                            // a canceled test, treating it as a skipped test.
                            if (Task_1.isTask(result)) {
                                var resultTask_1 = result;
                                resultTask_1
                                    .finally(function () {
                                    if (resultTask_1.state === 3 /* Canceled */) {
                                        _this.skipped = 'Canceled';
                                        reject(exports.SKIP);
                                    }
                                })
                                    .catch(function (_error) { });
                            }
                        }, function () {
                            // Only cancel the result if it's actually a Task
                            if (Task_1.isTask(result)) {
                                result.cancel();
                            }
                            // If the test task was canceled between the time it failed and the time it resolved,
                            // reject it
                            if (_this.error) {
                                reject(_this.error);
                            }
                        })
                            .then(function () { resolve(); }, reject);
                        _this.restartTimeout();
                    });
                }
            })
                .finally(function () {
                // If we got here but the test task hasn't finished, the test was canceled
                if (_this._runTask && _this._runTask.state === 1 /* Pending */) {
                    _this._runTask.cancel();
                }
                _this._runTask = undefined;
                _this._timeElapsed = Date.now() - startTime;
                // Ensure the timeout timer is cleared so the testing process doesn't hang at exit
                if (_this._timer) {
                    clearTimeout(_this._timer);
                    _this._timer = undefined;
                }
            })
                .then(
            // Test completed successfully -- potentially passed
            function () {
                if (_this._usesRemote && !_this.isAsync) {
                    throw new Error('Remote used in synchronous test! Tests using this.remote must ' +
                        'return a promise or resolve a this.async deferred.');
                }
                _this._hasPassed = true;
            }, 
            // There was an error running the test; could be a skip, could be an assertion failure
            function (error) {
                if (error !== exports.SKIP) {
                    _this.error = error;
                    throw error;
                }
            })
                .finally(function () { return _this.executor.emit('testEnd', _this); });
        };
        /**
         * Skips this test.
         *
         * @param message If provided, will be stored in this test's `skipped` property.
         */
        Test.prototype.skip = function (message) {
            if (message === void 0) { message = 'skipped'; }
            this.skipped = message;
            throw exports.SKIP;
        };
        Test.prototype.toJSON = function () {
            var _this = this;
            var json = {};
            var properties = [
                'id',
                'parentId',
                'name',
                'sessionId',
                'timeElapsed',
                'timeout',
                'hasPassed',
                'skipped'
            ];
            properties.forEach(function (key) {
                var value = _this[key];
                if (typeof value !== 'undefined') {
                    json[key] = value;
                }
            });
            if (this.error) {
                json.error = {
                    name: this.error.name,
                    message: this.error.message,
                    stack: this.error.stack,
                    showDiff: Boolean(this.error.showDiff)
                };
                if (this.error.showDiff) {
                    json.error.actual = this.error.actual;
                    json.error.expected = this.error.expected;
                }
            }
            return json;
        };
        return Test;
    }());
    exports.default = Test;
    function isTest(value) {
        return value != null && typeof value.test === 'function' && typeof value.hasPassed === 'boolean';
    }
    exports.isTest = isTest;
    function isTestOptions(value) {
        return value != null && !(value instanceof Test) && value.name != null && value.test != null;
    }
    exports.isTestOptions = isTestOptions;
    function isTestFunction(value) {
        return typeof value === 'function';
    }
    exports.isTestFunction = isTestFunction;
    exports.SKIP = {};
});
//# sourceMappingURL=Test.js.map