UNPKG

nightwatch

Version:

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

784 lines (608 loc) 19.5 kB
const EventEmitter = require('events'); const {Key, Capabilities, Browser} = require('selenium-webdriver'); const lodashMerge = require('lodash/merge'); const HttpRequest = require('../http/request.js'); const Utils = require('../utils'); const Settings = require('../settings/settings.js'); const CommandQueue = require('./queue.js'); const Transport = require('../transport'); const Element = require('../element'); const ApiLoader = require('../api'); const ElementGlobal = require('../api/_loaders/element-global.js'); const Factory = require('../transport/factory.js'); const {isAndroid, isIos} = require('../utils/mobile'); const namespacedApi = require('../core/namespaced-api.js'); const cdp = require('../transport/selenium-webdriver/cdp.js'); const {LocateStrategy, Locator} = Element; const {Logger, isUndefined, isDefined, isObject, isFunction, isSafari, isChrome} = Utils; class NightwatchAPI { get WEBDRIVER_ELEMENT_ID() { return Transport.WEB_ELEMENT_ID; } get browserName() { if (this.capabilities && this.capabilities.browserName) { return this.capabilities.browserName; } if (this.desiredCapabilities instanceof Capabilities) { return this.desiredCapabilities.getBrowserName(); } return this.desiredCapabilities.browserName; } get platformName() { if (this.capabilities && this.capabilities.platformName) { return this.capabilities.platformName; } if (this.desiredCapabilities instanceof Capabilities) { return this.desiredCapabilities.getPlatform(); } return this.desiredCapabilities.platformName; } toString() { return 'Nightwatch API'; } constructor(sessionId, settings) { // session returned capabilities this.capabilities = {}; this.currentTest = null; // requested capabilities this.desiredCapabilities = settings.capabilities instanceof Capabilities ? settings.capabilities : settings.desiredCapabilities; this.sessionId = sessionId; this.options = settings; this.globals = settings.globals; } __isBrowserName(browser, alternateName) { const lowerCaseBrowserName = this.browserName && this.browserName.toLowerCase(); const browserNames = [this.browserName, lowerCaseBrowserName]; if (alternateName) { alternateName = Array.isArray(alternateName) ? alternateName : [alternateName]; return [browser, ...alternateName].some(name => browserNames.includes(name) ); } return browserNames.includes(browser); } __isPlatformName(platform) { if (typeof this.platformName === 'undefined') { return false; } return this.platformName.toLowerCase() === platform.toLowerCase(); } isIOS() { return isIos(this.desiredCapabilities); } isAndroid() { return isAndroid(this.desiredCapabilities); } isMobile() { return this.isIOS() || this.isAndroid(); } isSafari() { return isSafari(this.desiredCapabilities); } isChrome() { return isChrome(this.desiredCapabilities); } isFirefox() { return this.__isBrowserName(Browser.FIREFOX); } isEdge() { return this.__isBrowserName(Browser.EDGE, ['edge', 'msedge']); } isInternetExplorer() { return this.__isBrowserName(Browser.INTERNET_EXPLORER); } isOpera() { return this.capabilities.browserName === Browser.OPERA; } isAppiumClient() { if (this.options.selenium && this.options.selenium.use_appium) { return true; } // Handle BrowserStack case // (BrowserStack always returns platformName in capabilities) const isMobile = this.__isPlatformName('android') || this.__isPlatformName('ios'); if (Factory.usingBrowserstack(this.options) && isMobile) { return true; } return false; } } class NightwatchClient extends EventEmitter { static create(settings, argv) { const client = new NightwatchClient(settings, argv); if (!client.settings.disable_global_apis) { Object.defineProperty(global, 'browser', { configurable: true, get: function() { if (client) { return client.api; } return null; } }); } // clear namespaced api for (const namespace of Object.getOwnPropertyNames(namespacedApi)) { if (namespacedApi[namespace].__isProxy) { namespacedApi[namespace] = {}; } else if (Utils.isObject(namespacedApi[namespace])) { for (const key of Object.getOwnPropertyNames(namespacedApi[namespace])) { delete namespacedApi[namespace][key]; } } } return client; } constructor(userSettings = {}, argv = {}) { super(); this.setMaxListeners(0); this.settings = Settings.fromClient(userSettings, argv); Logger.setOptions(this.settings); this.isES6AsyncTestcase = false; this.isES6AsyncCommand = false; // backwards compatibility this.options = this.settings; this.__sessionId = null; this.__argv = argv; this.__locateStrategy = null; this.__transport = null; this.createCommandQueue(); this.__elementLocator = new Locator(this); this.__reporter = { logFailedAssertion(err) { }, registerTestError(err) { Logger.error(err); } }; this.__overridableCommands = new Set(); this.__api = new NightwatchAPI(this.sessionId, this.settings); this.__api.createElement = (locator, options = {}) => { return ElementGlobal.element({locator, client: this, options}); }; this .setLaunchUrl() .setScreenshotOptions() .setConfigLocateStrategy() .setLocateStrategy() .setSessionOptions() .createApis(); } get overridableCommands() { return this.__overridableCommands; } get api() { return this.__api; } get argv() { return this.__argv; } get queue() { return this.__commandQueue; } get locateStrategy() { return this.__locateStrategy; } get configLocateStrategy() { return this.__configLocateStrategy; } get sessionId() { return this.getSessionId(); } get usingCucumber() { if (!this.settings.test_runner) { return false; } return this.settings.test_runner.type === 'cucumber'; } getSessionId() { return this.__sessionId; } set sessionId(val) { this.__sessionId = val; } get transport() { return this.__transport; } get transportActions() { const actions = this.transport.Actions; const api = this.api; return new Proxy(actions, { get(target, name) { return function (...args) { let callback; let method; let sessionId = api.sessionId; const lastArg = args[args.length - 1]; const isLastArgFunction = Utils.isFunction(lastArg); if (isLastArgFunction) { callback = args.pop(); } else if (args.length === 0 || !isLastArgFunction) { callback = function(result) {return result}; } const definition = { args }; if (Array.isArray(args[0]) && Utils.isString(lastArg)) { sessionId = lastArg; definition.args = args[0]; } if (Utils.isString(name)) { if (name in target.session) { // actions that require the current session method = target.session[name]; definition.sessionId = api.sessionId || sessionId; } else { method = target[name]; } //return method(definition).then((result) => Utils.makePromise(callback, api, [result])); return method(definition) .then(async (result) => { if (result && result.error && ((result.error instanceof TypeError) || (result.error instanceof SyntaxError))) { throw result.error; } const newResult = await Utils.makePromise(callback, api, [result]); if (Utils.isUndefined(newResult)) { return result; } return newResult; }) .catch(err => { Logger.error(err); callback(err); }); } if (typeof name == 'symbol') { // this is in case of console.log(this.transportActions) const util = require('util'); const result = Object.keys(target).reduce((prev, key) => { if (key === 'session') { return prev; } prev[key] = target[key]; return prev; }, {}); Object.assign(result, target.session); const sorted = Object.keys(result).sort().reduce((prev, key) => { prev[key] = result[key]; return prev; }, {}); return util.inspect(sorted); } return Promise.resolve(); }; } }); } get elementLocator() { return this.__elementLocator; } get httpOpts() { return this.__httpOpts; } get startSessionEnabled() { return this.settings.start_session; } get unitTestingMode() { return this.settings.unit_tests_mode; } screenshotsEnabled() { return isObject(this.settings.screenshots) ? (this.settings.screenshots && this.options.screenshots.enabled === true) : false; } get reporter() { return this.__reporter || {}; } get client() { const {settings, api, locateStrategy, reporter, sessionId, elementLocator} = this; return { options: settings, settings, api, locateStrategy, reporter, sessionId, elementLocator }; } createApis() { this.setApiProperty('page', {}); this.setApiProperty('assert', {}); this.setApiProperty('verify', {}); if (!this.unitTestingMode) { this.setApiProperty('ensure', {}); this.setApiProperty('chrome', {}); this.setApiProperty('firefox', {}); } Object.defineProperty(this.__api, 'driver', { configurable: true, enumerable: true, get: function() { return this.transport && this.transport.driver; }.bind(this) }); this.setApiMethod('actions', (opts) => { return this.transport.driver.actions(opts); }); } ////////////////////////////////////////////////////////////////////////////////////////// // Setters ////////////////////////////////////////////////////////////////////////////////////////// setApiProperty(key, value) { if (Utils.isFunction(value)) { Object.defineProperty(this.__api, key, { get: value }); } else { this.__api[key] = value; } return this; } setApiOption(key, value) { this.__api.options[key] = value; return this; } /** * * @param key * @param args * @return {NightwatchClient} */ setApiMethod(key, ...args) { let fn; let context = this.__api; if (args.length === 1) { fn = args[0]; } else if (args.length === 2) { const namespace = typeof args[0] == 'string' ? context[args[0]] : args[0]; if (namespace) { context = namespace; } fn = args[1]; } if (!fn) { throw new Error('Method must be a declared.'); } context[key] = fn; return this; } setNamespacedApiMethod(key, ...args) { if (args.length < 1 || args.length > 2) { throw new Error('Invalid number of arguments passed.'); } const fn = args.pop(); if (!fn) { throw new Error('Method must be a declared.'); } const context = (typeof args[0] === 'string' ? namespacedApi[args[0]] : args[0]) || namespacedApi; context[key] = fn; return this; } /** * * @param {string} key * @param {string|object} namespace * @return {boolean} */ isApiMethodDefined(key, namespace) { let api = this.__api; if (namespace) { api = typeof namespace == 'string' ? api[namespace] : namespace; if (api === undefined) { return false; } } return api[key] !== undefined; } setReporter(reporter) { this.__reporter = reporter; return this; } ////////////////////////////////////////////////////////////////////////////////////////// // Options ////////////////////////////////////////////////////////////////////////////////////////// /** * @deprecated */ endSessionOnFail(val) { if (arguments.length === 0) { return this.settings.end_session_on_fail; } this.settings.end_session_on_fail = val; } setLaunchUrl() { let value = this.settings.baseUrl || this.settings.launchUrl || this.settings.launch_url || null; // For e2e and component testing on android emulator if (value && !this.settings.desiredCapabilities.real_mobile && this.settings.desiredCapabilities.avd) { value = value.replace('localhost', '10.0.2.2').replace('127.0.0.1', '10.0.2.2'); } this .setApiProperty('baseUrl', value) .setApiProperty('launchUrl', value) .setApiProperty('launch_url', value); return this; } setScreenshotOptions() { const {screenshots} = this.settings; if (this.screenshotsEnabled()) { this.setApiProperty('screenshotsPath', screenshots.path) .setApiOption('screenshotsPath', screenshots.path); } return this; } setConfigLocateStrategy() { this.__configLocateStrategy = this.settings.use_xpath ? LocateStrategy.XPATH : LocateStrategy.CSS_SELECTOR; return this; } setLocateStrategy(strategy = null) { if (strategy && LocateStrategy.isValid(strategy)) { this.__locateStrategy = strategy; return this; } this.__locateStrategy = this.configLocateStrategy; return this; } get initialCapabilities() { if (isObject(this.settings.capabilities) && Object.keys(this.settings.capabilities).length > 0) { return this.settings.capabilities; } return this.settings.desiredCapabilities; } setInitialCapabilities(value) { if (isObject(this.settings.capabilities) && Object.keys(this.settings.capabilities).length > 0) { this.settings.capabilities = value; } else { this.settings.desiredCapabilities = value; } } setSessionOptions() { this.setApiOption('desiredCapabilities', this.initialCapabilities); return this; } mergeCapabilities(props = {}) { if (isFunction(props)) { this.setInitialCapabilities(props); return; } lodashMerge(this.initialCapabilities, props); this.setSessionOptions(); } setHttpOptions() { this.settings.webdriver = this.settings.webdriver || {}; if (isUndefined(this.settings.webdriver.port)) { this.settings.webdriver.port = this.transport.defaultPort; } if (isUndefined(this.settings.webdriver.default_path_prefix)) { this.settings.webdriver.default_path_prefix = this.transport.defaultPathPrefix; } if (this.settings.testWorkersEnabled && this.settings.webdriver.start_process) { // when running in parallel with test workers, the port needs to be randomly assigned this.settings.webdriver.port = undefined; } const { port, host, timeout_options = {}, ssl = false, keep_alive, proxy, default_path_prefix, username, access_key, internal_server_error_retry_interval } = this.settings.webdriver; if (port) { this.httpOpts.setPort(port); } if (host) { this.httpOpts.setHost(host); } this.httpOpts.useSSL(ssl); this.httpOpts.setKeepAlive(keep_alive); if (isDefined(proxy)) { this.httpOpts.setProxy(proxy); } if (isDefined(timeout_options.timeout)) { this.httpOpts.setTimeout(timeout_options.timeout); } if (isDefined(timeout_options.retry_attempts)) { this.httpOpts.setRetryAttempts(timeout_options.retry_attempts); } if (isDefined(internal_server_error_retry_interval)) { this.httpOpts.setInternalServerRetryIntervel(internal_server_error_retry_interval); } if (isDefined(default_path_prefix)) { this.httpOpts.setDefaultPathPrefix(default_path_prefix); } if (username && isDefined(access_key)) { this .setApiOption('username', username) .setApiOption('accessKey', access_key); this.httpOpts.setCredentials({ username: username, key: access_key }); } HttpRequest.globalSettings = this.httpOpts.settings; return this; } ////////////////////////////////////////////////////////////////////////////////////////// // Initialize the APIs ////////////////////////////////////////////////////////////////////////////////////////// async initialize(loadNightwatchApis = true) { this.loadKeyCodes(); if (loadNightwatchApis) { return this.loadNightwatchApis(); } return this; } setCurrentTest() { this.setApiProperty('currentTest', () => this.reporter.currentTest); return this; } loadKeyCodes() { this.setApiProperty('Keys', Key); return this; } async loadNightwatchApis() { await ApiLoader.init(this); const assertApi = Object.assign({}, this.__api.assert); const verifyApi = Object.assign({}, this.__api.verify); if (!this.unitTestingMode) { const ensureApi = Object.assign({}, this.__api.ensure); this.__api.ensure = ApiLoader.makeAssertProxy(ensureApi); } this.__api.assert = ApiLoader.makeAssertProxy(assertApi); this.__api.verify = ApiLoader.makeAssertProxy(verifyApi); // Add proxies for namespaced API namespacedApi.assert = ApiLoader.makeAssertProxy(Object.assign({}, namespacedApi.assert)); namespacedApi.verify = ApiLoader.makeAssertProxy(Object.assign({}, namespacedApi.verify)); return this; } createTransport() { const HttpOptions = require('../http/options.js'); this.__httpOpts = HttpOptions.global; this.__transport = Transport.create(this); this.setHttpOptions(); return this; } createCommandQueue() { const {type: runner} = this.settings.test_runner; this.__commandQueue = new CommandQueue({ compatMode: this.settings.backwards_compatibility_mode, foreignRunner: runner !== 'default', mochaRunner: runner === 'mocha', cucumberRunner: runner === 'cucumber' }); } ////////////////////////////////////////////////////////////////////////////////////////// // Session ////////////////////////////////////////////////////////////////////////////////////////// /** * @return {Promise} */ createSession({argv, moduleKey = '', reuseBrowser = false} = {}) { if (!this.startSessionEnabled) { return Promise.resolve(); } return this.transport.createSession({argv, moduleKey, reuseBrowser}) .then(data => { this.sessionId = data.sessionId; this.setApiProperty('sessionId', data.sessionId); this.setApiProperty('capabilities', data.capabilities); Logger.info(`Received session with ID: ${data.sessionId}\n`); // Reset cdp connection every time a new webdriver session is created. cdp.resetConnection(); this.emit('nightwatch:session.create', data); return data; }); } /** * @deprecated * @return {Promise} */ startSession() { return this.createSession(); } } module.exports = NightwatchClient;