Skip to content

Commit 5d2f38a

Browse files
committed
[js] Port the By class to node with ES2015
Still maintains the legacy locator class (which is defined using the closure library) as a super type to maintain compatibility with the core WebDriver class (which is still defined using the closure library).
1 parent 0fa587d commit 5d2f38a

File tree

6 files changed

+423
-8
lines changed

6 files changed

+423
-8
lines changed

javascript/node/deploy.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,7 @@ function copyResources(outputDirPath, resources, exclusions) {
344344

345345
function generateDocs(outputDir, callback) {
346346
var libDir = path.join(outputDir, 'lib');
347+
347348
var excludedDirs = [
348349
path.join(outputDir, 'example'),
349350
path.join(libDir, 'test'),
@@ -352,6 +353,7 @@ function generateDocs(outputDir, callback) {
352353
];
353354

354355
var excludedFiles = [
356+
path.join(libDir, '_base.js'),
355357
path.join(libDir, 'safari/client.js'),
356358
path.join(libDir, 'webdriver/builder.js'),
357359
path.join(libDir, 'webdriver/firefoxdomexecutor.js'),
@@ -383,7 +385,9 @@ function generateDocs(outputDir, callback) {
383385
return files;
384386
};
385387

386-
var sourceFiles = getFiles(libDir);
388+
var sourceFiles = getFiles(libDir).filter(function(file) {
389+
return path.dirname(file) !== libDir;
390+
});
387391
var moduleFiles = getFiles(outputDir).filter(function(file) {
388392
return sourceFiles.indexOf(file) == -1;
389393
});

javascript/node/selenium-webdriver/CHANGES.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@
1212
the W3C spec and _not_ the numeric code used by the Selenium project's
1313
wire protocol.
1414

15+
### Changes for W3C WebDriver Spec Compliance
16+
17+
* Updated the `By` locators that are not in the W3C spec to delegated to using
18+
CSS selectors: `By.className`, `By.id`, `By.name`, and `By.tagName`.
19+
1520
## v2.48.2
1621

1722
* Added `WebElement#takeScreenshot()`.

javascript/node/selenium-webdriver/index.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,7 @@ exports.ActionSequence = base.require('webdriver.ActionSequence');
3838
exports.Builder = builder.Builder;
3939

4040

41-
/** @type {webdriver.By.} */
42-
exports.By = base.require('webdriver.By');
41+
exports.By = require('./lib/by').By;
4342

4443

4544
/** @type {function(new: webdriver.Capabilities)} */
Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
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+
const LegacyBy = require('./_base').require('webdriver.Locator');
25+
26+
/**
27+
* Short-hand expressions for the primary element locator strategies.
28+
* For example the following two statements are equivalent:
29+
*
30+
* var e1 = driver.findElement(By.id('foo'));
31+
* var e2 = driver.findElement({id: 'foo'});
32+
*
33+
* Care should be taken when using JavaScript minifiers (such as the
34+
* Closure compiler), as locator hashes will always be parsed using
35+
* the un-obfuscated properties listed.
36+
*
37+
* @typedef {(
38+
* {className: string}|
39+
* {css: string}|
40+
* {id: string}|
41+
* {js: string}|
42+
* {linkText: string}|
43+
* {name: string}|
44+
* {partialLinkText: string}|
45+
* {tagName: string}|
46+
* {xpath: string})}
47+
*/
48+
var ByHash;
49+
50+
51+
/**
52+
* Error thrown if an invalid character is encountered while escaping a CSS
53+
* identifier.
54+
* @see https://drafts.csswg.org/cssom/#serialize-an-identifier
55+
*/
56+
class InvalidCharacterError extends Error {
57+
constructor(msg) {
58+
super(msg);
59+
this.name = this.constructor.name;
60+
}
61+
}
62+
63+
64+
/**
65+
* Escapes a CSS string.
66+
* @param {string} css the string to escape.
67+
* @return {string} the escaped string.
68+
* @throws {TypeError} if the input value is not a string.
69+
* @throws {InvalidCharacterError} if the string contains an invalid character.
70+
* @see https://drafts.csswg.org/cssom/#serialize-an-identifier
71+
*/
72+
function escapeCss(css) {
73+
if (typeof css !== 'string') {
74+
throw new TypeError('input must be a string');
75+
}
76+
let ret = '';
77+
const n = css.length;
78+
for (let i = 0; i < n; i++) {
79+
const c = css.charCodeAt(i);
80+
if (c == 0x0) {
81+
throw new InvalidCharacterError();
82+
}
83+
84+
if ((c >= 0x0001 && c <= 0x001F)
85+
|| c == 0x007F
86+
|| (i == 0 && c >= 0x0030 && c <= 0x0039)
87+
|| (i == 1 && c >= 0x0030 && c <= 0x0039
88+
&& css.charCodeAt(0) == 0x002D)) {
89+
ret += '\\' + c.toString(16);
90+
continue;
91+
}
92+
93+
if (i == 0 && c == 0x002D && n == 1) {
94+
ret += '\\' + css.charAt(i);
95+
continue;
96+
}
97+
98+
if (c >= 0x0080
99+
|| c == 0x002D // -
100+
|| c == 0x005F // _
101+
|| (c >= 0x0030 && c <= 0x0039) // [0-9]
102+
|| (c >= 0x0041 && c <= 0x005A) // [A-Z]
103+
|| (c >= 0x0061 && c <= 0x007A)) { // [a-z]
104+
ret += css.charAt(i);
105+
continue;
106+
}
107+
108+
ret += '\\' + css.charAt(i);
109+
}
110+
return ret;
111+
}
112+
113+
114+
/**
115+
* Describes a mechanism for locating an element on the page.
116+
* @final
117+
*/
118+
class By extends LegacyBy {
119+
/**
120+
* @param {string} using the name of the location strategy to use.
121+
* @param {string} value the value to search for.
122+
*/
123+
constructor(using, value) {
124+
// TODO: this is a legacy hack that can be removed once fully off closure.
125+
super(using, value);
126+
127+
/** @type {string} */
128+
this.using = using;
129+
130+
/** @type {string} */
131+
this.value = value;
132+
}
133+
134+
/**
135+
* Locates elements that have a specific class name.
136+
*
137+
* @param {string} className The class name to search for.
138+
* @return {!By} The new locator.
139+
* @see http://www.w3.org/TR/2011/WD-html5-20110525/elements.html#classes
140+
* @see http://www.w3.org/TR/CSS2/selector.html#class-html
141+
*/
142+
static className(name) {
143+
let names = name.split(/\s+/g)
144+
.filter(s => s.length > 0)
145+
.map(s => escapeCss(s));
146+
return By.css('.' + names.join('.'));
147+
}
148+
149+
/**
150+
* Locates elements using a CSS selector.
151+
*
152+
* @param {string} selector The CSS selector to use.
153+
* @return {!By} The new locator.
154+
* @see http://www.w3.org/TR/CSS2/selector.html
155+
*/
156+
static css(selector) {
157+
return new By('css selector', selector);
158+
}
159+
160+
/**
161+
* Locates eleemnts by the ID attribute. This locator uses the CSS selector
162+
* `*[id="$ID"]`, _not_ `document.getElementById`.
163+
*
164+
* @param {string} id The ID to search for.
165+
* @return {!By} The new locator.
166+
*/
167+
static id(id) {
168+
return By.css('*[id="' + escapeCss(id) + '"]');
169+
}
170+
171+
/**
172+
* Locates link elements whose
173+
* {@linkplain webdriver.WebElement#getText visible text} matches the given
174+
* string.
175+
*
176+
* @param {string} text The link text to search for.
177+
* @return {!By} The new locator.
178+
*/
179+
static linkText(text) {
180+
return new By('link text', text);
181+
}
182+
183+
/**
184+
* Locates an elements by evaluating a
185+
* {@linkplain webdriver.WebDriver#executeScript JavaScript expression}.
186+
* The result of this expression must be an element or list of elements.
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): !webdriver.promise.Promise} A new,
191+
* JavaScript-based locator function.
192+
*/
193+
static js(script, var_args) {
194+
let args = Array.slice.call(arguments, 0);
195+
return function(driver) {
196+
return driver.executeScript.apply(driver, args);
197+
};
198+
}
199+
200+
/**
201+
* Locates elements whose `name` attribute has the given value.
202+
*
203+
* @param {string} name The name attribute to search for.
204+
* @return {!By} The new locator.
205+
*/
206+
static name(name) {
207+
return By.css('*[name="' + escapeCss(name) + '"]');
208+
}
209+
210+
/**
211+
* Locates link elements whose
212+
* {@linkplain webdriver.WebElement#getText visible text} contains the given
213+
* substring.
214+
*
215+
* @param {string} text The substring to check for in a link's visible text.
216+
* @return {!By} The new locator.
217+
*/
218+
static partialLinkText(text) {
219+
return new By('partial link text', text);
220+
}
221+
222+
/**
223+
* Locates elements with a given tag name.
224+
*
225+
* @param {string} name The tag name to search for.
226+
* @return {!By} The new locator.
227+
* @deprecated Use {@link By.css() By.css(tagName)} instead.
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+
return this.constructor.name + '(' + this.using + ', ' + this.value + ')';
251+
}
252+
}
253+
254+
255+
/**
256+
* Checks if a value is a valid locator.
257+
* @param {(By|Function|ByHash)} locator The value to check.
258+
* @return {(By|Function)} The valid locator.
259+
* @throws {TypeError} If the given value does not define a valid locator
260+
* strategy.
261+
*/
262+
function check(locator) {
263+
if (locator instanceof By || typeof locator === 'function') {
264+
return locator;
265+
}
266+
for (let key in locator) {
267+
if (locator.hasOwnProperty(key) && By.hasOwnProperty(key)) {
268+
return By[key](locator[key]);
269+
}
270+
}
271+
throw new TypeError('Invalid locator');
272+
}
273+
274+
275+
276+
// PUBLIC API
277+
278+
279+
exports.By = By;
280+
exports.checkedLocator = check;

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -241,11 +241,11 @@ test.suite(function(env) {
241241
});
242242
});
243243

244-
test.it('does not permit compound class names', function() {
245-
driver.get(Pages.xhtmlTestPage);
246-
driver.findElement(By.className('a b')).then(fail, pass);
247-
driver.findElements(By.className('a b')).then(fail, pass);
248-
function pass() {}
244+
test.it('permits compound class names', function() {
245+
return driver.get(Pages.xhtmlTestPage)
246+
.then(() => driver.findElement(By.className('nameA nameC')))
247+
.then(el => el.getText())
248+
.then(text => assert(text).equalTo('An H2 title'));
249249
});
250250
});
251251

0 commit comments

Comments
 (0)