"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.HotTester = void 0;
const HotStaq_1 = require("./HotStaq");
/**
 * Executes tests.
 */
class HotTester {
    constructor(processor, name, baseUrl, driver, testMaps = {}) {
        if (testMaps == null)
            throw new Error(`The testMaps parameter can be an empty object, but cannot be null! It is set to null.`);
        this.processor = processor;
        this.name = name;
        this.timeout = 10000;
        this.baseUrl = baseUrl;
        this.testMaps = testMaps;
        this.driver = driver;
        // Back-reference so the driver can flip our `finishedLoading`
        // to false on cross-page navigations — without it, navigate("/foo")
        // returns immediately with the previous page's stale paths cached
        // and the next executeTestPagePath fails with "Test path X does
        // not have a function".
        if (this.driver != null)
            this.driver.tester = this;
        this.finishedLoading = false;
        this.hasBeenSetup = false;
        this.hasBeenDestroyed = false;
        this.currentPage = null;
        this.numFailures = 0;
    }
    /**
     * Executed when setting up the tester.
     */
    setup(isWebRoute, url, destinationKey) {
        return __awaiter(this, void 0, void 0, function* () {
        });
    }
    /**
     * Executed when destroying up the tester.
     */
    destroy() {
        return __awaiter(this, void 0, void 0, function* () {
        });
    }
    /**
     * Waits for the API to finish loading all data.
     *
     * `finishedLoading` flips to true when the test page POSTs its test
     * paths to HotTesterAPI. If the browser never gets there — page-side
     * JS error, missing register call, anything — this used to spin
     * forever and the CI job ran all the way to its 15-30 minute
     * wallclock limit. Bound the wait so we get a clear error instead.
     * Override the cap with HOTSTAQ_WAIT_FOR_DATA_TIMEOUT_MS.
     */
    waitForData() {
        return __awaiter(this, void 0, void 0, function* () {
            const timeoutMs = parseInt(process.env["HOTSTAQ_WAIT_FOR_DATA_TIMEOUT_MS"] || "120000", 10) || 120000;
            const start = Date.now();
            while (this.finishedLoading === false) {
                if (Date.now() - start > timeoutMs)
                    throw new Error(`HotTester: waitForData timed out after ${timeoutMs}ms — ` +
                        `the test page never registered its test paths with HotTesterAPI. ` +
                        `Check that HotStaq.min.js loaded, the page didn't error before ` +
                        `Hot.CurrentPage.createTestPath, and testerAPIUrl in HotSite.yaml ` +
                        `points at this server. Override the cap with ` +
                        `HOTSTAQ_WAIT_FOR_DATA_TIMEOUT_MS.`);
                yield HotStaq_1.HotStaq.wait(10);
            }
        });
    }
    /**
     * Get a destination JSON object to use.
     */
    static interpretDestination(mapName, testDest) {
        let destination = testDest.destination;
        let newDestination = {
            mapName: mapName,
            url: "",
            api: "",
            paths: []
        };
        let strs = destination.split(/\-\>/g);
        let type = strs[0];
        if (type.length < 2)
            return (null);
        if ((type[0] === "/") && (type[1] === "/"))
            return (null);
        let getType = (typeStr, typeDelimiter) => {
            let pos = typeStr.indexOf(typeDelimiter);
            let typeValue = "";
            if (pos > -1) {
                typeValue = typeStr.substr(pos + typeDelimiter.length);
                typeValue = typeValue.trim();
            }
            return (typeValue);
        };
        newDestination.url = getType(type, "url:");
        newDestination.api = getType(type, "api:");
        for (let iIdx = 1; iIdx < strs.length; iIdx++) {
            let newPathStr = strs[iIdx];
            let newPath = {
                cmd: "",
                dest: "",
                path: ""
            };
            newPathStr = newPathStr.trim();
            newPath.dest = getType(newPathStr, "dest:");
            newPath.cmd = getType(newPathStr, "cmd:");
            newPath.path = getType(newPathStr, "path:");
            if ((newPath.dest == "") && (newPath.cmd == "") && (newPath.path == ""))
                newPath.path = newPathStr;
            newDestination.paths.push(newPath);
        }
        return (newDestination);
    }
    /**
     * Execute an API's test path.
     */
    executeTestAPIPath(destination_1, method_1, testName_1) {
        return __awaiter(this, arguments, void 0, function* (destination, method, testName, skipEventCalls = false, continueWhenTestIsComplete = false) {
            let runTestPath = true;
            if (method == null)
                throw new Error(`Trying to access null method on destination map ${destination.mapName}.`);
            // A dumb hack to prevent any recursion that could occur.
            if (skipEventCalls === false) {
                if (this.onTestAPIPathStart != null)
                    runTestPath = yield this.onTestAPIPathStart(destination, method, testName, continueWhenTestIsComplete);
            }
            let result = null;
            if (runTestPath === true) {
                let testCaseObject = method.testCases[testName];
                if (testCaseObject == null)
                    throw new Error(`HotTester: Test case object ${testName} does not exist!`);
                result = yield testCaseObject.func(this.driver);
            }
            if (skipEventCalls === false) {
                if (this.onTestAPIPathEnd != null)
                    yield this.onTestAPIPathEnd(destination, method, testName, result, continueWhenTestIsComplete);
            }
            return (result);
        });
    }
    /**
     * Execute all test paths in an API route.
     *
     * @fixme This needs a better implementation...
     */
    executeTestAPIPaths(destination) {
        return __awaiter(this, void 0, void 0, function* () {
            let results = [];
            let testMap = this.testMaps[destination.mapName];
            if (testMap == null)
                throw new Error(`HotTester: API Map ${destination.mapName} does not exist!`);
            if (this.processor.api == null)
                throw new Error(`HotTester: Associated processor does not have an API!`);
            let route = this.processor.api.routes[destination.api];
            if (route == null)
                throw new Error(`HotTester: API does not have route ${destination.api}!`);
            // Iterate through each (path, testName) pair in the destination. The
            // list is encoded as [path0, test0, path1, test1, ...]; bound on
            // `iIdx + 1 < length` so an odd-length tail can't dereference an
            // undefined `nextStop` (the bug that surfaced as
            // `Cannot read properties of undefined (reading 'path')` at runtime).
            for (let iIdx = 0; iIdx + 1 < destination.paths.length; iIdx += 2) {
                let stop = destination.paths[iIdx];
                let pathName = stop.path;
                let method = route.getMethod(pathName);
                let nextStop = destination.paths[iIdx + 1];
                let testName = nextStop.path;
                if (method == null)
                    throw new Error(`Unable to find method related to path ${pathName} in map ${destination.mapName}`);
                let result = yield this.executeTestAPIPath(destination, method, testName);
                results.push(result);
            }
            return (results);
        });
    }
    /**
     * Execute a test page path.
     */
    executeTestPagePath(destination_1, stop_1) {
        return __awaiter(this, arguments, void 0, function* (destination, stop, skipEventCalls = false, continueWhenTestIsComplete = false) {
            yield this.waitForData();
            let runTestPath = true;
            let testMap = this.testMaps[destination.mapName];
            /// @fixme For some reason the errors being thrown here are not being thrown.
            if (testMap == null)
                throw new Error(`HotTester: Web Map ${destination.mapName} does not exist!`);
            let page = this.currentPage;
            if (page == null)
                throw new Error(`HotTester: A current page was not set!`);
            this.driver.page = page;
            let testPathName = stop.path;
            let testPath = page.testPaths[testPathName];
            // A dumb hack to prevent any recursion that could occur.
            if (skipEventCalls === false) {
                if (this.onTestPagePathStart != null)
                    runTestPath = yield this.onTestPagePathStart(destination, stop, continueWhenTestIsComplete);
            }
            let result = null;
            if (runTestPath === true) {
                if (testPath == null)
                    throw new Error(`HotTester: Test path ${testPathName} does not have a function!`);
                result = yield testPath(this.driver);
            }
            if (skipEventCalls === false) {
                if (this.onTestPagePathEnd != null)
                    yield this.onTestPagePathEnd(destination, testPath, result, continueWhenTestIsComplete);
            }
            if (runTestPath === true)
                yield HotStaq_1.HotStaq.wait(this.driver.pageTestDelay);
            return (result);
        });
    }
    /**
     * Execute a command. Example:
     * page:Dashboard -> cmd:click(button)
     */
    executeCommand(destination, stop, cmd) {
        return __awaiter(this, void 0, void 0, function* () {
            /**
             * Check if the input command matches.
             */
            let hasCmd = (input, cmd, hasArguments) => {
                let result = false;
                if (stop.cmd === cmd)
                    result = true;
                const pos = stop.cmd.indexOf("(");
                // If there's parenthesis, get the incoming command.
                if (pos > -1) {
                    let inputCmd = stop.cmd.substr(0, pos);
                    if (inputCmd === cmd)
                        result = true;
                }
                return (result);
            };
            /**
             * Get the arguments in a command.
             */
            let getCmdArgs = (input) => {
                let results = [];
                const match = input.match(/\(([^)]+)\)/);
                if (!match)
                    return ([]);
                results = match[1].split(',').map(arg => arg.trim());
                return (results);
            };
            let cmdFunc = null;
            let args = [];
            if (hasCmd(stop.cmd, "url", true) === true) {
                args = getCmdArgs(stop.cmd);
                cmdFunc = (cmdArgs) => __awaiter(this, void 0, void 0, function* () {
                    let input = cmdArgs[0];
                    // @ts-ignore
                    if (this.driver["waitForTestElement"] == null)
                        throw new Error(`HotTester: Driver ${this.driver.constructor.name} does not support waitForTestElement!`);
                    // @ts-ignore
                    yield this.driver.navigateToUrl(input);
                });
            }
            if (hasCmd(stop.cmd, "waitForTestObject", true) === true) {
                args = getCmdArgs(stop.cmd);
                cmdFunc = (cmdArgs) => __awaiter(this, void 0, void 0, function* () {
                    let testObject = cmdArgs[0];
                    // @ts-ignore
                    if (this.driver["waitForTestElement"] == null)
                        throw new Error(`HotTester: Driver ${this.driver.constructor.name} does not support waitForTestElement!`);
                    // @ts-ignore
                    yield this.driver.waitForTestElement(testObject);
                });
            }
            if (hasCmd(stop.cmd, "waitForTesterAPIData", false) === true) {
                cmdFunc = (cmdArgs) => __awaiter(this, void 0, void 0, function* () {
                    this.finishedLoading = false;
                    yield this.waitForData();
                });
            }
            if (hasCmd(stop.cmd, "wait", true) === true) {
                args = getCmdArgs(stop.cmd);
                cmdFunc = (cmdArgs) => __awaiter(this, void 0, void 0, function* () {
                    let numMilliseconds = parseInt(cmdArgs[0]);
                    yield HotStaq_1.HotStaq.wait(numMilliseconds);
                });
            }
            if (hasCmd(stop.cmd, "print", true) === true) {
                args = getCmdArgs(stop.cmd);
                cmdFunc = (cmdArgs) => __awaiter(this, void 0, void 0, function* () {
                    let input = cmdArgs[0];
                    yield this.driver.print(input);
                });
            }
            if (hasCmd(stop.cmd, "println", true) === true) {
                args = getCmdArgs(stop.cmd);
                cmdFunc = (cmdArgs) => __awaiter(this, void 0, void 0, function* () {
                    let input = cmdArgs[0];
                    yield this.driver.println(input);
                });
            }
            if (hasCmd(stop.cmd, "click", true) === true) {
                args = getCmdArgs(stop.cmd);
                cmdFunc = (cmdArgs) => __awaiter(this, void 0, void 0, function* () {
                    let elmName = cmdArgs[0];
                    // @ts-ignore
                    if (this.driver["waitForTestElement"] == null)
                        throw new Error(`HotTester: Driver ${this.driver.constructor.name} does not support waitForTestElement!`);
                    // @ts-ignore
                    const elm = yield this.driver.waitForTestElement(elmName);
                    yield elm.click();
                });
            }
            if (hasCmd(stop.cmd, "sendKeys", true) === true) {
                args = getCmdArgs(stop.cmd);
                cmdFunc = (cmdArgs) => __awaiter(this, void 0, void 0, function* () {
                    let elmName = cmdArgs[0];
                    let input = cmdArgs[1];
                    // @ts-ignore
                    if (this.driver["waitForTestElement"] == null)
                        throw new Error(`HotTester: Driver ${this.driver.constructor.name} does not support waitForTestElement!`);
                    // @ts-ignore
                    const elm = yield this.driver.waitForTestElement(elmName);
                    yield elm.sendKeys(input);
                });
            }
            if (hasCmd(stop.cmd, "clear", true) === true) {
                args = getCmdArgs(stop.cmd);
                cmdFunc = (cmdArgs) => __awaiter(this, void 0, void 0, function* () {
                    let elmName = cmdArgs[0];
                    // @ts-ignore
                    if (this.driver["waitForTestElement"] == null)
                        throw new Error(`HotTester: Driver ${this.driver.constructor.name} does not support waitForTestElement!`);
                    // @ts-ignore
                    const elm = yield this.driver.waitForTestElement(elmName);
                    yield elm.clear();
                });
            }
            if (hasCmd(stop.cmd, "debugger", true) === true) {
                args = getCmdArgs(stop.cmd);
                cmdFunc = (cmdArgs) => __awaiter(this, void 0, void 0, function* () {
                    // Keep this. This is correct.
                    debugger;
                });
            }
            if (cmdFunc == null)
                throw new Error(`HotTester: Command ${stop.cmd} does not exist!`);
            yield this.onCommand(destination, stop, cmd, args, cmdFunc);
        });
    }
    /**
     * Execute all test paths in a page.
     */
    executeTestPagePaths(destination_1) {
        return __awaiter(this, arguments, void 0, function* (destination, continueWhenTestIsComplete = false) {
            let results = [];
            let testMap = this.testMaps[destination.mapName];
            /// @fixme For some reason the errors being thrown here are not being thrown.
            if (testMap == null)
                throw new Error(`HotTester: Web Map ${destination.mapName} does not exist!`);
            // Iterate through each path in the destination until complete.
            for (let iIdx = 0; iIdx < destination.paths.length; iIdx++) {
                let stop = destination.paths[iIdx];
                let result = null;
                if (stop.dest !== "") {
                    if (testMap.destinations instanceof Array)
                        throw new Error(`HotTester: When using type 'dest' in a destination string, all destinations in map ${destination.mapName} must be named.`);
                    let testDest = testMap.destinations[stop.dest];
                    let newDestination = HotTester.interpretDestination(destination.mapName, testDest);
                    if (newDestination != null)
                        result = yield this.executeTestPagePaths(newDestination);
                }
                if (stop.cmd !== "")
                    yield this.executeCommand(destination, stop, stop.cmd);
                if (stop.path !== "")
                    result = yield this.executeTestPagePath(destination, stop, false, continueWhenTestIsComplete);
                results.push(result);
            }
            return (results);
        });
    }
    /**
     * Execute the tests.
     */
    execute(mapName) {
        return __awaiter(this, void 0, void 0, function* () {
            let map = this.testMaps[mapName];
            if (map == null)
                throw new Error(`HotTester: Map ${mapName} does not exist!`);
            this.processor.logger.verbose(`HotTester: Executing map ${mapName}...`);
            // Process routes testing first.
            let routeKey = this.processor.getRouteKeyFromName(mapName);
            let executeDestination = (testDest_1, ...args_1) => __awaiter(this, [testDest_1, ...args_1], void 0, function* (testDest, destinationKey = "") {
                if (testDest.autoStart === false)
                    return;
                try {
                    this.processor.logger.verbose(() => `HotTester: Executing ${mapName} destination ${JSON.stringify(testDest)}...`);
                    let destination = HotTester.interpretDestination(mapName, testDest);
                    let isWebRoute = false;
                    let runTestPaths = true;
                    let url = this.baseUrl;
                    if (destination.url !== "") {
                        isWebRoute = true;
                        url += destination.url;
                    }
                    if (this.setup != null) {
                        if (this.hasBeenSetup === false) {
                            yield this.setup(isWebRoute, url, destinationKey);
                            this.hasBeenSetup = true;
                            this.hasBeenDestroyed = false;
                        }
                    }
                    if (this.onTestStart != null)
                        runTestPaths = yield this.onTestStart(destination, url, destinationKey);
                    if (runTestPaths === true) {
                        if (destination.url !== "")
                            yield this.executeTestPagePaths(destination);
                        if (destination.api !== "")
                            yield this.executeTestAPIPaths(destination);
                    }
                    if (this.onTestEnd != null)
                        yield this.onTestEnd(destination);
                    if (this.destroy != null) {
                        if (this.hasBeenDestroyed === false) {
                            yield this.destroy();
                            this.hasBeenDestroyed = true;
                            this.hasBeenSetup = false;
                        }
                    }
                }
                catch (ex) {
                    throw ex;
                }
            });
            // If the map destinations are in an array, just execute those in order.
            if (map.destinations instanceof Array) {
                for (let iIdx = 0; iIdx < map.destinations.length; iIdx++) {
                    let testDest = map.destinations[iIdx];
                    yield executeDestination(testDest);
                }
            }
            else {
                // If there's a destination order, use that.
                if (map.destinationOrder.length > 0) {
                    let hasExecutedKeys = [];
                    // Go through the destination order and execute each one.
                    for (let iIdx = 0; iIdx < map.destinationOrder.length; iIdx++) {
                        let orderKey = map.destinationOrder[iIdx];
                        let testDest = map.destinations[orderKey];
                        if (testDest == null)
                            throw new Error(`HotTester: Destination ${orderKey} does not exist!`);
                        hasExecutedKeys.push(orderKey);
                        yield executeDestination(testDest, orderKey);
                    }
                    // Execute the rest of the destinations that have not been executed yet.
                    for (let key in map.destinations) {
                        let executeDest = true;
                        for (let iIdx = 0; iIdx < hasExecutedKeys.length; iIdx++) {
                            let executedKey = hasExecutedKeys[iIdx];
                            if (executedKey === key) {
                                executeDest = false;
                                break;
                            }
                        }
                        if (executeDest === true) {
                            let testDest = map.destinations[key];
                            yield executeDestination(testDest, key);
                        }
                    }
                }
                else {
                    // Execute the destinations in any order.
                    for (let key in map.destinations) {
                        let testDest = map.destinations[key];
                        yield executeDestination(testDest, key);
                    }
                }
            }
        });
    }
}
exports.HotTester = HotTester;
//# sourceMappingURL=HotTester.js.map