Path: blob/trunk/javascript/selenium-webdriver/lib/select.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/*18* Licensed to the Software Freedom Conservancy (SFC) under one19* or more contributor license agreements. See the NOTICE file20* distributed with this work for additional information21* regarding copyright ownership. The SFC licenses this file22* to you under the Apache License, Version 2.0 (the23* "License"); you may not use this file except in compliance24* with the License. You may obtain a copy of the License at25*26* http://www.apache.org/licenses/LICENSE-2.027*28* Unless required by applicable law or agreed to in writing,29* software distributed under the License is distributed on an30* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY31* KIND, either express or implied. See the License for the32* specific language governing permissions and limitations33* under the License.34*/3536'use strict'3738const { By } = require('./by')39const error = require('./error')4041/**42* ISelect interface makes a protocol for all kind of select elements (standard html and custom43* model)44*45* @interface46*/47// eslint-disable-next-line no-unused-vars48class ISelect {49/**50* @return {!Promise<boolean>} Whether this select element supports selecting multiple options at the same time? This51* is done by checking the value of the "multiple" attribute.52*/53isMultiple() {}5455/**56* @return {!Promise<!Array<!WebElement>>} All options belonging to this select tag57*/58getOptions() {}5960/**61* @return {!Promise<!Array<!WebElement>>} All selected options belonging to this select tag62*/63getAllSelectedOptions() {}6465/**66* @return {!Promise<!WebElement>} The first selected option in this select tag (or the currently selected option in a67* normal select)68*/69getFirstSelectedOption() {}7071/**72* Select all options that display text matching the argument. That is, when given "Bar" this73* would select an option like:74*75* <option value="foo">Bar</option>76*77* @param {string} text The visible text to match against78* @return {Promise<void>}79*/80selectByVisibleText(text) {} // eslint-disable-line8182/**83* Select all options that have a value matching the argument. That is, when given "foo" this84* would select an option like:85*86* <option value="foo">Bar</option>87*88* @param {string} value The value to match against89* @return {Promise<void>}90*/91selectByValue(value) {} // eslint-disable-line9293/**94* Select the option at the given index. This is done by examining the "index" attribute of an95* element, and not merely by counting.96*97* @param {Number} index The option at this index will be selected98* @return {Promise<void>}99*/100selectByIndex(index) {} // eslint-disable-line101102/**103* Clear all selected entries. This is only valid when the SELECT supports multiple selections.104*105* @return {Promise<void>}106*/107deselectAll() {}108109/**110* Deselect all options that display text matching the argument. That is, when given "Bar" this111* would deselect an option like:112*113* <option value="foo">Bar</option>114*115* @param {string} text The visible text to match against116* @return {Promise<void>}117*/118deselectByVisibleText(text) {} // eslint-disable-line119120/**121* Deselect all options that have a value matching the argument. That is, when given "foo" this122* would deselect an option like:123*124* @param {string} value The value to match against125* @return {Promise<void>}126*/127deselectByValue(value) {} // eslint-disable-line128129/**130* Deselect the option at the given index. This is done by examining the "index" attribute of an131* element, and not merely by counting.132*133* @param {Number} index The option at this index will be deselected134* @return {Promise<void>}135*/136deselectByIndex(index) {} // eslint-disable-line137}138139/**140* @implements ISelect141*/142class Select {143/**144* Create an Select Element145* @param {WebElement} element Select WebElement.146*/147constructor(element) {148if (element === null) {149throw new Error(`Element must not be null. Please provide a valid <select> element.`)150}151152this.element = element153154this.element.getAttribute('tagName').then(function (tagName) {155if (tagName.toLowerCase() !== 'select') {156throw new Error(`Select only works on <select> elements`)157}158})159160this.element.getAttribute('multiple').then((multiple) => {161this.multiple = multiple !== null && multiple !== 'false'162})163}164165/**166*167* Select option with specified index.168*169* <example>170<select id="selectbox">171<option value="1">Option 1</option>172<option value="2">Option 2</option>173<option value="3">Option 3</option>174</select>175const selectBox = await driver.findElement(By.id("selectbox"));176await selectObject.selectByIndex(1);177* </example>178*179* @param index180*/181async selectByIndex(index) {182if (index < 0) {183throw new Error('Index needs to be 0 or any other positive number')184}185186let options = await this.element.findElements(By.tagName('option'))187188if (options.length === 0) {189throw new Error("Select element doesn't contain any option element")190}191192if (options.length - 1 < index) {193throw new Error(194`Option with index "${index}" not found. Select element only contains ${options.length - 1} option elements`,195)196}197198for (let option of options) {199if ((await option.getAttribute('index')) === index.toString()) {200await this.setSelected(option)201}202}203}204205/**206*207* Select option by specific value.208*209* <example>210<select id="selectbox">211<option value="1">Option 1</option>212<option value="2">Option 2</option>213<option value="3">Option 3</option>214</select>215const selectBox = await driver.findElement(By.id("selectbox"));216await selectObject.selectByVisibleText("Option 2");217* </example>218*219*220* @param {string} value value of option element to be selected221*/222async selectByValue(value) {223let matched = false224let isMulti = await this.isMultiple()225226let options = await this.element.findElements(By.xpath('.//option[@value = ' + escapeQuotes(value) + ']'))227228for (let option of options) {229await this.setSelected(option)230231if (!isMulti) {232return233}234matched = true235}236237if (!matched) {238throw new Error(`Cannot locate option with value: ${value}`)239}240}241242/**243*244* Select option with displayed text matching the argument.245*246* <example>247<select id="selectbox">248<option value="1">Option 1</option>249<option value="2">Option 2</option>250<option value="3">Option 3</option>251</select>252const selectBox = await driver.findElement(By.id("selectbox"));253await selectObject.selectByVisibleText("Option 2");254* </example>255*256* @param {String|Number} text text of option element to get selected257*258*/259async selectByVisibleText(text) {260text = typeof text === 'number' ? text.toString() : text261262const xpath = './/option[normalize-space(.) = ' + escapeQuotes(text) + ']'263264const options = await this.element.findElements(By.xpath(xpath))265266for (let option of options) {267await this.setSelected(option)268if (!(await this.isMultiple())) {269return270}271}272273let matched = Array.isArray(options) && options.length > 0274275if (!matched && text.includes(' ')) {276const subStringWithoutSpace = getLongestSubstringWithoutSpace(text)277let candidates278if ('' === subStringWithoutSpace) {279candidates = await this.element.findElements(By.tagName('option'))280} else {281const xpath = './/option[contains(., ' + escapeQuotes(subStringWithoutSpace) + ')]'282candidates = await this.element.findElements(By.xpath(xpath))283}284285const trimmed = text.trim()286287for (let option of candidates) {288const optionText = await option.getText()289if (trimmed === optionText.trim()) {290await this.setSelected(option)291if (!(await this.isMultiple())) {292return293}294matched = true295}296}297}298299if (!matched) {300throw new Error(`Cannot locate option with text: ${text}`)301}302}303304/**305* Returns a list of all options belonging to this select tag306* @returns {!Promise<!Array<!WebElement>>}307*/308async getOptions() {309return await this.element.findElements({ tagName: 'option' })310}311312/**313* Returns a boolean value if the select tag is multiple314* @returns {Promise<boolean>}315*/316async isMultiple() {317return this.multiple318}319320/**321* Returns a list of all selected options belonging to this select tag322*323* @returns {Promise<void>}324*/325async getAllSelectedOptions() {326const opts = await this.getOptions()327const results = []328for (let options of opts) {329if (await options.isSelected()) {330results.push(options)331}332}333return results334}335336/**337* Returns first Selected Option338* @returns {Promise<Element>}339*/340async getFirstSelectedOption() {341return (await this.getAllSelectedOptions())[0]342}343344/**345* Deselects all selected options346* @returns {Promise<void>}347*/348async deselectAll() {349if (!this.isMultiple()) {350throw new Error('You may only deselect all options of a multi-select')351}352353const options = await this.getOptions()354355for (let option of options) {356if (await option.isSelected()) {357await option.click()358}359}360}361362/**363*364* @param {string|Number}text text of option to deselect365* @returns {Promise<void>}366*/367async deselectByVisibleText(text) {368if (!(await this.isMultiple())) {369throw new Error('You may only deselect options of a multi-select')370}371372/**373* convert value into string374*/375text = typeof text === 'number' ? text.toString() : text376377const optionElement = await this.element.findElement(378By.xpath('.//option[normalize-space(.) = ' + escapeQuotes(text) + ']'),379)380if (await optionElement.isSelected()) {381await optionElement.click()382}383}384385/**386*387* @param {Number} index index of option element to deselect388* Deselect the option at the given index.389* This is done by examining the "index"390* attribute of an element, and not merely by counting.391* @returns {Promise<void>}392*/393async deselectByIndex(index) {394if (!(await this.isMultiple())) {395throw new Error('You may only deselect options of a multi-select')396}397398if (index < 0) {399throw new Error('Index needs to be 0 or any other positive number')400}401402let options = await this.element.findElements(By.tagName('option'))403404if (options.length === 0) {405throw new Error("Select element doesn't contain any option element")406}407408if (options.length - 1 < index) {409throw new Error(410`Option with index "${index}" not found. Select element only contains ${options.length - 1} option elements`,411)412}413414for (let option of options) {415if ((await option.getAttribute('index')) === index.toString()) {416if (await option.isSelected()) {417await option.click()418}419}420}421}422423/**424*425* @param {String} value value of an option to deselect426* @returns {Promise<void>}427*/428async deselectByValue(value) {429if (!(await this.isMultiple())) {430throw new Error('You may only deselect options of a multi-select')431}432433let matched = false434435let options = await this.element.findElements(By.xpath('.//option[@value = ' + escapeQuotes(value) + ']'))436437if (options.length === 0) {438throw new Error(`Cannot locate option with value: ${value}`)439}440441for (let option of options) {442if (await option.isSelected()) {443await option.click()444}445matched = true446}447448if (!matched) {449throw new Error(`Cannot locate option with value: ${value}`)450}451}452453async setSelected(option) {454if (!(await option.isSelected())) {455if (!(await option.isEnabled())) {456throw new error.UnsupportedOperationError(`You may not select a disabled option`)457}458await option.click()459}460}461}462463function escapeQuotes(toEscape) {464if (toEscape.includes(`"`) && toEscape.includes(`'`)) {465const quoteIsLast = toEscape.lastIndexOf(`"`) === toEscape.length - 1466const substrings = toEscape.split(`"`)467468// Remove the last element if it's an empty string469if (substrings[substrings.length - 1] === '') {470substrings.pop()471}472473let result = 'concat('474475for (let i = 0; i < substrings.length; i++) {476result += `"${substrings[i]}"`477result += i === substrings.length - 1 ? (quoteIsLast ? `, '"')` : `)`) : `, '"', `478}479return result480}481482if (toEscape.includes('"')) {483return `'${toEscape}'`484}485486// Otherwise return the quoted string487return `"${toEscape}"`488}489490function getLongestSubstringWithoutSpace(text) {491let words = text.split(' ')492let longestString = ''493for (let word of words) {494if (word.length > longestString.length) {495longestString = word496}497}498return longestString499}500501module.exports = { Select, escapeQuotes }502503504