Skip to content

Instantly share code, notes, and snippets.

@caseywatts
Last active Dec 12, 2017
Embed
What would you like to do?
sel pattern

Sel Pattern

Short link: caseywatts.com/selpattern

Simplest Example

const sel = {
  addonCard: 'a.card'
};

it('displays a link to the addon', function() {
  expect(find(sel.addonCard).length).to.eq(1);
});

Motivation

When the DOM changes and tests have to be updated, that can be time-consuming and error-prone.

One common way people deal with this is by introducing page objects, such as ember-cli-page-object. These accomplish a few things:

  • DRY - DRYs up selectors, putting them in one place
  • Separate File - page objects are usually defined in a separate file
  • DSL - Introduce a new domain-specific language (DSL) for interacting with the DOM

Sometimes page objects feel too heavy, so we've been trying what we call the sel pattern. A super light weight way to get the benefits of page objects we wanted, but without the complexity. The sel pattern uses a simple javascript object in the same file as the test. They can be static or dynamic. We call it sel as short for selector - it makes it easier to read in-context that the full word, and it also gives us a short way to describe this pattern :)

The sel pattern does just this:

  • DRY - DRYs up selectors, putting them in one place (usually just one - or at most a few)
  • code completion (within the file where it's defined at least)

From here, we could still refactor into page objects later if we wanted to - but so far we haven't felt it would be worthwhile. With the sel pattern, we don't need to learn/use an additional page object DSL. We also don't have to open a separate file to understand what exactly is being selected.

Other

I'm a little jealous of React's enzyme - I wish I could manually write fewer test selectors, and leverage components' names.

Thanks to Todd Evanoff for helping with this :)

// what's scope vs itemScope vs item? better learn the new DSL!
const page = create({
visit: visitable('/companies/123/users'),
companyUsers: collection({
scope: '[data-test-target="company-users"]',
itemScope: '[data-test-target="company-user"]',
item: {
email: text('[data-test-company-user-email]')
}
})
});
it('shows a list of company users', function() {
this.companyUsers.forEach((companyUser, index) => {
expect(page.companyUsers(index).email).to.equal(companyUser.email, 'shows the email for the user');
});
});
// straightforward js ✨
const sel = {
companyUserEmail(companyUser) {
const { id } = companyUser;
return `[data-test-company-user=${id}] [data-test-company-user-email]`;
}
};
it('shows a list of company users', function() {
this.companyUsers.forEach((companyUser) => {
const emailInDOM = find(sel.companyUserEmail(companyUser)).text().trim();
expect(emailInDOM).to.equal(companyUser.email, 'shows the email for the user');
});
});
// potentially a lot of duplication of selectors, though
it('shows a list of company users', function() {
this.companyUsers.forEach((companyUser) => {
const emailInDOM = find(`[data-test-company-user=${companyUser.id}] [data-test-company-user-email]`).text().trim();
expect(emailInDOM).to.equal(companyUser.email, 'shows the email for the user');
});
});
// test example
import sel from 'APPNAME/tests/helpers/addon-list-selectors';
it('displays a link to the addon', function() {
expect(find(sel.addonCard)[0].textContent.trim()).to.eq(this.firstAddonName);
});
// this file might be at "tests/helpers/addon-list-selectors.js"
// it can be imported in several tests
export default {
addonCard: 'a.card'
}
const sel = {
addonCard: 'a.card'
};
it('displays a link to the addon', function() {
expect(find(sel.addonCard)[0].textContent.trim()).to.eq(this.firstAddonName);
});
// potentially a lot of duplication of selectors, though
it('displays a link to the addon', function() {
expect(find(sel.addonCard).length).to.eq(1);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment