Skip to content

Commit a107dd7

Browse files
committed
Fixes nightwatchjs#1115 - Adds support for object-based selectors in page elements format
1 parent e9f66f1 commit a107dd7

File tree

13 files changed

+1162
-226
lines changed

13 files changed

+1162
-226
lines changed

lib/api/element-commands.js

+36-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ var util = require('util');
22
var events = require('events');
33
var Logger = require('../util/logger.js');
44
var Utils = require('../util/utils.js');
5+
var Element = require('../page-object/element.js');
56

67
module.exports = function(client) {
78
var Protocol = require('./protocol.js')(client);
@@ -341,10 +342,36 @@ module.exports = function(client) {
341342
events.EventEmitter.call(this);
342343

343344
var $this = this;
344-
var el = Protocol.element(using, value, function(result) {
345+
346+
// element commands support a single element but we use elements() to capture
347+
// a full list of matching elements for the selector so that we can support
348+
// the use of element.index if available. If no index is used, the first
349+
// element is matched
350+
351+
var el = Protocol.elements(using, value, function(result) {
352+
353+
// if the request was successful but no elements were returned, that's an error
354+
// since we expect at least one to exist, so we change the status to failing.
355+
356+
if (result.status === 0 && result.value.length === 0) {
357+
result.status = -1;
358+
}
359+
345360
if (result.status !== 0) {
361+
346362
callback.call(client.api, result);
347-
var errorMessage = 'ERROR: Unable to locate element: "' + value + '" using: ' + using;
363+
364+
var elem = Element.fromSelector(value, using);
365+
var errorMessage = 'ERROR: Unable to locate element: "' + elem.selector + '" using: "' + elem.locateStrategy + '"';
366+
367+
// when an index is specified, include it in the error message
368+
// but ignore it otherwise since its optional and only extra
369+
// noise if not used
370+
371+
if (elem.index != undefined) {
372+
errorMessage += ' at index: ' + elem.index;
373+
}
374+
348375
var stack = originalStackTrace.split('\n');
349376

350377
stack.shift();
@@ -354,8 +381,14 @@ module.exports = function(client) {
354381
client.errors.push(errorMessage + '\n' + stack.join('\n'));
355382

356383
$this.emit('complete', el, $this);
384+
357385
} else {
358-
result = result.value.ELEMENT;
386+
387+
// we only care about the first element. If an index was used, it would
388+
// have already been applied to the result and first element ([0])
389+
// would be that element
390+
391+
result = result.value[0].ELEMENT;
359392

360393
args.push(function(r) {
361394
callback.call(client.api, r);

lib/api/element-commands/_elementByRecursion.js

+8-4
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ var events = require('events');
88
* @param {function} [callback] Optional callback function to be called when the command finishes.
99
* @api protocol
1010
*/
11-
1211
function ElementByRecursion(client) {
1312
events.EventEmitter.call(this);
1413
this.protocol = require('../protocol.js')(client);
@@ -18,18 +17,23 @@ util.inherits(ElementByRecursion, events.EventEmitter);
1817

1918
ElementByRecursion.prototype.command = function(elements, callback) {
2019
var self = this;
21-
var allElements = elements.slice();
2220

21+
var allElements = elements.slice();
2322
var topElement = allElements.shift();
24-
var el = this.protocol.element(topElement.locateStrategy, topElement.selector, function checkResult(result) {
23+
24+
// since we're dealing with an array of element objects the using
25+
// parameter (null) in the protocol commands is redundant since
26+
// its already encoded in the element object (topElement)
27+
28+
var el = this.protocol.element(null, topElement, function checkResult(result) {
2529
if (result.status !== 0) {
2630
callback(result);
2731
self.emit('complete', el, self);
2832
} else {
2933
var nextElement = allElements.shift();
3034
var parentId = result.value.ELEMENT;
3135
if (nextElement) {
32-
self.protocol.elementIdElement(parentId, nextElement.locateStrategy, nextElement.selector, checkResult);
36+
self.protocol.elementIdElement(parentId, null, nextElement, checkResult);
3337
} else {
3438
callback(result);
3539
self.emit('complete', el, self);

lib/api/element-commands/_elementsByRecursion.js

+6-3
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ var Q = require('q');
99
* @param {function} [callback] Optional callback function to be called when the command finishes.
1010
* @api protocol
1111
*/
12-
1312
function ElementsByRecursion(client) {
1413
events.EventEmitter.call(this);
1514
this.protocol = require('../protocol.js')(client);
@@ -65,7 +64,11 @@ ElementsByRecursion.prototype.command = function(elements, callback) {
6564
var allElements = elements.slice();
6665
var topElement = allElements.shift();
6766

68-
var el = this.protocol.elements(topElement.locateStrategy, topElement.selector, function checkResult() {
67+
// since we're dealing with an array of element objects the using
68+
// parameter (null) in the protocol commands is redundant since
69+
// its already encoded in the element object (topElement)
70+
71+
var el = this.protocol.elements(null, topElement, function checkResult() {
6972
var result = aggregateResults(arguments);
7073
if (result.value.length === 0) {
7174
callback(result);
@@ -77,7 +80,7 @@ ElementsByRecursion.prototype.command = function(elements, callback) {
7780
if (nextElement) {
7881
var promises = [];
7982
result.value.forEach(function(el) {
80-
var p = deferredElementIdElements(el.ELEMENT, nextElement.locateStrategy, nextElement.selector, checkResult);
83+
var p = deferredElementIdElements(el.ELEMENT, null, nextElement, checkResult);
8184
promises.push(p);
8285
});
8386

lib/api/element-commands/_waitForElement.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ WaitForElement.prototype.checkElement = function() {
204204
this.getProtocolCommand(function(result) {
205205
var now = new Date().getTime();
206206

207-
if (result.value && result.value.length > 0) {
207+
if (result.status === 0 && result.value && result.value.length > 0) {
208208
if (result.value.length > 1) {
209209
var message = 'WaitForElement found ' + result.value.length + ' elements for selector "' + self.selector + '".';
210210
if (self.throwOnMultipleElementsReturned) {

lib/api/expect/_baseAssertion.js

+10-1
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,16 @@ BaseAssertion.prototype.scheduleRetry = function() {
208208
};
209209

210210
BaseAssertion.prototype.formatMessage = function() {
211-
this.message = Utils.format(this.message || this.message, this.selector);
211+
212+
// selector may be a simple string or an object. If an
213+
// object, show a more meaningful string representation
214+
215+
var selectorStr = String(this.selector);
216+
if (selectorStr === '[object Object]') {
217+
selectorStr = JSON.stringify(this.selector);
218+
}
219+
220+
this.message = Utils.format(this.message, selectorStr);
212221
this.message += this.messageParts.join('');
213222
};
214223

lib/api/protocol.js

+117-48
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
var elementByRecursion = require('./element-commands/_elementByRecursion.js');
22
var elementsByRecursion = require('./element-commands/_elementsByRecursion.js');
3+
var Element = require('../page-object/element.js');
34

45
module.exports = function(Nightwatch) {
56

@@ -164,35 +165,35 @@ module.exports = function(Nightwatch) {
164165
* @api protocol
165166
*/
166167
Actions.element = function(using, value, callback) {
167-
if (using == 'recursion') {
168-
return new elementByRecursion(Nightwatch).command(value, callback);
168+
169+
var elem = Element.fromSelector(value, using);
170+
171+
if (elem.locateStrategy == 'recursion') {
172+
173+
// when using recursion, the selector of the element is an
174+
// array of elements which each need to individually
175+
// and sequentially get resolved on top of each other
176+
177+
return new elementByRecursion(Nightwatch).command(elem.selector, callback);
169178
}
170179

171-
return element(using, value, callback);
180+
return element(elem, callback);
172181
};
173182

174183
/*!
175184
* element protocol action
176185
*
177-
* @param {string} using
178-
* @param {string} value
186+
* @param {Object} elem
179187
* @param {function} callback
180188
* @private
181189
*/
182-
function element(using, value, callback) {
183-
var strategies = ['class name', 'css selector', 'id', 'name', 'link text',
184-
'partial link text', 'tag name', 'xpath'];
185-
using = using.toLocaleLowerCase();
190+
function element(elem, callback) {
186191

187-
if (strategies.indexOf(using) === -1) {
188-
throw new Error('Provided locating strategy is not supported: ' +
189-
using + '. It must be one of the following:\n' +
190-
strategies.join(', '));
191-
}
192+
validateStrategy(elem.locateStrategy);
192193

193194
return postRequest('/element', {
194-
using: using,
195-
value: value
195+
using: elem.locateStrategy,
196+
value: elem.selector
196197
}, callback);
197198
}
198199

@@ -207,19 +208,14 @@ module.exports = function(Nightwatch) {
207208
* @api protocol
208209
*/
209210
Actions.elementIdElement = function(id, using, value, callback) {
210-
var strategies = ['class name', 'css selector', 'id', 'name', 'link text',
211-
'partial link text', 'tag name', 'xpath'];
212-
using = using.toLocaleLowerCase();
213211

214-
if (strategies.indexOf(using) === -1) {
215-
throw new Error('Provided locating strategy is not supported: ' +
216-
using + '. It must be one of the following:\n' +
217-
strategies.join(', '));
218-
}
212+
var elem = Element.fromSelector(value, using);
213+
214+
validateStrategy(elem.locateStrategy);
219215

220216
return postRequest('/element/' + id + '/element', {
221-
using: using,
222-
value: value
217+
using: elem.locateStrategy,
218+
value: elem.selector
223219
}, callback);
224220
};
225221

@@ -234,32 +230,37 @@ module.exports = function(Nightwatch) {
234230
* @api protocol
235231
*/
236232
Actions.elements = function(using, value, callback) {
237-
if (using == 'recursion') {
238-
return new elementsByRecursion(Nightwatch).command(value, callback);
233+
234+
var elem = Element.fromSelector(value, using);
235+
236+
if (elem.locateStrategy == 'recursion') {
237+
238+
// when using recursion, the selector of the element is an
239+
// array of elements which each need to individually
240+
// and sequentially get resolved on top of each other
241+
242+
return new elementsByRecursion(Nightwatch).command(elem.selector, callback);
239243
}
240244

241-
return elements(using, value, callback);
245+
return elements(elem, callback);
242246
};
243247

244248
/*!
245249
* elements protocol action
246250
*
247-
* @param {string} using
248-
* @param {string} value
251+
* @param {Object} elem
249252
* @param {function} callback
250253
* @private
251254
*/
252-
function elements(using, value, callback) {
253-
var check = /class name|css selector|id|name|link text|partial link text|tag name|xpath/gi;
254-
if (!check.test(using)) {
255-
throw new Error('Please provide any of the following using strings as the first parameter: ' +
256-
'class name, css selector, id, name, link text, partial link text, tag name, or xpath. Given: ' + using);
257-
}
255+
function elements(elem, callback) {
256+
257+
validateStrategy(elem.locateStrategy);
258258

259259
return postRequest('/elements', {
260-
using: using,
261-
value: value
262-
}, callback);
260+
using: elem.locateStrategy,
261+
value: elem.selector
262+
}, createIndexedElementCallback(elem, callback)
263+
);
263264
}
264265

265266
/**
@@ -273,21 +274,89 @@ module.exports = function(Nightwatch) {
273274
* @api protocol
274275
*/
275276
Actions.elementIdElements = function(id, using, value, callback) {
277+
278+
var elem = Element.fromSelector(value, using);
279+
280+
validateStrategy(elem.locateStrategy);
281+
282+
return postRequest('/element/' + id + '/elements', {
283+
using: elem.locateStrategy,
284+
value: elem.selector
285+
}, createIndexedElementCallback(elem, callback)
286+
);
287+
};
288+
289+
/**
290+
* Wraps an elements protocol request callback to include logic to select
291+
* a single element with an index if one was specified.
292+
* @param {Object} elem
293+
* @param {function} callback
294+
* @private
295+
*/
296+
function createIndexedElementCallback(elem, callback) {
297+
return function (result) {
298+
299+
var usingIndex = elem.index != undefined;
300+
if (usingIndex && result && result.status === 0) {
301+
302+
// if an element index is specified, we need to make sure its
303+
// within the range of elements found. If not, we patch the
304+
// result to show a failure
305+
306+
if (result.value.length <= elem.index) {
307+
308+
result.status = -1;
309+
310+
var errorId = 'NoSuchElement';
311+
var errorInfo = errorById(errorId);
312+
if (errorInfo) {
313+
result.message = errorInfo.message;
314+
result.errorStatus = errorInfo.status;
315+
}
316+
317+
} else {
318+
319+
// with a valid element index, make the results that element
320+
// as the first and only result
321+
322+
result.value = [result.value[elem.index]];
323+
}
324+
}
325+
326+
return callback(result);
327+
};
328+
}
329+
330+
/**
331+
* Looks up error status info from an id string rather than id number
332+
*/
333+
function errorById(id) {
334+
var errorCodes = require('./errors.json');
335+
for (var status in errorCodes) {
336+
if (errorCodes[status].id === id) {
337+
return {
338+
status: status,
339+
id: id,
340+
message: errorCodes[status].message
341+
};
342+
}
343+
}
344+
345+
return null;
346+
}
347+
348+
function validateStrategy(using) {
349+
350+
var usingLow = String(using).toLocaleLowerCase();
276351
var strategies = ['class name', 'css selector', 'id', 'name', 'link text',
277352
'partial link text', 'tag name', 'xpath'];
278-
using = using.toLocaleLowerCase();
279353

280-
if (strategies.indexOf(using) === -1) {
354+
if (strategies.indexOf(usingLow) === -1) {
281355
throw new Error('Provided locating strategy is not supported: ' +
282356
using + '. It must be one of the following:\n' +
283357
strategies.join(', '));
284358
}
285-
286-
return postRequest('/element/' + id + '/elements', {
287-
using: using,
288-
value: value
289-
}, callback);
290-
};
359+
}
291360

292361
/**
293362
* Get the element on the page that currently has focus.

lib/core/queue.js

+1
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ AsyncTree.prototype.runCommand = function(node, callback) {
158158
}
159159
return node.context;
160160
} catch (err) {
161+
err.originalStack = err.stack;
161162
err.stack = node.stackTrace;
162163
err.name = 'Error while running ' + node.name + ' command';
163164
this.emit('error', err);

0 commit comments

Comments
 (0)