This is a short guide to get you started building powerful UI testing APIs using EmberCLI Page Object.

Basic Component

The primary build block of page objects is a component. It consists of a scope, attributes, custom methods, and nested components.

Assume there is a simple <SearchForm /> component with a text field and a submit button on it.

Example

<form class="search-form">
  <input type="search">
  <button>Search</button>
</form>

Let’s generate a page object component definition for it with the help of the corresponding generator:

$ ember generate page-object-component search-form

installing
  create tests/pages/components/search-form.js

We can describe the form as follows:

// your-app/tests/pages/components/search-form.js
import { triggerable } from 'ember-cli-page-object';

export default {
  scope: '.search-form',

  text: { scope: 'input[type="search"]' },

  submit: triggerable('submit')
};

All the components are supplied with a set of default attributes. In many cases, you need only to define a component’s scope, as seen with the text nested component here.

In order to use a component, you have to import its definition and pass it to the Page Object’s create function:

// my-app/tests/integration/components/search-form-test.js
import { create } from 'ember-cli-page-object';
import SearchForm from 'my-app/tests/pages/components/search-form';

const searchForm = create(SearchForm);

module('SearchForm', // ...
  test('it renders', async function(assert) {
    await render(hbs`<SearchForm @text="initial text" />`);

    // using the default `isVisible` attribute, checks that `.search-form` is displayed
    assert.ok(searchForm.isVisible);

    // using the default `value` attribute, check the input value of `.search-form input[type=`search"]"
    assert.equal(searchForm.text.value, 'initial text');
  });

All the action attributes are asynchronous:

  test('using actions', async function(assert) {
    let lastSearched;
    this.search = (text) => lastSearched = text;

    await render(hbs`<SearchForm onSubmit={{action search}} />`);

    await searchForm.text.fillIn('new text');
    await searchForm.submit();

    assert.deepEqual(lastSearched,  'new text' )
  });

In addition, each action returns the invoked page object node, which allows for the chaining of subsequent actions.

For example. without chaining:

  await searchForm.text.fillIn('test')
  await searchForm.text.blur()

And, the same result with chaining:

  await searchForm.text
    .fillIn('test')
    .blur();

Pages

For comprehensive testing of our application, we must rely on more than testing individual components in isolation (ie acceptance testing). We can map each “page” in our app (route + template) to a page-object, composing various page-object-components together to form a complete representation of the page.

Suppose we have a search page in our application. Let’s generate a page-object for it.

$ ember generate page-object search-page

installing
  create tests/pages/search-page.js

The generator created a file inside the directory /tests/pages. Using this directory allows us to more easily distinguish pages from components, which are located under /tests/pages/components/.

The generated page object looks like the following:

// my-app/tests/pages/my-page.js
import { create, visitable } from 'ember-cli-page-object';

export default create({
  visit: visitable('/')
});

The page is provided with a visitable attribute to navigate to the associated page in our app.

You also might have noticed that rather than exporting a plain definition, we export a page object instance, which can be used in your tests directly after the page is imported.

We can include any number of nested components, attributes, or methods in the definition, just as we did for page-object-components.

Let’s update the page object as follows:

// my-app/tests/pages/my-page.js
import { create, visitable, collection } from 'ember-cli-page-object';
import SearchForm from 'my-app/tests/pages/components/search-form';

export default create({
  visit: visitable('/'),

  results: collection('ol li article'),

  searchForm: SearchForm,

  /**
   * Note, we can also declare native methods on definitions,
   * in order to provide higher level APIs for tests
   */
  async search(text) {
    await this.searchForm.text.fillIn(text);
    await this.searchForm.submit();

    return this.results;
  }
});

A simple application test using a page-object might look like:

// my-app/tests/acceptance/my-page-test.js
import myPage from 'my-app/tests/pages/my-page';

module('Search Page', // ...
  test('it works', async function(assert) {
    await myPage.visit();

    const results = await myPage.search('some');

    assert.equal(results.length, 1);
    assert.ok(results[0].contains('Awesome search result!'));
  })