Skip to content

Commit

Permalink
Add support for page object-formatted object-based selectors
Browse files Browse the repository at this point in the history
  • Loading branch information
senocular committed Oct 4, 2016
1 parent bbc6dd2 commit d8993c1
Show file tree
Hide file tree
Showing 10 changed files with 264 additions and 217 deletions.
5 changes: 4 additions & 1 deletion lib/api/element-commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ var util = require('util');
var events = require('events');
var Logger = require('../util/logger.js');
var Utils = require('../util/utils.js');
var Element = require('../page-object/element.js');

module.exports = function(client) {
var Protocol = require('./protocol.js')(client);
Expand Down Expand Up @@ -344,7 +345,9 @@ module.exports = function(client) {
var el = Protocol.element(using, value, function(result) {
if (result.status !== 0) {
callback.call(client.api, result);
var errorMessage = 'ERROR: Unable to locate element: "' + value + '" using: ' + using;

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

stack.shift();
Expand Down
4 changes: 2 additions & 2 deletions lib/api/element-commands/_elementByRecursion.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@ ElementByRecursion.prototype.command = function(elements, callback) {
var allElements = elements.slice();

var topElement = allElements.shift();
var el = this.protocol.element(topElement.locateStrategy, topElement.selector, function checkResult(result) {
var el = this.protocol.element(null, topElement, function checkResult(result) {
if (result.status !== 0) {
callback(result);
self.emit('complete', el, self);
} else {
var nextElement = allElements.shift();
var parentId = result.value.ELEMENT;
if (nextElement) {
self.protocol.elementIdElement(parentId, nextElement.locateStrategy, nextElement.selector, checkResult);
self.protocol.elementIdElement(parentId, null, nextElement, checkResult);
} else {
callback(result);
self.emit('complete', el, self);
Expand Down
4 changes: 2 additions & 2 deletions lib/api/element-commands/_elementsByRecursion.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ ElementsByRecursion.prototype.command = function(elements, callback) {
var allElements = elements.slice();
var topElement = allElements.shift();

var el = this.protocol.elements(topElement.locateStrategy, topElement.selector, function checkResult() {
var el = this.protocol.elements(null, topElement, function checkResult() {
var result = aggregateResults(arguments);
if (result.value.length === 0) {
callback(result);
Expand All @@ -77,7 +77,7 @@ ElementsByRecursion.prototype.command = function(elements, callback) {
if (nextElement) {
var promises = [];
result.value.forEach(function(el) {
var p = deferredElementIdElements(el.ELEMENT, nextElement.locateStrategy, nextElement.selector, checkResult);
var p = deferredElementIdElements(el.ELEMENT, null, nextElement, checkResult);
promises.push(p);
});

Expand Down
8 changes: 7 additions & 1 deletion lib/api/expect/_baseAssertion.js
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,13 @@ BaseAssertion.prototype.scheduleRetry = function() {
};

BaseAssertion.prototype.formatMessage = function() {
this.message = Utils.format(this.message || this.message, this.selector);

var selectorStr = String(this.selector);
if (selectorStr === '[object Object]') {
selectorStr = JSON.stringify(this.selector);
}

this.message = Utils.format(this.message, selectorStr);
this.message += this.messageParts.join('');
};

Expand Down
84 changes: 38 additions & 46 deletions lib/api/protocol.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
var elementByRecursion = require('./element-commands/_elementByRecursion.js');
var elementsByRecursion = require('./element-commands/_elementsByRecursion.js');
var Element = require('../page-object/element.js');

module.exports = function(Nightwatch) {

Expand Down Expand Up @@ -164,11 +165,12 @@ module.exports = function(Nightwatch) {
* @api protocol
*/
Actions.element = function(using, value, callback) {
if (using == 'recursion') {
return new elementByRecursion(Nightwatch).command(value, callback);
var elem = Element.fromSelector(value, using);
if (elem.locateStrategy == 'recursion') {
return new elementByRecursion(Nightwatch).command(elem.selector, callback);
}

return element(using, value, callback);
return element(elem, callback);
};

/*!
Expand All @@ -179,20 +181,12 @@ module.exports = function(Nightwatch) {
* @param {function} callback
* @private
*/
function element(using, value, callback) {
var strategies = ['class name', 'css selector', 'id', 'name', 'link text',
'partial link text', 'tag name', 'xpath'];
using = using.toLocaleLowerCase();

if (strategies.indexOf(using) === -1) {
throw new Error('Provided locating strategy is not supported: ' +
using + '. It must be one of the following:\n' +
strategies.join(', '));
}
function element(elem, callback) {
validateStrategy(elem.locateStrategy);

return postRequest('/element', {
using: using,
value: value
using: elem.locateStrategy,
value: elem.selector
}, callback);
}

Expand All @@ -207,19 +201,12 @@ module.exports = function(Nightwatch) {
* @api protocol
*/
Actions.elementIdElement = function(id, using, value, callback) {
var strategies = ['class name', 'css selector', 'id', 'name', 'link text',
'partial link text', 'tag name', 'xpath'];
using = using.toLocaleLowerCase();

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

return postRequest('/element/' + id + '/element', {
using: using,
value: value
using: elem.locateStrategy,
value: elem.selector
}, callback);
};

Expand All @@ -234,11 +221,12 @@ module.exports = function(Nightwatch) {
* @api protocol
*/
Actions.elements = function(using, value, callback) {
if (using == 'recursion') {
return new elementsByRecursion(Nightwatch).command(value, callback);
var elem = Element.fromSelector(value, using);
if (elem.locateStrategy == 'recursion') {
return new elementsByRecursion(Nightwatch).command(elem.selector, callback);
}

return elements(using, value, callback);
return elements(elem, callback);
};

/*!
Expand All @@ -249,17 +237,14 @@ module.exports = function(Nightwatch) {
* @param {function} callback
* @private
*/
function elements(using, value, callback) {
var check = /class name|css selector|id|name|link text|partial link text|tag name|xpath/gi;
if (!check.test(using)) {
throw new Error('Please provide any of the following using strings as the first parameter: ' +
'class name, css selector, id, name, link text, partial link text, tag name, or xpath. Given: ' + using);
}
function elements(elem, callback) {
validateStrategy(elem.locateStrategy);

return postRequest('/elements', {
using: using,
value: value
}, callback);
using: elem.locateStrategy,
value: elem.selector
}, callback
);
}

/**
Expand All @@ -273,21 +258,28 @@ module.exports = function(Nightwatch) {
* @api protocol
*/
Actions.elementIdElements = function(id, using, value, callback) {
var elem = Element.fromSelector(value, using);
validateStrategy(elem.locateStrategy);

return postRequest('/element/' + id + '/elements', {
using: elem.locateStrategy,
value: elem.selector
}, callback
);
};

function validateStrategy(using) {

var usingLow = String(using).toLocaleLowerCase();
var strategies = ['class name', 'css selector', 'id', 'name', 'link text',
'partial link text', 'tag name', 'xpath'];
using = using.toLocaleLowerCase();

if (strategies.indexOf(using) === -1) {
if (strategies.indexOf(usingLow) === -1) {
throw new Error('Provided locating strategy is not supported: ' +
using + '. It must be one of the following:\n' +
strategies.join(', '));
}

return postRequest('/element/' + id + '/elements', {
using: using,
value: value
}, callback);
};
}

/**
* Get the element on the page that currently has focus.
Expand Down
147 changes: 26 additions & 121 deletions lib/page-object/command-wrapper.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
var Element = require('./element.js');

module.exports = new (function() {

/**
Expand Down Expand Up @@ -32,27 +34,6 @@ module.exports = new (function() {
return parent.section[sectionName];
}

/**
* Calls use(Css|Xpath|Recursion) command
*
* Uses `useXpath`, `useCss`, and `useRecursion` commands.
*
* @param {Object} client The Nightwatch instance
* @param {string} desiredStrategy (css selector|xpath|recursion)
* @returns {null}
*/
function setLocateStrategy(client, desiredStrategy) {
var methodMap = {
xpath : 'useXpath',
'css selector' : 'useCss',
recursion : 'useRecursion'
};

if (desiredStrategy in methodMap) {
client.api[methodMap[desiredStrategy]]();
}
}

/**
* Creates a closure that enables calling commands and assertions on the page or section.
* For all element commands and assertions, it fetches element's selector and locate strategy
Expand All @@ -66,119 +47,43 @@ module.exports = new (function() {
* @returns {function}
*/
function makeWrappedCommand(parent, commandFn, commandName, isChaiAssertion) {

var isSectionSelector = isChaiAssertion && commandName === 'section';

return function() {
var args = Array.prototype.slice.call(arguments);
var prevLocateStrategy = parent.client.locateStrategy;
var elementCommand = isElementCommand(args);

if (elementCommand) {
var firstArg;
var desiredStrategy;
var callbackIndex;
var originalCallback;
var elementOrSectionName = args.shift();
var getter = (isChaiAssertion && commandName === 'section') ? getSection : getElement;
var elementOrSection = getter(parent, elementOrSectionName);
var ancestors = getAncestorsWithElement(elementOrSection);

if (ancestors.length === 1) {
firstArg = elementOrSection.selector;
desiredStrategy = elementOrSection.locateStrategy;
} else {
firstArg = ancestors;
desiredStrategy = 'recursion';
}

setLocateStrategy(parent.client, desiredStrategy);
args.unshift(firstArg);

// if a callback is being used with this command, wrap it in
// a function that allows us to restore the locate strategy
// to its original value before the callback is called

callbackIndex = findCallbackIndex(args);
if (callbackIndex !== -1) {
originalCallback = args[callbackIndex];

args[callbackIndex] = function callbackWrapper() {

// restore the locate strategy directly through client.locateStrategy.
// setLocateStrategy() can't be used since it uses the api commands
// which get added to the command queue and will not update the
// strategy in time for the callback which is getting immediately
// called after

parent.client.locateStrategy = prevLocateStrategy;
return originalCallback.apply(parent.client.api, arguments);
};
}
}

var c = commandFn.apply(parent.client, args);
if (elementCommand) {
setLocateStrategy(parent.client, prevLocateStrategy);
}
return (isChaiAssertion ? c : parent);
parseElementSelector(args, parent, isSectionSelector);
var result = commandFn.apply(parent.client, args);
return isChaiAssertion ? result : parent;
};
}

/**
* Identifies element references (@-prefixed selectors) within an argument
* list and converts it into an element object with the appropriate
* selector or recursion chain of selectors.
*
* @param {Array} args
* @return {boolean}
* @param {Array} args The argument list to check for an element selector.
* @param {Object} parent The parent page or section.
* @param {boolean} isSectionSelector When true, indicates that the selector references
* a selector within a section rather than an elements definition.
*/
function isElementCommand(args) {
return (args.length > 0) && (args[0].toString().indexOf('@') === 0);
}
function parseElementSelector (args, parent, isSectionSelector) {

/**
* Identifies the location of a callback function within an arguments array.
*
* @param {Array} args Arguments array in which to find the location of a callback.
* @returns {number} Index location of the callback in the args array. If not found, -1 is returned.
*/
function findCallbackIndex(args) {

if (args.length === 0) {
return -1;
}

// callbacks will usually be the last argument. waitFor methods allow an additional
// message argument to follow the callback which will also need to be checked for.

// last argument

var index = args.length - 1;
if (typeof args[index] === 'function') {
return index;
}

// second to last argument (waitfor calls)
// currently only support first argument for @-elements
var possibleElementSelector = args[0];
var inputElement = possibleElementSelector && Element.fromSelector(possibleElementSelector);
if (inputElement && inputElement.hasElementSelector()) {

index--;
if (typeof args[index] === 'function') {
return index;
}
var getter = isSectionSelector ? getSection : getElement;
var elementOrSection = getter(parent, inputElement.selector);

return -1;
}
Element.copyDefaults(inputElement, elementOrSection);
inputElement.selector = elementOrSection.selector; // force replacement of @-selector

/**
* Retrieves an array of ancestors of the supplied element. The last element in the array is the element object itself
*
* @param {Object} element The element
* @returns {Array}
*/
function getAncestorsWithElement(element) {
var elements = [];
function addElement(e) {
elements.unshift(e);
if (e.parent && e.parent.selector) {
addElement(e.parent);
}
inputElement = inputElement.getRecursiveLookupElement() || inputElement;
args[0] = inputElement;
}
addElement(element);
return elements;
}

/**
Expand Down
Loading

0 comments on commit d8993c1

Please sign in to comment.