E2E Testing with Nightwatch: Part Three
It's been quite some time since I promised to do a part three on Nightwatch.js pages. I want to thank Marissa, who took the time out of her busy schedule to reach out to me about this part of the tutorial. I have had a lot on my plate lately so I haven't written in three months. I'm sorry about that.
Moving on...
In this article, I'm going to talk about Page Objects
. Without further ado, let's begin.
Note: Most of the things I write here are what you would find in the Nightwatch.js documentation.
What is a Page Object?
A page object allows a software client to do anything and see anything that a human can by abstracting away the underlying HTML actions needed to access and manipulate the page.
A comprehensive introduction to Page Objects can be found in this article.
Configure Page Objects
In order to configure page objects in Nightwatch, we need to add the page_objects_path
property to our nightwatch.json
file. If you take a look at it now, you'll see that we had done that already.
If that's not the case, go on and add it to your nightwatch.json
config file. We assigned "pages"
to the page_objects_path
property in our config. By doing this, we tell Nightwatch to read the page objects from the folder (or folders) specified in the page_objects_path
configuration property.
You can also pass an array of folders to the page_objects_path
property, allowing you to split page objects into smaller groups.
There are several properties exposed to use when we use page objects, and we'll look at them shortly.
The URL property
This property is optional. When provided, it designates the page's URL. Call the navigate
method on the page object to navigate to the page.
The URL will usually be defined as a string:
export default {
url: 'https://cjdocs.herokuapp.com',
};
You can also pass a function to it, in the case of a dynamic URL. One use case would be when you want to support different test environments. You can create a function that gets called in the context of the page, thus allowing you to do:
export default {
url: function() {
return this.api.launchUrl + 'auth/signin';
},
};
The Elements property
The elements
property is used to define elements on your page that your tests will interact with through commands and assertions. This helps keep your code DRY, especially in larger integration tests.
We don't have to call useXpath
or useCss
in our tests to switch between locate strategies as this is done internally for us. By default, locateStrategy
is set to CSS, but you can also specify XPath:
export default {
elements: {
searchBar: {
selector: 'input[type=text]'
},
submit: {
selector: '//[@name="q"]',
locateStrategy: 'xpath'
}
}
};
You can use the shorthand if you're creating elements with the same locate strategy as the default:
export default {
elements: {
searchBar: 'input[type=text]'
}
};
Using the elements
property, we can refer to the element by prefixing "@" to its name, rather than selector, when calling element commands and assertions (e.g. click
, etc).
One option is to define an array of objects:
var sharedElements = {
mailLink: 'a[href*="mail.google.com"]'
};
export default {
elements: [
sharedElements,
{ searchBar: 'input[type=text]' }
]
};
The Sections property
The sections
property allows you to define sections of a page. It can be useful for two things:
Creating a section looks like this:
export default {
sections: {
menu: {
selector: '#gb',
elements: {
mail: {
selector: 'a[href="mail"]'
},
images: {
selector: 'a[href="imghp"]'
}
}
}
}
};
Note: every command and assertion on a section (other than expect
assertions) returns that section for chaining. If desired, you can nest sections under other sections for complex DOM structures.
The Commands property
The commands
property allows you to add commands to your page object. It is a useful way to encapsulate logic about the page that would otherwise live in a test or multiple tests.
Nightwatch will call the command on the context of the page or section. You can call client commands like pause
via this.api
. When chaining, each function should return the page object or section.
The example below uses a command to encapsulate logic for clicking the submit button:
const googleCommands = {
submit: function() {
this.api.pause(1000);
return this.waitForElementVisible('@submitButton', 1000)
.click('@submitButton')
.waitForElementNotPresent('@submitButton');
}
};
export default {
commands: [googleCommands],
elements: {
searchBar: {
selector: 'input[type=text]'
},
submitButton: {
selector: 'button[name=btnG]'
}
}
};
Those are all of the properties available to page objects. Now it's time we see some of these properties in action. Yay!
I'll be using my "still in the works" app here.
Write Tests
I'll be testing the sign in a page of the app. Let's go ahead and do just that.
We will need to create a page file in the pages
directory we created earlier. I called mine signinPage.js
and added it to the pages
directory.
signinPage.js
:
const signinCommands = {
signin(email, password) {
return this
.waitForElementVisible('@emailInput')
.setValue('@emailInput', email)
.setValue('@passwordInput', password)
.waitForElementVisible('@signinButton')
.click('@signinButton')
}
};
export default {
url: 'https://cjdocs.herokuapp.com/auth/signin',
commands: [signinCommands],
elements: {
emailInput: {
selector: 'input[type=email]'
},
passwordInput: {
selector: 'input[name=password]'
},
signinButton: {
selector: 'button[type=submit]'
}
}
};
Next, we create another page object called instancesPage.js
. This will be used to abstract away what we would search for when test passes the authentication phase.
instancesPage.js
:
export default {
elements: {
homepageWelcomeTitle: {
selector: '//div/h1',
locateStrategy: 'xpath'
}
}
};
Finally, we will create our test file, called signin.spec.js
.
signin.spec.js
:
'User can sign in'(client) {
const signinPage = client.page.signinPage();
const instancesPage = client.page.instancesPage();
signinPage
.navigate()
.signin(process.env.EMAIL, process.env.PASSWORD);
instancesPage.expect.element('@homepageWelcomeTitle').text.to.contain('Welcome to the CJDocs Home!');
client.end();
}
After this, you can run your tests with the command npm test
or yarn test
.
That's all there is to using page objects. Not so difficult, I suppose?
Conclusion
Page objects really help make our code DRY, thereby abstracting some common functionalities into one place and just calling those defined methods/properties so we can have a fully functional tests without repetitive code.
I hope you learned something new today. Hit the like button if you enjoyed it. Let me know in the comments if you have a question or if you want me to follow up on something.
What would you like me to write about? If you have something in mind you've been wanting to learn for some time now, hit me up in the DM and I'll be sure to reply.
You can find me on Twitter @codejockie.
Very nice article. Great job!
I actually started following your steps and it works just fine. But there is one issue with the Nightwatch POM approach that I wanted to ask if you found the way around by the chance.
The issue is that it does not support generic locators. For example, when you have a lot of similar buttons with the same locator but different text values, so instead of creating separate locators for each button you’d have a function that accepts the button name and formats the locator accordingly.
Any ideas on how to do that using the Nightwatch POM structure?