Skip to content

Commit 63ab684

Browse files
committed
fixes nightwatchjs#1115 - adds optional index property to element objects
1 parent 74ea6cc commit 63ab684

File tree

8 files changed

+403
-22
lines changed

8 files changed

+403
-22
lines changed

lib/api/element-commands.js

+10-7
Original file line numberDiff line numberDiff line change
@@ -341,12 +341,15 @@ module.exports = function(client) {
341341
function CommandAction(using, value, protocolAction, args, callback, originalStackTrace) {
342342
events.EventEmitter.call(this);
343343

344-
var $this = this;
345-
var el = Protocol.element(using, value, function(result) {
344+
var elem = Element.fromSelector(value, using);
345+
var multipleElements = Element.requiresFiltering(elem);
346+
var elementAction = multipleElements ? 'elements' : 'element';
347+
348+
var self = this;
349+
var el = Protocol[elementAction](using, value, function(result) {
346350
if (result.status !== 0) {
347351
callback.call(client.api, result);
348352

349-
var elem = Element.fromSelector(value, using);
350353
var errorMessage = 'ERROR: Unable to locate element: "' + elem.selector + '" using: "' + elem.locateStrategy + '"';
351354
var stack = originalStackTrace.split('\n');
352355

@@ -356,18 +359,18 @@ module.exports = function(client) {
356359
client.results.errors++;
357360
client.errors.push(errorMessage + '\n' + stack.join('\n'));
358361

359-
$this.emit('complete', el, $this);
362+
self.emit('complete', el, self);
360363
} else {
361-
result = result.value.ELEMENT;
364+
var elemId = multipleElements ? result.value[0].ELEMENT : result.value.ELEMENT;
362365

363366
args.push(function(r) {
364367
callback.call(client.api, r);
365368
});
366369

367-
args.unshift(result);
370+
args.unshift(elemId);
368371

369372
var c = Protocol[protocolAction].apply(Protocol, args).once('complete', function() {
370-
$this.emit('complete', c, $this);
373+
self.emit('complete', c, self);
371374
});
372375
}
373376
});

lib/api/element-commands/_elementByRecursion.js

+15-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
var util = require('util');
22
var events = require('events');
3+
var Element = require('../../page-object/element.js');
34

45
/**
56
* Search for an element on the page, starting with the first element of the array, and where each element in the passed array is nested under the previous one. The located element will be returned as a WebElement JSON object.
@@ -21,15 +22,26 @@ ElementByRecursion.prototype.command = function(elements, callback) {
2122
var allElements = elements.slice();
2223

2324
var topElement = allElements.shift();
24-
var el = this.protocol.element(null, topElement, function checkResult(result) {
25+
var multipleElements = Element.requiresFiltering(topElement);
26+
var elementAction = multipleElements ? 'elements' : 'element';
27+
28+
var el = this.protocol[elementAction](null, topElement, function checkResult(result) {
2529
if (result.status !== 0) {
2630
callback(result);
2731
self.emit('complete', el, self);
2832
} else {
33+
34+
if (multipleElements) {
35+
result.value = result.value[0];
36+
}
37+
2938
var nextElement = allElements.shift();
30-
var parentId = result.value.ELEMENT;
39+
3140
if (nextElement) {
32-
self.protocol.elementIdElement(parentId, null, nextElement, checkResult);
41+
var parentId = result.value.ELEMENT;
42+
multipleElements = Element.requiresFiltering(nextElement);
43+
elementAction = multipleElements ? 'elementIdElements' : 'elementIdElement';
44+
self.protocol[elementAction](parentId, null, nextElement, checkResult);
3345
} else {
3446
callback(result);
3547
self.emit('complete', el, self);

lib/api/protocol.js

+60-2
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ module.exports = function(Nightwatch) {
243243
return postRequest('/elements', {
244244
using: elem.locateStrategy,
245245
value: elem.selector
246-
}, callback
246+
}, filterElementsForCallback(elem, callback)
247247
);
248248
}
249249

@@ -264,7 +264,7 @@ module.exports = function(Nightwatch) {
264264
return postRequest('/element/' + id + '/elements', {
265265
using: elem.locateStrategy,
266266
value: elem.selector
267-
}, callback
267+
}, filterElementsForCallback(elem, callback)
268268
);
269269
};
270270

@@ -281,6 +281,64 @@ module.exports = function(Nightwatch) {
281281
}
282282
}
283283

284+
/**
285+
* Wraps an elements protocol request callback to include logic to select
286+
* a subset of elements if that request requires filtering.
287+
*
288+
* @param {Object} elem
289+
* @param {function} callback
290+
* @private
291+
*/
292+
function filterElementsForCallback(elem, callback) {
293+
if (!Element.requiresFiltering(elem)) {
294+
return callback;
295+
}
296+
297+
return callback && function elementsCallbackWrapper(result) {
298+
if (result && result.status === 0) {
299+
300+
var filtered = Element.applyFiltering(elem, result.value);
301+
if (filtered) {
302+
303+
result.value = filtered;
304+
305+
} else {
306+
307+
result.status = -1;
308+
result.value = [];
309+
var errorId = 'NoSuchElement';
310+
var errorInfo = findErrorById(errorId);
311+
if (errorInfo) {
312+
result.message = errorInfo.message;
313+
result.errorStatus = errorInfo.status;
314+
}
315+
}
316+
}
317+
318+
return callback(result);
319+
};
320+
}
321+
322+
/**
323+
* Looks up error status info from an id string rather than id number.
324+
*
325+
* @param {string} id String id to look up an error with.
326+
*/
327+
function findErrorById(id) {
328+
var errorCodes = require('./errors.json');
329+
for (var status in errorCodes) {
330+
if (errorCodes[status].id === id) {
331+
return {
332+
status: status,
333+
id: id,
334+
message: errorCodes[status].message
335+
};
336+
}
337+
}
338+
339+
return null;
340+
}
341+
284342
/**
285343
* Get the element on the page that currently has focus.
286344
*

lib/page-object/element.js

+50-2
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,25 @@ function Element(definition, options) {
2222

2323
this.selector = definition.selector;
2424
this.locateStrategy = definition.locateStrategy || options.locateStrategy;
25+
this.index = definition.index;
2526
this.parent = options.parent;
2627
}
2728

2829
Element.prototype.toString = function() {
2930
if (Array.isArray(this.selector)) { // recursive
3031
return this.selector.join(',');
3132
}
33+
34+
var index = parseInt(this.index, 10);
35+
var indexStr = isNaN(index) ? '' : '[' + index + ']';
36+
3237
if (!this.name) { // inline (not defined in page or section)
3338
return this.selector;
3439
}
40+
3541
var classType = this.constructor.name;
3642
var prefix = this.constructor === Element ? '@' : '';
37-
return classType + '[name=' + prefix + this.name + ']';
43+
return classType + '[name=' + prefix + this.name + indexStr + ']';
3844
};
3945

4046
/**
@@ -78,7 +84,7 @@ Element.prototype.getRecursiveLookupElement = function () {
7884
* @param {Object} source The object to capture values from.
7985
*/
8086
Element.copyDefaults = function(target, source) {
81-
var props = ['name', 'parent', 'selector', 'locateStrategy'];
87+
var props = ['name', 'parent', 'selector', 'locateStrategy', 'index'];
8288
props.forEach(function(prop) {
8389
if (target[prop] === undefined || target[prop] === null) {
8490
target[prop] = source[prop];
@@ -119,6 +125,48 @@ Element.fromSelector = function(value, using) {
119125
return new Element(definition, options);
120126
};
121127

128+
/**
129+
* Returns true when an elements() request is needed to capture
130+
* the result of the Element definition. When false, it means the
131+
* Element targets the first result the selector match meaning an
132+
* element() (single match only) result can be used.
133+
*
134+
* @param {Object} element The Element instance to check to see if
135+
* it will apply filtering.
136+
*/
137+
Element.requiresFiltering = function(element) {
138+
139+
var usingIndex = !isNaN(parseInt(element.index, 10));
140+
if (usingIndex) {
141+
return true;
142+
}
143+
144+
return false;
145+
};
146+
147+
/**
148+
* Filters an elements() results array to a more specific set based
149+
* on an Element object's definition.
150+
*
151+
* @param {Object} element The Element instance to check to see if
152+
* it will apply filtering.
153+
* @param {Array} resultElements Array of WebElement JSON objects
154+
* returned from a call to elements() or elementIdElements().
155+
* @returns {Array} A filtered version of the elements array or, if
156+
* the filter failed (no matches found) null.
157+
*/
158+
Element.applyFiltering = function(element, resultElements) {
159+
160+
var index = parseInt(element.index, 10);
161+
var usingIndex = !isNaN(index);
162+
if (usingIndex) {
163+
var foundElem = resultElements[index];
164+
return foundElem ? [foundElem] : null; // null = not found
165+
}
166+
167+
return resultElements;
168+
};
169+
122170
/**
123171
* Gets a simple description of the element based on whether or not
124172
* it is used as an inline selector in a command call or a definition

lib/page-object/section.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ var CommandWrapper = require('./command-wrapper.js');
77
* Class that all sections subclass from
88
*
99
* @param {Object} definition User-defined section options defined in page object
10-
* @param {Object} options Additional options to be given to the element.
10+
* @param {Object} options Additional options to be given to the section.
1111
* @constructor
1212
*/
1313
function Section(definition, options) {

test/extra/pageobjects/simplePageObj.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,20 @@ module.exports = {
99
elements: {
1010
loginAsString: '#weblogin',
1111
loginCss: { selector: '#weblogin' },
12+
loginIndexed: { selector: '#weblogin', index: 1 },
1213
loginXpath: { selector: '//weblogin', locateStrategy: 'xpath' },
1314
loginId: { selector: 'weblogin', locateStrategy: 'id' }
1415
},
1516
sections: {
1617
signUp: {
1718
selector: '#signupSection',
1819
sections: {
19-
getStarted: { selector: '#getStarted' }
20+
getStarted: {
21+
selector: '#getStarted',
22+
elements: {
23+
start: { selector: '#getStartedStart' }
24+
}
25+
}
2026
},
2127
elements: {
2228
help: { selector: '#helpBtn' }

test/lib/nockselements.js

+6-6
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@ module.exports = {
88
_requestUri: 'http://localhost:10195',
99
_protocolUri: '/wd/hub/session/1352110219202/',
1010

11-
elementFound : function(selector, using) {
11+
elementFound : function(selector, using, foundElem) {
1212
nock(this._requestUri)
1313
.persist()
1414
.post(this._protocolUri + 'element', {'using':using || 'css selector','value':selector || '#nock'})
1515
.reply(200, {
1616
status: 0,
1717
state: 'success',
18-
value: { ELEMENT: '0' }
18+
value: foundElem || { ELEMENT: '0' }
1919
});
2020
return this;
2121
},
@@ -57,14 +57,14 @@ module.exports = {
5757
return this;
5858
},
5959

60-
elementByXpath : function(selector) {
60+
elementByXpath : function(selector, foundElem) {
6161
nock(this._requestUri)
6262
.persist()
6363
.post(this._protocolUri + 'element', {'using':'xpath','value':selector || '//[@id="nock"]'})
6464
.reply(200, {
6565
status: 0,
6666
state: 'success',
67-
value: { ELEMENT: '0' }
67+
value: foundElem || { ELEMENT: '0' }
6868
});
6969
return this;
7070
},
@@ -81,15 +81,15 @@ module.exports = {
8181
return this;
8282
},
8383

84-
elementId : function (id, selector, using) {
84+
elementId : function (id, selector, using, foundElem) {
8585
nock(this._requestUri)
8686
.persist()
8787
.post(this._protocolUri + 'element/' + (id || 0) + '/element',
8888
{'using':using || 'css selector','value':selector || '#nock'})
8989
.reply(200, {
9090
status: 0,
9191
state : 'success',
92-
value: { ELEMENT: '0' }
92+
value: foundElem || { ELEMENT: '0' }
9393
});
9494
return this;
9595
},

0 commit comments

Comments
 (0)