This style guide was originally created by Carmen Popoviciu and Andres Dominguez. It is based on Carmen's Protractor style guide and Google's Protractor style guide.
Carmen and Andres gave a talk about this style guide at AngularConnect in London. Here's the video in case you want to watch it.
Why?
This rule is a bit controversial, in the sense that opinions are very divided when it comes to what the best practice is. Some developers argue that e2e tests should use mocks for everything in order to avoid external network calls and have a second set of integration tests to test the APIs and database. Other developers argue that e2e tests should operate on the entire system and be as close to the 'real deal' as possible.
Why?
Why?
beforeAll and afterAllThis rule holds true unless the operations performed to initialize the state of the tests are too expensive. For example, if your e2e tests would require that you create a new user before each spec is executed, you might end up with too high test run times. However, this does not mean you should make tests directly depend on one another. So, instead of creating a user in one of your tests and expect that record to be there for all other subsequent tests, you could harvest the power of jasmine's beforeAll (since Jasmine 2.1) to create the user.
/* avoid */
it('should create user', function() {
browser.get('#/user-list');
userList.newButton.click();
userProperties.name.sendKeys('Teddy B');
userProperties.saveButton.click();
browser.get('#/user-list');
userList.search('Teddy B');
expect(userList.getNames()).toEqual(['Teddy B']);
});
it('should update user', function() {
browser.get('#/user-list');
userList.clickOn('Teddy B');
userProperties.name.clear().sendKeys('Teddy C');
userProperties.saveButton.click();
browser.get('#/user-list');
userList.search('Teddy C');
expect(userList.getNames()).toEqual(['Teddy C']);
});
/* recommended */
describe('when the user Teddy B is created', function(){
beforeAll(function() {
browser.get('#/user-list');
userList.newButton.click();
userProperties.name.sendKeys('Teddy B');
userProperties.saveButton.click();
browser.get('#/user-list');
});
it('should exist', function() {
userList.search('Teddy B');
expect(userList.getNames()).toEqual(['Teddy B']);
userList.clear();
});
describe('and gets updated to Teddy C', function() {
beforeAll(function() {
userList.clickOn('Teddy B');
userProperties.name.clear().sendKeys('Teddy C');
userProperties.saveButton.click();
browser.get('#/user-list');
});
it('should be Teddy C', function() {
userList.search('Teddy C');
expect(userList.getNames()).toEqual(['Teddy C']);
userList.clear();
});
});
});
Why?
Why?
Why?
Why?
/* avoid */
var elem = element(by.xpath('/*/p[2]/b[2]/following-sibling::node()' +
'[count(.|/*/p[2]/b[2]/following-sibling::br[1]/preceding-sibling::node())' +
'=' +
' count((/*/p[2]/b[2]/following-sibling::br[1]/preceding-sibling::node()))' +
']'));
by.model and by.binding.<ul class="red">
<li>{{color.name}}</li>
<li>{{color.shade}}</li>
<li>{{color.code}}</li>
</ul>
<div class="details">
<div class="personal">
<input ng-model="person.name">
</div>
</div>
/* avoid */
var nameElement = element.all(by.css('.red li')).get(0);
var personName = element(by.css('.details .personal input'));
/* recommended */
var nameElement = element(by.binding('color.name'));
var personName = element(by.model('person.name'));
Why?
Why?
by.linkText, by.buttonText,
by.cssContainingText.Why?
Page Objects help you write cleaner tests by encapsulating information about the elements on your application page. A page object can be reused across multiple tests, and if the template of your application changes, you only need to update the page object.
Why?
/* avoid */
/* question-spec.js */
describe('Question page', function() {
it('should answer any question', function() {
var question = element(by.model('question.text'));
var answer = element(by.binding('answer'));
var button = element(by.css('.question-button'));
question.sendKeys('What is the purpose of life?');
button.click();
expect(answer.getText()).toEqual("Chocolate!");
});
});
/* recommended */
/* question-spec.js */
var QuestionPage = require('./question-page');
describe('Question page', function() {
var question = new QuestionPage();
it('should ask any question', function() {
question.ask('What is the purpose of meaning?');
expect(question.answer.getText()).toEqual('Chocolate');
});
});
/* recommended */
/* question-page.js */
var QuestionPage = function() {
this.question = element(by.model('question.text'));
this.answer = element(by.binding('answer'));
this.button = element(by.className('question-button'));
this.ask = function(question) {
this.question.sendKeys(question);
this.button.click();
};
};
module.exports = QuestionPage;
Why?
Why?
/* avoid */
var UserProfilePage = function() {};
var UserSettingsPage = function() {};
module.exports = UserPropertiesPage;
module.exports = UserSettingsPage;
/* recommended */
/** @constructor */
var UserPropertiesPage = function() {};
module.exports = UserPropertiesPage;
var UserPage = require('./user-properties-page');
var MenuPage = require('./menu-page');
var FooterPage = require('./footer-page');
describe('User properties page', function() {
...
});
Why?
var UserPropertiesPage = require('./user-properties-page');
var MenuPage = require('./menu-page');
var FooterPage = require('./footer-page');
describe('User properties page', function() {
var userProperties = new UserPropertiesPage();
var menu = new MenuPage();
var footer = new FooterPage();
// specs
});
Why?
<form>
Name: <input type="text" ng-model="ctrl.user.name">
E-mail: <input type="text" ng-model="ctrl.user.email">
<button id="save-button">Save</button>
</form>
/** @constructor */
var UserPropertiesPage = function() {
// List all public elements here.
this.name = element(by.model('ctrl.user.name'));
this.email = element(by.model('ctrl.user.email'));
this.saveButton = $('#save-button');
};
Why?
/**
* Page object for the user properties view.
* @constructor
*/
var UserPropertiesPage = function() {
this.newPhoneButton = $('button.new-phone');
/**
* Encapsulate complex operations in a function.
* @param {string} phone Phone number.
* @param {string} contactType Phone type (work, home, etc.).
*/
this.addContactPhone = function(phone, contactType) {
this.newPhoneButton.click();
$$('#phone-list .phone-row').first().then(function(row) {
row.element(by.model('item.phoneNumber')).sendKeys(phone);
row.element(by.model('item.contactType')).sendKeys(contactType);
});
};
};
Why?
Why?
For example, the Protractor website has navigation bar with multiple dropdown menus. Each menu has multiple options. A page object for the menu would look like this:
/**
* Page object for Protractor website menu.
* @constructor
*/
var MenuPage = function() {
this.dropdown = function(dropdownName) {
/**
* Dropdown api. Used to click on an element under a dropdown.
* @param {string} dropdownName
* @return {{option: Function}}
*/
var openDropdown = function() {
element(by.css('.navbar-nav'))
.element(by.linkText(dropdownName))
.click();
};
return {
/**
* Get an option element under a dropdown.
* @param {string} optionName
* @return {ElementFinder}
*/
option: function(optionName) {
openDropdown();
return element(by.css('.dropdown.open'))
.element(by.linkText(optionName));
}
}
};
};
module.exports = MenuPage;
var Menu = require('./menu');
describe('protractor website', function() {
var menu = new Menu();
it('should navigate to API view', function() {
browser.get('http://www.protractortest.org/#/');
menu.dropdown('Reference').option('Protractor API').click();
expect(browser.getCurrentUrl())
.toBe('http://www.protractortest.org/#/api');
});
});
Why?
Why?
/* avoid */
|-- project-folder
|-- app
|-- css
|-- img
|-- partials
home.html
profile.html
contacts.html
|-- js
|-- controllers
|-- directives
|-- services
app.js
...
index.html
|-- test
|-- unit
|-- e2e
home-page.js
home-spec.js
profile-page.js
profile-spec.js
contacts-page.js
contacts-spec.js
/* recommended */
|-- project-folder
|-- app
|-- css
|-- img
|-- partials
home.html
profile.html
contacts.html
|-- js
|-- controllers
|-- directives
|-- services
app.js
...
index.html
|-- test
|-- unit
|-- e2e
|-- page-objects
home-page.js
profile-page.js
contacts-page.js
home-spec.js
profile-spec.js
contacts-spec.js