"use strict"; /** * Steps for web applications. * * @mixin BrowserSteps */ var fs = require("fs"); var path = require("path"); const util = require("util"); var _ = require("lodash"); var fp = require("lodash/fp"); const getPort = require("get-port"); var expect = require("chai").expect; var selenium = require("selenium-standalone"); var wdio = require("webdriverio"); const uuid = require("uuid/v4"); var U = require("glace-utils"); var LOG = U.logger; var CONF = require("../config"); const utils = require("../utils"); var defChromeOpts; var BrowserSteps = { /* imports */ __wdio: wdio, installSeleniumDrivers: async function (opts) { /** * Step to install selenium drivers. * * @async * @memberOf BrowserSteps * @method installSeleniumDrivers * @instance * @arg {object} [opts] - [selenium-standalone](https://github.com/vvo/selenium-standalone) options. * @return {Promise<void>} * @throws {Error} If there was error during drivers installation. */ opts = U.defVal(opts, {}); opts.logger = LOG.debug.bind(LOG); opts = fp.merge(CONF.web.seleniumOpts, opts); const driversInfo = fp.merge({ [CONF.web.browser]: {} }, opts.drivers); if (this._checkInstalledDrivers(driversInfo)) { LOG.info("Selenium drivers are installed already"); return; } allure.step("Install selenium drivers"); LOG.info("Installing selenium drivers..."); await new Promise((resolve, reject) => { selenium.install(opts, err => { if (err) return reject(err); resolve(); }); }); LOG.info("Selenium drivers are installed"); allure.pass(); }, startSeleniumServer: async function (opts) { /** * Step to start selenium server. Step recall will be skipped if * selenium server wasn't stopped before. * * @async * @memberOf BrowserSteps * @method startSeleniumServer * @arg {object} [opts] - [selenium-standalone](https://github.com/vvo/selenium-standalone) options. * @instance * @return {Promise<boolean>} `true` if step is executed, `false` if skipped. * @throws {Error} If there was error on selenium start. */ if (this._seleniumProc) { LOG.warn("Step to start selenium server was passed already"); return false; }; opts = U.defVal(opts, {}); opts = fp.merge(CONF.web.seleniumOpts, opts); if (!CONF.webdriver.port) CONF.webdriver.port = await getPort(); opts.seleniumArgs = ["-port", CONF.webdriver.port]; allure.step(`Start selenium server on port ${CONF.webdriver.port}`); LOG.info(`Starting selenium server on port ${CONF.webdriver.port}...`); this._seleniumProc = await new Promise((resolve, reject) => { selenium.start(opts, (err, child) => { if (err) return reject(err); resolve(child); }); }); LOG.info(`Selenium server is started with PID ${this._seleniumProc.pid}`); allure.pass(); return true; }, stopSeleniumServer: async function () { /** * Step to stop selenium server. Step call will be skipped if selenium * server wasn't started before. * * @async * @memberOf BrowserSteps * @method stopSeleniumServer * @instance * @return {Promise<boolean>} `true` if step is executed, `false` if skipped. * @throws {Error} If there was error on selenium stop. */ if (!this._seleniumProc) { LOG.warn("Step to start selenium server wasn't passed yet"); return false; }; allure.step("Stop selenium server"); LOG.info("Stopping selenium server..."); await new Promise((resolve, reject) => { this._seleniumProc.on("exit", (code, signal) => { LOG.debug(`Selenium server was stopped with code ${code} and signal ${signal}`); delete this._seleniumProc; resolve(); }); this._seleniumProc.on("error", reject); var result = this._seleniumProc.kill("SIGINT"); if (!result) reject("Oops! Can't kill selenium server"); }); LOG.info("Selenium server is stopped"); allure.pass(); return true; }, getLaunchedBrowsers: function (opts={}) { /** * Step to get launched browsers. * * @async * @memberOf BrowserSteps * @method getLaunchedBrowsers * @instance * @arg {object} [opts] - Step options. * @arg {boolean} [opts.check=true] - Flag to check that there are launched * browsers. * @return {object} - Dictionary: `key` - browser uniq name, * `val` - webdriver instance. * @throws {Error} If no one browser is launched. */ opts.check = U.defVal(opts.check, true); allure.step("Get launched browsers"); LOG.info("Getting launched browsers..."); const launchedBrowsers = this._webdrivers().all(); if (opts.check) { expect(launchedBrowsers, "No one browser is launched").to.not.be.empty; } LOG.info("Got launched browsers"); allure.pass(); return launchedBrowsers; }, getCurrentBrowser: function (opts={}) { /** * Step to get current browser (webdriver instance). * * @async * @memberOf BrowserSteps * @method getCurrentBrowser * @instance * @arg {object} [opts] - Step options. * @arg {boolean} [opts.check=true] - Flag to check that there is launched * browser. * @return {object} - Dictionary: `key` - browser uniq name, * `val` - webdriver instance. * @throws {Error} If no one browser is launched. */ opts.check = U.defVal(opts.check, true); allure.step("Get current browser"); LOG.info("Getting current browser..."); let currBrowser = null; if (this.webdriver) { currBrowser = { [this._webdrivers().getKey(this.webdriver)]: this.webdriver }; } if (opts.check) { expect(currBrowser, "No one browser is launched").to.exist; } LOG.info("Got current browser"); allure.pass(); return currBrowser; }, launchBrowser: async function (opts) { /** * Step to launch browser. * * @async * @memberOf BrowserSteps * @method launchBrowser * @instance * @arg {object} [opts] - Step options. * @arg {string} [opts.name=random] - Browser name. * @arg {object} [opts.webdriver=CONF.webdriver] - Webdriver config. * @arg {boolean} [opts.tryExisting=false] - Try to launch existing browser. * @arg {boolean} [opts.check=true] - Flag to check that browser was * launched. * @arg {string[]} [opts.chromeOpts=[]] - List of chrome options. * @return {Promise<boolean>} `true` if step is executed, `false` if skipped. * @throws {AssertionError} If browser wasn't launched. * * @example * * await $.launchBrowser(); * await $.launchBrowser({ chromeOpts: ["headless", "user-data-dir=/path/to/dir"] }); */ opts = U.defVal(opts, {}); const check = U.defVal(opts.check, true); opts.tryExisting = U.defVal(opts.tryExisting, false); this.webUrl = U.defVal(this.webUrl, CONF.web.url); let name, isNewBrowser = false; if (!opts.tryExisting || !this.webdriver) { const webdriverConf = U.defVal(opts.webdriver, CONF.webdriver); name = U.defVal( opts.name, `${webdriverConf.desiredCapabilities.browserName}-${uuid()}`); expect(this._webdrivers().get(name), `Browser name "${name}" is used already. Choose another.`).to.not.exist; this.webdriver = this.__wdio.remote(webdriverConf); isNewBrowser = true; } else { name = this._webdrivers().getKey(this.webdriver); if (await this.webdriver.session()) { LOG.debug(`Browser "${name}" is launched already`); return false; } } allure.step(`Launch browser "${name}" via selenium`); LOG.info(`Launching browser "${name}" via selenium...`); if (CONF.web.platform === "pc" && this.webdriver.desiredCapabilities.browserName === "chrome") { this._setChromeOpts(opts.chromeOpts); } await this.webdriver.init(); await this._setTimeouts(); if (check) { expect(await this.webdriver.session(), "Browser wasn't launched").to.exist; }; LOG.info("Browser is launched"); if (CONF.web.platform === "pc" && CONF.web.width && CONF.web.height) { await this.setViewport(); }; allure.pass(); if (isNewBrowser) this._webdrivers().push(this.webdriver, name); return true; }, /** * Helper to webdriver timeouts. * @ignore */ _setTimeouts: async function () { var pageLoad = CONF.web.pageTimeout * 1000; try { await this.webdriver.timeouts("page load", pageLoad); } catch (e) { LOG.error(util.format("Can't set webdriver timeouts", e)); await this.webdriver.timeouts({ pageLoad: pageLoad }); } }, /** * Helper to set chrome options. * * It composes chrome options list in next order: * - user provided options * - proxy options if they are not present in list * - default config options if they are not present in list * * @method * @instance * @protected * @arg {array} [chromeOpts=[]] - List of chrome options. */ _setChromeOpts: function (chromeOpts) { var opt; /* store origin chrome options, because then webdriver options will be overridden */ defChromeOpts = U.defVal(defChromeOpts, this.webdriver.desiredCapabilities.chromeOptions.args, []); chromeOpts = _.clone(U.defVal(chromeOpts, [])); if (this.globalProxy && this.globalProxy.isRunning) { var proxyOptions = [ "ignore-certificate-errors", `proxy-server=${U.hostname}:${this.globalProxy.getPort()}`, `proxy-bypass-list=localhost,127.0.0.1,${U.hostname}`, ]; for (opt of proxyOptions) { if (!isOptPresent(opt, chromeOpts)) chromeOpts.push(opt); }; }; for (opt of defChromeOpts) { if (!isOptPresent(opt, chromeOpts)) chromeOpts.push(opt); }; this.webdriver.desiredCapabilities.chromeOptions.args = chromeOpts; }, setViewport: async function (opts) { /** * Step to set browser viewport size. * * @async * @memberOf BrowserSteps * @method setViewport * @instance * @arg {object} [opts] - Step options. * @arg {number} [opts.width] - Browser viewport width. * @arg {number} [opts.height] - Browser viewport height. * @arg {boolean} [opts.check=true] - Flag to check step result. * @return {boolean} `true` when step is executed. * @throws {AssertionError} If width or height values are not a number. * @throws {AssertionError} If viewport size wasn't changed correctly. */ opts = U.defVal(opts, {}); var width = U.defVal(opts.width, CONF.web.width); var height = U.defVal(opts.height, CONF.web.height); var check = U.defVal(opts.check, true); expect(width, "Invalid browser viewport width").to.be.a("number"); expect(height, "Invalid browser viewport height").to.be.a("number"); allure.step(`Set browser viewport to [width=${width}, height=${height}]`); LOG.info(`Setting browser viewport to [width=${width}, height=${height}]...`); await this .webdriver .setViewportSize({ width: width, height: height }); if (check) { var viewport = await this.webdriver.getViewportSize(); expect(viewport.width, "Invalid browser viewport width") .to.be.equal(width); expect(viewport.height, "Invalid browser viewport height") .to.be.equal(height); }; LOG.info("Browser viewport size is set"); allure.pass(); return true; }, closeBrowser: async function (opts) { /** * Step to close browser. * * @async * @memberOf BrowserSteps * @method closeBrowser * @instance * @arg {object} [opts] - Step options. * @arg {object} [opts.switchPrev=true] - Flag to switch browser to last launched. * @arg {boolean} [opts.check=true] - Flag to check that browser was closed. * @return {Promise<boolean>} `true` if step is executed, `false` if skipped. * @throws {AssertionError} If browser wasn't closed. */ if (!this._webdrivers().top()) { LOG.debug("No one browser is launched yet"); return false; }; opts = U.defVal(opts, {}); opts.switchPrev = U.defVal(opts.switchPrev, true); opts.check = U.defVal(opts.check, true); const name = this._webdrivers().getKey(this.webdriver); const webdriver = this.webdriver; if (opts.switchPrev) { expect(this._webdrivers().pop(), "Browsers collection is misordered.").to.be.equal(this.webdriver); this.webdriver = this._webdrivers().top(); } allure.step(`Close browser "${name}"`); LOG.info(`Closing browser "${name}"...`); await webdriver.end(); await this.pause(1, "webdriver process will be stopped"); if (opts.check) { expect(await webdriver.session(), "Browser wasn't closed").to.not.exist; }; LOG.info("Browser is closed"); allure.pass(); return true; }, restartBrowser: async function (opts={}) { /** * Step to restart browser. * * @async * @memberOf BrowserSteps * @method restartBrowser * @instance * @arg {object} [opts={}] - Step options. * @return {Promise<void>} */ opts.switchPrev = false; opts.tryExisting = true; allure.step("Restart browser"); await this.closeBrowser(opts); await this.launchBrowser(opts); allure.pass(); }, switchBrowser: function (name) { /** * Step to switch to browser by name. * * @memberOf BrowserSteps * @method switchBrowser * @instance * @arg {string} * @arg {string} name - Browser uniq name. * @return {object} - Webdriver instance. */ allure.step(`Switch browser to "${name}"`); LOG.info(`Switching browser to "${name}"...`); const webdriver = this._webdrivers().pop(name); this._webdrivers().push(webdriver); this.webdriver = webdriver; LOG.info("Browser is switched"); allure.pass(); return this.webdriver; }, closeAllBrowsers: async function (opts={}) { /** * Step to close all browsers. * * @async * @memberOf BrowserSteps * @method closeAllBrowsers * @instance * @arg {object} [opts] - Step options. * @arg {boolean} [opts.check=true] - Flag to check that browser was closed. * @return {Promise<void>} */ opts.switchPrev = true; let exitStatus = true; while (exitStatus) { exitStatus = await this.closeBrowser(opts); } }, openUrl: async function (webUrl, opts) { /** * Step to open URL in browser. * * @async * @memberOf BrowserSteps * @method openUrl * @instance * @arg {string} webUrl - URL which should be opened in browser. * @arg {object} [opts] - Step options. * @arg {boolean} [opts.check=true] - Flag to check that URL is opened. * @arg {number} [opts.timeout] - Timeout to wait for URL will be opened * in browser, sec. Default value is `CONF.web.pageTimeout`. * @return {Promise<void>} * @throws {Error} If URL wasn't opened after timeout. */ opts = U.defVal(opts, {}); var check = U.defVal(opts.check, true); var timeout = U.defVal(opts.timeout, CONF.web.pageTimeout) * 1000; allure.step(`Open URL "${webUrl}" in browser`); LOG.info(`Openning URL "${webUrl}" in browser...`); await this.webdriver.url(webUrl); if (check) { var errMsg = `Browser didn't navigate to "${webUrl}" ` + `during ${timeout} ms`; await this.webdriver.waitUntil(async () => { var curUrl = await this.webdriver.getUrl(); LOG.debug(`Compare current URL "${curUrl}" with expected "${webUrl}"`); return curUrl.startsWith(webUrl); }, timeout, errMsg); }; LOG.info("URL is opened"); allure.pass(); }, openApp: async function (opts) { /** * Step to open application URL in browser. * * @async * @memberOf BrowserSteps * @method openApp * @instance * @arg {object} [opts] - Step options. * @return {Promise<void>} * @throws {AssertionError} If application URL is not defined. */ allure.step("Open web application"); expect(this.webUrl, "Web URL isn't defined").to.exist; await this.openUrl(this.webUrl, opts); allure.pass(); }, _webdrivers: function () { if (!this.__webdrivers) { this.__webdrivers = new utils.OrderedCollection(); } return this.__webdrivers; }, /** * Helper to check whether selenium drivers are installed or no. * * @ignore */ _checkInstalledDrivers: function (driversInfo) { const driversDir = path.join( path.dirname(require.resolve("selenium-standalone")), ".selenium"); if (!fs.existsSync(driversDir)) return false; const browserNames = Object.keys(driversInfo); const seleniumConf = require("selenium-standalone/lib/default-config"); driversInfo = fp.merge(seleniumConf.drivers, driversInfo); let isDriversInstalled; for (const browserName of browserNames) { const driverName = `${browserName}driver`; const driverDir = path.join(driversDir, driverName); if (!fs.existsSync(driverDir)) return false; const driverVersion = driversInfo[browserName].version; expect(driverVersion).to.exist; isDriversInstalled = false; for (let driverFile of fs.readdirSync(driverDir)) { driverFile = path.parse(driverFile).name.toLowerCase(); if (driverFile.startsWith(driverVersion) && driverFile.includes(driverName)) { isDriversInstalled = true; break; } } } return isDriversInstalled; }, }; module.exports = BrowserSteps; /** * Helper to define whether chrome option is present in options list. * * @ignore * @function */ var isOptPresent = (opt, opts) => { var optStart = opt.split("=")[0]; for (var o of opts) { if (o.split("=")[0] === optStart) return true; }; return false; };