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;
|
47 |
|
48 |
|
49 | /**
|
50 | * Error thrown if an invalid character is encountered while escaping a CSS
|
51 | * identifier.
|
52 | * @see https://drafts.csswg.org/cssom/#serialize-an-identifier
|
53 | */
|
54 | class InvalidCharacterError extends Error {
|
55 | constructor() {
|
56 | super();
|
57 | this.name = this.constructor.name;
|
58 | }
|
59 | }
|
60 |
|
61 |
|
62 | /**
|
63 | * Escapes a CSS string.
|
64 | * @param {string} css the string to escape.
|
65 | * @return {string} the escaped string.
|
66 | * @throws {TypeError} if the input value is not a string.
|
67 | * @throws {InvalidCharacterError} if the string contains an invalid character.
|
68 | * @see https://drafts.csswg.org/cssom/#serialize-an-identifier
|
69 | */
|
70 | function escapeCss(css) {
|
71 | if (typeof css !== 'string') {
|
72 | throw new TypeError('input must be a string');
|
73 | }
|
74 | let ret = '';
|
75 | const n = css.length;
|
76 | for (let i = 0; i < n; i++) {
|
77 | const c = css.charCodeAt(i);
|
78 | if (c == 0x0) {
|
79 | throw new InvalidCharacterError();
|
80 | }
|
81 |
|
82 | if ((c >= 0x0001 && c <= 0x001F)
|
83 | || c == 0x007F
|
84 | || (i == 0 && c >= 0x0030 && c <= 0x0039)
|
85 | || (i == 1 && c >= 0x0030 && c <= 0x0039
|
86 | && css.charCodeAt(0) == 0x002D)) {
|
87 | ret += '\\' + c.toString(16) + ' ';
|
88 | continue;
|
89 | }
|
90 |
|
91 | if (i == 0 && c == 0x002D && n == 1) {
|
92 | ret += '\\' + css.charAt(i);
|
93 | continue;
|
94 | }
|
95 |
|
96 | if (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)) { // [a-z]
|
102 | ret += css.charAt(i);
|
103 | continue;
|
104 | }
|
105 |
|
106 | ret += '\\' + css.charAt(i);
|
107 | }
|
108 | return ret;
|
109 | }
|
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.split(/\s+/g)
|
139 | .filter(s => s.length > 0)
|
140 | .map(s => escapeCss(s));
|
141 | return By.css('.' + names.join('.'));
|
142 | }
|
143 |
|
144 | /**
|
145 | * Locates elements using a CSS selector.
|
146 | *
|
147 | * @param {string} selector The CSS selector to use.
|
148 | * @return {!By} The new locator.
|
149 | * @see http://www.w3.org/TR/CSS2/selector.html
|
150 | */
|
151 | static css(selector) {
|
152 | return new By('css selector', selector);
|
153 | }
|
154 |
|
155 | /**
|
156 | * Locates eleemnts by the ID attribute. This locator uses the CSS selector
|
157 | * `*[id="$ID"]`, _not_ `document.getElementById`.
|
158 | *
|
159 | * @param {string} id The ID to search for.
|
160 | * @return {!By} The new locator.
|
161 | */
|
162 | static id(id) {
|
163 | return By.css('*[id="' + escapeCss(id) + '"]');
|
164 | }
|
165 |
|
166 | /**
|
167 | * Locates link elements whose
|
168 | * {@linkplain webdriver.WebElement#getText visible text} matches the given
|
169 | * string.
|
170 | *
|
171 | * @param {string} text The link text to search for.
|
172 | * @return {!By} The new locator.
|
173 | */
|
174 | static linkText(text) {
|
175 | return new By('link text', text);
|
176 | }
|
177 |
|
178 | /**
|
179 | * Locates an elements by evaluating a
|
180 | * {@linkplain webdriver.WebDriver#executeScript JavaScript expression}.
|
181 | * The result of this expression must be an element or list of elements.
|
182 | *
|
183 | * @param {!(string|Function)} script The script to execute.
|
184 | * @param {...*} var_args The arguments to pass to the script.
|
185 | * @return {function(!./webdriver.WebDriver): !./promise.Promise}
|
186 | * A new JavaScript-based locator function.
|
187 | */
|
188 | static js(script, var_args) {
|
189 | let args = Array.prototype.slice.call(arguments, 0);
|
190 | return function(driver) {
|
191 | return driver.executeScript.apply(driver, args);
|
192 | };
|
193 | }
|
194 |
|
195 | /**
|
196 | * Locates elements whose `name` attribute has the given value.
|
197 | *
|
198 | * @param {string} name The name attribute to search for.
|
199 | * @return {!By} The new locator.
|
200 | */
|
201 | static name(name) {
|
202 | return By.css('*[name="' + escapeCss(name) + '"]');
|
203 | }
|
204 |
|
205 | /**
|
206 | * Locates link elements whose
|
207 | * {@linkplain webdriver.WebElement#getText visible text} contains the given
|
208 | * substring.
|
209 | *
|
210 | * @param {string} text The substring to check for in a link's visible text.
|
211 | * @return {!By} The new locator.
|
212 | */
|
213 | static partialLinkText(text) {
|
214 | return new By('partial link text', text);
|
215 | }
|
216 |
|
217 | /**
|
218 | * Locates elements with a given tag name.
|
219 | *
|
220 | * @param {string} name The tag name to search for.
|
221 | * @return {!By} The new locator.
|
222 | * @deprecated Use {@link By.css() By.css(tagName)} instead.
|
223 | */
|
224 | static tagName(name) {
|
225 | return By.css(name);
|
226 | }
|
227 |
|
228 | /**
|
229 | * Locates elements matching a XPath selector. Care should be taken when
|
230 | * using an XPath selector with a {@link webdriver.WebElement} as WebDriver
|
231 | * will respect the context in the specified in the selector. For example,
|
232 | * given the selector `//div`, WebDriver will search from the document root
|
233 | * regardless of whether the locator was used with a WebElement.
|
234 | *
|
235 | * @param {string} xpath The XPath selector to use.
|
236 | * @return {!By} The new locator.
|
237 | * @see http://www.w3.org/TR/xpath/
|
238 | */
|
239 | static xpath(xpath) {
|
240 | return new By('xpath', xpath);
|
241 | }
|
242 |
|
243 | /** @override */
|
244 | toString() {
|
245 | // The static By.name() overrides this.constructor.name. Shame...
|
246 | return `By(${this.using}, ${this.value})`;
|
247 | }
|
248 | }
|
249 |
|
250 |
|
251 | /**
|
252 | * Checks if a value is a valid locator.
|
253 | * @param {!(By|Function|ByHash)} locator The value to check.
|
254 | * @return {!(By|Function)} The valid locator.
|
255 | * @throws {TypeError} If the given value does not define a valid locator
|
256 | * strategy.
|
257 | */
|
258 | function check(locator) {
|
259 | if (locator instanceof By || typeof locator === 'function') {
|
260 | return locator;
|
261 | }
|
262 | for (let key in locator) {
|
263 | if (locator.hasOwnProperty(key) && By.hasOwnProperty(key)) {
|
264 | return By[key](locator[key]);
|
265 | }
|
266 | }
|
267 | throw new TypeError('Invalid locator');
|
268 | }
|
269 |
|
270 |
|
271 |
|
272 | // PUBLIC API
|
273 |
|
274 | module.exports = {
|
275 | By: By,
|
276 | checkedLocator: check,
|
277 | };
|