Table of Contents generated with DocToc
- Overview
- I have no idea what I'm doing.
- I have some experience using Node.
- I have no idea how to write page objects.
- I haven't written page objects in Javascript.
- Deconstructing a page.
- About this Tutorial
This is going to cover the basics of writing page objects in an Angular app using Protractor. I am going to assume you are brand new to the Node stack, and will walk you through setting that up. For those who are already familiar with Node, Protractor, and the page object model, you may want to jump ahead to the part where we talk about page objects in Protractor.
That's Ok. Start here by downloading node:
Download for Windows.
NOTE: this tutorial should, in theory, work with Windows (not Cygwin). However, I can't any make guarantees about that. Use at your own risk.
For Mac:
- Get
brew
. brew install npm
(brew includes node when you install npm)
For Linux.
NOTE: you will need to follow the instructions included above for updating your aptitude repositories to use Protractor.
npm
is the Node package manager, and gets helpful stuff for you.
Node projects are all self-contained. You keep their dependencies (listed in the node_modules
directory) isolated from your other projects. This is a Good Thing.
Usually, you don't want to have node modules installed globally, meaning they are available everywhere. Some exceptions are listed below. Run this command to get command-line access to these applications:
npm install -g protractor mocha
And then install this project's dependencies locally:
npm install
You will have all dependencies in this project's package.json
file installed in your node_modules
file.
Ok. Make sure you have Protractor available globally. Use npm install -g protractor
to achieve this.
Once you've installed all the dependencies for this project, the next piece to getting set up is to get a selenium webserver running on your system. Here's how to do this:
For mac, this is easy:
$>: brew install selenium-server-standalone
...
$>: selenium-server
Once you run this command, it becomes a dedicated process that will use up your terminal. Just leave it be and open a new terminal window to continue working.
For everyone else:
- Download the
selenium-server-standalone.jar
file found on this page. - Figure out where you downloaded it (usually, this is in your home directory, under "Downloads").
- Start a selenium server with something like this command:
$>: java -jar ~/Downloads/selenium-server-standalone-V.vv.x.jar
NOTE:
V.vv.x
is the version of your jarfile that you downloaded. Don't copy and paste this line.
- Another note: it may be helpful for you to create an alias to this command, as you'll use it frequently.
If all works well, you should see the following output when you run the actual protractor tests:
$>: protractor test/protractor.conf.js
------------------------------------
PID: 15185 (capability: firefox #1)
------------------------------------
Using the selenium server at http://localhost:4444/wd/hub
Main page
✓ should be at the correct URL
1 passing (2s)
Page objects are, in essence, a way of deconstructing a website's user interface into an API that represents what a user might do using that website. For instance, a good example of a high level protractor test might look like this:
it('should jump forward to page 7 using pagination', function() {
pagination.jumpToPage(7);
expect(pagination.getCurrentPageNumber()).toEqual(7);
});
it('should jump backward to page 2 using pagination', function() {
pagination.jumpToPage(2);
expect(pagination.getCurrentPageNumber()).toEqual(2);
});
it('should navigate forward one page at a time', function() {
pagination.nextPage();
expect(pagination.getCurrentPageNumber()).toEqual(3);
});
it('should navigate backwards one page at a time', function() {
pagination.previousPage();
pagination.previousPage();
expect(pagination.getCurrentPageNumber()).toEqual(1);
});
The intent of these tests are clear, and easy to follow. Also, if the way pagination is handled changes, these tests won't break as long as these functions still deliver on this functionality. Maintainence becomes a much simpler task when you abstract over the low-level details of a page.
If you've written page objects before, you might have done so using java or ruby, two very popular languages for doing this task in. Javascript is a bit different; the Selenium port of WebDriver to javascript uses an asynchronous paradigm, and at first can be very confusing to look at.
Here's a basic example, and one you'll see everywhere in this tutorial:
return this.lblResultCount.getText().then(function (countText) {
return parseInt(countText.split(' ')[0], 10);
});
Here's what happens:
- Let's say that
lblResultCount
's text looks like "241 Results found this search". - The first line says,
return
the text of the results count,then
pass that text in ascountText
. - Finally, return an integer representing the first word of the text (in this case, 241).
You can chain together these promises as deep as you need to. As you become comfortable with this chaining, we'll start to leverage it as nested calls to deconstruct a page into simple components that are easy to organize and manage as your test base grows.
Despite the name page objects, a page object isn't an entire page; there are many page objects on a page, and defining them requires that we explore the application a bit by hand, and look for similarities between the different pages. Anything that gets repeated over many pages needs to be stored seperately. Then we combine them together when we go to write the actual tests.
For instance, here's a screencap of a page that we'll be dissecting later in this tutorial, highlighted for clarity.
Let's go through and define each of these page objects, and see if we can't mock them out in an outline first:
The constant "Epik Vote" link takes the user to the home page, and the Sign Out button is always visible not matter where a user goes. This bar should be considered our "Base" object (even though it doesn't do much of anything).
You may have noticed that in the Base object, there's no "Sign Out Button" object, or "Epik Vote" object. The reason for that is because Selenium provides great utilities for interacting with images, links, buttons, and so on. These elements are already as simple as they need to be. Abstracting over these elements any further would be a waste of time, and add unneeded complexity to the project. We're going to focus on the objects on our page that deliver information to the user. These are typically more complex, and require more attention.
This table actually has very little going on in and of itself. It's what's inside the table that is interesting to us. We'll probably just denote how to find the table, and reference that in our Columns and Rows objects.
Just like with tabs, you don't want to denote your columns by their index. Name them instead. If they support sorting, they'll need a .sort()
method, perhaps taking an argument to designate what direction the sort should be in. Some other properties of a column might be name
, text
, data
, and other convenience methods.
Unlike columns and tabs, we don't want to name our rows, since there's nothing we can name them by reliably. Our rows will be an array since we can't predict what all of the rows are going to look like, but we can know what each row will look like. We should focus on that instead:
it('should have data in the first row', function () {
rows.getRowByNumber(1).then(function (row) {
row.name.then(function (name) {
expect(name).toEqual('Do you support Battleground Texas');
});
row.link.then(function (link) {
expect(link).toEqual('/political-polls/do-you-support-battleground-texas')
});
row.votes.then(function (votes) {
expect(votes).toBeGreaterThan(0);
});
row.isVisited().then(function (result) {
expect(result).toBe(false);
});
});
});
There are a few ways you could construct this tab object.
One way looks like this, and admittedly, I've seen it a lot:
it('should have tabs', function () {
tabs[0].click();
expect(tabs[1].getText()).toEqual('Polls');
});
This works for now, but I don't like it because it treats the tabs like they're in a list. That means if a tab element ever gets removed, it may break lots of tests in many places because tabs[1]
isn't there anymore. I'll show you how to do it more like this:
it('should have tabs', function () {
tabs.getTabs().then(function (tabs) {
tabs.profile.then(function (profileTab) {
profileTab.visit();
});
tabs.polls.then(function (pollsTab) {
expect(pollsTab.name).toEqual('Polls');
});
});
});
As you can see, a tabs object is much more flexible. It becomes easier to update the definition of the object itself, and keep the references to the object symantically intact. It always helps to try and drive around an app by something you define, instead of relying on the way the page looks or is structured.
This tutorial will be broken up into various sections. This is just "Chapter 0" of the tutorial. You'll need to continue to learn more about how to construct these page objects using Astrolabe, a utility that keeps our page objects neat and tidy for us.
To continue this tutorial, checkout the branch chapter-1
via the command line:
$> git checkout -b chapter-1 origin/chapter-1
Or just jump to it in your browser.
And we'll start defining our first pages, and write our first test.