"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getTimeoutError = exports.getSessionError = exports.setupDirectConnect = exports.getEnvironmentVars = exports.CustomRequestError = exports.getErrorFromResponseBody = exports.getPrototype = exports.isSuccessfulResponse = exports.startWebDriverSession = void 0;
const lodash_merge_1 = __importDefault(require("lodash.merge"));
const logger_1 = __importDefault(require("@wdio/logger"));
const protocols_1 = require("@wdio/protocols");
const factory_1 = __importDefault(require("./request/factory"));
const command_1 = __importDefault(require("./command"));
const utils_1 = require("@wdio/utils");
const protocols_2 = require("@wdio/protocols");
const constants_1 = require("./constants");
const log = (0, logger_1.default)('webdriver');
const BROWSER_DRIVER_ERRORS = [
    'unknown command: wd/hub/session',
    'HTTP method not allowed',
    "'POST /wd/hub/session' was not found.",
    'Command not found' // iedriver
];
/**
 * start browser session with WebDriver protocol
 */
async function startWebDriverSession(params) {
    /**
     * validate capabilities to check if there are no obvious mix between
     * JSONWireProtocol and WebDriver protoocol, e.g.
     */
    if (params.capabilities) {
        const extensionCaps = Object.keys(params.capabilities).filter((cap) => cap.includes(':'));
        const invalidWebDriverCaps = Object.keys(params.capabilities)
            .filter((cap) => !protocols_2.CAPABILITY_KEYS.includes(cap) && !cap.includes(':'));
        /**
         * if there are vendor extensions, e.g. sauce:options or appium:app
         * used (only WebDriver compatible) and caps that aren't defined
         * in the WebDriver spec
         */
        if (extensionCaps.length && invalidWebDriverCaps.length) {
            throw new Error(`Invalid or unsupported WebDriver capabilities found ("${invalidWebDriverCaps.join('", "')}"). ` +
                'Ensure to only use valid W3C WebDriver capabilities (see https://w3c.github.io/webdriver/#capabilities).' +
                'If you run your tests on a remote vendor, like Sauce Labs or BrowserStack, make sure that you put them ' +
                'into vendor specific capabilities, e.g. "sauce:options" or "bstack:options". Please reach out to ' +
                'to your vendor support team if you have further questions.');
        }
    }
    /**
     * the user could have passed in either w3c style or jsonwp style caps
     * and we want to pass both styles to the server, which means we need
     * to check what style the user sent in so we know how to construct the
     * object for the other style
     */
    const [w3cCaps, jsonwpCaps] = params.capabilities && params.capabilities.alwaysMatch
        /**
         * in case W3C compliant capabilities are provided
         */
        ? [params.capabilities, params.capabilities.alwaysMatch]
        /**
         * otherwise assume they passed in jsonwp-style caps (flat object)
         */
        : [{ alwaysMatch: params.capabilities, firstMatch: [{}] }, params.capabilities];
    const sessionRequest = factory_1.default.getInstance('POST', '/session', {
        capabilities: w3cCaps,
        desiredCapabilities: jsonwpCaps // JSONWP compliant
    });
    let response;
    try {
        response = await sessionRequest.makeRequest(params);
    }
    catch (err) {
        log.error(err);
        const message = (0, exports.getSessionError)(err, params);
        throw new Error('Failed to create session.\n' + message);
    }
    const sessionId = response.value.sessionId || response.sessionId;
    /**
     * save actual receveived session details
     */
    params.capabilities = response.value.capabilities || response.value;
    return { sessionId, capabilities: params.capabilities };
}
exports.startWebDriverSession = startWebDriverSession;
/**
 * check if WebDriver requests was successful
 * @param  {Number}  statusCode status code of request
 * @param  {Object}  body       body payload of response
 * @return {Boolean}            true if request was successful
 */
function isSuccessfulResponse(statusCode, body) {
    /**
     * response contains a body
     */
    if (!body || typeof body.value === 'undefined') {
        log.debug('request failed due to missing body');
        return false;
    }
    /**
     * ignore failing element request to enable lazy loading capability
     */
    if (body.status === 7 && body.value && body.value.message &&
        (body.value.message.toLowerCase().startsWith('no such element') ||
            // Appium
            body.value.message === 'An element could not be located on the page using the given search parameters.' ||
            // Internet Explorter
            body.value.message.toLowerCase().startsWith('unable to find element'))) {
        return true;
    }
    /**
     * if it has a status property, it should be 0
     * (just here to stay backwards compatible to the jsonwire protocol)
     */
    if (body.status && body.status !== 0) {
        log.debug(`request failed due to status ${body.status}`);
        return false;
    }
    const hasErrorResponse = body.value && (body.value.error || body.value.stackTrace || body.value.stacktrace);
    /**
     * check status code
     */
    if (statusCode === 200 && !hasErrorResponse) {
        return true;
    }
    /**
     * if an element was not found we don't flag it as failed request because
     * we lazy load it
     */
    if (statusCode === 404 && body.value && body.value.error === 'no such element') {
        return true;
    }
    /**
     * that has no error property (Appium only)
     */
    if (hasErrorResponse) {
        log.debug('request failed due to response error:', body.value.error);
        return false;
    }
    return true;
}
exports.isSuccessfulResponse = isSuccessfulResponse;
/**
 * creates the base prototype for the webdriver monad
 */
function getPrototype({ isW3C, isChrome, isFirefox, isMobile, isSauce, isSeleniumStandalone }) {
    const prototype = {};
    const ProtocolCommands = (0, lodash_merge_1.default)(
    /**
     * if mobile apply JSONWire and WebDriver protocol because
     * some legacy JSONWire commands are still used in Appium
     * (e.g. set/get geolocation)
     */
    isMobile
        ? (0, lodash_merge_1.default)({}, protocols_1.JsonWProtocol, protocols_1.WebDriverProtocol)
        : isW3C ? protocols_1.WebDriverProtocol : protocols_1.JsonWProtocol, 
    /**
     * only apply mobile protocol if session is actually for mobile
     */
    isMobile ? (0, lodash_merge_1.default)({}, protocols_1.MJsonWProtocol, protocols_1.AppiumProtocol) : {}, 
    /**
     * only apply special Chrome commands if session is using Chrome
     */
    isChrome ? protocols_1.ChromiumProtocol : {}, 
    /**
     * only apply special Firefox commands if session is using Firefox
     */
    isFirefox ? protocols_1.GeckoProtocol : {}, 
    /**
     * only Sauce Labs specific vendor commands
     */
    isSauce ? protocols_1.SauceLabsProtocol : {}, 
    /**
     * only apply special commands when running tests using
     * Selenium Grid or Selenium Standalone server
     */
    isSeleniumStandalone ? protocols_1.SeleniumProtocol : {});
    for (const [endpoint, methods] of Object.entries(ProtocolCommands)) {
        for (const [method, commandData] of Object.entries(methods)) {
            prototype[commandData.command] = { value: (0, command_1.default)(method, endpoint, commandData, isSeleniumStandalone) };
        }
    }
    return prototype;
}
exports.getPrototype = getPrototype;
/**
 * helper method to determine the error from webdriver response
 * @param  {Object} body body object
 * @return {Object} error
 */
function getErrorFromResponseBody(body, requestOptions) {
    if (!body) {
        return new Error('Response has empty body');
    }
    if (typeof body === 'string' && body.length) {
        return new Error(body);
    }
    if (typeof body !== 'object') {
        return new Error('Unknown error');
    }
    return new CustomRequestError(body, requestOptions);
}
exports.getErrorFromResponseBody = getErrorFromResponseBody;
//Exporting for testability
class CustomRequestError extends Error {
    constructor(body, requestOptions) {
        const errorObj = body.value || body;
        let errorMessage = errorObj.message || errorObj.class || 'unknown error';
        /**
         * improve error message for Chrome and Safari on invalid selectors
         */
        if (typeof errorObj.error === 'string' && errorObj.error.includes('invalid selector')) {
            errorMessage = (`The selector "${requestOptions.value}" used with strategy "${requestOptions.using}" is invalid! ` +
                'For more information on selectors visit the WebdriverIO docs at: https://webdriver.io/docs/selectors');
        }
        super(errorMessage);
        if (errorObj.error) {
            this.name = errorObj.error;
        }
        else if (errorObj.message && errorObj.message.includes('stale element reference')) {
            this.name = 'stale element reference';
        }
        else {
            this.name = errorObj.name || 'WebDriver Error';
        }
        Error.captureStackTrace(this, CustomRequestError);
    }
}
exports.CustomRequestError = CustomRequestError;
/**
 * return all supported flags and return them in a format so we can attach them
 * to the instance protocol
 * @param  {Object} options   driver instance or option object containing these flags
 * @return {Object}           prototype object
 */
function getEnvironmentVars({ isW3C, isMobile, isIOS, isAndroid, isChrome, isFirefox, isSauce, isSeleniumStandalone }) {
    return {
        isW3C: { value: isW3C },
        isMobile: { value: isMobile },
        isIOS: { value: isIOS },
        isAndroid: { value: isAndroid },
        isFirefox: { value: isFirefox },
        isChrome: { value: isChrome },
        isSauce: { value: isSauce },
        isSeleniumStandalone: { value: isSeleniumStandalone }
    };
}
exports.getEnvironmentVars = getEnvironmentVars;
/**
 * Decorate the client's options object with host updates based on the presence of
 * directConnect capabilities in the new session response. Note that this
 * mutates the object.
 * @param  {Client} params post-new-session client
 */
function setupDirectConnect(client) {
    const capabilities = client.capabilities;
    const directConnectProtocol = capabilities.directConnectProtocol || capabilities['appium:directConnectProtocol'];
    const directConnectHost = capabilities.directConnectHost || capabilities['appium:directConnectHost'];
    let directConnectPath = capabilities.directConnectPath;
    if (!(directConnectPath || directConnectPath === '')) {
        directConnectPath = capabilities['appium:directConnectPath'];
    }
    const directConnectPort = capabilities.directConnectPort || capabilities['appium:directConnectPort'];
    if (directConnectProtocol && directConnectHost && directConnectPort &&
        (directConnectPath || directConnectPath === '')) {
        log.info('Found direct connect information in new session response. ' +
            `Will connect to server at ${directConnectProtocol}://` +
            `${directConnectHost}:${directConnectPort}${directConnectPath}`);
        client.options.protocol = directConnectProtocol;
        client.options.hostname = directConnectHost;
        client.options.port = directConnectPort;
        client.options.path = directConnectPath;
    }
}
exports.setupDirectConnect = setupDirectConnect;
/**
 * get human readable message from response error
 * @param {Error} err response error
 */
const getSessionError = (err, params = {}) => {
    var _a;
    // browser driver / service is not started
    if (err.code === 'ECONNREFUSED') {
        return `Unable to connect to "${params.protocol}://${params.hostname}:${params.port}${params.path}", make sure browser driver is running on that address.` +
            '\nIf you use services like chromedriver see initialiseServices logs above or in wdio.log file as the service might had problems to start the driver.';
    }
    if (err.message === 'unhandled request') {
        return 'The browser driver couldn\'t start the session. Make sure you have set the "path" correctly!';
    }
    if (!err.message) {
        return 'See wdio.* logs for more information.';
    }
    // wrong path: selenium-standalone
    if (err.message.includes('Whoops! The URL specified routes to this help page.')) {
        return "It seems you are running a Selenium Standalone server and point to a wrong path. Please set `path: '/wd/hub'` in your wdio.conf.js!";
    }
    // wrong path: chromedriver, geckodriver, etc
    if (BROWSER_DRIVER_ERRORS.some(m => err && err.message && err.message.includes(m))) {
        return "Make sure to set `path: '/'` in your wdio.conf.js!";
    }
    // edge driver on localhost
    if (err.message.includes('Bad Request - Invalid Hostname') && err.message.includes('HTTP Error 400')) {
        return "Run edge driver on 127.0.0.1 instead of localhost, ex: --host=127.0.0.1, or set `hostname: 'localhost'` in your wdio.conf.js";
    }
    const w3cCapMessage = '\nMake sure to add vendor prefix like "goog:", "appium:", "moz:", etc to non W3C capabilities.' +
        '\nSee more https://www.w3.org/TR/webdriver/#capabilities';
    // Illegal w3c capability passed to selenium standalone
    if (err.message.includes('Illegal key values seen in w3c capabilities')) {
        return err.message + w3cCapMessage;
    }
    // wrong host/port, port in use, illegal w3c capability passed to selenium grid
    if (err.message === 'Response has empty body') {
        return 'Make sure to connect to valid hostname:port or the port is not in use.' +
            '\nIf you use a grid server ' + w3cCapMessage;
    }
    if (err.message.includes('failed serving request POST /wd/hub/session: Unauthorized') && ((_a = params.hostname) === null || _a === void 0 ? void 0 : _a.endsWith('saucelabs.com'))) {
        return 'Session request was not authorized because you either did provide a wrong access key or tried to run ' +
            'in a region that has not been enabled for your user. If have registered a free trial account it is connected ' +
            'to a specific region. Ensure this region is set in your configuration (https://webdriver.io/docs/options.html#region).';
    }
    return err.message;
};
exports.getSessionError = getSessionError;
/**
 * return timeout error with information about the executing command on which the test hangs
 */
const getTimeoutError = (error, requestOptions) => {
    const cmdName = getExecCmdName(requestOptions);
    const cmdArgs = getExecCmdArgs(requestOptions);
    const cmdInfoMsg = `when running "${cmdName}" with method "${requestOptions.method}"`;
    const cmdArgsMsg = cmdArgs ? ` and args ${cmdArgs}` : '';
    const timeoutErr = new Error(`${error.message} ${cmdInfoMsg}${cmdArgsMsg}`);
    return Object.assign(timeoutErr, error);
};
exports.getTimeoutError = getTimeoutError;
function getExecCmdName(requestOptions) {
    const { href } = requestOptions.url;
    const res = href.match(constants_1.REG_EXPS.commandName) || [];
    return res[1] || href;
}
function getExecCmdArgs(requestOptions) {
    const { json: cmdJson } = requestOptions;
    if (typeof cmdJson !== 'object') {
        return '';
    }
    const transformedRes = (0, utils_1.transformCommandLogResult)(cmdJson);
    if (typeof transformedRes === 'string') {
        return transformedRes;
    }
    if (typeof cmdJson.script === 'string') {
        const scriptRes = cmdJson.script.match(constants_1.REG_EXPS.execFn) || [];
        return `"${scriptRes[1] || cmdJson.script}"`;
    }
    return Object.keys(cmdJson).length ? `"${JSON.stringify(cmdJson)}"` : '';
}
