This library is deprecated and won't be updated in future, since protractor support is ending as well: angular/protractor#5502
Simple module, that helps you build your own page fragments, that are inherited from ElementFinder/ElementArrayFinder objects, that brings awesome posibilities to your ProtractorJS tests.
You might heard other names for this pattern: Page Components, Page Composition, HTML Elements, Custom WebElements, WebElement inheritance, Page Elements. This is all about the same.
As any other NPM package:
npm install protractor-element-extend --save-dev
JS:
let BaseFragment = require('protractor-element-extend').BaseFragment
let BaseArrayFragment = require('protractor-element-extend').BaseArrayFragment
TS:
import {BaseFragment, BaseArrayFragment} from 'protractor-element-extend'
To declare your own fragment, declare your class, extend it from BaseFragment, and pass ElementFinder(WebElement in protractorJS) that represents this fragment to super constructor.
Here is example how Checkbox fragment can be declared:
import {BaseFragment} from 'protractor-element-extend'
import {browser, ExpectedConditions as EC} from 'protractor'
class Checkbox extends BaseFragment {
constructor(element) {
super(element) //that's it! now your 'this' reference, is reference to element passed to super constructor
}
//You can extend it with any methods that you what, and use internal reference to your element
select() {
this.isSelected().then(selected => {
if(!selected) {
this.click()
// Notice that because your element is valid ElementFinder - you can pass it as parameter to ExpectedConditions.
browser.wait(EC.elementToBeSelected(this), 5000, `Checkbox ${this.locator()} must became selected after click, but it wasn't`)
} else {
console.warn(`Checkbox ${this.locator()} was already selected, skipping select`)
}
})
}
unselect() {
this.isSelected().then(selected => {
if(selected) {
this.click()
// Notice that because your element is valid ElementFinder - you can pass it as parameter to ExpectedConditions!
browser.wait(EC.not(EC.elementToBeSelected(this)), 5000, `Checkbox ${this.locator()} must became unselected after click, but it wasn't`)
} else {
console.warn(`Checkbox ${this.locator()} was already unselected, skipping unselect`)
}
})
}
}
Often needed to work with own custom collection of own fragments, not only single fragment. For this purpose BaseArrayFragment is added. This object extends ElementArrayFragment, and overrides methods that return single elements, to return your custom fragments. .map() .filter() .reduce() .each()
and other will receive your custom fragment as parameter as well.
Here is example how SearchResultsCollection fragment can be declared:
import { BaseArrayFragment, BaseFragment } from 'protractor-element-extend'
import { browser, ExpectedConditions as EC, $$ } from 'protractor'
// Describing single search result on our page. Notice that constructor declaration could be skipped, in this case constructor from BaseFragment will be used
class SearchResult extends BaseFragment {
name() {
return $('.name').getText()
}
isDiscounted() {
return this.$('.discount-label').isDisplayed()
}
open() {
this.$('button.open').click()
}
}
// Generics - <SearchResults> are needs to be defined to provide typings support.
class SearchResultsCollection extends BaseArrayFragment<SearchResult> {
constructor(elementsToExtend: ElementArrayFinder) {
// You should pass ElementArrayFinder into super constructor, and constructor(class) that will be used to wrap each element in your collection
super(elementsToExtend, SearchResult);
}
findResultsWithDiscount() {
// This will return new SearchResults object with only those SearchResult objects that has isDiscounted == true
return this.filter(searchRes => searchRes.isDiscounted())
}
}
// Initializing is the same as BaseFragment, but you need to pass ElementArrayFinder now.
let searchResults = new SearchResultsCollection($$('.search-result'))
// Awesome readability for your tests
searchResults.findResultsWithDiscount().first().open()
BaseArrayFragment also supports additional array methods that does not exist in ElementArrayFinder in ProtractorJS:
.every()
.some()
.find()
You can wrap any ElementFinder into your fragment:
let checkbox = new Checkbox($$('.checkbox').last())
You can use your fragments everywhere where ElementFinder is expected. For example - inside browser.wait
browser.wait(EC.elementToBeClickable(checkbox), 5000, 'Checkbox should be clickable')
Or inside executeScript
:
let checkbox = new Checkbox($('div.checkbox'))
var tag = browser.executeScript('return arguments[0].tagName', checkbox);
expect(tag).toEqual('div');
You can override ElementFinder or ElementArrayFinder methods, to get even more powerful functionality
...
import {promise} from 'protractor'
class Checkbox extends BaseFragment {
...
// Ovveriding isDisplayed function that is used in ExpectedCondition.visibilityOf()
isDisplayed() {
let fragmentDisplayed = super.isDisplayed()
let loaderDisplayed = $('.loader').isDispayed()
// This will return promise, that will be resolved to true, if element is displayed, but loader is not displayed.
return promise.all(fragmentDisplayed, loaderDisplayed).then(displArray=> displArray[0] && !displArray[1])
}
}
...
let checkbox = new Checkbox($('.checkbox'))
browser.wait(EC.visibilityOf(checkbox), 3000, 'Checkbox should be visible, but loader should not be visible')
Try to use fragments by placing fragments one into another:
class LoginForm extends BaseFragment {
loginField:TextField
passwordField:TextField
rememberMe:Checkbox
constructor() {
super($('.loginform'))
// Notice that we are searching only inside 'this' element, this brings additional stability to tests
this.loginField = new TextField(this.$('.loginfield'))
this.passwordField = new TextField(this.$('.passwordfield'))
this.rememberMe = new Checkbox(this.$('.rememberme'))
}
login(username='test', password='test', rememberme=true) {
this.loginField.type(username)
this.passwordField.type(password)
rememberme && this.rememberMe.select()
this.$('button.login').click()
}
}
Currently tested on NodeJS:
- 6
- 7
- 8
- 9
- 10
ProtractorJS:
- 5.x
This lib should work on protractor 4.x without modifications (but this is untested). Protractor 3.x will require browser_
reference rename. PRs are welcome!
Typings for TypeScript are included.
- Better logging for fragments. Provide possibility to set
name
attribute, and if it is not set - try to generate best we can withlocator()
- Want some feature? Feel free to create issue!
Source code for ElementFinder and ElementArrayFinder -
https://github.com/angular/protractor/blob/master/lib/element.ts
Generics in TypeScript -