UNPKG

nightwatch

Version:

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

255 lines (254 loc) 10.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const fs = require('fs'); const path = require('path'); const { until, WebElement, WebElementPromise, Condition } = require('selenium-webdriver'); const { ShadowRoot } = require('selenium-webdriver/lib/webdriver'); const { Logger, isFunction, createPromise } = require('../../utils/'); const { WEB_ELEMENT_ID } = require('../../transport/selenium-webdriver/session.js'); const { ScopedElementLocator } = require('./element-locator.js'); class ScopedWebElement { static get methodAliases() { return { 'find': ['findElement', 'get'], 'findAll': ['findElements', 'getAll'], 'findByText': ['getByText'], 'findByRole': ['getByRole'], 'findByPlaceholderText': ['getByPlaceholderText'], 'findByLabelText': ['getByLabelText'], 'findByAltText': ['getByAltText'], 'findAllByText': ['getAllByText'], 'findAllByRole': ['getAllByRole'], 'findAllByPlaceholderText': ['getAllByPlaceholderText'], 'findAllByAltText': ['getAllByAltText'], 'getRect': ['getSize', 'getLocation'], 'getProperty': ['property'], 'getCssProperty': ['css'] }; } static getMethodNames() { const methodsDir = path.resolve(__dirname, 'commands'); const methodFiles = fs.readdirSync(methodsDir); return methodFiles .map(fileName => path.parse(fileName).name) .flatMap(commandName => { const aliases = ScopedWebElement.methodAliases[commandName]; const commandNames = [{ commandName }]; if (aliases) { commandNames.push(...aliases.map(commandNameAlias => ({ commandName: commandNameAlias, originalCommandName: commandName }))); } return commandNames; }) .filter(({ commandName: fileName }) => !fileName.startsWith('_')); } get driver() { return this.nightwatchInstance.transport.driver; } get reporter() { return this.nightwatchInstance.reporter; } get queue() { return this.nightwatchInstance.queue; } get __nightwatchScopedWebElement__() { return true; } constructor(selector = 'html', parentElement, nightwatchInstance) { this.nightwatchInstance = nightwatchInstance; this.parentScopedElement = parentElement; this.webElement = this.createWebElementPromise({ selector, parentElement }); } async waitUntilElementsLocated(selector) { const { timeout, condition, retryInterval } = ScopedElementLocator.create(selector, this.nightwatchInstance); const webElements = await this.driver.wait(until.elementsLocated(condition), timeout, null, retryInterval); if (webElements.length === 0) { return null; } return webElements[0]; } async locateElements({ parentElement, selector, timeout, retryInterval }) { const createLocateElement = () => { return parentElement && parentElement.webElement ? function (locator) { return new Condition('for at least one element to be located ' + locator, function (driver) { return parentElement.webElement.findElements(locator).then(function (elements) { return elements.length > 0 ? elements : null; }); }); } : until.elementsLocated; }; const locateFn = createLocateElement(); const webElements = await this.driver.wait(locateFn(selector), timeout, null, retryInterval); return webElements; } async findElementUsingRecursion({ parentElement, recursiveElement, timeout, retryInterval }) { const allElements = Array.isArray(recursiveElement.selector) ? recursiveElement.selector.slice() : [ recursiveElement ]; let result = null; while (allElements.length > 0) { const nextElement = allElements.shift(); if (result) { parentElement = { webElement: result }; } const { condition, index } = ScopedElementLocator.create(nextElement, this.nightwatchInstance); result = await this.findElement({ parentElement, selector: condition, index, timeout, retryInterval }); if (!result) { break; } } return result; } async findElement({ parentElement, selector, index, timeout, retryInterval }) { const webElements = await this.locateElements({ parentElement, selector, timeout, retryInterval }); if (webElements.length === 0) { const err = new Error(`Cannot find element with "${selector}" selector in ${timeout} milliseconds.`); this.reporter.registerTestError(err); return; } if (index > webElements.length) { const err = new Error(`The index "${index}" is out of bounds for selector "${selector}".`); this.reporter.registerTestError(err); return; } return webElements[index]; } async findElementAction({ parentElement, condition, index, timeout, retryInterval, abortOnFailure, suppressNotFoundErrors }) { const createAction = () => async ({ args }) => { const parentElement = args[0]; try { if (condition.usingRecursion) { return await this.findElementUsingRecursion({ parentElement, recursiveElement: condition, timeout, retryInterval }); } return await this.findElement({ parentElement, selector: condition, index, timeout, retryInterval }); } catch (error) { if (suppressNotFoundErrors) { return null; } const narrowedError = createNarrowedError({ error, condition, timeout }); Logger.error(narrowedError); if (abortOnFailure) { this.reporter.registerTestError(narrowedError); // TODO: find a way to reject here without unhandled promise rejection // reject(narrowedError); } return null; } }; const node = this.queueAction({ name: 'find', createAction, namespace: 'element', args: [parentElement] }); node.printArgs = function () { if (condition.selector) { return `{ ${condition.selector} }`; } return `{ ${condition} }`; }; return node.deferred.promise; } createWebElementPromise({ selector, parentElement }) { if (!selector) { return null; } // eslint-disable-next-line no-async-promise-executor return new WebElementPromise(this.driver, new Promise(async (resolve, reject) => { if (selector instanceof Promise) { try { selector = await selector; } catch (error) { return reject(error); } } if (!selector) { return resolve(null); } if (selector instanceof WebElement) { return resolve(selector); } if (selector instanceof ShadowRoot) { return resolve(selector); } if (selector[WEB_ELEMENT_ID] && isFunction(selector.getId)) { const webElement = new WebElement(this.driver, selector.getId()); return resolve(webElement); } const { index, timeout, condition, retryInterval, abortOnFailure, suppressNotFoundErrors } = ScopedElementLocator.create(selector, this.nightwatchInstance); if (condition instanceof WebElement || condition instanceof WebElementPromise) { return resolve(condition); } const webElement = await this.findElementAction({ parentElement, condition, index, timeout, retryInterval, abortOnFailure, suppressNotFoundErrors }); resolve(webElement); })); } /** * Keeps in sync operation sequence produced by methods. */ waitFor(promise, { isRoot = false } = {}) { if (isRoot) { this.webElement = promise; } else { this.webElement = new WebElementPromise(this.driver, this.then(async (element) => { await promise; return element; })); } return this; } createRootElementCommand(name, context, args) { const result = context[name](...args, { isRoot: true }); return result; } createNode(commandName, args) { const createAction = (actions, webElement) => function () { if (isFunction(commandName)) { return commandName(webElement, ...args).then((result) => { node.deferred.resolve(result); }); } return actions[commandName](webElement, ...args).then((result) => { // eslint-disable-next-line no-prototype-builtins node.deferred.resolve(result.hasOwnProperty('value') ? result.value : result); }); }; const node = this.queueAction({ name: commandName, createAction }); return node; } runQueuedCommand(commandName, { args = [], namespace, isRoot = false, name } = {}) { const node = this.createNode(commandName, args, namespace); if (name) { node.name = name; } return this.waitFor(node.deferred.promise, { isRoot }); } queueAction({ name, createAction, namespace = 'element()', args = [], rejectPromise = true } = {}) { const opts = { args, context: null, deferred: createPromise(), commandFn: createAction.call(this, this.nightwatchInstance.transportActions, this.webElement), namespace, rejectPromise, commandName: name }; return this.queue.add(opts); } executeMethod(context, methodName, ...args) { const methodPath = path.resolve(__dirname, 'commands', `${methodName}.js`); const methodFunc = require(methodPath); return methodFunc.command.call(context, ...args); } then(onFulfilled, onRejected) { return this.webElement.then(onFulfilled, onRejected); } } function createNarrowedError({ error, condition, timeout }) { return error.name === 'TimeoutError' ? new Error(`Timed out while waiting for element "${condition}" to be present for ${timeout} milliseconds.`) : error; } module.exports = ScopedWebElement;