Chimp Widgets
UI Abstraction Layer
THIS IS DEPRECATED - USE WEBDRIVER.IO PAGE OBJECTS INSTEAD
Chimp Widgets Does not work in Synchronous Mode
If you have upgraded to the latest synchronous version of Chimp, the widgets framework will not work. You need to use the Async version with the
--sync=false
flag. Please see the migration page for more details.We are working on an updated Sync widgets framework.
The management of step definitions is arguably one of the biggest weaknesses of Cucumber and can get out of hand quite fast. There are multiple reasons that contribute to this problem but chimp-widgets
tackles one of them specifically: The duplication of DOM selectors in step definitions.
Let's see an example of some innocent looking steps:
this.When(/^I go to my projects$/, function(done) {
this.browser.url(process.env.ROOT_URL + 'projects')
.waitForVisible('.projects-list')
.call(done);
});
this.Then(/^I should see my projects$/, function(done) {
this.browser.waitForVisible('.projects-list').call(done);
});
this.When(/^I select the first project$/, function(done) {
this.browser.click('.projects-list .project:nth-child(1)')
.call(done);
});
When we look closer we realize that they contain several (implicit) facts about a project page and its list markup:
- The URL of the project page
process.env.ROOT_URL + 'projects'
- The CSS selector of the project list
.projects-list
- The nested CSS selector to access a project
.projects-list .project
The problem is hidden behind the word implicit, because those three steps also have in common that they never talk about the real thing (the project page) explicitly. To understand what is happening you have to parse the calls to the WebdriverIO API and infer the action targets from DOM selectors.
Let's make the interaction with the DOM more explicit:
this.When(/^I go to my projects$/, function() {
return this.ProjectsPage.visit();
});
this.Then(/^I should see my projects$/, function() {
return new this.ProjectsPage().waitForVisible();
});
this.When(/^I select the first project$/, function() {
return new this.ProjectsPage().selectProjectAt(1);
});
Wow, look at that – we got rid of all the DOM selectors and used some kind of class to represent a real world concept (project page) within our application. This class and its instances expose a simple API that hide the implementation details for the reader. Additionally we could get rid of the done
parameter because all API methods return a Promise
to indicate success or failure to the test runner.
How
Let's look at the definition of the ProjectsPage
class:
module.exports = function() {
this.Before(function(done) {
this.ProjectsPage = this.widgets.Widget.extend({
selector: '.projects-list',
selectProjectAt: function(index) {
this.find('.project:nth-child('+index+')').click();
}
});
this.ProjectsPage.url = process.env.ROOT_URL + 'projects';
done();
});
};
Let's analyze what we did here, step by step:
- Setup a standard
Before
hook frommeteor-cucumber
which provides access to the world, so we can expose our widget class to all step definitions. - Extend the
Widget
class by providing prototype properties and methods. - Define the CSS
selector
of the widget, which is used to scope all WebdriverIO calls to instances of the widget. - Define a custom method
selectProjectAt
which uses theWidget::find
method to create a new (anonymous) widget instance that is scoped within theProjectsPage
selector. The resulting widget has the selector.projects-list .project:nth-child(index)
which is what we used to select a project in our first step definitions example - Call
click
on the ad-hoc project widget
Now you may ask: Where do visit
, waitForVisible
and click
come from? That's the really cool part: You can use the complete WebdriverIO API without any changes. You just leave out the first param (selector) to all API calls because your widgets are taking care of that transparently. The second nice side effect is that the chimp-widgets API wraps the raw WebdriverIO calls with Promises. That's why you can just return new this.ProjectsPage().waitForVisible();
in your step definition and and the test runner handles the resolved or rejected promise.
API
this.widgets.Widget
this.widgets.Widget
The base class of all widgets that wraps the complete WebdriverIO API by scoping all calls to the provided selector
and returning Promises only.
Here is the bare-bones definition of a widget:
this.MyWidget = this.widgets.Widget.extend({
selector: '.my-dom-selector',
});
find(selector)
find(selector)
Creates and immediately returns a new widget instance with selector
scoped within the parent widget. Calling new this.MyWidget().find('.test')
is the same as creating new this.widgets.Widget('.my-dom-selector .test')
.
It is important to know that although the method is called
find
, there is no interaction with WebdriverIO happening. It really just nests CSS selectors so that API calls on the nested widget target nested DOM elements.
hasText(expected)
hasText(expected)
Convenience method around the WebdriverIO getText
method. Is the same as calling widget.getText().should.eventually.become(expected)
.
Exposed WebdriverIO Methods:
The cool thing about chimp-widgets is that you don't have to learn a lot of new concepts. It exposes exactly the same API as WebdriverIO client
or browser
but scopes it to the widget selector
and wraps it into a Promise automatically.
Here is the complete list of supported WebdriverIO methods:
Widget.API = [
// Actions
'addValue', 'clearElement', 'click', 'doubleClick', 'dragAndDrop',
'leftClick', 'middleClick', 'moveToObject', 'rightClick', 'setValue',
'submitForm',
// Property
'getAttribute', 'getCssProperty', 'getElementSize', 'getHTML',
'getLocation', 'getLocationInView', 'getSource', 'getTagName',
'getText', 'getTitle', 'getValue',
// State
'isEnabled', 'isExisting', 'isSelected', 'isVisible',
// Utility
'waitForChecked', 'waitForEnabled', 'waitForExist', 'waitForSelected',
'waitForText', 'waitForValue', 'waitForVisible'
]
Creating anynomous ad-hoc widgets:
Sometimes it is too cumbersome to define a separate class to represent a DOM object. That's where creating ad-hoc instances can be helpful: new this.widgets.Widget(selector)
.
this.widgets.List
this.widgets.List
Documentation coming soon.
In the meanwhile check out this Leaderboard
class defined in the chimp-widgets-demo, which represents the leaderboard from the official Meteor examples:
var widgets = this.widgets;
// Define a widget by extending the base class
this.Leaderboard = widgets.List.extend({
selector: '.leaderboard', // All widgets need a CSS selector
itemSelector: '.player', // Selector of nested items
// Define high-level methods that you can call from steps
givePointsTo: function(name, points) {
self = this;
return this.selectPlayer(name).then(function() {
return self.givePointsToSelectedPlayer(points);
});
},
selectPlayer: function(name) {
return this.getPlayerByName(name).then(function(player) {
return player.click();
});
},
getPlayerByName: function(name) {
return this.findWhere(function(player) {
return player.find('.name').getText().then(function(text) {
return text.match(new RegExp(name, "g"));
});
});
},
givePointsToSelectedPlayer: function() {
return new widgets.Widget('.details .inc').click();
},
checkScoreOf: function(name, expectedPoints) {
return this.getPlayerByName(name).then(function(player) {
return player.find('.score').getText().should.become(expectedPoints);
});
},
checkPlayerIsAbove: function(player1, player2) {
var self = this;
return this.getPlayerPosition(player1).then(function(position1) {
return self.getPlayerPosition(player2).then(function(position2) {
return self.Promise.resolve(position1 < position2).should.become(true);
});
});
},
getPlayerPosition: function(name) {
var Promise = this.Promise;
return Promise.any(this.map(function(player, position) {
return player.find('.name').getText().then(function(playerName) {
if(playerName === name) {
return Promise.resolve(position);
}
else {
return Promise.reject();
}
});
}));
}
});
// Set the URL of the leaderboard screen statically
this.Leaderboard.url = process.env.ROOT_URL;
Updated less than a minute ago