san650/ember-cli-page-object

View on GitHub
addon/src/properties/collection.js

Summary

Maintainability
A
2 hrs
Test Coverage
import Ceibo from '@ro0gr/ceibo';
import { buildSelector, assignDescriptors } from '../-private/helpers';
import { isPageObject, getPageObjectDefinition } from '../-private/meta';
import { create } from '../create';
import { count } from './count';
import { throwBetterError } from '../-private/better-errors';
 
/**
* Creates a enumerable that represents a collection of items. The collection is zero-indexed
* and has the following public methods and properties:
*
* - `length` - The number of items in the collection.
* - `objectAt()` - Returns the page for the item at the specified index.
* - `filter()` - Filters the items in the array and returns the ones which match the predicate function.
* - `filterBy()` - Filters the items of the array by the specified property, returning all that are truthy or that match an optional value.
* - `forEach()` - Runs a function for each item in the collection
* - `map()` - maps over the elements of the collection
* - `mapBy()` - maps over the elements of the collecton by the specified property
* - `findOne()` - finds first item of the array with assert by specified function
* - `findOneBy()` - finds first item of the array with assert by property
* - `toArray()` - returns an array containing all the items in the collection
* - `[Symbol.iterator]()` - if supported by the environment, this allows the collection to be iterated with `for/of` and spread with `...` like a normal array
*
* @example
*
* // <table>
* // <tbody>
* // <tr>
* // <td>Mary<td>
* // <td>Watson</td>
* // </tr>
* // <tr>
* // <td>John<td>
* // <td>Doe</td>
* // </tr>
* // </tbody>
* // </table>
*
* import { create, collection, text } from 'ember-cli-page-object';
*
* const page = create({
* users: collection('table tr', {
* firstName: text('td', { at: 0 }),
* lastName: text('td', { at: 1 })
* })
* });
*
* assert.equal(page.users.length, 2);
* assert.equal(page.users.objectAt(1).firstName, 'John');
* assert.equal(page.users.objectAt(1).lastName, 'Doe');
*
* @example
*
* // <div class="admins">
* // <table>
* // <tbody>
* // <tr>
* // <td>Mary<td>
* // <td>Watson</td>
* // </tr>
* // <tr>
* // <td>John<td>
* // <td>Doe</td>
* // </tr>
* // </tbody>
* // </table>
* // </div>
*
* // <div class="normal">
* // <table>
* // </table>
* // </div>
*
* import { create, collection, text } from 'ember-cli-page-object';
*
* const page = create({
* scope: '.admins',
*
* users: collection('table tr', {
* firstName: text('td', { at: 0 }),
* lastName: text('td', { at: 1 })
* })
* });
*
* assert.equal(page.users.length, 2);
*
* @example
*
* // <table>
* // <caption>User Index</caption>
* // <tbody>
* // <tr>
* // <td>Mary<td>
* // <td>Watson</td>
* // </tr>
* // <tr>
* // <td>John<td>
* // <td>Doe</td>
* // </tr>
* // </tbody>
* // </table>
*
* import { create, collection, text } from 'ember-cli-page-object';
*
* const page = create({
* scope: 'table',
*
* users: collection('tr', {
* firstName: text('td', { at: 0 }),
* lastName: text('td', { at: 1 }),
* })
* });
*
* let john = page.users.filter((item) => item.firstName === 'John' )[0];
* assert.equal(john.lastName, 'Doe');
*
* @example
* <caption>If the browser you run tests [supports](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy#Browser_compatibility) Proxy, you can use array accessors to access elements by index</caption>
*
* // <table>
* // <tr>
* // <td>Mary<td>
* // </tr>
* // <tr>
* // <td>John<td>
* // </tr>
* // </table>
*
* import { create, collection } from 'ember-cli-page-object';
*
* const page = create({
* users: collection('tr')
* });
*
* // This only works on browsers that support `Proxy`
* assert.equal(page.users[0].text, 'Mary');
* assert.equal(page.users[1].text, 'John');
*
*
* @param {String} scopeOrDefinition - Selector to define the items of the collection
* @param {Object} [definitionOrNothing] - Object with the definition of item properties
* @param {boolean} definition.resetScope - Override parent's scope
* @return {Descriptor}
*/
export function collection(scope, definition) {
if (typeof scope !== 'string') {
throw new Error('collection requires `scope` as the first argument');
}
 
if (isPageObject(definition)) {
//extract the stored definition from the page object
definition = getPageObjectDefinition(definition);
}
 
let descriptor = {
isDescriptor: true,
 
setup(node, key) {
// Set the value on the descriptor so that it will be picked up and applied by Ceibo.
// This does mutate the descriptor, but because `setup` is always called before the
// value is assigned we are guaranteed to get a new, unique Collection instance each time.
descriptor.value = proxyIfSupported(
new Collection(scope, definition, node, key)
);
},
};
 
return descriptor;
}
 
export class Collection {
constructor(scope, definition, parent, key) {
this.scope = scope;
this.definition = definition || {};
this.parent = parent;
this.key = key;
 
this._itemCounter = create(
{
count: count(scope, {
resetScope: this.definition.resetScope,
testContainer: this.definition.testContainer,
}),
},
{ parent }
);
 
this._items = [];
}
 
get length() {
return this._itemCounter.count;
}
 
objectAt(index) {
let { key } = this;
 
if (typeof this._items[index] === 'undefined') {
let { scope, definition, parent } = this;
let itemScope = buildSelector({}, scope, { at: index });
 
let finalizedDefinition = assignDescriptors({}, definition);
 
finalizedDefinition.scope = itemScope;
 
let tree = create(finalizedDefinition, { parent });
 
// Change the key of the root node
Ceibo.meta(tree).key = `${key}[${index}]`;
 
this._items[index] = tree;
}
 
return this._items[index];
}
 
filter(...args) {
return this.toArray().filter(...args);
}
 
filterBy(propertyKey, value) {
return this.toArray().filter((i) => {
if (typeof value !== 'undefined') {
return i[propertyKey] === value;
} else {
return Boolean(i[propertyKey]);
}
});
}
 
forEach(...args) {
return this.toArray().forEach(...args);
}
 
map(...args) {
return this.toArray().map(...args);
}
 
mapBy(propertyKey) {
return this.toArray().map((i) => {
return i[propertyKey];
});
}
 
Similar blocks of code found in 2 locations. Consider refactoring.
findOneBy(...args) {
const elements = this.filterBy(...args);
this._assertFoundElements(elements, ...args);
return elements[0];
}
 
Similar blocks of code found in 2 locations. Consider refactoring.
findOne(...args) {
const elements = this.filter(...args);
this._assertFoundElements(elements, ...args);
return elements[0];
}
 
_assertFoundElements(elements, ...args) {
const argsToText =
args.length === 1 ? 'condition' : `${args[0]}: "${args[1]}"`;
if (elements.length > 1) {
throwBetterError(
this.parent,
this.key,
`${elements.length} elements found by ${argsToText}, but expected 1`
);
}
 
if (elements.length === 0) {
throwBetterError(
this.parent,
this.key,
`cannot find element by ${argsToText}`
);
}
}
 
toArray() {
let { length } = this;
 
let array = [];
 
for (let i = 0; i < length; i++) {
array.push(this.objectAt(i));
}
 
return array;
}
}
 
if (typeof Symbol !== 'undefined' && Symbol.iterator) {
Collection.prototype[Symbol.iterator] = function () {
let i = 0;
let items = this.toArray();
let next = () => ({ done: i >= items.length, value: items[i++] });
 
return { next };
};
}
 
Function `proxyIfSupported` has a Cognitive Complexity of 10 (exceeds 5 allowed). Consider refactoring.
function proxyIfSupported(instance) {
if (window.Proxy) {
return new window.Proxy(instance, {
get: function (target, name) {
if (typeof name === 'number' || typeof name === 'string') {
let index = parseInt(name, 10);
 
if (!isNaN(index)) {
return target.objectAt(index);
}
}
 
return target[name];
},
});
} else {
return instance;
}
}