UNPKG

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
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;