nightwatch
Version:
Easy to use Node.js based end-to-end testing solution for web applications using the W3C WebDriver API.
319 lines (265 loc) • 9.8 kB
JavaScript
const Element = require('../element');
const Utils = require('../utils');
function isValidAssertion(commandName) {
return ['assert', 'verify', 'expect'].indexOf(commandName) > -1;
}
class Command {
static get TYPE_ELEMENT() {
return 'element';
}
static get TYPE_SECTION() {
return 'section';
}
static isPossibleElementSelector(item, commandName = '') {
if (!item) {
return false;
}
if (Array.isArray(item)) {
return false;
}
if (Utils.isObject(item)) {
/*eslint no-prototype-builtins: 'warn'*/
return item.hasOwnProperty('selector') && Utils.isString(item.selector);
}
const Api = require('../api');
return Utils.isString(item) && (item.startsWith('@') || Api.isElementCommand(commandName));
}
constructor(parent, commandName, isChaiAssertion) {
this.parent = parent;
this.commandName = commandName;
this.isChaiAssertion = isChaiAssertion;
}
/**
* 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
* For elements nested under sections, it sets 'recursion' as the locate strategy and passes as its first argument to the command an array of its ancestors + self
* If the command or assertion is not on an element, it calls it with the untouched passed arguments
*
* @param {function} commandFn The actual command function
* @returns {function}
*/
createWrapper(commandFn) {
const self = this;
return function(...args) {
const result = self.executeCommand(commandFn, args);
const {client = {}} = self.parent;
const {isES6AsyncTestcase} = client;
if ((result instanceof Promise) && (self.parent.constructor.name === 'Page' || isES6AsyncTestcase)) {
return result;
}
return self.isChaiAssertion ? result : self.parent;
};
}
validate(elementOrSection, strategy, type) {
let target = null;
let available = null;
let typeAvailable = 'elements';
let prefix;
let showStrategy = '';
let showAvailable;
switch (type) {
case Command.TYPE_ELEMENT:
target = available = this.parent.elements;
prefix = 'Element';
break;
case Command.TYPE_SECTION:
target = available = this.parent.section;
typeAvailable = 'sections';
prefix = 'Section';
break;
}
let isValid = false;
if (elementOrSection in target) {
isValid = true;
}
if (isValid && strategy) {
isValid = target[elementOrSection].locateStrategy && target[elementOrSection].locateStrategy === strategy;
}
if (!isValid) {
showAvailable = Object.keys(available);
if (strategy) {
showStrategy = `[locateStrategy='${strategy}']`;
showAvailable = showAvailable.map(item => `${item}[locateStrategy='${target[item].locateStrategy}']`);
}
throw new Error(`${prefix} "${elementOrSection}${showStrategy}" was not found in "${this.parent.name}". Available ${typeAvailable}: ${showAvailable.join(', ')}`);
}
}
/**
* Given an element name, returns that element object
*
* @param {string} elementName Name of element
* @param {string} [strategy]
* @returns {Element} The element object
*/
getElement(elementName, strategy = null) {
this.validate(elementName, strategy, Command.TYPE_ELEMENT);
return this.parent.elements[elementName];
}
/**
* Given a section name, returns that section object
*
* @param {string} sectionName Name of section
* @param {string} [strategy]
* @returns {Element} The section object
*/
getSection(sectionName, strategy = null) {
this.validate(sectionName, strategy, Command.TYPE_SECTION);
return this.parent.section[sectionName];
}
getSelectorFromArgs(args) {
let selectorArg = args[0];
const isSelector = Command.isPossibleElementSelector(selectorArg, this.commandName);
if (isSelector) {
// check if both strategy and selector are specified as args
const {LocateStrategy} = Utils;
const isStrategySpecified = LocateStrategy.isValid(selectorArg);
if (isStrategySpecified && Utils.isString(args[1])) {
selectorArg = {
selector: args[1],
locateStrategy: args[0]
};
}
return selectorArg;
}
return null;
}
/**
* 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 The argument list to check for an element selector.
*/
parseElementSelector(args) {
let selector = this.getSelectorFromArgs(args);
if (!selector) {
return;
}
// currently only support first argument for @-elements
let inputElement = Element.createFromSelector(selector);
if (inputElement.hasElementSelector()) {
const nameSections = inputElement.selector.substring(1).split(':');
const name = nameSections[0];
const pseudoSelector = nameSections[1] || null;
// When true, indicates that the selector references a selector within a section rather than an elements definition.
// eg: .expect.section('@footer').to.be.visible
const isSectionSelector = this.isChaiAssertion && this.commandName === 'section';
const getter = isSectionSelector ? this.getSection : this.getElement;
const strategy = Utils.isObject(selector) && selector.locateStrategy || null;
const elementOrSection = getter.call(this, name, strategy);
elementOrSection.pseudoSelector = pseudoSelector;
Element.copyDefaults(inputElement, elementOrSection);
inputElement.locateStrategy = elementOrSection.locateStrategy;
inputElement.selector = elementOrSection.selector; // force replacement of @-selector
inputElement = inputElement.getRecursiveLookupElement() || inputElement;
args[0] = inputElement;
} else {
// if we're calling an element on a section using a css/xpath selector,
// then we need to retrieve the element using recursion
const Section = require('./section.js');
if (this.parent instanceof Section) {
args[0] = Element.createFromSelector({
locateStrategy: 'recursion',
selector: [this.parent, inputElement]
});
}
}
}
/**
* @param {Function} commandFn
* @param {Array} args
*/
executeCommand(commandFn, args) {
let parseArgs;
if (Utils.isObject(args[0]) && Array.isArray(args[0].args)) {
parseArgs = args[0].args;
} else {
parseArgs = args;
}
this.parseElementSelector(parseArgs);
return commandFn.apply(this.parent.client, args);
}
}
class CommandLoader {
/**
* Entry point to add commands (elements commands, assertions, etc) to the page or section
*
* @param {Object} parent The parent page or section
* @param {function} commandLoader function that retrieves commands
* @returns {null}
*/
static addWrappedCommands(parent, commandLoader) {
const commands = {
get '__pageObjectItem__' () {
return parent;
}
};
const wrappedCommands = commandLoader(commands);
CommandLoader.applyCommandsToTarget(parent, parent, wrappedCommands);
const ApiLoader = require('../api');
parent.assert = ApiLoader.makeAssertProxy(parent.assert);
parent.verify = ApiLoader.makeAssertProxy(parent.verify);
}
/**
* Adds commands (elements commands, assertions, etc) to the page or section
*
* @param {Object} parent The parent page or section
* @param {Object} target What the command is added to (parent|section or assertion object on parent|section)
* @param {Object} commands
* @returns {null}
*/
static applyCommandsToTarget(parent, target, commands) {
Object.keys(commands).forEach(function(commandName) {
if (isValidAssertion(commandName)) {
target[commandName] = target[commandName] || {};
const isChaiAssertion = commandName === 'expect';
const assertions = commands[commandName];
Object.keys(assertions).forEach(function(assertionName) {
target[commandName][assertionName] = CommandLoader.addCommand({
target: target[commandName],
commandFn: assertions[assertionName],
commandName: assertionName,
parent,
isChaiAssertion
});
});
} else {
target[commandName] = CommandLoader.addCommand({
target,
commandFn: commands[commandName],
commandName,
parent,
isChaiAssertion: false
});
}
});
}
/**
* @param parent
* @param target
* @param {Array} commands
*/
static wrapProtocolCommands(parent, target, commands) {
commands.forEach(commandName => {
const originalFn = target[commandName];
target[commandName] = (function () {
const command = new Command(parent, commandName, false);
return function(...args) {
command.parseElementSelector(args);
return originalFn.apply(target, args);
};
})();
});
}
static addCommand({target, commandFn, commandName, parent, isChaiAssertion, overwrite = false}) {
if (target[commandName] && !overwrite) {
const err = new TypeError(`Error while loading the page object commands: the command "${commandName}" is already defined.`);
err.displayed = true;
err.showTrace = false;
throw err;
}
const command = new Command(parent, commandName, isChaiAssertion);
return command.createWrapper(commandFn);
}
}
module.exports = CommandLoader;