"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());
    });
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.HotTestSeleniumDriver = void 0;
const HotTestElement_1 = require("./HotTestElement");
const HotTestDriver_1 = require("./HotTestDriver");
const HotStaq_1 = require("./HotStaq");
const selenium_webdriver_1 = require("selenium-webdriver");
const chrome_1 = __importDefault(require("selenium-webdriver/chrome"));
const firefox_1 = __importDefault(require("selenium-webdriver/firefox"));
/**
 * Runs and executes tests using Selenium.
 */
class HotTestSeleniumDriver extends HotTestDriver_1.HotTestDriver {
    constructor(processor, page = null) {
        super(processor, page);
        this.driver = null;
        this.session = null;
        this.browser = "chrome";
        this.headless = false;
        this.disableGPUAndSandbox = false;
        this.openDevTools = false;
        this.disableDevShmUsage = false;
        this.remoteServer = "";
        this.windowSize = null;
    }
    /**
     * Disconnect this server or destroy anything associated with this HotTestDriver.
     */
    destroy() {
        return __awaiter(this, void 0, void 0, function* () {
            if (this.driver != null) {
                yield this.driver.quit();
                this.driver = null;
                this.session = null;
            }
        });
    }
    /**
     * Load the selenium driver.
     *
     * If a WebDriver session already exists, return without spinning up a
     * fresh one — every destination would otherwise call this and get its
     * own browser, dropping cookies (refreshToken, session) set by an
     * earlier destination's login flow. Reusing the session lets a multi-
     * step suite (login → /account → /change-password) keep the
     * authentication state. Override with HOTSTAQ_FORCE_NEW_SESSION=1.
     */
    loadSeleniumDriver() {
        return __awaiter(this, void 0, void 0, function* () {
            if (this.driver != null && process.env["HOTSTAQ_FORCE_NEW_SESSION"] !== "1") {
                this.processor.logger.verbose(`HotTestSeleniumDriver: Reusing existing session...`);
                return;
            }
            let createWindowSize = () => {
                if (this.windowSize == null)
                    this.windowSize = { width: 1920, height: 1080 };
            };
            let builder = new selenium_webdriver_1.Builder();
            builder = builder.forBrowser(this.browser);
            // Capture browser console + driver logs so a failed waitForData
            // can dump them (see HotTesterMochaSelenium.setup). Without this
            // the WebDriver logging API returns nothing on Chrome.
            try {
                const prefs = new selenium_webdriver_1.logging.Preferences();
                prefs.setLevel(selenium_webdriver_1.logging.Type.BROWSER, selenium_webdriver_1.logging.Level.ALL);
                prefs.setLevel(selenium_webdriver_1.logging.Type.DRIVER, selenium_webdriver_1.logging.Level.WARNING);
                builder = builder.setLoggingPrefs(prefs);
            }
            catch (e) { /* older selenium-webdriver — skip */ }
            if (process.env["SELENIUM_REMOTE_SERVER"] != null)
                this.remoteServer = process.env["SELENIUM_REMOTE_SERVER"];
            if (process.env["SELENIUM_WINDOW_WIDTH"] != null) {
                createWindowSize();
                this.windowSize.width = parseFloat(process.env["SELENIUM_WINDOW_WIDTH"]);
            }
            if (process.env["SELENIUM_WINDOW_HEIGHT"] != null) {
                createWindowSize();
                this.windowSize.height = parseFloat(process.env["SELENIUM_WINDOW_HEIGHT"]);
            }
            if (this.remoteServer !== "") {
                this.processor.logger.verbose(`HotTestSeleniumDriver: Connecting to remote server ${this.remoteServer}`);
                builder = builder.usingServer(this.remoteServer);
                createWindowSize();
                this.headless = true;
            }
            if (this.browser === "firefox") {
                let options = new firefox_1.default.Options();
                this.processor.logger.verbose(`HotTestSeleniumDriver: Using Firefox with options`);
                if (this.openDevTools === true) {
                    options = options.addArguments("-devtools");
                    this.processor.logger.verbose(`  * devtools`);
                }
                if (this.headless === true) {
                    options = options.addArguments("-headless");
                    this.processor.logger.verbose(`  * headless`);
                }
                if (this.disableDevShmUsage === true) {
                    options = options.addArguments("-disable-dev-shm-usage");
                    this.processor.logger.verbose(`  * Disable Shared Memory`);
                }
                if (this.windowSize != null) {
                    options = options.windowSize(this.windowSize);
                    this.processor.logger.verbose(`  * Window Size: ${this.windowSize.width}x${this.windowSize.height}`);
                }
                if (process.env["USER_DATA_DIR"] != null) {
                    options = options.addArguments(`-profile ${process.env["USER_DATA_DIR"]}`);
                    this.processor.logger.verbose(`  * User data dir: ${process.env["USER_DATA_DIR"]}`);
                }
                if (process.env.NODE_TLS_REJECT_UNAUTHORIZED != null) {
                    if (process.env.NODE_TLS_REJECT_UNAUTHORIZED == "0") {
                        options.setAcceptInsecureCerts(true);
                        this.processor.logger.verbose(`  * Accepting insecure certs`);
                    }
                }
                builder = builder.setFirefoxOptions(options);
            }
            if (this.browser === "chrome") {
                let options = new chrome_1.default.Options();
                this.processor.logger.verbose(`HotTestSeleniumDriver: Using Chrome with options`);
                // CHROMEDRIVER_PATH lets the caller pin the driver binary so
                // Selenium Manager doesn't try to download it at runtime — that
                // download is the prime suspect for the 30-minute test-web hangs
                // in CI runners with constrained egress. CHROME_BINARY_PATH
                // pairs with it for environments where the chromium executable
                // is also non-default (Debian's `chromium` rather than `google-
                // chrome`). Both are no-ops when unset.
                const chromedriverPath = process.env["CHROMEDRIVER_PATH"];
                if (chromedriverPath != null && chromedriverPath !== "") {
                    const svc = new chrome_1.default.ServiceBuilder(chromedriverPath);
                    builder = builder.setChromeService(svc);
                    this.processor.logger.verbose(`  * chromedriver path: ${chromedriverPath}`);
                }
                const chromeBinaryPath = process.env["CHROME_BINARY_PATH"];
                if (chromeBinaryPath != null && chromeBinaryPath !== "") {
                    options.setChromeBinaryPath(chromeBinaryPath);
                    this.processor.logger.verbose(`  * chrome binary: ${chromeBinaryPath}`);
                }
                if (this.disableGPUAndSandbox === true) {
                    options.addArguments("--disable-gpu", "--no-sandbox");
                    this.processor.logger.verbose(`  * Disabled GPU and No sandbox`);
                }
                if (this.openDevTools === true) {
                    options.addArguments("--auto-open-devtools-for-tabs");
                    this.processor.logger.verbose(`  * Auto open devtools`);
                }
                if (this.disableDevShmUsage === true) {
                    options.addArguments("--disable-dev-shm-usage");
                    this.processor.logger.verbose(`  * Disable Shared Memory`);
                }
                if (this.headless === true) {
                    options.addArguments("--headless");
                    this.processor.logger.verbose(`  * Headless`);
                }
                if (this.windowSize != null) {
                    options.windowSize(this.windowSize);
                    this.processor.logger.verbose(`  * Window Size: ${this.windowSize.width}x${this.windowSize.height}`);
                }
                if (process.env["USER_DATA_DIR"] != null) {
                    options.addArguments(`--user-data-dir=${process.env["USER_DATA_DIR"]}`);
                    this.processor.logger.verbose(`  * User data dir: ${process.env["USER_DATA_DIR"]}`);
                }
                if (process.env.NODE_TLS_REJECT_UNAUTHORIZED != null) {
                    if (process.env.NODE_TLS_REJECT_UNAUTHORIZED == "0") {
                        options.setAcceptInsecureCerts(true);
                        this.processor.logger.verbose(`  * Accepting insecure certs`);
                    }
                }
                builder = builder.setChromeOptions(options);
            }
            this.processor.logger.verbose(`HotTestSeleniumDriver: Starting session...`);
            // Bound builder.build() with a hard timeout. In CI runners with
            // constrained egress, Selenium Manager's auto-download of
            // chromedriver can hang silently — turning a "tests can't start"
            // failure into the full 30-minute job timeout. Cap at 60s (override
            // with SELENIUM_BUILD_TIMEOUT_MS) so the failure is fast and the
            // log says exactly what happened.
            const buildTimeoutMs = parseInt(process.env["SELENIUM_BUILD_TIMEOUT_MS"] || "60000", 10) || 60000;
            const buildPromise = builder.build();
            const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error(`HotTestSeleniumDriver: Selenium driver did not start within ${buildTimeoutMs}ms. ` +
                `Set CHROMEDRIVER_PATH and CHROME_BINARY_PATH to skip Selenium Manager's auto-download, ` +
                `or raise SELENIUM_BUILD_TIMEOUT_MS to wait longer.`)), buildTimeoutMs));
            this.driver = (yield Promise.race([buildPromise, timeoutPromise]));
            this.session = yield this.driver.getSession();
            this.processor.logger.verbose(`HotTestSeleniumDriver: Session started...`);
        });
    }
    /**
     * Get a test object by it's name. If a * is used, it will be used as a
     * wildcard for the object's name. If a > is used, then the name will
     * be treated as a CSS selector.
     */
    getTestObjectByName(name) {
        let selector = this.parseTestObject(name);
        return (selenium_webdriver_1.By.css(selector));
    }
    /**
     * Navigate to a url.
     */
    navigateToUrl(url) {
        return __awaiter(this, void 0, void 0, function* () {
            yield this.driver.get(url);
            this.processor.logger.verbose(`HotTestSeleniumDriver: navigateToUrl - ${url}`);
        });
    }
    /**
     * Test-friendly alias for navigateToUrl. Accepts either a fully
     * qualified URL or a path that's resolved against the driver's
     * current origin — that way `.hott` test paths can write
     * `await driver.navigate("/account")` instead of having to know
     * the test server's host:port.
     */
    navigate(urlOrPath) {
        return __awaiter(this, void 0, void 0, function* () {
            let target = urlOrPath;
            if (urlOrPath.startsWith("/")) {
                try {
                    const cur = yield this.driver.getCurrentUrl();
                    target = new URL(urlOrPath, cur).toString();
                }
                catch (e) { /* fall through with the bare path */ }
            }
            // Reset the tester's finishedLoading so the next executeTestPagePath
            // blocks on the *new* page's pageLoaded event, not the previous
            // page's already-fired one — otherwise the runner immediately
            // looks up the next test path against stale caches and fails
            // with "Test path X does not have a function".
            if (this.tester != null)
                this.tester.finishedLoading = false;
            yield this.navigateToUrl(target);
        });
    }
    /**
     * Wait for a test element using Selenium Webdriver.
     */
    waitForTestElement(name_1) {
        return __awaiter(this, arguments, void 0, function* (name, options = new HotTestElement_1.HotTestElementOptions()) {
            let nameStr = "";
            if (typeof (name) === "string")
                nameStr = name;
            else
                nameStr = name.name;
            let foundElm = null;
            this.processor.logger.verbose(`HotTestSeleniumDriver: waitForTestElement - Searching for ${nameStr}`);
            if (options.mustBeVisible === false)
                foundElm = yield this.driver.wait(selenium_webdriver_1.until.elementLocated(this.getTestObjectByName(nameStr)));
            else {
                let elms = yield this.driver.wait(selenium_webdriver_1.until.elementsLocated(this.getTestObjectByName(nameStr)));
                for (let iIdx = 0; iIdx < elms.length; iIdx++) {
                    let elm = elms[iIdx];
                    if ((yield elm.isDisplayed()) === true) {
                        foundElm = elm;
                        break;
                    }
                }
            }
            this.processor.logger.verbose(`HotTestSeleniumDriver: waitForTestElement - Found ${nameStr}`);
            return (foundElm);
        });
    }
    /**
     * Get a test element using selenium webdriver.
     */
    findTestElement(name_1) {
        return __awaiter(this, arguments, void 0, function* (name, options = new HotTestElement_1.HotTestElementOptions()) {
            let nameStr = "";
            if (typeof (name) === "string")
                nameStr = name;
            else
                nameStr = name.name;
            let foundElm = null;
            this.processor.logger.verbose(`HotTestSeleniumDriver: findTestElement - Searching for ${nameStr}`);
            if (options.mustBeVisible === false) {
                let elms = yield this.driver.findElements(this.getTestObjectByName(nameStr));
                if (elms.length > 0)
                    foundElm = elms[0];
            }
            else {
                let elms = yield this.driver.findElements(this.getTestObjectByName(nameStr));
                for (let iIdx = 0; iIdx < elms.length; iIdx++) {
                    let elm = elms[iIdx];
                    if ((yield elm.isDisplayed()) === true) {
                        foundElm = elm;
                        break;
                    }
                }
            }
            if (foundElm != null)
                this.processor.logger.verbose(`HotTestSeleniumDriver: findTestElement - Found ${nameStr}`);
            else
                this.processor.logger.verbose(`HotTestSeleniumDriver: findTestElement - Not found ${nameStr}`);
            return (foundElm);
        });
    }
    /**
     * Run a command using Selenium Webdriver.
     */
    runCommand(testElm_1) {
        return __awaiter(this, arguments, void 0, function* (testElm, funcName = "", valueStr = "", options = new HotTestElement_1.HotTestElementOptions()) {
            let name = "";
            let func = "";
            let value = "";
            if (typeof (testElm) !== "string") {
                name = testElm.name;
                func = testElm.func;
                value = testElm.value;
            }
            else {
                name = testElm;
                func = funcName;
                value = valueStr;
            }
            this.processor.logger.verbose(`HotTestSeleniumDriver: runCommand - Running Selenium function ${func} with value ${value} using element ${name}`);
            let elm = yield this.findTestElement(name, options);
            let result = null;
            if (elm == null) {
                if (options.ignoreMissingElementError === true)
                    return (result);
                throw new Error(`HotTestSeleniumDriver: Unable to find test element ${name}!`);
            }
            if (func != null) {
                if (func !== "") {
                    // Click hardening: layouts with sticky footers, fixed
                    // banners, or BotBlocker overlays routinely intercept the
                    // raw `elm.click()`. Scroll the target into the centre of
                    // the viewport first; if Selenium still rejects the click
                    // as intercepted, fall back to a JS click which bypasses
                    // hit-testing entirely.
                    if (func === "click") {
                        try {
                            yield this.driver.executeScript("arguments[0].scrollIntoView({block:'center', inline:'center'});", elm);
                        }
                        catch (_e) { /* best-effort scroll */ }
                        try {
                            result = yield elm.click();
                        }
                        catch (clickErr) {
                            const msg = (clickErr && clickErr.message) || String(clickErr);
                            if (msg.indexOf("intercepted") > -1 || msg.indexOf("not clickable") > -1) {
                                this.processor.logger.verbose(`HotTestSeleniumDriver: click intercepted on ${name}; ` +
                                    `falling back to JS click.`);
                                result = yield this.driver.executeScript("arguments[0].click();", elm);
                            }
                            else
                                throw clickErr;
                        }
                    }
                    else if (value != null) {
                        /// @ts-ignore
                        result = yield elm[func](value);
                    }
                    else {
                        /// @ts-ignore
                        result = yield elm[func]();
                    }
                }
            }
            this.processor.logger.verbose(() => `HotTestSeleniumDriver: runCommand - Finished Running Selenium function ${func}`);
            return (result);
        });
    }
    /**
     * Assert that a named test element exists in the DOM. Used by .hott
     * test paths that just need to verify a control rendered (e.g. an
     * error message, a confirmation banner) without comparing its value.
     *
     * Throws when the element is missing — same shape as
     * `assertElementValue` so test paths can mix the two freely.
     */
    assertElementExists(name_1) {
        return __awaiter(this, arguments, void 0, function* (name, errorMessage = "", options = new HotTestElement_1.HotTestElementOptions()) {
            let elm = yield this.findTestElement(name, options);
            if (elm == null) {
                let realName = "";
                if (typeof (name) === "string")
                    realName = name;
                else
                    realName = name.name;
                if (options.ignoreMissingElementError === true)
                    return (null);
                throw new Error(errorMessage !== ""
                    ? `${errorMessage} (test element ${realName} not found)`
                    : `Unable to find test element ${realName}`);
            }
            this.processor.logger.verbose(`HotTestSeleniumDriver: assertElementExists - Found ${typeof name === "string" ? name : name.name}`);
            return (elm);
        });
    }
    /**
     * Read a test element's current value. Returns the input's `value`
     * attribute if present, falling back to the element's text content —
     * mirrors the lookup order in assertElementValue so test paths that
     * just need the value (e.g. to check the field was pre-populated)
     * don't have to drop down to executeScript.
     */
    getTestElementValue(name_1) {
        return __awaiter(this, arguments, void 0, function* (name, options = new HotTestElement_1.HotTestElementOptions()) {
            const elm = yield this.findTestElement(name, options);
            if (elm == null) {
                if (options.ignoreMissingElementError === true)
                    return ("");
                const realName = (typeof name === "string") ? name : name.name;
                throw new Error(`HotTestSeleniumDriver: getTestElementValue - element ${realName} not found`);
            }
            const attrValue = yield elm.getAttribute("value");
            if (attrValue != null && attrValue !== "")
                return (attrValue);
            return (yield elm.getText());
        });
    }
    /**
     * An expression to test.
     */
    assertElementValue(name_1, value_1) {
        return __awaiter(this, arguments, void 0, function* (name, value, errorMessage = "", options = new HotTestElement_1.HotTestElementOptions()) {
            let elm = yield this.findTestElement(name, options);
            if (elm == null) {
                let realName = "";
                if (typeof (name) === "string")
                    realName = name;
                else
                    realName = name.name;
                if (options.ignoreMissingElementError === true)
                    return;
                throw new Error(`Unable to find test object ${realName}`);
            }
            this.processor.logger.verbose(`HotTestSeleniumDriver: assertElementValue - Asserting`);
            let elmValue = yield elm.getText();
            let elmValue2 = "";
            let elmValue3 = "";
            let finalElmValue = elmValue;
            if (finalElmValue === "") {
                elmValue2 = yield elm.getAttribute("value");
                if (elmValue2 != null) {
                    if (elmValue2 !== "")
                        finalElmValue = elmValue2;
                }
            }
            if (finalElmValue === "") {
                elmValue3 = yield elm.getAttribute("innerHTML");
                if (elmValue3 != null) {
                    if (elmValue3 !== "")
                        finalElmValue = elmValue3;
                }
            }
            if (finalElmValue != value)
                throw new Error(`Error: ${errorMessage}. Expected: ${JSON.stringify(value)}, Actual: ${JSON.stringify(finalElmValue)}`);
        });
    }
    /**
     * Run a series of test elements.
     */
    run(executions) {
        return __awaiter(this, void 0, void 0, function* () {
            let results = [];
            for (let iIdx = 0; iIdx < executions.length; iIdx++) {
                let execution = executions[iIdx];
                let testElm = null;
                let func = "";
                let value = "";
                if (typeof (execution) === "string") {
                    testElm = this.page.testElements[execution];
                    /// @fixme This is going to wreck selecting test elements by wildcards.
                    if (testElm == null)
                        throw new Error(`HotTestDriver: Unable to find test element ${execution}`);
                    func = testElm.func;
                    value = testElm.value;
                }
                if (execution instanceof Array) {
                    let name = execution[0];
                    testElm = this.page.testElements[name];
                    // This null catch is specifically to help find wildcard test elements.
                    if (testElm == null) {
                        testElm = new HotTestElement_1.HotTestElement(name);
                        func = execution[1];
                        value = execution[2];
                    }
                    else {
                        func = testElm.func;
                        value = testElm.value;
                        if (execution.length > 1)
                            func = execution[1];
                        if (execution.length > 2)
                            value = execution[2];
                    }
                }
                testElm.func = func;
                testElm.value = value;
                let result = yield this.runCommand(testElm);
                yield HotStaq_1.HotStaq.wait(this.commandDelay);
                results.push(result);
            }
            return (results);
        });
    }
}
exports.HotTestSeleniumDriver = HotTestSeleniumDriver;
//# sourceMappingURL=HotTestSeleniumDriver.js.map