As a developer who writes lots of tests, I've been becoming fond of the Page Object pattern to help organize logic around interacting with pages in the browser.

What is a Page Object?

A page object is basically an object that represents a page or a section of a page. This object knows how to interact with elements on that page/section. This layer of abstraction encapsulates all css selectors, clicks, scrolls, and any other event tied to an element on a page/section.

Why should I use Page Objects?

When working on a project with many tests, the suite starts to grow into different files littered with many css selectors and logic around interacting with certain elements. Eventually a lot of copy and paste code starts to happen. Page objects help make tests more DRY, readable, and easier to write.

Introducing ember-cli-page-object

ember-cli-page-object which is an ember addon that helps
facilitate the use of page objects.

Page objects are primarily used in integration tests (or acceptance tests) and component integration tests. When writing these tests you are interacting with a page or element on a page.

ember-cli-page-object provides a set of helpers to define pages/sections with elements, where elements can be text snippets, input fields, buttons, links, etc. After these objects are define, ember-cli-page-object provides a clean API for interacting with them.

Okay now that you have an idea of what page objects are, first let's see an example of tests without page objects, followed by tests with page objects.

Suppose we have an ember blog app that has a posts index page and posts show page, both with a comments section. Below we have tests for the following scenarios:

  • Blog posts show on index page
  • Comments can be added to a post on index page

The follow examples assume that ember-cli-mirage is included in the project (to help setup data)

The Old Way (without page objects)

posts-index-test.js
test('Blog posts show sorted by date descending', function(assert) {
  server.create('post', {
    title: 'Ember Testing Tricks',
    content: 'foo bar baz',
    date: yesterday()
  });
  server.create('post', {
    title: 'Speeding Up Ember Build',
    content: 'lorem ipsum',
    date: today()
  });

  visit('/posts');

  andThen(() => {
    assert.strictEqual(
      find('.post:eq(0) .title').text().trim(),
      'Speeding Up Ember Build'
    );
    assert.strictEqual(
      find('.post:eq(0) .content').text().trim(),
      'lorem ipsum'
    );
    assert.strictEqual(
      find('.post:eq(1) .title').text().trim(),
      'Ember Testing Tricks'
    );
    assert.strictEqual(
      find('.post:eq(1) .content').text().trim(),
      'foo bar baz'
    );
  });
});

test('Comments can be added to a post', function(assert) {
  let post = server.create('post', {
    title: 'Ember Testing Tricks',
    content: 'foo bar baz',
    date: yesterday()
  });

  visit('/posts');

  andThen(() => {
    click('.post .comments .btn.toggle-comments'); // Show comment form
  });

  andThen(() => {
    assert.notOk(find('.comment').length);

    fillIn('.comments-form .comment-text-field', 'cool blog post!');
    fillIn('.comments-form .commenter-email-field', 'ryu@streetfighter.com');
    click('.comments-form .submit');
  });

  andThen(() => {
    assert.strictEqual(find('.comment').length, 1);
    assert.strictEqual(
      find('.comment .comment-text').text().trim(),
      'cool blog post!'
    );
    assert.strictEqual(
      find('.comment .commenter-email').text().trim(),
      'ryu@streetfighter.com'
    );
  });
});

The New Way (with page objects)

Looking at the tests above, we have a posts index page and a few sections (post & comments).

Here are the page objects:

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

import post from 'bloggr/tests/pages/sections/post';

export default create({
  visit: visitable('/posts'),
  posts: collection({
    itemScope: post.scope,
    item: post
  })
});

tests/pages/sections/post.js
import {
  text
} from 'ember-cli-page-object';

import comments from 'bloggr/tests/pages/sections/comments';

export default {
  scope: '.post',
  title: text('.title'),
  content: text('.content'),
  commentsSection: comments
};

tests/pages/sections/comments.js
import {
  text,
  clickable,
  fillable,
  collection
} from 'ember-cli-page-object';

export default {
  scope: '.comments',
  toggleComments: clickable('.btn.toggle-comments'),
  form: {
    scope: '.comment-form',
    comment: fillable('.comment-text-field'),
    email: fillable('.commenter-email-field'),
    submit: clickable('.submit')
  },
  comments: collection({
    itemScope: '.comment',
    item: {
      text: text('.comment-text'),
      email: text('.commenter-email')
    }
  })
};

With the page objects defined above, here are the tests using page objects:

import page from 'bloggr/tests/pages/posts';

test('Blog posts show sorted by date descending', function(assert) {
  server.create('post', {
    title: 'Ember Testing Tricks',
    content: 'foo bar baz',
    date: yesterday()
  });
  server.create('post', {
    title: 'Speeding Up Ember Build',
    content: 'lorem ipsum',
    date: today()
  });

  page.visit();

  andThen(() => {
    assert.strictEqual(page.posts(0).title, 'Speeding Up Ember Build');
    assert.strictEqual(page.posts(0).content, 'lorem ipsum');
    assert.strictEqual(page.posts(1).title, 'Ember Testing Tricks');
    assert.strictEqual(page.posts(1).content, 'foo bar baz');
  });
});

test('Comments can be added to a post', function(assert) {
  let post = server.create('post', {
    title: 'Ember Testing Tricks',
    content: 'foo bar baz',
    date: yesterday()
  });

  page.visit();

  let commentsSection = page.posts(0).commentsSection;
  commentsSection.toggleComments();

  andThen(() => {
    assert.notOk(commentsSection.comments().count);
    let form = commentsSection.form;
    form 
      .comment('cool blog post!')
      .email('ryu@streetfighter.com')
      .submit();
  });

  andThen(() => {
    assert.strictEqual(commentsSection.comments().count, 1);
    assert.strictEqual(commentsSection.comments(0).text 'cool blog post!');
    assert.strictEqual(commentsSection.comments(0).email 'ryu@streetfighter.com');
  });
});


Now that we have page objects defined, it will be fairly easy to write tests for the post show page assuming we wanted to test the blog post shows correctly & comments can be added.

The page objects serve as a central location for logic surround a page or section, so these can be used across your entire suite. If a redesign occurs, then changing selectors would only need to happen in the page objects, instead of searching and replacing all instances of selectors within your tests.

Check out the docs for Ember Page Objects here.