| // Licensed to the Software Freedom Conservancy (SFC) under one |
| // or more contributor license agreements. See the NOTICE file |
| // distributed with this work for additional information |
| // regarding copyright ownership. The SFC licenses this file |
| // to you under the Apache License, Version 2.0 (the |
| // "License"); you may not use this file except in compliance |
| // with the License. You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, |
| // software distributed under the License is distributed on an |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| // KIND, either express or implied. See the License for the |
| // specific language governing permissions and limitations |
| // under the License. |
| |
| /* |
| * Licensed to the Software Freedom Conservancy (SFC) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The SFC licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| 'use strict' |
| |
| const { By, escapeCss } = require('./by') |
| const error = require('./error') |
| |
| /** |
| * ISelect interface makes a protocol for all kind of select elements (standard html and custom |
| * model) |
| * |
| * @interface |
| */ |
| // eslint-disable-next-line no-unused-vars |
| class ISelect { |
| /** |
| * @return {!Promise<boolean>} Whether this select element supports selecting multiple options at the same time? This |
| * is done by checking the value of the "multiple" attribute. |
| */ |
| isMultiple() {} // eslint-disable-line |
| |
| /** |
| * @return {!Promise<!Array<!WebElement>>} All options belonging to this select tag |
| */ |
| getOptions() {} // eslint-disable-line |
| |
| /** |
| * @return {!Promise<!Array<!WebElement>>} All selected options belonging to this select tag |
| */ |
| getAllSelectedOptions() {} // eslint-disable-line |
| |
| /** |
| * @return {!Promise<!WebElement>} The first selected option in this select tag (or the currently selected option in a |
| * normal select) |
| */ |
| getFirstSelectedOption() {} // eslint-disable-line |
| |
| /** |
| * Select all options that display text matching the argument. That is, when given "Bar" this |
| * would select an option like: |
| * |
| * <option value="foo">Bar</option> |
| * |
| * @param {string} text The visible text to match against |
| * @return {Promise<void>} |
| */ |
| selectByVisibleText(text) {} // eslint-disable-line |
| |
| /** |
| * Select all options that have a value matching the argument. That is, when given "foo" this |
| * would select an option like: |
| * |
| * <option value="foo">Bar</option> |
| * |
| * @param {string} value The value to match against |
| * @return {Promise<void>} |
| */ |
| selectByValue(value) {} // eslint-disable-line |
| |
| /** |
| * Select the option at the given index. This is done by examining the "index" attribute of an |
| * element, and not merely by counting. |
| * |
| * @param {Number} index The option at this index will be selected |
| * @return {Promise<void>} |
| */ |
| selectByIndex(index) {} // eslint-disable-line |
| |
| /** |
| * Clear all selected entries. This is only valid when the SELECT supports multiple selections. |
| * |
| * @return {Promise<void>} |
| */ |
| deselectAll() {} // eslint-disable-line |
| |
| /** |
| * Deselect all options that display text matching the argument. That is, when given "Bar" this |
| * would deselect an option like: |
| * |
| * <option value="foo">Bar</option> |
| * |
| * @param {string} text The visible text to match against |
| * @return {Promise<void>} |
| */ |
| deselectByVisibleText(text) {} // eslint-disable-line |
| |
| /** |
| * Deselect all options that have a value matching the argument. That is, when given "foo" this |
| * would deselect an option like: |
| * |
| * @param {string} value The value to match against |
| * @return {Promise<void>} |
| */ |
| deselectByValue(value) {} // eslint-disable-line |
| |
| /** |
| * Deselect the option at the given index. This is done by examining the "index" attribute of an |
| * element, and not merely by counting. |
| * |
| * @param {Number} index The option at this index will be deselected |
| * @return {Promise<void>} |
| */ |
| deselectByIndex(index) {} // eslint-disable-line |
| } |
| |
| /** |
| * @implements ISelect |
| */ |
| class Select { |
| /** |
| * Create an Select Element |
| * @param {WebElement} element Select WebElement. |
| */ |
| constructor(element) { |
| this.element = element |
| |
| this.element.getAttribute('tagName').then(function (tagName) { |
| if (tagName.toLowerCase() !== 'select') { |
| throw new Error(`Select only works on <select> elements`) |
| } |
| }) |
| } |
| |
| /** |
| * |
| * Select option with specified index. |
| * |
| * <example> |
| <select id="selectbox"> |
| <option value="1">Option 1</option> |
| <option value="2">Option 2</option> |
| <option value="3">Option 3</option> |
| </select> |
| const selectBox = await driver.findElement(By.id("selectbox")); |
| await selectObject.selectByIndex(1); |
| * </example> |
| * |
| * @param index |
| */ |
| async selectByIndex(index) { |
| if (index < 0) { |
| throw new Error('Index needs to be 0 or any other positive number') |
| } |
| |
| let options = await this.element.findElements(By.tagName('option')) |
| |
| if (options.length === 0) { |
| throw new Error("Select element doesn't contain any option element") |
| } |
| |
| if (options.length - 1 < index) { |
| throw new Error( |
| `Option with index "${index}" not found. Select element only contains ${options.length - 1} option elements`, |
| ) |
| } |
| |
| for (let option of options) { |
| if ((await option.getAttribute('index')) === index.toString()) { |
| await this.setSelected(option) |
| } |
| } |
| } |
| |
| /** |
| * |
| * Select option by specific value. |
| * |
| * <example> |
| <select id="selectbox"> |
| <option value="1">Option 1</option> |
| <option value="2">Option 2</option> |
| <option value="3">Option 3</option> |
| </select> |
| const selectBox = await driver.findElement(By.id("selectbox")); |
| await selectObject.selectByVisibleText("Option 2"); |
| * </example> |
| * |
| * |
| * @param {string} value value of option element to be selected |
| */ |
| async selectByValue(value) { |
| let matched = false |
| let isMulti = await this.isMultiple() |
| |
| let options = await this.element.findElements({ |
| css: 'option[value =' + escapeCss(value) + ']', |
| }) |
| |
| for (let option of options) { |
| await this.setSelected(option) |
| |
| if (!isMulti) { |
| return |
| } |
| matched = true |
| } |
| |
| if (!matched) { |
| throw new Error(`Cannot locate option with value: ${value}`) |
| } |
| } |
| |
| /** |
| * |
| * Select option with displayed text matching the argument. |
| * |
| * <example> |
| <select id="selectbox"> |
| <option value="1">Option 1</option> |
| <option value="2">Option 2</option> |
| <option value="3">Option 3</option> |
| </select> |
| const selectBox = await driver.findElement(By.id("selectbox")); |
| await selectObject.selectByVisibleText("Option 2"); |
| * </example> |
| * |
| * @param {String|Number} text text of option element to get selected |
| * |
| */ |
| async selectByVisibleText(text) { |
| text = typeof text === 'number' ? text.toString() : text |
| |
| const normalized = text |
| .trim() // strip leading and trailing white-space characters |
| .replace(/\s+/, ' ') // replace sequences of whitespace characters by a single space |
| |
| /** |
| * find option element using xpath |
| */ |
| const formatted = /"/.test(normalized) |
| ? 'concat("' + normalized.split('"').join('", \'"\', "') + '")' |
| : `"${normalized}"` |
| const dotFormat = `[. = ${formatted}]` |
| const spaceFormat = `[normalize-space(text()) = ${formatted}]` |
| |
| const selections = [ |
| `./option${dotFormat}`, |
| `./option${spaceFormat}`, |
| `./optgroup/option${dotFormat}`, |
| `./optgroup/option${spaceFormat}`, |
| ] |
| |
| const optionElement = await this.element.findElement({ |
| xpath: selections.join('|'), |
| }) |
| await this.setSelected(optionElement) |
| } |
| |
| /** |
| * Returns a list of all options belonging to this select tag |
| * @returns {!Promise<!Array<!WebElement>>} |
| */ |
| async getOptions() { |
| return await this.element.findElements({ tagName: 'option' }) |
| } |
| |
| /** |
| * Returns a boolean value if the select tag is multiple |
| * @returns {Promise<boolean>} |
| */ |
| async isMultiple() { |
| return (await this.element.getAttribute('multiple')) !== null |
| } |
| |
| /** |
| * Returns a list of all selected options belonging to this select tag |
| * |
| * @returns {Promise<void>} |
| */ |
| async getAllSelectedOptions() { |
| const opts = await this.getOptions() |
| const results = [] |
| for (let options of opts) { |
| if (await options.isSelected()) { |
| results.push(options) |
| } |
| } |
| return results |
| } |
| |
| /** |
| * Returns first Selected Option |
| * @returns {Promise<Element>} |
| */ |
| async getFirstSelectedOption() { |
| return (await this.getAllSelectedOptions())[0] |
| } |
| |
| /** |
| * Deselects all selected options |
| * @returns {Promise<void>} |
| */ |
| async deselectAll() { |
| if (!this.isMultiple()) { |
| throw new Error('You may only deselect all options of a multi-select') |
| } |
| |
| const options = await this.getOptions() |
| |
| for (let option of options) { |
| if (await option.isSelected()) { |
| await option.click() |
| } |
| } |
| } |
| |
| /** |
| * |
| * @param {string|Number}text text of option to deselect |
| * @returns {Promise<void>} |
| */ |
| async deselectByVisibleText(text) { |
| if (!(await this.isMultiple())) { |
| throw new Error('You may only deselect options of a multi-select') |
| } |
| |
| /** |
| * convert value into string |
| */ |
| text = typeof text === 'number' ? text.toString() : text |
| |
| const normalized = text |
| .trim() // strip leading and trailing white-space characters |
| .replace(/\s+/, ' ') // replace sequences of whitespace characters by a single space |
| |
| /** |
| * find option element using xpath |
| */ |
| const formatted = /"/.test(normalized) |
| ? 'concat("' + normalized.split('"').join('", \'"\', "') + '")' |
| : `"${normalized}"` |
| const dotFormat = `[. = ${formatted}]` |
| const spaceFormat = `[normalize-space(text()) = ${formatted}]` |
| |
| const selections = [ |
| `./option${dotFormat}`, |
| `./option${spaceFormat}`, |
| `./optgroup/option${dotFormat}`, |
| `./optgroup/option${spaceFormat}`, |
| ] |
| |
| const optionElement = await this.element.findElement({ |
| xpath: selections.join('|'), |
| }) |
| if (await optionElement.isSelected()) { |
| await optionElement.click() |
| } |
| } |
| |
| /** |
| * |
| * @param {Number} index index of option element to deselect |
| * Deselect the option at the given index. |
| * This is done by examining the "index" |
| * attribute of an element, and not merely by counting. |
| * @returns {Promise<void>} |
| */ |
| async deselectByIndex(index) { |
| if (!(await this.isMultiple())) { |
| throw new Error('You may only deselect options of a multi-select') |
| } |
| |
| if (index < 0) { |
| throw new Error('Index needs to be 0 or any other positive number') |
| } |
| |
| let options = await this.element.findElements(By.tagName('option')) |
| |
| if (options.length === 0) { |
| throw new Error("Select element doesn't contain any option element") |
| } |
| |
| if (options.length - 1 < index) { |
| throw new Error( |
| `Option with index "${index}" not found. Select element only contains ${options.length - 1} option elements`, |
| ) |
| } |
| |
| for (let option of options) { |
| if ((await option.getAttribute('index')) === index.toString()) { |
| if (await option.isSelected()) { |
| await option.click() |
| } |
| } |
| } |
| } |
| |
| /** |
| * |
| * @param {String} value value of an option to deselect |
| * @returns {Promise<void>} |
| */ |
| async deselectByValue(value) { |
| if (!(await this.isMultiple())) { |
| throw new Error('You may only deselect options of a multi-select') |
| } |
| |
| let matched = false |
| |
| let options = await this.element.findElements({ |
| css: 'option[value =' + escapeCss(value) + ']', |
| }) |
| |
| for (let option of options) { |
| if (await option.isSelected()) { |
| await option.click() |
| } |
| matched = true |
| } |
| |
| if (!matched) { |
| throw new Error(`Cannot locate option with value: ${value}`) |
| } |
| } |
| |
| async setSelected(option) { |
| if (!(await option.isSelected())) { |
| if (!(await option.isEnabled())) { |
| throw new error.UnsupportedOperationError(`You may not select a disabled option`) |
| } |
| await option.click() |
| } |
| } |
| } |
| |
| module.exports = { Select } |