Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
seleniumhq
GitHub Repository: seleniumhq/selenium
Path: blob/trunk/javascript/selenium-webdriver/lib/select.js
2884 views
1
// Licensed to the Software Freedom Conservancy (SFC) under one
2
// or more contributor license agreements. See the NOTICE file
3
// distributed with this work for additional information
4
// regarding copyright ownership. The SFC licenses this file
5
// to you under the Apache License, Version 2.0 (the
6
// "License"); you may not use this file except in compliance
7
// with the License. You may obtain a copy of the License at
8
//
9
// http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing,
12
// software distributed under the License is distributed on an
13
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
// KIND, either express or implied. See the License for the
15
// specific language governing permissions and limitations
16
// under the License.
17
18
/*
19
* Licensed to the Software Freedom Conservancy (SFC) under one
20
* or more contributor license agreements. See the NOTICE file
21
* distributed with this work for additional information
22
* regarding copyright ownership. The SFC licenses this file
23
* to you under the Apache License, Version 2.0 (the
24
* "License"); you may not use this file except in compliance
25
* with the License. You may obtain a copy of the License at
26
*
27
* http://www.apache.org/licenses/LICENSE-2.0
28
*
29
* Unless required by applicable law or agreed to in writing,
30
* software distributed under the License is distributed on an
31
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
32
* KIND, either express or implied. See the License for the
33
* specific language governing permissions and limitations
34
* under the License.
35
*/
36
37
'use strict'
38
39
const { By } = require('./by')
40
const error = require('./error')
41
42
/**
43
* ISelect interface makes a protocol for all kind of select elements (standard html and custom
44
* model)
45
*
46
* @interface
47
*/
48
// eslint-disable-next-line no-unused-vars
49
class ISelect {
50
/**
51
* @return {!Promise<boolean>} Whether this select element supports selecting multiple options at the same time? This
52
* is done by checking the value of the "multiple" attribute.
53
*/
54
isMultiple() {}
55
56
/**
57
* @return {!Promise<!Array<!WebElement>>} All options belonging to this select tag
58
*/
59
getOptions() {}
60
61
/**
62
* @return {!Promise<!Array<!WebElement>>} All selected options belonging to this select tag
63
*/
64
getAllSelectedOptions() {}
65
66
/**
67
* @return {!Promise<!WebElement>} The first selected option in this select tag (or the currently selected option in a
68
* normal select)
69
*/
70
getFirstSelectedOption() {}
71
72
/**
73
* Select all options that display text matching the argument. That is, when given "Bar" this
74
* would select an option like:
75
*
76
* &lt;option value="foo"&gt;Bar&lt;/option&gt;
77
*
78
* @param {string} text The visible text to match against
79
* @return {Promise<void>}
80
*/
81
selectByVisibleText(text) {} // eslint-disable-line
82
83
/**
84
* Select all options that have a value matching the argument. That is, when given "foo" this
85
* would select an option like:
86
*
87
* &lt;option value="foo"&gt;Bar&lt;/option&gt;
88
*
89
* @param {string} value The value to match against
90
* @return {Promise<void>}
91
*/
92
selectByValue(value) {} // eslint-disable-line
93
94
/**
95
* Select the option at the given index. This is done by examining the "index" attribute of an
96
* element, and not merely by counting.
97
*
98
* @param {Number} index The option at this index will be selected
99
* @return {Promise<void>}
100
*/
101
selectByIndex(index) {} // eslint-disable-line
102
103
/**
104
* Clear all selected entries. This is only valid when the SELECT supports multiple selections.
105
*
106
* @return {Promise<void>}
107
*/
108
deselectAll() {}
109
110
/**
111
* Deselect all options that display text matching the argument. That is, when given "Bar" this
112
* would deselect an option like:
113
*
114
* &lt;option value="foo"&gt;Bar&lt;/option&gt;
115
*
116
* @param {string} text The visible text to match against
117
* @return {Promise<void>}
118
*/
119
deselectByVisibleText(text) {} // eslint-disable-line
120
121
/**
122
* Deselect all options that have a value matching the argument. That is, when given "foo" this
123
* would deselect an option like:
124
*
125
* @param {string} value The value to match against
126
* @return {Promise<void>}
127
*/
128
deselectByValue(value) {} // eslint-disable-line
129
130
/**
131
* Deselect the option at the given index. This is done by examining the "index" attribute of an
132
* element, and not merely by counting.
133
*
134
* @param {Number} index The option at this index will be deselected
135
* @return {Promise<void>}
136
*/
137
deselectByIndex(index) {} // eslint-disable-line
138
}
139
140
/**
141
* @implements ISelect
142
*/
143
class Select {
144
/**
145
* Create an Select Element
146
* @param {WebElement} element Select WebElement.
147
*/
148
constructor(element) {
149
if (element === null) {
150
throw new Error(`Element must not be null. Please provide a valid <select> element.`)
151
}
152
153
this.element = element
154
155
this.element.getAttribute('tagName').then(function (tagName) {
156
if (tagName.toLowerCase() !== 'select') {
157
throw new Error(`Select only works on <select> elements`)
158
}
159
})
160
161
this.element.getAttribute('multiple').then((multiple) => {
162
this.multiple = multiple !== null && multiple !== 'false'
163
})
164
}
165
166
/**
167
*
168
* Select option with specified index.
169
*
170
* <example>
171
<select id="selectbox">
172
<option value="1">Option 1</option>
173
<option value="2">Option 2</option>
174
<option value="3">Option 3</option>
175
</select>
176
const selectBox = await driver.findElement(By.id("selectbox"));
177
await selectObject.selectByIndex(1);
178
* </example>
179
*
180
* @param index
181
*/
182
async selectByIndex(index) {
183
if (index < 0) {
184
throw new Error('Index needs to be 0 or any other positive number')
185
}
186
187
let options = await this.element.findElements(By.tagName('option'))
188
189
if (options.length === 0) {
190
throw new Error("Select element doesn't contain any option element")
191
}
192
193
if (options.length - 1 < index) {
194
throw new Error(
195
`Option with index "${index}" not found. Select element only contains ${options.length - 1} option elements`,
196
)
197
}
198
199
for (let option of options) {
200
if ((await option.getAttribute('index')) === index.toString()) {
201
await this.setSelected(option)
202
}
203
}
204
}
205
206
/**
207
*
208
* Select option by specific value.
209
*
210
* <example>
211
<select id="selectbox">
212
<option value="1">Option 1</option>
213
<option value="2">Option 2</option>
214
<option value="3">Option 3</option>
215
</select>
216
const selectBox = await driver.findElement(By.id("selectbox"));
217
await selectObject.selectByVisibleText("Option 2");
218
* </example>
219
*
220
*
221
* @param {string} value value of option element to be selected
222
*/
223
async selectByValue(value) {
224
let matched = false
225
let isMulti = await this.isMultiple()
226
227
let options = await this.element.findElements(By.xpath('.//option[@value = ' + escapeQuotes(value) + ']'))
228
229
for (let option of options) {
230
await this.setSelected(option)
231
232
if (!isMulti) {
233
return
234
}
235
matched = true
236
}
237
238
if (!matched) {
239
throw new Error(`Cannot locate option with value: ${value}`)
240
}
241
}
242
243
/**
244
*
245
* Select option with displayed text matching the argument.
246
*
247
* <example>
248
<select id="selectbox">
249
<option value="1">Option 1</option>
250
<option value="2">Option 2</option>
251
<option value="3">Option 3</option>
252
</select>
253
const selectBox = await driver.findElement(By.id("selectbox"));
254
await selectObject.selectByVisibleText("Option 2");
255
* </example>
256
*
257
* @param {String|Number} text text of option element to get selected
258
*
259
*/
260
async selectByVisibleText(text) {
261
text = typeof text === 'number' ? text.toString() : text
262
263
const xpath = './/option[normalize-space(.) = ' + escapeQuotes(text) + ']'
264
265
const options = await this.element.findElements(By.xpath(xpath))
266
267
for (let option of options) {
268
await this.setSelected(option)
269
if (!(await this.isMultiple())) {
270
return
271
}
272
}
273
274
let matched = Array.isArray(options) && options.length > 0
275
276
if (!matched && text.includes(' ')) {
277
const subStringWithoutSpace = getLongestSubstringWithoutSpace(text)
278
let candidates
279
if ('' === subStringWithoutSpace) {
280
candidates = await this.element.findElements(By.tagName('option'))
281
} else {
282
const xpath = './/option[contains(., ' + escapeQuotes(subStringWithoutSpace) + ')]'
283
candidates = await this.element.findElements(By.xpath(xpath))
284
}
285
286
const trimmed = text.trim()
287
288
for (let option of candidates) {
289
const optionText = await option.getText()
290
if (trimmed === optionText.trim()) {
291
await this.setSelected(option)
292
if (!(await this.isMultiple())) {
293
return
294
}
295
matched = true
296
}
297
}
298
}
299
300
if (!matched) {
301
throw new Error(`Cannot locate option with text: ${text}`)
302
}
303
}
304
305
/**
306
* Returns a list of all options belonging to this select tag
307
* @returns {!Promise<!Array<!WebElement>>}
308
*/
309
async getOptions() {
310
return await this.element.findElements({ tagName: 'option' })
311
}
312
313
/**
314
* Returns a boolean value if the select tag is multiple
315
* @returns {Promise<boolean>}
316
*/
317
async isMultiple() {
318
return this.multiple
319
}
320
321
/**
322
* Returns a list of all selected options belonging to this select tag
323
*
324
* @returns {Promise<void>}
325
*/
326
async getAllSelectedOptions() {
327
const opts = await this.getOptions()
328
const results = []
329
for (let options of opts) {
330
if (await options.isSelected()) {
331
results.push(options)
332
}
333
}
334
return results
335
}
336
337
/**
338
* Returns first Selected Option
339
* @returns {Promise<Element>}
340
*/
341
async getFirstSelectedOption() {
342
return (await this.getAllSelectedOptions())[0]
343
}
344
345
/**
346
* Deselects all selected options
347
* @returns {Promise<void>}
348
*/
349
async deselectAll() {
350
if (!this.isMultiple()) {
351
throw new Error('You may only deselect all options of a multi-select')
352
}
353
354
const options = await this.getOptions()
355
356
for (let option of options) {
357
if (await option.isSelected()) {
358
await option.click()
359
}
360
}
361
}
362
363
/**
364
*
365
* @param {string|Number}text text of option to deselect
366
* @returns {Promise<void>}
367
*/
368
async deselectByVisibleText(text) {
369
if (!(await this.isMultiple())) {
370
throw new Error('You may only deselect options of a multi-select')
371
}
372
373
/**
374
* convert value into string
375
*/
376
text = typeof text === 'number' ? text.toString() : text
377
378
const optionElement = await this.element.findElement(
379
By.xpath('.//option[normalize-space(.) = ' + escapeQuotes(text) + ']'),
380
)
381
if (await optionElement.isSelected()) {
382
await optionElement.click()
383
}
384
}
385
386
/**
387
*
388
* @param {Number} index index of option element to deselect
389
* Deselect the option at the given index.
390
* This is done by examining the "index"
391
* attribute of an element, and not merely by counting.
392
* @returns {Promise<void>}
393
*/
394
async deselectByIndex(index) {
395
if (!(await this.isMultiple())) {
396
throw new Error('You may only deselect options of a multi-select')
397
}
398
399
if (index < 0) {
400
throw new Error('Index needs to be 0 or any other positive number')
401
}
402
403
let options = await this.element.findElements(By.tagName('option'))
404
405
if (options.length === 0) {
406
throw new Error("Select element doesn't contain any option element")
407
}
408
409
if (options.length - 1 < index) {
410
throw new Error(
411
`Option with index "${index}" not found. Select element only contains ${options.length - 1} option elements`,
412
)
413
}
414
415
for (let option of options) {
416
if ((await option.getAttribute('index')) === index.toString()) {
417
if (await option.isSelected()) {
418
await option.click()
419
}
420
}
421
}
422
}
423
424
/**
425
*
426
* @param {String} value value of an option to deselect
427
* @returns {Promise<void>}
428
*/
429
async deselectByValue(value) {
430
if (!(await this.isMultiple())) {
431
throw new Error('You may only deselect options of a multi-select')
432
}
433
434
let matched = false
435
436
let options = await this.element.findElements(By.xpath('.//option[@value = ' + escapeQuotes(value) + ']'))
437
438
if (options.length === 0) {
439
throw new Error(`Cannot locate option with value: ${value}`)
440
}
441
442
for (let option of options) {
443
if (await option.isSelected()) {
444
await option.click()
445
}
446
matched = true
447
}
448
449
if (!matched) {
450
throw new Error(`Cannot locate option with value: ${value}`)
451
}
452
}
453
454
async setSelected(option) {
455
if (!(await option.isSelected())) {
456
if (!(await option.isEnabled())) {
457
throw new error.UnsupportedOperationError(`You may not select a disabled option`)
458
}
459
await option.click()
460
}
461
}
462
}
463
464
function escapeQuotes(toEscape) {
465
if (toEscape.includes(`"`) && toEscape.includes(`'`)) {
466
const quoteIsLast = toEscape.lastIndexOf(`"`) === toEscape.length - 1
467
const substrings = toEscape.split(`"`)
468
469
// Remove the last element if it's an empty string
470
if (substrings[substrings.length - 1] === '') {
471
substrings.pop()
472
}
473
474
let result = 'concat('
475
476
for (let i = 0; i < substrings.length; i++) {
477
result += `"${substrings[i]}"`
478
result += i === substrings.length - 1 ? (quoteIsLast ? `, '"')` : `)`) : `, '"', `
479
}
480
return result
481
}
482
483
if (toEscape.includes('"')) {
484
return `'${toEscape}'`
485
}
486
487
// Otherwise return the quoted string
488
return `"${toEscape}"`
489
}
490
491
function getLongestSubstringWithoutSpace(text) {
492
let words = text.split(' ')
493
let longestString = ''
494
for (let word of words) {
495
if (word.length > longestString.length) {
496
longestString = word
497
}
498
}
499
return longestString
500
}
501
502
module.exports = { Select, escapeQuotes }
503
504