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