UNPKG

nightwatch

Version:

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

341 lines (279 loc) 9.29 kB
const {By, RelativeBy, until} = require('selenium-webdriver'); const Element = require('./index.js'); const ElementsByRecursion = require('./locate/elements-by-recursion.js'); const SingleElementByRecursion = require('./locate/single-element-by-recursion.js'); const AVAILABLE_LOCATORS = { 'css selector': 'css', 'id': 'id', 'link text': 'linkText', 'name': 'name', 'partial link text': 'partialLinkText', 'tag name': 'tagName', 'xpath': 'xpath', 'className': 'className' }; class LocateElement { /** * @param {object|string} element * @return {By|RelativeBy} */ static create(element) { if (!element) { throw new Error(`Error while trying to locate element: missing element definition; got: "${element}".`); } const byInstance = LocateElement.locateInstanceOfBy(element); if (byInstance !== null) { return byInstance; } const elementInstance = LocateElement.createElementInstance(element); return By[AVAILABLE_LOCATORS[elementInstance.locateStrategy]](elementInstance.selector); } /** * @param {object} element * @return {By|RelativeBy|null} */ static locateInstanceOfBy(element) { if (element instanceof By) { return element; } if (element.by instanceof By) { return element.by; } if (element.value instanceof RelativeBy) { return element.value; } return null; } /** * @param {object|string} element * @return {Element} */ static createElementInstance(element) { if (typeof element != 'object' && typeof element != 'string') { throw new Error(`Invalid element definition type; expected string or object, but got: ${typeof element}.`); } let selector; let strategy; if (typeof element == 'object' && element.value && element.using) { selector = element.value; strategy = element.using; } else { selector = element; } return Element.createFromSelector(selector, strategy); } get api() { return this.nightwatchInstance.api; } get reporter() { return this.nightwatchInstance.reporter; } get settings() { return this.nightwatchInstance.settings; } get transport() { return this.nightwatchInstance.transport; } get desiredCapabilities() { return this.nightwatchInstance.session.desiredCapabilities; } constructor(nightwatchInstance) { this.nightwatchInstance = nightwatchInstance; } resolveElementRecursively({element}) { return this.findElement({element}).then(response => { const firstElement = element.selector[element.selector.length - 1]; firstElement.resolvedElement = response; const {selector, locateStrategy, name} = firstElement; const {status, value} = response; const WebdriverElementId = response && response.WebdriverElementId || null; const result = { selector, locateStrategy, name, WebdriverElementId, response: { status, value } }; if (!isNaN(firstElement.index)) { result.index = firstElement.index; } return result; }); } /** * Finds a single element by using the multiple locate elements, which instead of throwing an error returns an empty array * * @param {Element} element * @param {String} commandName * @param {String} id * @param {String} transportAction * @param {Boolean} [returnSingleElement] * @param {Boolean} [cacheElementId] * @return {Promise} */ async findElement({element, commandName, id, transportAction = 'locateMultipleElements', returnSingleElement = true, cacheElementId = true}) { if (element && (element.webElement || element.webElementId)) { const elementId = await (element.webElement || element.webElementId).getId(); element.setResolvedElement(elementId); return { value: this.transport.toElement(elementId), status: 0 }; } if (element.resolvedElement && cacheElementId) { return Promise.resolve({ value: this.transport.toElement(element.resolvedElement), status: 0 }); } if (element.usingRecursion) { if (transportAction === 'locateSingleElement') { return this.findSingleElementUsingRecursion(element); } return this.findElementsUsingRecursion({element, returnSingleElement}); } const result = await this.executeProtocolAction({element, commandName, id, transportAction, cacheElementId}); return this.handleLocateElement({result, element, returnSingleElement}); } locateMultipleElements(element) { const commandName = 'locateMultipleElements'; const transportAction = 'locateMultipleElements'; return this.executeProtocolAction({element, commandName, transportAction}); } handleLocateElement({result, element, returnSingleElement}) { const {status = 0, error} = result || {}; let {value} = result; if (error instanceof Error) { return { error, status: -1, value }; } let WebdriverElementId; const elementResult = this.transport.resolveElement(result, element, true); if (elementResult) { WebdriverElementId = this.transport.getElementId(elementResult); element.setResolvedElement(WebdriverElementId); } if (returnSingleElement) { value = elementResult; } return { value, status, WebdriverElementId }; } /** * @returns Promise({{ * value, * status, * result, * now * }}) */ executeProtocolAction(opts = {}) { const {id, element, transportAction, commandName, cacheElementId} = opts; const args = { id }; if (element instanceof By) { args.by = element; } else { args.using = element.locateStrategy; args.value = element.selector; } return this.sendElementsAction({transportAction, args, element, cacheElementId}) .catch(err => { if (this.transport.invalidSessionError(err)) { return new Error(this.transport.getErrorMessage(err)); } throw err; }) .then(result => { if (this.transport.isResultSuccess(result) && Element.requiresFiltering(element)) { return this.filterElements(element, result); } return result; }) // Catch errors from filterElements and from invalid session .catch(error => { return { value: null, status: -1, error }; }); } async sendElementsAction({transportAction, args, element, cacheElementId} = {}) { if (cacheElementId && transportAction === 'locateMultipleElements' && args && !(args.value instanceof RelativeBy)) { const timeout = element.timeout || this.settings.globals.waitForConditionTimeout; const retryInterval = element.retryInterval || this.settings.globals.waitForConditionPollInterval; try { const results = await this.transport.driver.wait(until.elementsLocated(args), timeout, null, retryInterval); const {elementKey} = this.transport; const value = await Promise.all(results.map(async webElement => { const elementId = await webElement.getId(); return {[elementKey]: elementId}; })); return { status: 0, value }; } catch (err) { if (err.name !== 'TimeoutError') { throw err; } throw new NoSuchElementError({element, ms: timeout}); } } return this.transport.executeProtocolAction(transportAction, args); } /** * Selects a subset of elements if the result requires filtering. * * @param {Element} element * @param {object} result * @return {*} */ filterElements(element, result) { let filtered = Element.applyFiltering(element, result.value); if (filtered) { result.value = filtered; return result; } const errorResult = this.transport.getElementNotFoundResult(result); throw new Error(`Element ${element.toString()} not found.${errorResult.message ? (' ' + errorResult.message) : ''}`); } /** * @param {Element} element * @param {Boolean} returnSingleElement * @return {Promise} */ findElementsUsingRecursion({element, returnSingleElement = true}) { let recursion = new ElementsByRecursion(this.nightwatchInstance); return recursion.locateElements({element, returnSingleElement}); } findSingleElementUsingRecursion(element) { let recursion = new SingleElementByRecursion(this.nightwatchInstance); return recursion.locateElement(element.selector); } } class NoSuchElementError extends Error { constructor({element, ms, abortOnFailure, retries}) { super(); this.selector = element.selector; this.abortOnFailure = abortOnFailure; this.name = 'NoSuchElementError'; this.retries = retries; this.strategy = element.locateStrategy; this.message = `Timed out while waiting for element "${this.selector}" with "${this.strategy}" to be present for ${ms} milliseconds.`; } } module.exports = LocateElement; module.exports.NoSuchElementError = NoSuchElementError; module.exports.AVAILABLE_LOCATORS = AVAILABLE_LOCATORS;