UNPKG

ember-cli-page-object

Version:

This ember-cli addon eases the construction of page objects on your acceptance and integration tests

188 lines (181 loc) 5.37 kB
import Ceibo from '@ro0gr/ceibo'; function isPresent(value) { return typeof value !== 'undefined'; } class Selector { constructor(node, scope, selector, filters) { this.targetNode = node; this.targetScope = scope || ''; this.targetSelector = selector || ''; this.targetFilters = filters; } toString() { let scope; let filters; if (this.targetFilters.resetScope) { scope = this.targetScope; } else { scope = this.calculateScope(this.targetNode, this.targetScope); } if (`${scope} ${this.targetSelector}`.indexOf(',') > -1) { throw new Error('Usage of comma separated selectors is not supported. Please make sure your selector targets a single selector.'); } filters = this.calculateFilters(this.targetFilters); let selector = `${scope} ${this.targetSelector}${filters}`.trim(); if (!selector.length) { // When an empty selector is resolved take the first direct child of the // testing container. selector = ':first'; } return selector; } calculateFilters() { let filters = []; if (this.targetFilters.visible) { filters.push(`:visible`); } if (this.targetFilters.contains) { filters.push(`:contains("${this.targetFilters.contains}")`); } if (typeof this.targetFilters.at === 'number') { filters.push(`:eq(${this.targetFilters.at})`); } else if (this.targetFilters.last) { filters.push(':last'); } return filters.join(''); } calculateScope(node, targetScope) { let scopes = this.getScopes(node); scopes.reverse(); scopes.push(targetScope); return scopes.join(' ').trim(); } getScopes(node) { let scopes = []; if (node.scope) { scopes.push(node.scope); } if (!node.resetScope && Ceibo.parent(node)) { scopes = scopes.concat(this.calculateScope(Ceibo.parent(node))); } return scopes; } } function guardMultiple(items, selector, supportMultiple) { if (items.length > 1 && !supportMultiple) { throw new Error(`"${selector}" matched more than one element. If you want to select many elements, use collections instead.`); } } /** * @public * * Builds a CSS selector from a target selector and a PageObject or a node in a PageObject, along with optional parameters. * * @example * * const component = PageObject.create({ scope: '.component'}); * * buildSelector(component, '.my-element'); * // returns '.component .my-element' * * @example * * const page = PageObject.create({}); * * buildSelector(page, '.my-element', { at: 0 }); * // returns '.my-element:eq(0)' * * @example * * const page = PageObject.create({}); * * buildSelector(page, '.my-element', { contains: "Example" }); * // returns ".my-element :contains('Example')" * * @example * * const page = PageObject.create({}); * * buildSelector(page, '.my-element', { last: true }); * // returns '.my-element:last' * * @param {Ceibo} node - Node of the tree * @param {string} targetSelector - CSS selector * @param {Object} options - Additional options * @param {boolean} options.resetScope - Do not use inherited scope * @param {string} options.contains - Filter by using :contains('foo') pseudo-class * @param {number} options.at - Filter by index using :eq(x) pseudo-class * @param {boolean} options.last - Filter by using :last pseudo-class * @param {boolean} options.visible - Filter by using :visible pseudo-class * @return {string} Fully qualified selector */ function buildSelector(node, targetSelector, options) { return new Selector(node, options.scope, targetSelector, options).toString(); } /** * @public * * Return the root of a node's tree * * @param {Ceibo} node - Node of the tree * @return {Ceibo} node - Root node of the tree */ function getRoot(node) { let parent = Ceibo.parent(node); let root = node; while (parent) { root = parent; parent = Ceibo.parent(parent); } return root; } function getAllValuesForProperty(node, property) { let iterator = node; let values = []; while (isPresent(iterator)) { if (isPresent(iterator[property])) { values.push(iterator[property]); } iterator = Ceibo.parent(iterator); } return values; } /** * @public * * Return full scope of node (includes all ancestors scopes) * * @param {Ceibo} node - Node of the tree * @return {string} Full scope of node */ function fullScope(node) { let scopes = getAllValuesForProperty(node, 'scope'); return scopes.reverse().join(' '); } /** * @public * * Returns the value of property defined on the closest ancestor of given * node. * * @param {Ceibo} node - Node of the tree * @param {string} property - Property to look for * @return {?Object} The value of property on closest node to the given node */ function findClosestValue(node, property) { if (isPresent(node[property])) { return node[property]; } let parent = Ceibo.parent(node); if (isPresent(parent)) { return findClosestValue(parent, property); } } function assignDescriptors(target, source) { Object.getOwnPropertyNames(source).forEach(key => { const descriptor = Object.getOwnPropertyDescriptor(source, key); Object.defineProperty(target, key, descriptor); }); return target; } export { assignDescriptors, buildSelector, findClosestValue, fullScope, getRoot, guardMultiple };