Describe functional fragments of the DOM

Definitions

Components are created from definitions, which are just plain objects with attributes, methods, and nested definitions on them.

Example

<form class="awesome-form">
  <input id="firstName" placeholder="First name">
  <input id="lastName" placeholder="Last name">
  <button>Create</button>
</form>
import {
  clickable,
  fillable
} from 'ember-cli-page-object';

const FormDefinition = {
  scope: '.awesome-form',

  firstName: fillable('#firstName'),
  lastName: fillable('#lastName'),
  submit: clickable('button')
};

Component instances are built by the create function:

Usage

import { create } from 'ember-cli-page-object';

const form = create(FormDefinition);

await form
  .firstName('John')
  .lastName('Doe')
  .submit();

The best way to describe complex interfaces is through the composition of simpler component definitions.

import { create, visitable } from 'ember-cli-page-object';

const PageDefinition = {
  visit: visitable('/users/new'),

  form: FormDefinition
}

const myPage = create(PageDefinition);

await myPage.visit()
  .form
  .firstName('John')
  .lastName('Doe')
  .submit();

Scopes

The scope attribute, which refers to the CSS selector that encloses a component, is used to target the corresponding DOM element during testing. Parent scopes are included when calculating a nested component’s selector.

Given the following HTML


<div class="article">
  <p>Lorem ipsum dolor</p>
</div>
<div class="footer">
  <p>Copyright Acme Inc.</p>
</div>

the following configuration will match the article paragraph element

const page = create({
  scope: '.article',

  textBody: {
    scope: 'p'
  }
});

assert.equal(page.textBody.text, 'Lorem ipsum dolor.');

When page.textBody.text is evaluated, each link in the hierarchy of page objects has its scope incorporated into the final selector that’s used to find an element whose text is returned (.article p in this example).

You can avoid applying the parent scope to a particular component by setting the resetScope attribute on the component’s definition to true.

const form = create({
  scope: '.my-form',

  dialog: {
    scope: '.some-dialog',

    resetScope: true
  }
});

await form.clickOn('Cancel');

assert.ok(form.dialog.isVisible);

Attributes

Attributes are just Page Object aware wrappers around low level DOM operations. They allow you to configure a component’s testable behavior in a declarative fashion.

By default, attribute uses a parent component’s scope:

import { create, value } from 'ember-cli-page-object';

const input = create({
  scope: 'input[name="my-input"]',

  value: value()
})

assert.equal(input.value, 'some value');

In the assert statement above, the value attribute queries a DOM element with the selector input[name="my-input"] and returns its DOM value property.

You can add further specificity by passing a selector as the attribute’s argument:

import { create, text } from 'ember-cli-page-object';

const customSelect = create({
  scope: '.my-select',

  value: text('.selected')
})

assert.equal(customSelect.value, 'some value');

In the assert statement above, the text attribute queries a DOM element with the selector .my-select .trigger and returns its text value.

Actions

Actions are a special kind of attribute that allow page objects to perform async operations on the DOM.

import { create, fillable, triggerable } from 'ember-cli-page-object';

const form = create({
  scope: 'form.search-form',

  fillIn: fillable('input[type="search"]'),

  submit: triggerable('submit')
})

await form.fillIn('some text');
await form.submit();

The result of an action is a Promise-like chainable page object node.

Chaining allows you to write scenarios in the following way:

await form
  .fillIn('some text')
  .submit();

Default attributes

The following commonly used attributes are included in every component page object by default to help reduce the boilerplate.

Functions and Getters

You can also use native functions and getters in your declarations, in order to provide higher level APIs for tests:

import { collection } from 'ember-cli-page-object';

const MyForm = {
  errors: collection('ul.errors > li'),

  title: {
    scope: '#firstName'
  },

  description: {
    scope: '#lastName'
  },

  submitButton: {
    scope: 'button'
  },

  async submit(data) {
    await this.title.fillIn(data.username)
    await this.description.fillIn(data.password)

    return this.submitButton.click();
  }

  get errorMessages() {
    return this.errors.map((e) => e.text);
  }
}