Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
seleniumhq
GitHub Repository: seleniumhq/selenium
Path: blob/trunk/javascript/selenium-webdriver/lib/by.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
'use strict'
19
20
/**
21
* @fileoverview Factory methods for the supported locator strategies.
22
*/
23
24
/**
25
* Short-hand expressions for the primary element locator strategies.
26
* For example the following two statements are equivalent:
27
*
28
* var e1 = driver.findElement(By.id('foo'));
29
* var e2 = driver.findElement({id: 'foo'});
30
*
31
* Care should be taken when using JavaScript minifiers (such as the
32
* Closure compiler), as locator hashes will always be parsed using
33
* the un-obfuscated properties listed.
34
*
35
* @typedef {(
36
* {className: string}|
37
* {css: string}|
38
* {id: string}|
39
* {js: string}|
40
* {linkText: string}|
41
* {name: string}|
42
* {partialLinkText: string}|
43
* {tagName: string}|
44
* {xpath: string})} ByHash
45
*/
46
47
/**
48
* Error thrown if an invalid character is encountered while escaping a CSS
49
* identifier.
50
* @see https://drafts.csswg.org/cssom/#serialize-an-identifier
51
*/
52
class InvalidCharacterError extends Error {
53
constructor() {
54
super()
55
this.name = this.constructor.name
56
}
57
}
58
59
/**
60
* Escapes a CSS string.
61
* @param {string} css the string to escape.
62
* @return {string} the escaped string.
63
* @throws {TypeError} if the input value is not a string.
64
* @throws {InvalidCharacterError} if the string contains an invalid character.
65
* @see https://drafts.csswg.org/cssom/#serialize-an-identifier
66
*/
67
function escapeCss(css) {
68
if (typeof css !== 'string') {
69
throw new TypeError('input must be a string')
70
}
71
let ret = ''
72
const n = css.length
73
for (let i = 0; i < n; i++) {
74
const c = css.charCodeAt(i)
75
if (c == 0x0) {
76
throw new InvalidCharacterError()
77
}
78
79
if (
80
(c >= 0x0001 && c <= 0x001f) ||
81
c == 0x007f ||
82
(i == 0 && c >= 0x0030 && c <= 0x0039) ||
83
(i == 1 && c >= 0x0030 && c <= 0x0039 && css.charCodeAt(0) == 0x002d)
84
) {
85
ret += '\\' + c.toString(16) + ' '
86
continue
87
}
88
89
if (i == 0 && c == 0x002d && n == 1) {
90
ret += '\\' + css.charAt(i)
91
continue
92
}
93
94
if (
95
c >= 0x0080 ||
96
c == 0x002d || // -
97
c == 0x005f || // _
98
(c >= 0x0030 && c <= 0x0039) || // [0-9]
99
(c >= 0x0041 && c <= 0x005a) || // [A-Z]
100
(c >= 0x0061 && c <= 0x007a)
101
) {
102
// [a-z]
103
ret += css.charAt(i)
104
continue
105
}
106
107
ret += '\\' + css.charAt(i)
108
}
109
return ret
110
}
111
112
/**
113
* Describes a mechanism for locating an element on the page.
114
* @final
115
*/
116
class By {
117
/**
118
* @param {string} using the name of the location strategy to use.
119
* @param {string} value the value to search for.
120
*/
121
constructor(using, value) {
122
/** @type {string} */
123
this.using = using
124
125
/** @type {string} */
126
this.value = value
127
}
128
129
/**
130
* Locates elements that have a specific class name.
131
*
132
* @param {string} name The class name to search for.
133
* @return {!By} The new locator.
134
* @see http://www.w3.org/TR/2011/WD-html5-20110525/elements.html#classes
135
* @see http://www.w3.org/TR/CSS2/selector.html#class-html
136
*/
137
static className(name) {
138
let names = name
139
.split(/\s+/g)
140
.filter((s) => s.length > 0)
141
.map((s) => escapeCss(s))
142
return By.css('.' + names.join('.'))
143
}
144
145
/**
146
* Locates elements using a CSS selector.
147
*
148
* @param {string} selector The CSS selector to use.
149
* @return {!By} The new locator.
150
* @see http://www.w3.org/TR/CSS2/selector.html
151
*/
152
static css(selector) {
153
return new By('css selector', selector)
154
}
155
156
/**
157
* Locates elements by the ID attribute. This locator uses the CSS selector
158
* `*[id="$ID"]`, _not_ `document.getElementById`.
159
*
160
* @param {string} id The ID to search for.
161
* @return {!By} The new locator.
162
*/
163
static id(id) {
164
return By.css('*[id="' + escapeCss(id) + '"]')
165
}
166
167
/**
168
* Locates link elements whose
169
* {@linkplain webdriver.WebElement#getText visible text} matches the given
170
* string.
171
*
172
* @param {string} text The link text to search for.
173
* @return {!By} The new locator.
174
*/
175
static linkText(text) {
176
return new By('link text', text)
177
}
178
179
/**
180
* Locates elements by evaluating a `script` that defines the body of
181
* a {@linkplain webdriver.WebDriver#executeScript JavaScript function}.
182
* The return value of this function must be an element or an array-like
183
* list of elements. When this locator returns a list of elements, but only
184
* one is expected, the first element in this list will be used as the
185
* single element value.
186
*
187
* @param {!(string|Function)} script The script to execute.
188
* @param {...*} var_args The arguments to pass to the script.
189
* @return {function(!./webdriver.WebDriver): !Promise}
190
* A new JavaScript-based locator function.
191
*/
192
static js(script, ...var_args) {
193
return function (driver) {
194
return driver.executeScript.call(driver, script, ...var_args)
195
}
196
}
197
198
/**
199
* Locates elements whose `name` attribute has the given value.
200
*
201
* @param {string} name The name attribute to search for.
202
* @return {!By} The new locator.
203
*/
204
static name(name) {
205
return By.css('*[name="' + escapeCss(name) + '"]')
206
}
207
208
/**
209
* Locates link elements whose
210
* {@linkplain webdriver.WebElement#getText visible text} contains the given
211
* substring.
212
*
213
* @param {string} text The substring to check for in a link's visible text.
214
* @return {!By} The new locator.
215
*/
216
static partialLinkText(text) {
217
return new By('partial link text', text)
218
}
219
220
/**
221
* Locates elements with a given tag name.
222
*
223
* @param {string} name The tag name to search for.
224
* @return {!By} The new locator.
225
*/
226
static tagName(name) {
227
return new By('tag name', name)
228
}
229
230
/**
231
* Locates elements matching a XPath selector. Care should be taken when
232
* using an XPath selector with a {@link webdriver.WebElement} as WebDriver
233
* will respect the context in the specified in the selector. For example,
234
* given the selector `//div`, WebDriver will search from the document root
235
* regardless of whether the locator was used with a WebElement.
236
*
237
* @param {string} xpath The XPath selector to use.
238
* @return {!By} The new locator.
239
* @see http://www.w3.org/TR/xpath/
240
*/
241
static xpath(xpath) {
242
return new By('xpath', xpath)
243
}
244
245
/** @override */
246
toString() {
247
// The static By.name() overrides this.constructor.name. Shame...
248
return `By(${this.using}, ${this.value})`
249
}
250
251
toObject() {
252
const tmp = {}
253
tmp[this.using] = this.value
254
return tmp
255
}
256
}
257
258
/**
259
* Start Searching for relative objects using the value returned from
260
* `By.tagName()`.
261
*
262
* Note: this method will likely be removed in the future please use
263
* `locateWith`.
264
* @param {By} tagName The value returned from calling By.tagName()
265
* @returns
266
*/
267
function withTagName(tagName) {
268
return new RelativeBy({ 'css selector': tagName })
269
}
270
271
/**
272
* Start searching for relative objects using search criteria with By.
273
* @param {string} by A By map that shows how to find the initial element
274
* @returns {RelativeBy}
275
*/
276
function locateWith(by) {
277
return new RelativeBy(getLocator(by))
278
}
279
280
function getLocator(locatorOrElement) {
281
let toFind
282
if (locatorOrElement instanceof By) {
283
toFind = locatorOrElement.toObject()
284
} else {
285
toFind = locatorOrElement
286
}
287
return toFind
288
}
289
290
/**
291
* Describes a mechanism for locating an element relative to others
292
* on the page.
293
* @final
294
*/
295
class RelativeBy {
296
/**
297
* @param {By} findDetails
298
* @param {Array<Object>} filters
299
*/
300
constructor(findDetails, filters = null) {
301
this.root = findDetails
302
this.filters = filters || []
303
}
304
305
/**
306
* Look for elements above the root element passed in
307
* @param {string|WebElement} locatorOrElement
308
* @return {!RelativeBy} Return this object
309
*/
310
above(locatorOrElement) {
311
this.filters.push({
312
kind: 'above',
313
args: [getLocator(locatorOrElement)],
314
})
315
return this
316
}
317
318
/**
319
* Look for elements below the root element passed in
320
* @param {string|WebElement} locatorOrElement
321
* @return {!RelativeBy} Return this object
322
*/
323
below(locatorOrElement) {
324
this.filters.push({
325
kind: 'below',
326
args: [getLocator(locatorOrElement)],
327
})
328
return this
329
}
330
331
/**
332
* Look for elements left the root element passed in
333
* @param {string|WebElement} locatorOrElement
334
* @return {!RelativeBy} Return this object
335
*/
336
toLeftOf(locatorOrElement) {
337
this.filters.push({
338
kind: 'left',
339
args: [getLocator(locatorOrElement)],
340
})
341
return this
342
}
343
344
/**
345
* Look for elements right the root element passed in
346
* @param {string|WebElement} locatorOrElement
347
* @return {!RelativeBy} Return this object
348
*/
349
toRightOf(locatorOrElement) {
350
this.filters.push({
351
kind: 'right',
352
args: [getLocator(locatorOrElement)],
353
})
354
return this
355
}
356
357
/**
358
* Look for elements above the root element passed in
359
* @param {string|WebElement} locatorOrElement
360
* @return {!RelativeBy} Return this object
361
*/
362
straightAbove(locatorOrElement) {
363
this.filters.push({
364
kind: 'straightAbove',
365
args: [getLocator(locatorOrElement)],
366
})
367
return this
368
}
369
370
/**
371
* Look for elements below the root element passed in
372
* @param {string|WebElement} locatorOrElement
373
* @return {!RelativeBy} Return this object
374
*/
375
straightBelow(locatorOrElement) {
376
this.filters.push({
377
kind: 'straightBelow',
378
args: [getLocator(locatorOrElement)],
379
})
380
return this
381
}
382
383
/**
384
* Look for elements left the root element passed in
385
* @param {string|WebElement} locatorOrElement
386
* @return {!RelativeBy} Return this object
387
*/
388
straightToLeftOf(locatorOrElement) {
389
this.filters.push({
390
kind: 'straightLeft',
391
args: [getLocator(locatorOrElement)],
392
})
393
return this
394
}
395
396
/**
397
* Look for elements right the root element passed in
398
* @param {string|WebElement} locatorOrElement
399
* @return {!RelativeBy} Return this object
400
*/
401
straightToRightOf(locatorOrElement) {
402
this.filters.push({
403
kind: 'straightRight',
404
args: [getLocator(locatorOrElement)],
405
})
406
return this
407
}
408
409
/**
410
* Look for elements near the root element passed in
411
* @param {string|WebElement} locatorOrElement
412
* @return {!RelativeBy} Return this object
413
*/
414
near(locatorOrElement) {
415
this.filters.push({
416
kind: 'near',
417
args: [getLocator(locatorOrElement)],
418
})
419
return this
420
}
421
422
/**
423
* Returns a marshalled version of the {@link RelativeBy}
424
* @return {!Object} Object representation of a {@link WebElement}
425
* that will be used in {@link #findElements}.
426
*/
427
marshall() {
428
return {
429
relative: {
430
root: this.root,
431
filters: this.filters,
432
},
433
}
434
}
435
436
/** @override */
437
toString() {
438
// The static By.name() overrides this.constructor.name. Shame...
439
return `RelativeBy(${JSON.stringify(this.marshall())})`
440
}
441
}
442
443
/**
444
* Checks if a value is a valid locator.
445
* @param {!(By|Function|ByHash)} locator The value to check.
446
* @return {!(By|Function)} The valid locator.
447
* @throws {TypeError} If the given value does not define a valid locator
448
* strategy.
449
*/
450
function check(locator) {
451
if (locator instanceof By || locator instanceof RelativeBy || typeof locator === 'function') {
452
return locator
453
}
454
455
if (
456
locator &&
457
typeof locator === 'object' &&
458
typeof locator.using === 'string' &&
459
typeof locator.value === 'string'
460
) {
461
return new By(locator.using, locator.value)
462
}
463
464
for (let key in locator) {
465
if (Object.prototype.hasOwnProperty.call(locator, key) && Object.prototype.hasOwnProperty.call(By, key)) {
466
return By[key](locator[key])
467
}
468
}
469
throw new TypeError('Invalid locator')
470
}
471
472
// PUBLIC API
473
474
module.exports = {
475
By,
476
RelativeBy,
477
withTagName,
478
locateWith,
479
escapeCss,
480
checkedLocator: check,
481
}
482
483