Sergey N. Bolshchikov
Developer Advocate, Wix
Website: http://bolshchikov.net
Github: http://github.com/bolshchikov
LinkedIn: https://il.linkedin.com/in/bolshchikov
Protractor: Tips & Tricks
Sergey N. Bolshchikov
Developer Advocate, Wix
Website: http://bolshchikov.net
Github: http://github.com/bolshchikov
LinkedIn: https://il.linkedin.com/in/bolshchikov
Protractor: Tips & Tricks
Developer,
what do you want?
Let’s test the s!#t
out of the code
This is not fun
1000+ tests of experience
Sharing best practices and tricks
Saving you several hours of pain
PROCEED WITH CAUTION
A LOT OF CODE
Best Practices
Problem 1
Unknown window size when
a browser is opened
10
11
12
Solution 1
Set the required window size
before every test
13
Solution 1
browser
.driver
.manage()
.window()
.setSize(1280, 1024);
14
Problem 2
Changing html structure (tags/classes)
will break tests
15
Solution 2
<button
class="button load-more"
ng-click="showMore()"
data-hook="load-items">Show More
</button>
16
Solution 2
it('should display load more, () => {
expect(element(by.dataHook('load-items'))
.isDisplayed())
.toBe(true);
});
17
Solution 2
// define new locator
by.addLocator('dataHook', (hook) => {
return document.querySelector('[data-hook=''
+ hook
+ '']');
});
18
Solution 2
<a
class="show-more"
ng-click="showMore()"
data-hook="load-items">Show More
</a>
19
Problem 3
Small change in html can cause
many changes in tests
20
Solution 3
Page Object and Page Fragments
that introduce additional level of
abstraction
21
22
23
Solution 3
class FiltersFragment {
constructor () {
this.container = $('#filters');
this.label = this.container.$('header');
this.items = this.container.$$('.item');
}
chooseItem(value) {
// ... logic goes here
}
}
24
Solution 3
// how to use
var Filters = require('../pages/filters');
describe('Filters Page', () => {
var filters;
beforeEach(() => {
browser.get('/');
filters = new Filters();
});
it('should have 8 filters', () => {
expect(filters.items.count()).toEqual(8);
});
});
25
Solution 3
// how to use
var Filters = require('../pages/filters');
describe('Filters Page', () => {
var filters;
beforeEach(() => {
browser.get('/');
filters = new Filters();
});
it('should have 8 filters', () => {
expect(filters.items.count()).toEqual(8);
});
});
26
Solution 3
// how to use
var Filters = require('../pages/filters');
describe('Filters Page', () => {
var filters;
beforeEach(() => {
browser.get('/');
filters = new Filters();
});
it('should have 8 filters', () => {
expect(filters.items.count()).toEqual(8);
});
});
27
Problem 4
Complicated tests are hard to read
28
Problem 4
expect($('#readMore'))
.getAttribute('class'))
.toContain('collapse');
29
Solution 4
expect($('#readMore')).toHaveClass('collapse');
30
Solution 4
this.addMatchers({
toHaveClass: (className) => {
return this.actual
.getAttribute('class')
.then((classes) => {
return classes
.split(' ')
.indexOf(className) !== -1;
});
}
});
31
Solution 4
this.addMatchers({
toHaveClass: (className) => {
return this.actual
.getAttribute('class')
.then((classes) => {
return classes
.split(' ')
.indexOf(className) !== -1;
});
}
});
32
Solution 4
this.addMatchers({
toHaveClass: (className) => {
return this.actual
.getAttribute('class')
.then((classes) => {
return classes
.split(' ')
.indexOf(className) !== -1;
});
}
});
33
Problem 5
Many tests lead to hairy long and
complicated test structure
34
Solution 5
e2e
|- fragments (page fragments)
|- libs (utility/helpers/matchers)
|- modals (object of modal windows)
|- pages (page objects)
|- spec (scenarios)
35
Tricks
Problem 1
Sharing values between tests
37
Solution 1
// config file
params: {
username: 'Sergey'
}
// use
browser.params.username
38
Problem 2
Setting a new value in select tag
39
Solution 2
function chooseOption(val) {
return element(by.css('option[value="' + val + '"]'))
.click();
}
40
Problem 3
Classical click is simple but
how about right mouse button click?
41
Solution 3
browser
.actions()
.mouseMove(element1)
.click(protractor.Button.RIGHT)
.perform();
42
Problem 4
Still can’t find a protractor method
that fits your purposes?
43
Solution 4
browser
.executeAsyncScript();
44
Problem 5
Testing non-angular application
with protractor?
45
Solution 5
browser.ignoreSynchronization = true;
browser
.wait(picasaAuthPage.waitForEnabledApproveButton())
.then(function () {
picasaAuthPage.clickApproveButton();
utils.switchToWindow(handles[0]);
browser.ignoreSynchronization = false;
done();
});
46
Solution 5
browser.ignoreSynchronization = true;
browser
.wait(picasaAuthPage.waitForEnabledApproveButton())
.then(function () {
picasaAuthPage.clickApproveButton();
utils.switchToWindow(handles[0]);
browser.ignoreSynchronization = false;
done();
});
47
Solution 5
browser.ignoreSynchronization = true;
browser
.wait(picasaAuthPage.waitForEnabledApproveButton())
.then(function () {
picasaAuthPage.clickApproveButton();
utils.switchToWindow(handles[0]);
browser.ignoreSynchronization = false;
done();
});
48
Performance
Problem 1 and Only
Running e2e tests
takes too much time
50
Solution 1
Use sharding configuration for running
several browser instances
51
Solution 1
shardTestFiles: true,
maxInstances: 4,
52
Solution 1
Note: sharding is per file so create
several testing files
53
Solution 2
Disable ng-animate
and css animations
54
Solution 2
var disableNgAnimate = function () {
angular
.module('disableNgAnimate', [])
.run(function ($animate) {
$animate.enabled(false);
});
};
browser.addMockModule('disableNgAnimate', disableNgAnimate);
55
Solution 2
var disableTransitions = function () {
var style = document.createElement('style');
style.type = 'text/css';
style.innerHTML = '* {' +
'-webkit-transition: none !important;' +
'-moz-transition: none !important;' +
'-o-transition: none !important;' +
'-ms-transition: none !important;' +
'transition: none !important;' +
'}';
document.getElementsByTagName('head')[0].appendChild(style);
56
Solution 2
var disableCssAnimate = function () {
angular
.module('disableCssAnimate', [])
.run(disableTransitions);
browser.addMockModule('disableCssAnimate', disableCssAnimate);
57
Solution 3
The right balance between
amount of expects and its
58
Run & Debugging
Problem 1
Hard to debug tests
60
Solution 1
Integration with IDE such as
Intellij/Webstorm
61
62
63
Problem 2
Something is wrong and
you have no idea why
64
Solution 2
Interactive REPL
Element explorer
65
Solution 2
webdriver-manager start
cd node_modules/protractor/bin
./elementexplorer.js
66
Problem 3
Run in CI in all major browsers
67
Solution 3
Use Sauce Labs with credentials
specified directly in protractor config
file.
68
Solution 3
export.config = {
sauceUser: process.env.SAUCE_USERNAME,
sauceKey: process.env.SAUCE_ACCESS_KEY,
capabilities['tunnel-identifier']: process.env.BUILD_NUMBER
}
69
Quīnta Essentia
Protractor Helpers
Library
Helper Functions
// Navigate to a url, maximizing the window
helpers.safeGet('./SomeUrl');
// hover over the element
helpers.displayHover($('.some-element'));
helpers.waitForElementToDisappear($('.some-element'), timeout);
helpers.selectOptionByIndex($('select'), 0);
if (helpers.isIE()) {
// Do FF stuff here . . .
}
helpers.clearAndSetValue(inputField, 'text to populate');
72
Data-hook Locators
<ul>
<li data-hook="first">First</li>
<li data-hook="second">Second</li>
</ul>
// click on the first data hook
element(by.dataHook('first')).click();
73
Custom Matchers
expect($('.some-element')).toBeDisplayed();
expect($('.some-element')).toHaveValue(expectedValue);
expect($('.some-element')).toHaveClass(className);
expect($('.some-element')).toBeDisabled();
expect($('.some-element')).toBeInvalidRequired();
74
Protractor Helpers: Before
it('should disable upgrade button when no upgrade permission', () => {
utils.runIfNotIE(() => {
utils.setPermissions([], false);
dashboardPage.navigateById(siteId);
var upgrade = dashboardPage.getUpgradeButton();
expect(upgrade.getAttribute('disabled')).toEqual('true');
expect(upgrade.getAttribute('href')).toEqual(null);
browser.actions().mouseMove(upgrade).perform();
var tooltip = dashboardPage.getUpgradeTooltip();
expect(tooltip.isDisplayed()).toBeTruthy();
expect(tooltip.getText()).toEqual('This feature is not open');
});
});
75
Protractor Helpers: After
it('should disable upgrade button when no upgrade permission', () => {
if (!helpers.isIE()) {
utils.setPermissions([], false);
dashboardPage.navigateById(siteId);
var upgrade = dashboardPage.getUpgradeButton();
expect(upgrade).toBeDisabled();
expect(upgrade.hasLink()).toHaveValue(null);
helpers.moveToElement(upgrade);
var tooltip = dashboardPage.getUpgradeTooltip();
expect(tooltip).toBeDisplayed();
expect(tooltip).toHaveText('This feature is not open');
}
});
76
Compare
expect(upgrade.getAttribute('disabled')).toEqual('true');
vs.
expect(upgrade).toBeDisabled();
expect(upgrade.getAttribute('href')).toEqual(null);
vs.
expect(upgrade.hasLink()).toHaveValue(null);
browser.actions().mouseMove(upgrade).perform();
vs.
helpers.moveToElement(upgrade);
77
Compare
expect(upgrade.getAttribute('disabled')).toEqual('true');
vs.
expect(upgrade).toBeDisabled();
expect(upgrade.getAttribute('href')).toEqual(null);
vs.
expect(upgrade.hasLink()).toHaveValue(null);
browser.actions().mouseMove(upgrade).perform();
vs.
helpers.moveToElement(upgrade);
78
Compare
expect(upgrade.getAttribute('disabled')).toEqual('true');
vs.
expect(upgrade).toBeDisabled();
expect(upgrade.getAttribute('href')).toEqual(null);
vs.
expect(upgrade.hasLink()).toHaveValue(null);
browser.actions().mouseMove(upgrade).perform();
vs.
helpers.moveToElement(upgrade);
79
Conclusion
E2E tests give you
confidence
Make tests readable and
maintainable again
Thank you!

Сергей Больщиков "Protractor Tips & Tricks"