ember-cli-page-object
Version:
This ember-cli addon eases the construction of page objects on your acceptance and integration tests
356 lines (300 loc) • 9.04 kB
JavaScript
export { assign } from '@ember/polyfills';
import { A } from '@ember/array';
import { assert } from '@ember/debug';
import { get } from '@ember/object';
import { isPresent } from '@ember/utils';
import Ceibo from 'ceibo';
import deprecate from './deprecate';
import { getContext as getEmberTestHelpersContext } from './compatibility';
import $ from '-jquery';
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) {
deprecate(
'comma-separated-selectors',
'Usage of comma separated selectors is deprecated in ember-cli-page-object',
'1.16.0',
'2.0.0',
);
}
filters = this.calculateFilters(this.targetFilters);
let selector = $.trim(`${scope} ${this.targetSelector}${filters}`);
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 $.trim(scopes.join(' '));
}
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) {
assert(
`"${selector}" matched more than one element. If you want to select many elements, use collections instead.`,
supportMultiple || items.length <= 1
);
}
/**
* @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();
}
/**
* @private
*
* Trim whitespaces at both ends and normalize whitespaces inside `text`
*
* Due to variations in the HTML parsers in different browsers, the text
* returned may vary in newlines and other white space.
*
* @see http://api.jquery.com/text/
*/
export function normalizeText(text) {
return $.trim(text).replace(/\n/g, ' ').replace(/\s\s*/g, ' ');
}
export function every(jqArray, cb) {
let arr = jqArray.get();
return A(arr).every((element) => cb($(element)));
}
/**
* @private
*
* Check if all options are in whitelist
*
*/
export function filterWhitelistedOption(options, whitelist) {
return whitelist.reduce((whitelisted, knownKey) => {
if (knownKey in options) {
whitelisted[knownKey] = options[knownKey];
}
return whitelisted;
}, {});
}
/**
* @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;
}
/**
* @public
*
* Return a test context if one was provided during `create()` or via `setContext()`
*
* @param {Ceibo} node - Node of the tree
* @return {Object} `moduleForComponent` test's `this` context, or null
*/
export function getContext(node) {
let root = getRoot(node);
let { context } = root;
if (typeof context === 'object' && context !== null && typeof context.$ === 'function') {
return context;
}
context = getEmberTestHelpersContext();
if (typeof context === 'object' && context !== null && typeof context.$ === 'function' && !context.element) {
return context
}
return null;
}
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);
}
}
/**
* @public
*
* Returns a boolean indicating whether an object contains a given property.
* The path to a nested property should be indicated by a dot-separated string.
*
* @param {Object} object - object to check for the target property
* @param {string} pathToProp - dot-separated path to property
* @return {Boolean}
*/
export function objectHasProperty(object, pathToProp) {
const pathSegments = pathToProp.split('.');
for (let i = 0; i < pathSegments.length; i++) {
const key = pathSegments[i];
if (object === null || object === undefined || !object.hasOwnProperty(key)) {
return false;
} else {
object = object[key];
}
}
return true;
}
/**
* @public
*
* Returns the value of an object property. If the property is a function,
* the return value is that function bound to its "owner."
*
* @param {Object} object - object on which to look up the target property
* @param {string} pathToProp - dot-separated path to property
* @return {Boolean|String|Number|Function|Null|Undefined} - value of property
*/
export function getProperty(object, pathToProp) {
const pathSegments = pathToProp.split('.');
if (pathSegments.length === 1) {
const value = get(object, pathToProp);
return typeof value === 'function' ? value.bind(object) : value;
}
const pathToPropOwner = pathSegments.slice(0, -1).join('.');
const propOwner = get(object, pathToPropOwner);
if (propOwner === null || propOwner === undefined) {
return undefined;
}
const propKey = pathSegments[pathSegments.length - 1];
const value = get(propOwner, propKey);
return typeof value === 'function' ? value.bind(propOwner) : value;
}
export function isPageObject(property){
if(property && typeof(property) === 'object'){
let meta = Ceibo.meta(property);
return (meta && meta.__poDef__)
} else{
return false;
}
}
export function getPageObjectDefinition(node){
if(!isPageObject(node)){
throw new Error('cannot get the page object definition from a node that is not a page object');
}else{
return Ceibo.meta(node).__poDef__;
}
}
export function storePageObjectDefinition(node, definition){
Ceibo.meta(node).__poDef__ = definition;
}