addon/src/-private/helpers.js
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;
}
}
export 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
*/
export 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
*/
export 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
*/
export 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
*/
export function findClosestValue(node, property) {
if (isPresent(node[property])) {
return node[property];
}
let parent = Ceibo.parent(node);
if (isPresent(parent)) {
return findClosestValue(parent, property);
}
}
export function assignDescriptors(target, source) {
Object.getOwnPropertyNames(source).forEach((key) => {
const descriptor = Object.getOwnPropertyDescriptor(source, key);
Object.defineProperty(target, key, descriptor);
});
return target;
}