From 068a1ba5bce959429291beadcc39f932eae53185 Mon Sep 17 00:00:00 2001 From: Harshit Agrawal Date: Wed, 28 Dec 2022 14:38:18 +0530 Subject: [PATCH] Added some method for better selector strategy --- lib/api/element-commands/findElement.js | 53 ++++++++++++++- .../element-commands/findElementByLabel.js | 44 +++++++++++++ .../findElementByPlaceholderText.js | 43 ++++++++++++ lib/api/element-commands/findElementByText.js | 43 ++++++++++++ lib/api/element-commands/findElements.js | 61 +++++++++++++++++ .../protocol/findElementMultipleORCriteria.js | 66 +++++++++++++++++++ lib/element/command.js | 23 ++++++- lib/utils/index.js | 8 +++ 8 files changed, 335 insertions(+), 6 deletions(-) create mode 100644 lib/api/element-commands/findElementByLabel.js create mode 100644 lib/api/element-commands/findElementByPlaceholderText.js create mode 100644 lib/api/element-commands/findElementByText.js create mode 100644 lib/api/protocol/findElementMultipleORCriteria.js diff --git a/lib/api/element-commands/findElement.js b/lib/api/element-commands/findElement.js index f4121e4575..1092bba988 100644 --- a/lib/api/element-commands/findElement.js +++ b/lib/api/element-commands/findElement.js @@ -25,20 +25,67 @@ module.exports = class FindElement extends FindElements { async elementFound(response) { if (response && response.value) { const elementId = this.transport.getElementId(response.value); - - response.value = Object.assign(response.value, { + const _this = this; + + Object.assign(response.value, { get getId() { return function() { return elementId; }; - } + }, + + findChildElement(...args) { + return _this.createElementChain(this, false, ...args); + }, + + findChildElements(...args) { + const promiseOnElements = _this.createElementChain(this, true, ...args); + + const getElementByIndex = (index) => { + const promiseOnGetElementByIndex = (async () => { + const elements = await promiseOnElements; + return elements[index]; + })(); + + promiseOnGetElementByIndex.findChildElements = this.findChildElements; + promiseOnGetElementByIndex.findChildElement = this.findChildElement; + + return promiseOnGetElementByIndex; + } + + promiseOnElements.getElementByIndex = getElementByIndex; + return promiseOnElements; + }, }); } return response; } + createElementChain(element, multiElements, ...args) { + let promise = Promise.resolve(element); + + promise = promise.then(async parent => { + const parentId = () => parent.WebdriverElementId ? parent.WebdriverElementId : parent.getId(); + + if (multiElements) { + return await this.api.findElements(...args, parentId); + } + + return await this.api.findElement(...args, parentId); + }); + + promise.findChildElement = element.findChildElement; + promise.findChildElements = element.findChildElements; + + return promise; + } + findElementAction() { + if (this.parentId) { + return this.findElement({id: this.parentId, cacheElementId: false, transportAction: 'locateSingleElementByElementId'}); + } + return this.findElement(); } }; diff --git a/lib/api/element-commands/findElementByLabel.js b/lib/api/element-commands/findElementByLabel.js new file mode 100644 index 0000000000..fe50b3f927 --- /dev/null +++ b/lib/api/element-commands/findElementByLabel.js @@ -0,0 +1,44 @@ +/** + * Returns input element related to label . The element will be returned as web element JSON object (with some convenience method). + * + * + * @example + * module.exports = { + * 'demo Test': function(browser) { + * const resultElement = await browser.findElementByLabel('username'); + * + * console.log('element Id:', resultElement.getId()); + * }, + * + * @syntax browser.findElementByLabel(label_text, callback) + * @syntax browser.findElementByLabel(label_text) + * @param {string} label_text The label_text used to locate the input element. Can be a string + * @param {object} {exact: true} Used to exact match of label_text + * @param {function} callback Callback function which is called with the result value. + * @method findElementByLabel + * @since 2.0.0 + * @api protocol.elements + */ + +const LocateStrategy = require('../../element/strategy.js'); +const FindElement = require('./findElement.js'); + +class FindElementByLabel extends FindElement { + get selector() { + if (this.args[1] && this.args[1].exact === true) { + return `//${this.__selector}[text()='${this.args[0]}']//following::input[1]`; + } + + return `//${this.__selector}[contains(text(), '${this.args[0]}')]//following::input[1]`; + } + + setStrategy() { + this.__strategy = LocateStrategy.XPATH; + + return this; + } + + setStrategyFromArgs() {} +} + +module.exports = FindElementByLabel; \ No newline at end of file diff --git a/lib/api/element-commands/findElementByPlaceholderText.js b/lib/api/element-commands/findElementByPlaceholderText.js new file mode 100644 index 0000000000..ef5c18e991 --- /dev/null +++ b/lib/api/element-commands/findElementByPlaceholderText.js @@ -0,0 +1,43 @@ +const LocateStrategy = require('../../element/strategy.js'); +const FindElement = require('./findElement.js'); +/** + * Returns an element's first child. The child element will be returned as web element JSON object (with an added .getId() convenience method). + * + * + * @example + * module.exports = { + * 'demo Test': function(browser) { + * const resultElement = await browser.getFirstElementChild('.features-container'); + * + * console.log('last child element Id:', resultElement.getId()); + * }, + * + * @syntax browser.getFirstElementChild(selector, callback) + * @syntax browser.getFirstElementChild(selector) + * @param {string} [using] The locator strategy to use. See [W3C Webdriver - locator strategies](https://www.w3.org/TR/webdriver/#locator-strategies) + * @param {string|object} selector The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide#element-properties). + * @param {function} callback Callback function which is called with the result value. + * @method findElementByLabel + * @since 2.0.0 + * @moreinfo developer.mozilla.org/en-US/docs/Web/API/Element/firstElementChild + * @api protocol.elements + */ +class FindElementByPlaceholderText extends FindElement { + get selector() { + if (this.args[1] && this.args[1].exact === true) { + return `//input[@placeholder='${this.args[0]}']`; + } + + return `//input[contains(@placeholder, '${this.args[0]}')]`; + } + + setStrategy() { + this.__strategy = LocateStrategy.XPATH; + + return this; + } + + setStrategyFromArgs() {} +} + +module.exports = FindElementByPlaceholderText; \ No newline at end of file diff --git a/lib/api/element-commands/findElementByText.js b/lib/api/element-commands/findElementByText.js new file mode 100644 index 0000000000..8db66e6dc8 --- /dev/null +++ b/lib/api/element-commands/findElementByText.js @@ -0,0 +1,43 @@ +const LocateStrategy = require('../../element/strategy.js'); +const FindElement = require('./findElement.js'); +/** + * Returns an element's first child. The child element will be returned as web element JSON object (with an added .getId() convenience method). + * + * + * @example + * module.exports = { + * 'demo Test': function(browser) { + * const resultElement = await browser.getFirstElementChild('.features-container'); + * + * console.log('last child element Id:', resultElement.getId()); + * }, + * + * @syntax browser.getFirstElementChild(selector, callback) + * @syntax browser.getFirstElementChild(selector) + * @param {string} [using] The locator strategy to use. See [W3C Webdriver - locator strategies](https://www.w3.org/TR/webdriver/#locator-strategies) + * @param {string|object} selector The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide#element-properties). + * @param {function} callback Callback function which is called with the result value. + * @method getFirstElementChild + * @since 2.0.0 + * @moreinfo developer.mozilla.org/en-US/docs/Web/API/Element/firstElementChild + * @api protocol.elements + */ +class FindElementByText extends FindElement { + get selector() { + if (this.args[1] && this.args[1].exact === true) { + return `//${this.__selector}[text()='${this.args[0]}']`; + } + + return `//${this.__selector}[contains(text(), '${this.args[0]}')]`; + } + + setStrategy() { + this.__strategy = LocateStrategy.XPATH; + + return this; + } + + setStrategyFromArgs() {} +} + +module.exports = FindElementByText; \ No newline at end of file diff --git a/lib/api/element-commands/findElements.js b/lib/api/element-commands/findElements.js index fe19241e4d..f953cd7646 100644 --- a/lib/api/element-commands/findElements.js +++ b/lib/api/element-commands/findElements.js @@ -1,3 +1,4 @@ +const { WebElement } = require('selenium-webdriver'); const BaseElementCommand = require('./_baseElementCommand.js'); /** @@ -36,12 +37,72 @@ module.exports = class Elements extends BaseElementCommand { } }); }); + + const driver = this.transport.driver; + + Object.assign(response.value, { + getElementByIndex(index) { + return response.value[index]; + }, + + filterVisibleElements() { + const promises = response.value.map(async element => { + const webElement = new WebElement(driver, element.getId()); + + if (await webElement.isDisplayed()) { + return element; + } + }) + + const visibleElements = (async() => (await Promise.all(promises)).filter((element) => { + return element; + }))(); + + return visibleElements; + }, + + filterByText(text) { + const promises = response.value.map(async element => { + const webElement = new WebElement(driver, element.getId()); + + if ((await webElement.getAttribute("textContent")).includes(text)) { + return element; + } + }) + + const elementsHasText = (async() => (await Promise.all(promises)).filter((element) => { + return element; + }))(); + + return elementsHasText; + }, + + filterByCSS(className) { + const promises = response.value.map(async element => { + const webElement = new WebElement(driver, element.getId()); + + if ((await webElement.getAttribute("class")).includes(className)) { + return element; + } + }) + + const elementsHasText = (async() => (await Promise.all(promises)).filter((element) => { + return element; + }))(); + + return elementsHasText; + }, + }) } return response; } findElementAction() { + if (this.parentId) { + return this.findElement({id: this.parentId, cacheElementId: false, transportAction: 'locateMultipleElementsByElementId', returnSingleElement: false}); + } + return this.findElement({returnSingleElement: false}); } diff --git a/lib/api/protocol/findElementMultipleORCriteria.js b/lib/api/protocol/findElementMultipleORCriteria.js new file mode 100644 index 0000000000..1135cd5f9e --- /dev/null +++ b/lib/api/protocol/findElementMultipleORCriteria.js @@ -0,0 +1,66 @@ +/** + * Search for an elements on the page, starting from the document root. The located element will be returned as web element JSON object (with an added .getId() convenience method). + * First argument is the element selector, either specified as a string or as an object (with 'selector' and 'locateStrategy' properties). + * + * @example + * module.exports = { + * 'demo Test': function(browser) { + * const resultElement = await browser.findElement('.features-container li:first-child'); + * + * console.log('Element Id:', resultElement.getId()); + * }, + * + * + * @link /#find-element + * @syntax browser.findElement(selector, callback) + * @syntax await browser.findElement(selector); + * @param {string} selector The search target. + * @param {function} [callback] Callback function to be invoked with the result when the command finishes. + * @since 1.7.0 + * @api protocol.elements + */ +const Utils = require('../../utils/index.js'); +const ProtocolAction = require('./_base-action.js'); + + +module.exports = class FindElementMultipleORCriteria extends ProtocolAction { + async command(...args) { + let cssSelectors , xpathSelector, callback = function() {}; + + args.forEach((selector) => { + if (Utils.isString(selector)) { + if (Utils.isValidXpath(selector)){ + xpathSelector = xpathSelector ? xpathSelector + " | " + selector : selector; + } else { + cssSelectors = cssSelectors ? cssSelectors + ", " + selector : selector; + } + } + }); + + if (Utils.isFunction(args.at(-1))) { + callback = args.at(-1); + } + + let result = []; + + if (cssSelectors) { + const cssRes = await this.api.findElements(cssSelectors, callback); + + if (!cssRes.error) { + result = result.concat(cssRes); + result = Object.assign(cssRes, result); + } + } + + if (xpathSelector) { + const xpathRes = await this.api.findElements('xpath', xpathSelector, callback) + + if (!xpathRes.error) { + result = result.concat(xpathRes); + result = Object.assign(xpathRes, result); + } + } + + return result; + } +}; diff --git a/lib/element/command.js b/lib/element/command.js index 8fda88135b..ca0856b2c9 100644 --- a/lib/element/command.js +++ b/lib/element/command.js @@ -5,6 +5,7 @@ const Utils = require('../utils'); const LocateStrategy = require('./strategy.js'); const Element = require('./index.js'); const {NoSuchElementError} = require('./locator.js'); +const { Strategies } = require('./strategy.js'); const {Logger, PeriodicPromise, isDefined} = Utils; class ElementCommand extends EventEmitter { @@ -100,6 +101,10 @@ class ElementCommand extends EventEmitter { return this.__suppressNotFoundErrors; } + get parentId() { + return this.__parentId; + } + set suppressNotFoundErrors(value) { this.__suppressNotFoundErrors = Utils.convertBoolean(value); } @@ -200,7 +205,7 @@ class ElementCommand extends EventEmitter { return this.complete(err, {}); } - async findElement({returnSingleElement = true, cacheElementId = true} = {}) { + async findElement({returnSingleElement = true, cacheElementId = true, id, transportAction = 'locateMultipleElements'} = {}) { const {element, commandName, WebdriverElementId} = this; try { @@ -212,7 +217,7 @@ class ElementCommand extends EventEmitter { }; } - return await this.elementLocator.findElement({element, commandName, returnSingleElement, cacheElementId}); + return await this.elementLocator.findElement({element, commandName, returnSingleElement, cacheElementId, id, transportAction}); } catch (err) { if (err.name === 'ReferenceError' || err.name === 'TypeError') { throw err; @@ -297,7 +302,15 @@ class ElementCommand extends EventEmitter { setArguments(requireValidation) { this.validateArgsCount(requireValidation); - this.__selector = this.args.shift(); + if (['findElementByText', 'findElementByLabel', 'findElementByPlaceholderText'].includes(this.commandName)) { + this.__selector = '*' + } else { + this.__selector = this.args.shift(); + } + + if(this.args[0] && this.args[0].name == 'parentId') { + this.__parentId = this.args.shift()(); + } this.setOutputMessage(); this.setCallback(); @@ -345,6 +358,10 @@ class ElementCommand extends EventEmitter { } } + if (Utils.isValidXpath(this.selector)) { + this.setStrategy(Strategies.XPATH); + } + return this; } diff --git a/lib/utils/index.js b/lib/utils/index.js index b7a7250e28..f727895154 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -605,6 +605,14 @@ class Utils { throw err; } } + + static isValidXpath(xpathExpression) { + if (this.isString(xpathExpression) && xpathExpression.length > 1 && xpathExpression.charAt(0) === '/') { + return true; + } + + return false; + } } lodashMerge(Utils, {