Path: blob/trunk/javascript/selenium-webdriver/lib/by.js
2884 views
// Licensed to the Software Freedom Conservancy (SFC) under one1// or more contributor license agreements. See the NOTICE file2// distributed with this work for additional information3// regarding copyright ownership. The SFC licenses this file4// to you under the Apache License, Version 2.0 (the5// "License"); you may not use this file except in compliance6// with the License. You may obtain a copy of the License at7//8// http://www.apache.org/licenses/LICENSE-2.09//10// Unless required by applicable law or agreed to in writing,11// software distributed under the License is distributed on an12// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY13// KIND, either express or implied. See the License for the14// specific language governing permissions and limitations15// under the License.1617'use strict'1819/**20* @fileoverview Factory methods for the supported locator strategies.21*/2223/**24* Short-hand expressions for the primary element locator strategies.25* For example the following two statements are equivalent:26*27* var e1 = driver.findElement(By.id('foo'));28* var e2 = driver.findElement({id: 'foo'});29*30* Care should be taken when using JavaScript minifiers (such as the31* Closure compiler), as locator hashes will always be parsed using32* the un-obfuscated properties listed.33*34* @typedef {(35* {className: string}|36* {css: string}|37* {id: string}|38* {js: string}|39* {linkText: string}|40* {name: string}|41* {partialLinkText: string}|42* {tagName: string}|43* {xpath: string})} ByHash44*/4546/**47* Error thrown if an invalid character is encountered while escaping a CSS48* identifier.49* @see https://drafts.csswg.org/cssom/#serialize-an-identifier50*/51class InvalidCharacterError extends Error {52constructor() {53super()54this.name = this.constructor.name55}56}5758/**59* Escapes a CSS string.60* @param {string} css the string to escape.61* @return {string} the escaped string.62* @throws {TypeError} if the input value is not a string.63* @throws {InvalidCharacterError} if the string contains an invalid character.64* @see https://drafts.csswg.org/cssom/#serialize-an-identifier65*/66function escapeCss(css) {67if (typeof css !== 'string') {68throw new TypeError('input must be a string')69}70let ret = ''71const n = css.length72for (let i = 0; i < n; i++) {73const c = css.charCodeAt(i)74if (c == 0x0) {75throw new InvalidCharacterError()76}7778if (79(c >= 0x0001 && c <= 0x001f) ||80c == 0x007f ||81(i == 0 && c >= 0x0030 && c <= 0x0039) ||82(i == 1 && c >= 0x0030 && c <= 0x0039 && css.charCodeAt(0) == 0x002d)83) {84ret += '\\' + c.toString(16) + ' '85continue86}8788if (i == 0 && c == 0x002d && n == 1) {89ret += '\\' + css.charAt(i)90continue91}9293if (94c >= 0x0080 ||95c == 0x002d || // -96c == 0x005f || // _97(c >= 0x0030 && c <= 0x0039) || // [0-9]98(c >= 0x0041 && c <= 0x005a) || // [A-Z]99(c >= 0x0061 && c <= 0x007a)100) {101// [a-z]102ret += css.charAt(i)103continue104}105106ret += '\\' + css.charAt(i)107}108return ret109}110111/**112* Describes a mechanism for locating an element on the page.113* @final114*/115class By {116/**117* @param {string} using the name of the location strategy to use.118* @param {string} value the value to search for.119*/120constructor(using, value) {121/** @type {string} */122this.using = using123124/** @type {string} */125this.value = value126}127128/**129* Locates elements that have a specific class name.130*131* @param {string} name The class name to search for.132* @return {!By} The new locator.133* @see http://www.w3.org/TR/2011/WD-html5-20110525/elements.html#classes134* @see http://www.w3.org/TR/CSS2/selector.html#class-html135*/136static className(name) {137let names = name138.split(/\s+/g)139.filter((s) => s.length > 0)140.map((s) => escapeCss(s))141return By.css('.' + names.join('.'))142}143144/**145* Locates elements using a CSS selector.146*147* @param {string} selector The CSS selector to use.148* @return {!By} The new locator.149* @see http://www.w3.org/TR/CSS2/selector.html150*/151static css(selector) {152return new By('css selector', selector)153}154155/**156* Locates elements by the ID attribute. This locator uses the CSS selector157* `*[id="$ID"]`, _not_ `document.getElementById`.158*159* @param {string} id The ID to search for.160* @return {!By} The new locator.161*/162static id(id) {163return By.css('*[id="' + escapeCss(id) + '"]')164}165166/**167* Locates link elements whose168* {@linkplain webdriver.WebElement#getText visible text} matches the given169* string.170*171* @param {string} text The link text to search for.172* @return {!By} The new locator.173*/174static linkText(text) {175return new By('link text', text)176}177178/**179* Locates elements by evaluating a `script` that defines the body of180* a {@linkplain webdriver.WebDriver#executeScript JavaScript function}.181* The return value of this function must be an element or an array-like182* list of elements. When this locator returns a list of elements, but only183* one is expected, the first element in this list will be used as the184* single element value.185*186* @param {!(string|Function)} script The script to execute.187* @param {...*} var_args The arguments to pass to the script.188* @return {function(!./webdriver.WebDriver): !Promise}189* A new JavaScript-based locator function.190*/191static js(script, ...var_args) {192return function (driver) {193return driver.executeScript.call(driver, script, ...var_args)194}195}196197/**198* Locates elements whose `name` attribute has the given value.199*200* @param {string} name The name attribute to search for.201* @return {!By} The new locator.202*/203static name(name) {204return By.css('*[name="' + escapeCss(name) + '"]')205}206207/**208* Locates link elements whose209* {@linkplain webdriver.WebElement#getText visible text} contains the given210* substring.211*212* @param {string} text The substring to check for in a link's visible text.213* @return {!By} The new locator.214*/215static partialLinkText(text) {216return new By('partial link text', text)217}218219/**220* Locates elements with a given tag name.221*222* @param {string} name The tag name to search for.223* @return {!By} The new locator.224*/225static tagName(name) {226return new By('tag name', name)227}228229/**230* Locates elements matching a XPath selector. Care should be taken when231* using an XPath selector with a {@link webdriver.WebElement} as WebDriver232* will respect the context in the specified in the selector. For example,233* given the selector `//div`, WebDriver will search from the document root234* regardless of whether the locator was used with a WebElement.235*236* @param {string} xpath The XPath selector to use.237* @return {!By} The new locator.238* @see http://www.w3.org/TR/xpath/239*/240static xpath(xpath) {241return new By('xpath', xpath)242}243244/** @override */245toString() {246// The static By.name() overrides this.constructor.name. Shame...247return `By(${this.using}, ${this.value})`248}249250toObject() {251const tmp = {}252tmp[this.using] = this.value253return tmp254}255}256257/**258* Start Searching for relative objects using the value returned from259* `By.tagName()`.260*261* Note: this method will likely be removed in the future please use262* `locateWith`.263* @param {By} tagName The value returned from calling By.tagName()264* @returns265*/266function withTagName(tagName) {267return new RelativeBy({ 'css selector': tagName })268}269270/**271* Start searching for relative objects using search criteria with By.272* @param {string} by A By map that shows how to find the initial element273* @returns {RelativeBy}274*/275function locateWith(by) {276return new RelativeBy(getLocator(by))277}278279function getLocator(locatorOrElement) {280let toFind281if (locatorOrElement instanceof By) {282toFind = locatorOrElement.toObject()283} else {284toFind = locatorOrElement285}286return toFind287}288289/**290* Describes a mechanism for locating an element relative to others291* on the page.292* @final293*/294class RelativeBy {295/**296* @param {By} findDetails297* @param {Array<Object>} filters298*/299constructor(findDetails, filters = null) {300this.root = findDetails301this.filters = filters || []302}303304/**305* Look for elements above the root element passed in306* @param {string|WebElement} locatorOrElement307* @return {!RelativeBy} Return this object308*/309above(locatorOrElement) {310this.filters.push({311kind: 'above',312args: [getLocator(locatorOrElement)],313})314return this315}316317/**318* Look for elements below the root element passed in319* @param {string|WebElement} locatorOrElement320* @return {!RelativeBy} Return this object321*/322below(locatorOrElement) {323this.filters.push({324kind: 'below',325args: [getLocator(locatorOrElement)],326})327return this328}329330/**331* Look for elements left the root element passed in332* @param {string|WebElement} locatorOrElement333* @return {!RelativeBy} Return this object334*/335toLeftOf(locatorOrElement) {336this.filters.push({337kind: 'left',338args: [getLocator(locatorOrElement)],339})340return this341}342343/**344* Look for elements right the root element passed in345* @param {string|WebElement} locatorOrElement346* @return {!RelativeBy} Return this object347*/348toRightOf(locatorOrElement) {349this.filters.push({350kind: 'right',351args: [getLocator(locatorOrElement)],352})353return this354}355356/**357* Look for elements above the root element passed in358* @param {string|WebElement} locatorOrElement359* @return {!RelativeBy} Return this object360*/361straightAbove(locatorOrElement) {362this.filters.push({363kind: 'straightAbove',364args: [getLocator(locatorOrElement)],365})366return this367}368369/**370* Look for elements below the root element passed in371* @param {string|WebElement} locatorOrElement372* @return {!RelativeBy} Return this object373*/374straightBelow(locatorOrElement) {375this.filters.push({376kind: 'straightBelow',377args: [getLocator(locatorOrElement)],378})379return this380}381382/**383* Look for elements left the root element passed in384* @param {string|WebElement} locatorOrElement385* @return {!RelativeBy} Return this object386*/387straightToLeftOf(locatorOrElement) {388this.filters.push({389kind: 'straightLeft',390args: [getLocator(locatorOrElement)],391})392return this393}394395/**396* Look for elements right the root element passed in397* @param {string|WebElement} locatorOrElement398* @return {!RelativeBy} Return this object399*/400straightToRightOf(locatorOrElement) {401this.filters.push({402kind: 'straightRight',403args: [getLocator(locatorOrElement)],404})405return this406}407408/**409* Look for elements near the root element passed in410* @param {string|WebElement} locatorOrElement411* @return {!RelativeBy} Return this object412*/413near(locatorOrElement) {414this.filters.push({415kind: 'near',416args: [getLocator(locatorOrElement)],417})418return this419}420421/**422* Returns a marshalled version of the {@link RelativeBy}423* @return {!Object} Object representation of a {@link WebElement}424* that will be used in {@link #findElements}.425*/426marshall() {427return {428relative: {429root: this.root,430filters: this.filters,431},432}433}434435/** @override */436toString() {437// The static By.name() overrides this.constructor.name. Shame...438return `RelativeBy(${JSON.stringify(this.marshall())})`439}440}441442/**443* Checks if a value is a valid locator.444* @param {!(By|Function|ByHash)} locator The value to check.445* @return {!(By|Function)} The valid locator.446* @throws {TypeError} If the given value does not define a valid locator447* strategy.448*/449function check(locator) {450if (locator instanceof By || locator instanceof RelativeBy || typeof locator === 'function') {451return locator452}453454if (455locator &&456typeof locator === 'object' &&457typeof locator.using === 'string' &&458typeof locator.value === 'string'459) {460return new By(locator.using, locator.value)461}462463for (let key in locator) {464if (Object.prototype.hasOwnProperty.call(locator, key) && Object.prototype.hasOwnProperty.call(By, key)) {465return By[key](locator[key])466}467}468throw new TypeError('Invalid locator')469}470471// PUBLIC API472473module.exports = {474By,475RelativeBy,476withTagName,477locateWith,478escapeCss,479checkedLocator: check,480}481482483