see https://github.com/angular/protractor/blob/master/docs/referenceConf.js
Welcome to the Protractor API docs page. These pages contain the Protractor reference materials.
see http://www.protractortest.org/#/api
只能看到迁移到官网之前Github 上的api
https://github.com/angular/protractor/blob/1.3.1/docs/api.md#api-elementfinder-prototype-element
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.
Video
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.
Table of contents
Test suites
Locator strategies
Page objects
Project structure
Why?
Unit tests are much faster than e2e tests
Avoid duplicate tests
Protractor can run your tests in parallel when you enable sharding. The files are executed across different browsers as they become available.
Make your tests independent at the file level because the order in which they run is not guaranteed and it's easier to run a test in isolation.
Avoid using if statements and for loops. When you add logic your test may pass without testing anything, or may run very slowly.
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?
Using the real application with all the dependencies gives you high confidence
Helps you spot some corner cases you might have overlooked
Why?
Jasmine is well documented
It is supported by Protractor out of the box
You can use beforeAll
and afterAll
This 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?
You can run tests in parallel with sharding
The execution order is not guaranteed
You can run suites in isolation
You can debug your tests (ddescribe/fdescribe/xdescribe/iit/fit/xit)
Why?
Assures you that the page under test is in a clean state
Why?
Makes sure the major parts of the application are correctly connected
Users usually don’t navigate by manually entering urls
Gives confidence about permissions
Why?
It's the slowest and most brittle locator strategy of all
Markup is very easily subject to change and therefore xpath locators require a lot of maintenance
xpath expressions are unreadable and very hard to debug
/* 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()))' + ']'));
Prefer protractor-specific locators such as 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?
These locators are usually specific, short, and easy to read.
It is easier to write your locator
The code is less likely to change than other markup
Why?
Both are very performant and readable locators
Access elements easier
Try to avoid text-based locators such as by.linkText
, by.buttonText
, by.cssContainingText
.
Why?
Text for buttons, links, and labels tends to change over time
Your tests should not break when you make minor text changes
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?
Encapsulate information about the elements on the page under test
They can be reused across multiple tests
Decouple the test logic from implementation details
/* 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?
Each page object should be defined in its own file.
Why? Keeps code clean and makes things easy to find.
Why?
Each page object should declare a single class. You only need to export one class.
/* avoid */ var UserProfilePage = function() {}; var UserSettingsPage = function() {}; module.exports = UserPropertiesPage; module.exports = UserSettingsPage;
/* recommended */ /** @constructor */ var UserPropertiesPage = function() {}; module.exports = UserPropertiesPage;
Why? One page object per file means there's only one class to export.
You should declare all the required modules at the top of your page object, test, or helper module.
var UserPage = require('./user-properties-page'); var MenuPage = require('./menu-page'); var FooterPage = require('./footer-page'); describe('User properties page', function() { ... });
Why?
The module dependencies should be clear and easy to find.
Create new instances of the page object at the top of your top-level describe.
Use upper case for the constructor name; lowercase for the instance name.
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?
Separates dependencies from the test code.
Makes the dependencies available to all specs of the suite.
All the elements that will be visible to the test should be declared in the constructor.
<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?
The user of the page object should have quick access to the available elements on a page
/** * 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?
Most elements are already exposed by the page object and can be used directly in the test.
Doing otherwise will not have any added value
Don't make any assertions in your page objects.
Why?
It is the responsibility of the test to do all the assertions.
A reader of the test should be able to understand the behavior of the application by looking at the test only
Some directives render complex HTML or they change frequently. Avoid code duplication by writing wrappers to interact with complex directives.
Dialogs or modals are frequently used across multiple views.
When the directive changes you only need to change the wrapper once.
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?
When you have a large team and multiple e2e tests people tend to write their own custom locators for the same directives.
Why?
Finding your e2e related files should be intuitive and easy
Makes the folder structure more readable
Clearly separates e2e tests from unit tests
/* 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
In vanilla WebDriverJS code, you might start your tests with
var webdriver = require('selenium-webdriver');var browser = new webdriver.Builder().usingServer().withCapabilities(c).build();
In Protractor, you are provided with global protractor
and browser
objects, which more or less match the webdriver
and browser
objects, respectively. So if you are already familiar with writing WebDriver code, the most basic way to start writing Protractor code is just to replace webdriver
with protractor
.
However, Protractor also provides some syntactic sugar to help you write your tests.
WebDriver Syntax | Protractor Syntax |
---|---|
webdriver.By |
by |
browser.findElement(...) |
element(...) |
browser.findElements(...) |
element.all(...) |
browser.findElement(webdriver.By.css(...)) |
$(...) |
browser.findElements(webdriver.By.css(...)) |
$$(...) |
If you need the vanilla WebDriver browser
object, you can access it via browser.driver
Using ElementFinders
In Protractor, you use the element
function to find and interact with elements through an ElementFinder
object. This extends a WebDriver WebElement
by adding chaining and utilities for dealing with lists. See locators#actions for details.
Jasmine Integration
Protractor uses jasminewd2
, which wraps around jasmine's expect
so that you can write:
expect(el.getText()).toBe('Hello, World!')
Instead of:
el.getText().then(function(text) { expect(text).toBe('Hello, World!');});
Protractor supports the two latest major versions of Chrome, Firefox, Safari, and IE. These are used in Protractor's own suite of tests. You can view the current status on Travis.
Note that because Protractor uses WebDriver to drive browsers, any issues with WebDriver implementations (such as FireFoxDriver, ChromeDriver, and IEDriver) will show up in Protractor. The chart below links to major known issues. You can search through all WebDriver issues at the Selenium issue tracker.
Driver | Support | Known Issues |
---|---|---|
ChromeDriver | Yes | Link |
FirefoxDriver | Yes | Link |
SafariDriver | Yes | Link |
IEDriver | Yes | Link |
OperaDriver | No | |
ios-Driver | No | |
Appium - iOS/Safari | Yes* | Link |
Appium - Android/Chrome | Yes* | Link |
Selendroid | Yes* | |
PhantomJS / GhostDriver | ** | Link |
(*) These drivers are not yet in the Protractor smoke tests.
(**) We recommend against using PhantomJS for tests with Protractor. There are many reported issues with PhantomJS crashing and behaving differently from real browsers.