Skip to content

Commit 68f3edc

Browse files
authored
[JS] Allow relativeby with findElement (SeleniumHQ#9396)
* [JS] Allow relativeby with findElement * [js] resolves custom locator issue triggered by previous change
1 parent f621dca commit 68f3edc

File tree

4 files changed

+103
-45
lines changed

4 files changed

+103
-45
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ javascript/node/selenium-webdriver/node_modules/
2020
javascript/node/selenium-webdriver/lib/atoms/find-elements.js
2121
javascript/node/selenium-webdriver/lib/atoms/get-attribute.js
2222
javascript/node/selenium-webdriver/lib/atoms/is-displayed.js
23+
javascript/node/selenium-webdriver/lib/atoms/mutation-listener.js
2324
javascript/safari-driver/node_modules/
2425
javascript/webdriver/devtools/types/
2526
.idea/vcs.xml

javascript/node/selenium-webdriver/CHANGES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## v4.0.0-beta.4
2+
3+
* Allow RelativeBy to work with findElement
4+
15
## v4.0.0-beta.3
26

37
* Removed support for legacy actions.

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

Lines changed: 80 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ const http = require('../http/index')
3636
const fs = require('fs')
3737
const { Capabilities } = require('./capabilities')
3838
const path = require('path')
39+
const { NoSuchElementError } = require('./error')
3940

4041
// Capability names that are defined in the W3C spec.
4142
const W3C_CAPABILITY_NAMES = new Set([
@@ -257,18 +258,18 @@ class IWebDriver {
257258
/**
258259
* @return {!command.Executor} The command executor used by this instance.
259260
*/
260-
getExecutor() { }
261+
getExecutor() {}
261262

262263
/**
263264
* @return {!Promise<!Session>} A promise for this client's session.
264265
*/
265-
getSession() { }
266+
getSession() {}
266267

267268
/**
268269
* @return {!Promise<!Capabilities>} A promise that will resolve with
269270
* the this instance's capabilities.
270271
*/
271-
getCapabilities() { }
272+
getCapabilities() {}
272273

273274
/**
274275
* Terminates the browser session. After calling quit, this instance will be
@@ -278,7 +279,7 @@ class IWebDriver {
278279
* @return {!Promise<void>} A promise that will be resolved when the
279280
* command has completed.
280281
*/
281-
quit() { }
282+
quit() {}
282283

283284
/**
284285
* Creates a new action sequence using this driver. The sequence will not be
@@ -457,7 +458,7 @@ class IWebDriver {
457458
timeout = undefined, // eslint-disable-line
458459
message = undefined, // eslint-disable-line
459460
pollTimeout = undefined // eslint-disable-line
460-
) { }
461+
) {}
461462

462463
/**
463464
* Makes the driver sleep for the given amount of time.
@@ -474,15 +475,15 @@ class IWebDriver {
474475
* @return {!Promise<string>} A promise that will be resolved with the current
475476
* window handle.
476477
*/
477-
getWindowHandle() { }
478+
getWindowHandle() {}
478479

479480
/**
480481
* Retrieves a list of all available window handles.
481482
*
482483
* @return {!Promise<!Array<string>>} A promise that will be resolved with an
483484
* array of window handles.
484485
*/
485-
getAllWindowHandles() { }
486+
getAllWindowHandles() {}
486487

487488
/**
488489
* Retrieves the current page's source. The returned source is a representation
@@ -492,15 +493,15 @@ class IWebDriver {
492493
* @return {!Promise<string>} A promise that will be resolved with the current
493494
* page source.
494495
*/
495-
getPageSource() { }
496+
getPageSource() {}
496497

497498
/**
498499
* Closes the current window.
499500
*
500501
* @return {!Promise<void>} A promise that will be resolved when this command
501502
* has completed.
502503
*/
503-
close() { }
504+
close() {}
504505

505506
/**
506507
* Navigates to the given URL.
@@ -517,15 +518,15 @@ class IWebDriver {
517518
* @return {!Promise<string>} A promise that will be resolved with the
518519
* current URL.
519520
*/
520-
getCurrentUrl() { }
521+
getCurrentUrl() {}
521522

522523
/**
523524
* Retrieves the current page title.
524525
*
525526
* @return {!Promise<string>} A promise that will be resolved with the current
526527
* page's title.
527528
*/
528-
getTitle() { }
529+
getTitle() {}
529530

530531
/**
531532
* Locates an element on the page. If the element cannot be found, a
@@ -589,23 +590,23 @@ class IWebDriver {
589590
* @return {!Promise<string>} A promise that will be resolved to the
590591
* screenshot as a base-64 encoded PNG.
591592
*/
592-
takeScreenshot() { }
593+
takeScreenshot() {}
593594

594595
/**
595596
* @return {!Options} The options interface for this instance.
596597
*/
597-
manage() { }
598+
manage() {}
598599

599600
/**
600601
* @return {!Navigation} The navigation interface for this instance.
601602
*/
602-
navigate() { }
603+
navigate() {}
603604

604605
/**
605606
* @return {!TargetLocator} The target locator interface for this
606607
* instance.
607608
*/
608-
switchTo() { }
609+
switchTo() {}
609610

610611
/**
611612
*
@@ -649,7 +650,6 @@ function filterNonW3CCaps(capabilities) {
649650
* @implements {IWebDriver}
650651
*/
651652
class WebDriver {
652-
653653
/**
654654
* @param {!(./session.Session|IThenable<!./session.Session>)} session Either
655655
* a known session or a promise that will be resolved to a session.
@@ -665,7 +665,7 @@ class WebDriver {
665665
// If session is a rejected promise, add a no-op rejection handler.
666666
// This effectively hides setup errors until users attempt to interact
667667
// with the session.
668-
this.session_.catch(function () { })
668+
this.session_.catch(function () {})
669669

670670
/** @private {!command.Executor} */
671671
this.executor_ = executor
@@ -761,12 +761,12 @@ class WebDriver {
761761
this.session_ = Promise.reject(
762762
new error.NoSuchSessionError(
763763
'This driver instance does not have a valid session ID ' +
764-
'(did you call WebDriver.quit()?) and may no longer be used.'
764+
'(did you call WebDriver.quit()?) and may no longer be used.'
765765
)
766766
)
767767

768768
// Only want the session rejection to bubble if accessed.
769-
this.session_.catch(function () { })
769+
this.session_.catch(function () {})
770770

771771
if (this.onQuit_) {
772772
return this.onQuit_.call(void 0)
@@ -827,15 +827,18 @@ class WebDriver {
827827
let timeoutMessage = resolveWaitMessage(message)
828828
reject(
829829
new error.TimeoutError(
830-
`${timeoutMessage}Timed out waiting for promise to resolve after ${Date.now() - start
830+
`${timeoutMessage}Timed out waiting for promise to resolve after ${
831+
Date.now() - start
831832
}ms`
832833
)
833834
)
834835
} catch (ex) {
835836
reject(
836837
new error.TimeoutError(
837-
`${ex.message
838-
}\nTimed out waiting for promise to resolve after ${Date.now() - start
838+
`${
839+
ex.message
840+
}\nTimed out waiting for promise to resolve after ${
841+
Date.now() - start
839842
}ms`
840843
)
841844
)
@@ -865,7 +868,7 @@ class WebDriver {
865868
if (typeof fn !== 'function') {
866869
throw TypeError(
867870
'Wait condition must be a promise-like object, function, or a ' +
868-
'Condition object'
871+
'Condition object'
869872
)
870873
}
871874

@@ -917,7 +920,7 @@ class WebDriver {
917920
if (!(value instanceof WebElement)) {
918921
throw TypeError(
919922
'WebElementCondition did not resolve to a WebElement: ' +
920-
Object.prototype.toString.call(value)
923+
Object.prototype.toString.call(value)
921924
)
922925
}
923926
return value
@@ -972,16 +975,46 @@ class WebDriver {
972975
/** @override */
973976
findElement(locator) {
974977
let id
975-
locator = by.checkedLocator(locator)
978+
let cmd = null
979+
980+
if (locator instanceof RelativeBy) {
981+
cmd = new command.Command(
982+
command.Name.FIND_ELEMENTS_RELATIVE
983+
).setParameter('args', locator.marshall())
984+
} else {
985+
locator = by.checkedLocator(locator)
986+
}
987+
976988
if (typeof locator === 'function') {
977989
id = this.findElementInternal_(locator, this)
978-
} else {
979-
let cmd = new command.Command(command.Name.FIND_ELEMENT)
990+
return new WebElementPromise(this, id)
991+
} else if (cmd === null) {
992+
cmd = new command.Command(command.Name.FIND_ELEMENT)
980993
.setParameter('using', locator.using)
981994
.setParameter('value', locator.value)
982-
id = this.execute(cmd)
983995
}
984-
return new WebElementPromise(this, id)
996+
997+
id = this.execute(cmd)
998+
if (locator instanceof RelativeBy) {
999+
return this.normalize_(id)
1000+
} else {
1001+
return new WebElementPromise(this, id)
1002+
}
1003+
}
1004+
1005+
/**
1006+
* @param {!Function} webElementPromise The webElement in unresolved state
1007+
* @return {!Promise<!WebElement>} First single WebElement from array of resolved promises
1008+
*/
1009+
async normalize_(webElementPromise) {
1010+
let result = await webElementPromise
1011+
if (result.length === 0) {
1012+
throw new NoSuchElementError(
1013+
'Cannot locate an element with provided parameters'
1014+
)
1015+
} else {
1016+
return result[0]
1017+
}
9851018
}
9861019

9871020
/**
@@ -1166,7 +1199,7 @@ class WebDriver {
11661199
caps['map_'].get(this.VENDOR_COMMAND_PREFIX + ':chromeOptions') ||
11671200
caps['map_'].get(this.VENDOR_CAPABILITY_PREFIX + ':edgeOptions') ||
11681201
caps['map_'].get('moz:debuggerAddress') ||
1169-
new Map();
1202+
new Map()
11701203
const debuggerUrl = seCdp || vendorInfo['debuggerAddress'] || vendorInfo
11711204
this._wsUrl = await this.getWsUrl(debuggerUrl, target)
11721205

@@ -1374,7 +1407,10 @@ class WebDriver {
13741407
.toString()
13751408
} catch {
13761409
mutationListener = fs
1377-
.readFileSync(path.resolve(__dirname, './atoms/mutation-listener.js'), 'utf-8')
1410+
.readFileSync(
1411+
path.resolve(__dirname, './atoms/mutation-listener.js'),
1412+
'utf-8'
1413+
)
13781414
.toString()
13791415
}
13801416

@@ -1696,7 +1732,7 @@ class Options {
16961732
} else if (typeof value !== 'undefined') {
16971733
throw TypeError(
16981734
'invalid timeouts configuration:' +
1699-
` expected "${key}" to be a number, got ${typeof value}`
1735+
` expected "${key}" to be a number, got ${typeof value}`
17001736
)
17011737
}
17021738
}
@@ -1757,7 +1793,7 @@ function legacyTimeout(driver, type, ms) {
17571793
*
17581794
* @record
17591795
*/
1760-
Options.Cookie = function () { }
1796+
Options.Cookie = function () {}
17611797

17621798
/**
17631799
* The name of the cookie.
@@ -2462,18 +2498,18 @@ class WebElement {
24622498
*/
24632499
async sendKeys(...args) {
24642500
let keys = []
2465-
; (await Promise.all(args)).forEach((key) => {
2466-
let type = typeof key
2467-
if (type === 'number') {
2468-
key = String(key)
2469-
} else if (type !== 'string') {
2470-
throw TypeError('each key must be a number of string; got ' + type)
2471-
}
2501+
;(await Promise.all(args)).forEach((key) => {
2502+
let type = typeof key
2503+
if (type === 'number') {
2504+
key = String(key)
2505+
} else if (type !== 'string') {
2506+
throw TypeError('each key must be a number of string; got ' + type)
2507+
}
24722508

2473-
// The W3C protocol requires keys to be specified as an array where
2474-
// each element is a single key.
2475-
keys.push(...key.split(''))
2476-
})
2509+
// The W3C protocol requires keys to be specified as an array where
2510+
// each element is a single key.
2511+
keys.push(...key.split(''))
2512+
})
24772513

24782514
if (!this.driver_.fileDetector_) {
24792515
return this.execute_(

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

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -439,7 +439,24 @@ suite(function (env) {
439439
for (let i = 0; i < elements.length; i++) {
440440
ids.push(await elements[i].getAttribute('id'))
441441
}
442-
assert.notEqual(ids.indexOf('third'), -1, `Elements are ${ids}`)
442+
assert.notDeepStrictEqual(ids.indexOf('third'), -1, `Elements are ${ids}`)
443+
})
444+
})
445+
446+
describe('RelativeBy with findElement', function () {
447+
it('finds an element above', async function () {
448+
await driver.get(Pages.relativeLocators)
449+
let below = await driver.findElement(By.id('below'))
450+
let element = await driver.findElement(withTagName('p').above(below))
451+
assert.deepStrictEqual(await element.getAttribute('id'), `mid`)
452+
})
453+
454+
it('should combine filters', async function () {
455+
await driver.get(Pages.relativeLocators)
456+
let element = await driver.findElement(
457+
withTagName('td').above(By.id('center')).toRightOf(By.id('second'))
458+
)
459+
assert.deepStrictEqual(await element.getAttribute('id'), `third`)
443460
})
444461
})
445462

0 commit comments

Comments
 (0)