ember-cli-page-object
Version:
This ember-cli addon eases the construction of page objects on your acceptance and integration tests
260 lines (232 loc) • 7.38 kB
JavaScript
import Ceibo from 'ceibo';
import deprecate from './-private/deprecate';
import { render, setContext, removeContext } from './-private/context';
import { assign, getPageObjectDefinition, isPageObject, storePageObjectDefinition } from './-private/helpers';
import { visitable } from './properties/visitable';
import dsl from './-private/dsl';
function assignDescriptors(target, source) {
Object.getOwnPropertyNames(source).forEach((key) => {
const descriptor = Object.getOwnPropertyDescriptor(source, key);
Object.defineProperty(target, key, descriptor);
});
return target;
}
//
// When running RFC268 tests, we have to play some tricks to support chaining.
// RFC268 helpers don't wait for things to settle by defaut, but return a
// promise that will resolve when everything settles. So this means
//
// page.clickOn('.foo');
// page.clickOn('.bar');
//
// will not wait after either of the clicks, whereas
//
// await page.clickOn('.foo');
// await page.clickOn('.bar');
//
// will wait after each of them. However, to preserve chaining behavior,
//
// page
// .clickOn('.foo')
// .clickOn('.bar');
//
// would need to wait between the clicks. However, if `clickOn()` just returned
// `page` this would be impossible because then it would be exactly the same as
// the first example, which must not wait between clicks.
//
// So the solution is to return something other than `page` from,
// `page.clickOn('.foo')`, but something that behaves just like `page` except
// waits for things to settle before invoking any async methods.
//
// To accomplish this, when building our Ceibo tree, we build a mirror copy of
// it (the "chained tree"). Anytime a chainable method is invoked, instead of
// returning the node whose method was invoked, we can return its mirror node in
// the chained tree. Then, anytime an async method is invoked on that node
// (meaning we are in a chaining scenario), the execution context can recognize
// it as a chained node and wait before invoking the target method.
//
// See https://github.com/san650/ceibo#examples for more info on how Ceibo
// builders work.
// This builder builds the primary tree
function buildObject(node, blueprintKey, blueprint, defaultBuilder) {
let definition;
// to allow page objects to exist in definitions, we store the definition that
// created the page object, allowing us to substitute a page object with its
// definition during creation
if (isPageObject(blueprint)) {
definition = getPageObjectDefinition(blueprint);
} else {
Object.getOwnPropertyNames(blueprint).forEach((key) => {
const { get, value } = Object.getOwnPropertyDescriptor(blueprint, key);
if (typeof get === 'function') {
Object.defineProperty(blueprint, key, {
value: {
isDescriptor: true,
get
}
});
} else if (typeof value === 'string' && !['scope', 'testContainer'].includes(key)) {
deprecate(
'string-properties-on-definition',
'do not use string values on definitions',
'1.17.0',
'2.0.0'
);
}
});
definition = blueprint;
}
let blueprintToStore = assign({}, definition);
//the _chainedTree is an implementation detail that shouldn't make it into the stored
if(blueprintToStore._chainedTree){
delete blueprintToStore._chainedTree;
}
blueprint = assign({}, dsl, definition);
const [ instance, blueprintToApply ] = defaultBuilder(node, blueprintKey, blueprint, defaultBuilder);
// persist definition once we have an instance
storePageObjectDefinition(instance, blueprintToStore);
return [ instance, blueprintToApply ];
}
/**
* Creates a new PageObject.
*
* By default, the resulting PageObject will respond to:
*
* - **Actions**: click, clickOn, fillIn, select
* - **Predicates**: contains, isHidden, isPresent, isVisible
* - **Queries**: text
*
* `definition` can include a key `context`, which is an
* optional integration test `this` context.
*
* If a context is passed, it is used by actions, queries, etc.,
* as the `this` in `this.$()`.
*
* If no context is passed, the global Ember acceptence test
* helpers are used.
*
* @example
*
* // <div class="title">My title</div>
*
* import PageObject, { text } from 'ember-cli-page-object';
*
* const page = PageObject.create({
* title: text('.title')
* });
*
* assert.equal(page.title, 'My title');
*
* @example
*
* // <div id="my-page">
* // My super text
* // <button>Press Me</button>
* // </div>
*
* const page = PageObject.create({
* scope: '#my-page'
* });
*
* assert.equal(page.text, 'My super text');
* assert.ok(page.contains('super'));
* assert.ok(page.isPresent);
* assert.ok(page.isVisible);
* assert.notOk(page.isHidden);
* assert.equal(page.value, 'my input value');
*
* // clicks div#my-page
* page.click();
*
* // clicks button
* page.clickOn('Press Me');
*
* // fills an input
* page.fillIn('name', 'John Doe');
*
* // selects an option
* page.select('country', 'Uruguay');
*
* @example Defining path
*
* const usersPage = PageObject.create('/users');
*
* // visits user page
* usersPage.visit();
*
* const userTasksPage = PageObject.create('/users/tasks', {
* tasks: collection({
* itemScope: '.tasks li',
* item: {}
* });
* });
*
* // get user's tasks
* userTasksPage.visit();
* userTasksPage.tasks().count
*
* @public
*
* @param {Object} definition - PageObject definition
* @param {Object} [definition.context] - A test's `this` context
* @param {Object} options - [private] Ceibo options. Do not use!
* @return {PageObject}
*/
export function create(definitionOrUrl, definitionOrOptions, optionsOrNothing) {
let definition;
let url;
let options;
if (typeof (definitionOrUrl) === 'string') {
url = definitionOrUrl;
definition = definitionOrOptions || {};
options = optionsOrNothing || {};
} else {
url = false;
definition = definitionOrUrl || {};
options = definitionOrOptions || {};
}
let { context } = definition;
// in the instance where the definition is a page object, we must use the stored definition directly
// or else we will fire off the Ceibo created getters which will error
definition = isPageObject(definition)
? assign({}, getPageObjectDefinition(definition))
: assignDescriptors({}, definition);
delete definition.context;
if (typeof url === 'string') {
deprecate(
'create-url-argument',
'Passing an URL argument to `create()` is deprecated',
'1.17.0',
"2.0.0",
);
}
if (url) {
definition.visit = visitable(url);
}
// Build the chained tree
let chainedBuilder = {
object: buildObject
};
let chainedTree = Ceibo.create(definition, assign({ builder: chainedBuilder }, options));
// Attach it to the root in the definition of the primary tree
definition._chainedTree = {
isDescriptor: true,
get() {
return chainedTree;
}
};
// Build the primary tree
let builder = {
object: buildObject
};
let page = Ceibo.create(definition, assign({ builder }, options));
if (page) {
page.render = render;
page.setContext = setContext;
page.removeContext = removeContext;
if (typeof context !== 'undefined') {
page.setContext(context);
}
}
return page;
}