UNPKG

nightwatch

Version:

Easy to use Node.js based end-to-end testing solution for web applications using the W3C WebDriver API.

417 lines (330 loc) 11.1 kB
const ora = require('ora'); const {Builder, Browser, error} = require('selenium-webdriver'); const Actions = require('./actions.js'); const SeleniumCapabilities = require('./options.js'); const {Logger, isObject} = require('../../utils'); const httpClient = require('./httpclient.js'); const Session = require('./session.js'); const BaseTransport = require('../'); const {colors} = Logger; const {isErrorResponse, checkLegacyResponse, checkResponse, WebDriverError} = error; class Transport extends BaseTransport { /** * @param {Builder} builder * @param {Capabilities} options */ static setBuilderOptions({builder, options}) { switch (options.getBrowserName()) { case Browser.CHROME: builder.setChromeOptions(options); break; case Browser.FIREFOX: builder.setFirefoxOptions(options); break; case Browser.SAFARI: builder.setSafariOptions(options); break; case Browser.EDGE: builder.setEdgeOptions(options); break; case Browser.OPERA: // TODO: implement break; case Browser.INTERNET_EXPLORER: builder.setIeOptions(options); break; } } /** * @override */ get ServiceBuilder() { return null; } get defaultPort() { return this.ServiceBuilder ? this.ServiceBuilder.defaultPort : 4444; } get Actions() { return this.actionsInstance.actions; } get reporter() { return this.nightwatchInstance.reporter; } get api() { return this.nightwatchInstance.api; } get settings() { return this.nightwatchInstance.settings; } get desiredCapabilities() { return this.settings.desiredCapabilities; } get defaultPathPrefix() { return ''; } get outputEnabled() { return this.settings.output; } get shouldStartDriverService() { return this.settings.webdriver.start_process; } get serviceName() { return this.ServiceBuilder.serviceName; } get elementKey() { return this.__elementKey || Session.WEB_ELEMENT_ID; } get initialCapabilities() { return this.seleniumCapabilities.initialCapabilities; } constructor(nightwatchInstance, {isSelenium = false, browserName} = {}) { super(nightwatchInstance); this.nightwatchInstance = nightwatchInstance; this.browserName = browserName; this.seleniumCapabilities = new SeleniumCapabilities({ settings: this.settings, browserName }); this.createHttpClient(); this.createActions(); } /** * @override */ setBuilderOptions({options, builder}) { Transport.setBuilderOptions({options, builder}); } createActions() { this.actionsInstance = new Actions(this); this.actionsInstance.loadActions(); } createHttpClient() { const http = require('selenium-webdriver/http'); http.HttpClient = httpClient(this.settings, http.Response); } getServerUrl() { if (this.shouldStartDriverService) { return this.defaultServerUrl; } return this.settings.webdriver.url; } //////////////////////////////////////////////////////////////////// // Session related //////////////////////////////////////////////////////////////////// async closeDriver() { if (this.driverService) { try { await this.driverService.stop(); this.driverService = null; this.stopped = true; } catch (err) { Logger.error(err); err.displayed = true; throw err; } } } async sessionFinished(reason) { this.emit('session:finished', reason); await this.closeDriver(); } async createDriverService({options, moduleKey}) { try { this.driverService = new this.ServiceBuilder(this.settings); moduleKey = this.settings.webdriver.log_file_name || moduleKey || ''; await this.driverService.setOutputFile(moduleKey).init(options); } catch (err) { this.showConnectSpinner(colors.red(`Failed to start ${this.serviceName}.`), 'warn'); throw err; } } /** * @param {Capabilities} options * @returns {Builder} */ createSessionBuilder(options) { const builder = new Builder(); builder.disableEnvironmentOverrides(); this.setBuilderOptions({builder, options}); return builder; } createSessionOptions(argv) { return this.seleniumCapabilities.create(argv); } createDriver({options}) { const builder = this.createSessionBuilder(options); this.builder = builder; return builder.build(); } async createSession({argv, moduleKey}) { const startTime = new Date(); const {host, port, start_process} = this.settings.webdriver; const portStr = port ? `port ${port}` : 'auto-generated port'; const options = this.createSessionOptions(argv); if (start_process) { this.showConnectSpinner(`Starting ${this.serviceName} on ${portStr}...\n`); await this.createDriverService({options, moduleKey}); } else { this.showConnectSpinner(`Connecting to ${host} on ${portStr}...\n`); } try { this.driver = await this.createDriver({options}); } catch (err) { const error = this.handleConnectError(err); this.showConnectSpinner(colors.red(`Failed to connect to ${this.serviceName} on ${host} with ${portStr}.`), 'warn'); throw error; } const session = new Session(this.driver); const sessionExports = await session.exported(); const {sessionInfo, sessionId, capabilities, elementKey} = sessionExports; this.__elementKey = elementKey; await this.showConnectInfo({startTime, host, port, start_process, sessionInfo}); return { sessionId, capabilities }; } //////////////////////////////////////////////////////////////////// // Output related //////////////////////////////////////////////////////////////////// async showConnectInfo({startTime, port, host, start_process, sessionInfo}) { this.showConnectSpinner(`Connected to ${colors.stack_trace(start_process ? this.serviceName : host)} on port ${colors.stack_trace(port)} ${colors.stack_trace('(' + (new Date() - startTime) + 'ms)')}.`); if (this.outputEnabled) { const {platform, browserVersion, platformVersion, browserName} = sessionInfo; // eslint-disable-next-line no-console console.info(` Using: ${colors.light_blue(browserName)} ${colors.brown('(' + (browserVersion) + ')')} on ${colors.cyan(platform.toUpperCase() + (platformVersion ? (' (' + platformVersion + ')') : ''))}.\n`); } } showConnectSpinner(msg, method = 'info') { if (!this.outputEnabled) { return; } if (this.connectSpinner) { this.connectSpinner[method](msg); } else { this.connectSpinner = ora(msg).start(); } } //////////////////////////////////////////////////////////////////// // Elements related //////////////////////////////////////////////////////////////////// getElementId(resultValue) { return resultValue[this.elementKey]; } toElement(resultValue) { return {[this.elementKey]: resultValue}; } mapWebElementIds(value) { if (Array.isArray(value)) { return value.reduce((prev, item) => { prev.push(this.getElementId(item)); return prev; }, []); } return value; } /** * Helper method * * @param {String} protocolAction * @param {Object} executeArgs * @return {Promise} */ executeProtocolAction(protocolAction, executeArgs) { if (isObject(protocolAction) && protocolAction.actionName) { const {actionName, args, sessionId = this.nightwatchInstance.sessionId} = protocolAction; return this.Actions.session[actionName]({ args, sessionId, sessionRequired: true }); } return this.Actions.session[protocolAction]({ args: executeArgs, sessionId: this.nightwatchInstance.sessionId, sessionRequired: true }); } //////////////////////////////////////////////////////////////////// // Error handling //////////////////////////////////////////////////////////////////// handleErrorResponse(result) { if (isErrorResponse(result)) { // will throw error if w3c response checkResponse(result); // will throw error if legacy response checkLegacyResponse(result); } } registerLastError(err, retryCount = 0) { this.lastError = err; this.retriesCount = retryCount; } getErrorMessage(result) { if (result instanceof Error) { return result.message; } return result.value && result.value.message; } handleConnectError(err) { err.reportShown = true; const errMsg = `An error occurred while creating a new ${this.serviceName} session:`; switch (err.code) { case 'ECONNREFUSED': err.sessionCreate = true; err.message = `${errMsg} ${err.message.replace('ECONNREFUSED connect ECONNREFUSED', 'Connection refused to')}. If the Webdriver/Selenium service is managed by Nightwatch, check if "start_process" is set to "true".`; break; default: err.message = `${errMsg} [${err.name}] ${err.message}`; } err.showTrace = false; if (!err.detailedErr && this.driverService) { const logPath = this.driverService.getOutputFilePath(); err.detailedErr = ` Verify if ${this.serviceName} is configured correctly; using:\n ${this.driverService.getSettingsFormatted()}\n`; err.extraDetail = (logPath ? `\n More info might be available in the log file: ${logPath}`: `\n Set webdriver.log_path in your Nightwatch config to retrieve more logs from ${this.serviceName}.`); } return err; } isResultSuccess(result = {}) { return !( (result instanceof Error) || (result.error instanceof Error) || result.status === -1 ); } getErrorResponse(result) { return result instanceof Error ? result : result.error; } staleElementReference(result) { return result instanceof error.StaleElementReferenceError; } elementClickInterceptedError(result) { return result instanceof error.ElementClickInterceptedError; } invalidElementStateError(result) { return result instanceof error.InvalidElementStateError; } elementNotInteractableError(result) { return result instanceof error.ElementNotInteractableError; } invalidWindowReference(result) { return result instanceof error.NoSuchWindowError; } invalidSessionError(result) { return result instanceof error.NoSuchSessionError; } isRetryableElementError(result) { const errorResponse = this.getErrorResponse(result); if (errorResponse instanceof WebDriverError && errorResponse.name === 'WebDriverError') { const errors = this.getRetryableErrorMessages(); return errors.some(item => errorResponse.message.includes(item)); } return ( this.staleElementReference(errorResponse) || this.elementClickInterceptedError(errorResponse) || this.invalidElementStateError(errorResponse) || this.elementNotInteractableError(errorResponse) ); } } module.exports = Transport;