Skip to content

Commit c114dbd

Browse files
authored
[js] Ensure 'selectVisibleByText' method is same as other languages (#13899)
1 parent a0a3914 commit c114dbd

File tree

4 files changed

+133
-23
lines changed

4 files changed

+133
-23
lines changed

common/src/web/select_space.html

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<html>
2+
<head>
3+
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
4+
<title>Multiple Selection test page</title>
5+
</head>
6+
<body>
7+
<select id="selectWithoutMultiple">
8+
<option value="one">one</option>
9+
<option value="two">&nbsp;&nbsp;two</option>
10+
<option value="three">&nbsp;&nbsp;&nbsp;three</option>
11+
<option value="four">&nbsp;&nbsp;&nbsp;&nbsp;four</option>
12+
<option value="five">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;five</option>
13+
</body>
14+
</html>

javascript/node/selenium-webdriver/lib/select.js

Lines changed: 81 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,10 @@ class Select {
153153
throw new Error(`Select only works on <select> elements`)
154154
}
155155
})
156+
157+
this.element.getAttribute('multiple').then((multiple) => {
158+
this.multiple = multiple !== null && multiple !== 'false'
159+
})
156160
}
157161

158162
/**
@@ -254,30 +258,46 @@ class Select {
254258
async selectByVisibleText(text) {
255259
text = typeof text === 'number' ? text.toString() : text
256260

257-
const normalized = text
258-
.trim() // strip leading and trailing white-space characters
259-
.replace(/\s+/, ' ') // replace sequences of whitespace characters by a single space
261+
const xpath = './/option[normalize-space(.) = ' + escapeQuotes(text) + ']'
260262

261-
/**
262-
* find option element using xpath
263-
*/
264-
const formatted = /"/.test(normalized)
265-
? 'concat("' + normalized.split('"').join('", \'"\', "') + '")'
266-
: `"${normalized}"`
267-
const dotFormat = `[. = ${formatted}]`
268-
const spaceFormat = `[normalize-space(text()) = ${formatted}]`
263+
const options = await this.element.findElements(By.xpath(xpath))
269264

270-
const selections = [
271-
`./option${dotFormat}`,
272-
`./option${spaceFormat}`,
273-
`./optgroup/option${dotFormat}`,
274-
`./optgroup/option${spaceFormat}`,
275-
]
265+
for (let option of options) {
266+
await this.setSelected(option)
267+
if (!(await this.isMultiple())) {
268+
return
269+
}
270+
}
276271

277-
const optionElement = await this.element.findElement({
278-
xpath: selections.join('|'),
279-
})
280-
await this.setSelected(optionElement)
272+
let matched = Array.isArray(options) && options.length > 0
273+
274+
if (!matched && text.includes(' ')) {
275+
const subStringWithoutSpace = getLongestSubstringWithoutSpace(text)
276+
let candidates
277+
if ('' === subStringWithoutSpace) {
278+
candidates = await this.element.findElements(By.tagName('option'))
279+
} else {
280+
const xpath = './/option[contains(., ' + escapeQuotes(subStringWithoutSpace) + ')]'
281+
candidates = await this.element.findElements(By.xpath(xpath))
282+
}
283+
284+
const trimmed = text.trim()
285+
286+
for (let option of candidates) {
287+
const optionText = await option.getText()
288+
if (trimmed === optionText.trim()) {
289+
await this.setSelected(option)
290+
if (!(await this.isMultiple())) {
291+
return
292+
}
293+
matched = true
294+
}
295+
}
296+
}
297+
298+
if (!matched) {
299+
throw new Error(`Cannot locate option with text: ${text}`)
300+
}
281301
}
282302

283303
/**
@@ -293,7 +313,7 @@ class Select {
293313
* @returns {Promise<boolean>}
294314
*/
295315
async isMultiple() {
296-
return (await this.element.getAttribute('multiple')) !== null
316+
return this.multiple
297317
}
298318

299319
/**
@@ -457,4 +477,42 @@ class Select {
457477
}
458478
}
459479

460-
module.exports = { Select }
480+
function escapeQuotes(toEscape) {
481+
if (toEscape.includes(`"`) && toEscape.includes(`'`)) {
482+
const quoteIsLast = toEscape.lastIndexOf(`"`) === toEscape.length - 1
483+
const substrings = toEscape.split(`"`)
484+
485+
// Remove the last element if it's an empty string
486+
if (substrings[substrings.length - 1] === '') {
487+
substrings.pop()
488+
}
489+
490+
let result = 'concat('
491+
492+
for (let i = 0; i < substrings.length; i++) {
493+
result += `"${substrings[i]}"`
494+
result += i === substrings.length - 1 ? (quoteIsLast ? `, '"')` : `)`) : `, '"', `
495+
}
496+
return result
497+
}
498+
499+
if (toEscape.includes('"')) {
500+
return `'${toEscape}'`
501+
}
502+
503+
// Otherwise return the quoted string
504+
return `"${toEscape}"`
505+
}
506+
507+
function getLongestSubstringWithoutSpace(text) {
508+
let words = text.split(' ')
509+
let longestString = ''
510+
for (let word of words) {
511+
if (word.length > longestString.length) {
512+
longestString = word
513+
}
514+
}
515+
return longestString
516+
}
517+
518+
module.exports = { Select, escapeQuotes }

javascript/node/selenium-webdriver/lib/test/fileserver.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ const Pages = (function () {
9494
addPage('scrollingPage', 'scrollingPage.html')
9595
addPage('selectableItemsPage', 'selectableItems.html')
9696
addPage('selectPage', 'selectPage.html')
97+
addPage('selectSpacePage', 'select_space.html')
9798
addPage('simpleTestPage', 'simpleTest.html')
9899
addPage('simpleXmlDocument', 'simple.xml')
99100
addPage('sleepingPage', 'sleep')

javascript/node/selenium-webdriver/test/select_test.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
const assert = require('node:assert')
2121
const { Select, By } = require('..')
2222
const { Pages, suite } = require('../lib/test')
23+
const { escapeQuotes } = require('../lib/select')
2324

2425
let singleSelectValues1 = {
2526
name: 'selectomatic',
@@ -87,6 +88,42 @@ suite(
8788
}
8889
})
8990

91+
it('Should be able to select by visible text with spaces', async function () {
92+
await driver.get(Pages.selectSpacePage)
93+
94+
const elem = await driver.findElement(By.id('selectWithoutMultiple'))
95+
const select = new Select(elem)
96+
await select.selectByVisibleText(' five')
97+
let selectedElement = await select.getFirstSelectedOption()
98+
selectedElement.getText().then((text) => {
99+
assert.strictEqual(text, ' five')
100+
})
101+
})
102+
103+
it('Should convert an unquoted string into one with quotes', async function () {
104+
assert.strictEqual(escapeQuotes('abc'), '"abc"')
105+
assert.strictEqual(escapeQuotes('abc aqewqqw'), '"abc aqewqqw"')
106+
assert.strictEqual(escapeQuotes(''), '""')
107+
assert.strictEqual(escapeQuotes(' '), '" "')
108+
assert.strictEqual(escapeQuotes(' abc '), '" abc "')
109+
})
110+
111+
it('Should add double quotes to a string that contains a single quote', async function () {
112+
assert.strictEqual(escapeQuotes("f'oo"), `"f'oo"`)
113+
})
114+
115+
it('Should add single quotes to a string that contains a double quotes', async function () {
116+
assert.strictEqual(escapeQuotes('f"oo'), `'f"oo'`)
117+
})
118+
119+
it('Should provide concatenated strings when string to escape contains both single and double quotes', async function () {
120+
assert.strictEqual(escapeQuotes(`f"o'o`), `concat("f", '"', "o'o")`)
121+
})
122+
123+
it('Should provide concatenated strings when string ends with quote', async function () {
124+
assert.strictEqual(escapeQuotes(`'"`), `concat("'", '"')`)
125+
})
126+
90127
it('Should select by multiple index', async function () {
91128
await driver.get(Pages.formPage)
92129

0 commit comments

Comments
 (0)