UNPKG

11.5 kBJavaScriptView Raw
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})}
45 */
46var 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 */
53class 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 */
68function 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 */
117class 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 */
270function 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 */
279function locateWith(by) {
280 return new RelativeBy(getLocator(by));
281}
282
283function 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 */
298class 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 */
401function 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
428module.exports = {
429 By: By,
430 RelativeBy: RelativeBy,
431 withTagName: withTagName,
432 locateWith: locateWith,
433 checkedLocator: check,
434}