Page Object Model is a design pattern that focuses on making a Test Automation Framework extensible, maintainable, and easy to understand. In this article, we will see how we can create a Page Object structure for Nightwatch JS.
Step 1: Create a folder named ‘pageObjects’ and then mention the folder path in the nightwatch.conf.js file:
1 | page_objects_path: "pageObjects" |
Step 2: Let’s write a simple test and then implement Page Object for it.
1. Log in to the Orange CRM Website
2. Go to My Info Page, update the first and last name
3. Validate that the first and last names are updated correctly
4. Logout
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | module.exports = { before: function (browser) { //Declaring Global Timeout browser .globals.waitForConditionTimeout = 7000 }, 'Login': function (browser) { browser .url('https://opensource-demo.orangehrmlive.com/') .waitForElementVisible('#txtUsername') .waitForElementVisible('#txtPassword') .setValue('#txtUsername', 'Admin') .setValue('#txtPassword', 'admin123') .click('#btnLogin') .assert.urlContains('/index.php/dashboard') }, 'Check Personal Info': function (browser) { browser .waitForElementVisible('#menu_pim_viewMyDetails') .click('#menu_pim_viewMyDetails') .click('#btnSave') .clearValue('#personal_txtEmpFirstName') .setValue('#personal_txtEmpFirstName', 'Peter') .clearValue('#personal_txtEmpLastName') .setValue('#personal_txtEmpLastName', 'Sullivan') .click('#btnSave') .assert.value('#personal_txtEmpFirstName', 'Peter') .assert.value('#personal_txtEmpLastName', 'Sullivan') }, 'Logout': function (browser) { browser .waitForElementVisible('a[href*="dashboard"]') .click('a[href*="dashboard"]') .waitForElementVisible('#welcome') .click('#welcome') .waitForElementVisible('a[href*="logout"]') .click('a[href*="logout"]') .assert.urlContains('/index.php/auth/login') }, after: function (browser) { browser.end(); } } |
Step 3: Now, let’s move the locators from the test file and manage them from a single location. This is very helpful in case of maintenance.
Now, inside the pageObjects folder, we will create three js files, as in our test we would be working on three different pages. The idea is to create one js file per page.
Step 4: Now we will move the locators of the corresponding pages to their respective JS files.
a) home.js:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | module.exports = { url: 'https://opensource-demo.orangehrmlive.com/', elements: { username: { selector: '#txtUsername' }, password: { selector: '#txtPassword', }, loginBtn: { selector: '#btnLogin' } } } |
b) myInfo.js:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | module.exports = { elements: { myInfoTab: { selector: '#menu_pim_viewMyDetails' }, editSaveBtn: { selector: '#btnSave', }, firstName: { selector: '#personal_txtEmpFirstName' }, lastName: { selector: '#personal_txtEmpLastName' } } } |
c) dashboard.js:
1 2 3 4 5 6 7 8 9 10 11 12 13 | module.exports = { elements: { dashboardTab: { selector: 'a[href*="dashboard"]' }, welcomeText: { selector: '#welcome', }, logoutBtn: { selector: 'a[href*="logout"]' } } } |
Step 5: Page objects are defined in modules and parsed into factory functions that create page object instances. These factories are accessible through the page reference within the command API (accessible through the “client” or “browser” object) using the name of the module that defines them. For eg.
1 | var home = browser.page.home() |
Next, we will access the locators of the specific page using their respective page object. For eg. If we want to input the text ‘Admin’ in the ‘username’ field on the home page, we can write something like:
1 | home.setValue('@username', 'Admin') |
Now keeping these two things in mind, we will create three page objects for our three pages and update our test like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | module.exports = { before: function (browser) { //Declaring Global Timeout browser .globals.waitForConditionTimeout = 7000 }, 'Login': function (browser) { var home = browser.page.home() home .navigate() .waitForElementVisible('@username') .waitForElementVisible('@password') .setValue('@username', 'Admin') .setValue('@password', 'admin123') .click('@loginBtn') .assert.urlContains('/index.php/dashboard') }, 'Check Personal Info': function (browser) { var myInfo = browser.page.myInfo() myInfo .waitForElementVisible('@myInfoTab') .click('@myInfoTab') .click('@editSaveBtn') .clearValue('@firstName') .setValue('@firstName', 'Peter') .clearValue('@lastName') .setValue('@lastName', 'Sullivan') .click('@editSaveBtn') .assert.value('@firstName', 'Peter') .assert.value('@lastName', 'Sullivan') }, 'Logout': function (browser) { var dashboard = browser.page.dashboard() dashboard .waitForElementVisible('@dashboardTab') .click('@dashboardTab') .waitForElementVisible('@welcomeText') .click('@welcomeText') .waitForElementVisible('@logoutBtn') .click('@logoutBtn') .assert.urlContains('/index.php/auth/login') }, after: function (browser) { browser.end(); } } |
Step 6: We can further condense the code by writing page specific custom commands. This approach is also helpful in preventing repetitive code. In our test we are performing login on the homepage and logout on the dashboard page. So we will create custom commands for both in their respective pages.
Now, home.js will look something like this. We have added all the lines of code that are responsible for login under one function and in the tests we will only use login().
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | var homeCommands = { login: function () { return this.navigate() .waitForElementVisible('@username') .waitForElementVisible('@password') .setValue('@username', 'Admin') .setValue('@password', 'admin123') .click('@loginBtn') } } module.exports = { url: 'https://opensource-demo.orangehrmlive.com/', commands: [homeCommands], elements: { username: { selector: '#txtUsername' }, password: { selector: '#txtPassword', }, loginBtn: { selector: '#btnLogin' } } } |
Similarly, for dashboard.js we will add all the lines of code responsible for logout under one function and in the tests we will only use logout().
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | var dashboardCommands = { logout: function () { return this.waitForElementVisible('@dashboardTab') .click('@dashboardTab') .waitForElementVisible('@welcomeText') .click('@welcomeText') .waitForElementVisible('@logoutBtn') .click('@logoutBtn') } } module.exports = { commands: [dashboardCommands], elements: { dashboardTab: { selector: 'a[href*="dashboard"]' }, welcomeText: { selector: '#welcome', }, logoutBtn: { selector: 'a[href*="logout"]' } } } |
Now, we will create a custom command for updating the first name and last name. Here we will create a parameterized function where we will pass the first and the last name from the test itself.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | var myInfoCommands = { updateMyInfo: function (firstName, lastName) { return this.waitForElementVisible('@myInfoTab') .click('@myInfoTab') .click('@editSaveBtn') .clearValue('@firstName') .setValue('@firstName', firstName) .clearValue('@lastName') .setValue('@lastName', lastName) .click('@editSaveBtn') } } module.exports = { commands: [myInfoCommands], elements: { myInfoTab: { selector: '#menu_pim_viewMyDetails' }, editSaveBtn: { selector: '#btnSave', }, firstName: { selector: '#personal_txtEmpFirstName' }, lastName: { selector: '#personal_txtEmpLastName' } } } |
So in the test, we will write:
1 | .updateMyInfo('Peter', 'Sullivan') |
Step 7: After making all the changes our test will look like:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | module.exports = { before: function (browser) { //Declaring Global Timeout browser .globals.waitForConditionTimeout = 7000 }, 'Login': function (browser) { var home = browser.page.home() home .login() .assert.urlContains('/index.php/dashboard') }, 'Check Personal Info': function (browser) { var myInfo = browser.page.myInfo() myInfo .updateMyInfo('Peter', 'Sullivan') .assert.value('@firstName', 'Peter') .assert.value('@lastName', 'Sullivan') }, 'Logout': function (browser) { var dashboard = browser.page.dashboard() dashboard .logout() .assert.urlContains('/index.php/auth/login') }, after: function (browser) { browser.end(); } } |
After Test Execution:
Do check out 🙂
Github:Â https://github.com/alapanme/NightwatchJS
All Nightwatch JS Articles: https://testersdock.com/nightwatch-js-tutorial/
Can we call a method defined in page object in another age object. If so how?