Exploring the concept of using nested page objects with Protractor

The Inquiry:

How can we effectively define nested Page Objects in Protractor?

Situation:

Our webpage is complex, with various sections such as a filter panel, grid, summary part, and control panel. It's impractical to cram all element and method definitions into a single file and page object - it leads to confusion and maintenance challenges.

Answer №1

When it comes to Page Objects and their maintenance, there is a general topic worth exploring. Recently, I came across a Page Object Design Pattern technique that caught my attention and made perfect sense to me.

Instead of creating child page objects within the parent page objects, a better approach would be to embrace JavaScript's Prototypal Inheritance. This method offers numerous benefits, but let me first demonstrate how we can implement it:

Let's begin by defining our parent page object ParentPage:

// parentPage.js
var ParentPage = function () {
// defining common elements
this.someElement = element(by.id("someid"));

// defining common methods
ParentPage.prototype.open = function (path) {
browser.get('/' + path)
}
}

module.exports = new ParentPage();  //export instance of this parent page object

We always export an instance of a page object rather than creating it in the test. Since we are working on end-to-end tests, we view the page as a stateless entity, similar to each stateless http request.

Now, let's move on to creating our child page objects ChildPage, utilizing the Object.create method to inherit the prototype of our parent page:

//childPage.js
var ParentPage = require('./parentPage')
var ChildPage = Object.create(ParentPage, {
/**
 * define elements
 */
username: { get: function () { return element(by.css('#username')); } },
password: { get: function () { return element(by.css('#password')); } },
form:     { get: function () { return element(by.css('#login')); } },
/**
 * define or overwrite parent page methods
 */
open: { value: function() {
    ParentPage.open.call(this, 'login'); // overriding parent page's open method
} },
submit: { value: function() {
    this.form.click();
} }
});
module.exports = ChildPage

Locators are defined within getter functions; these functions are evaluated when you access the property, not during object generation. This ensures that you request the element before performing any action on it.

The Object.create method returns an instance of the page so we can immediately start using it.

// childPage.spec.js
var ChildPage = require('../pageobjects/childPage');
describe('login form', function () {
it('test user login', function () {
    ChildPage.open();
    ChildPage.username.sendKeys('foo');
    ChildPage.password.sendKeys('bar');
    ChildPage.submit();
});

In the above code snippet, notice how we only require the child page object and utilize/override parent page objects in our specs. The following are the advantages of this design pattern:

  • Reduces tight coupling between parent and child page objects
  • Promotes inheritance among page objects
  • Lazy loading of elements
  • Encapsulation of methods and actions
  • Makes the element relationship cleaner and easier to understand, instead of
    parentPage.childPage.someElement.click();

I stumbled upon this design pattern in webdriverIO's developer guide, where most of the techniques I explained are derived from. Feel free to explore it further and share your thoughts!

Answer №2

The concept involves organizing the Page Object as a package - a directory with index.js serving as the main entry point. The parent page object would serve as a container for child page objects, each representing a distinct part of a screen.

Inside the index.js, the parent page object would hold all the definitions for the child page objects, like so:

var ChildPage1 = require("./page.child1.po"),
    ChildPage2 = require("./page.child2.po"),

var ParentPage = function () {
    // additional elements and methods can be defined here
    this.someElement = element(by.id("someid"));

    // child page objects
    this.childPage1 = new ChildPage1(this);
    this.childPage2 = new ChildPage2(this);
}

module.exports = new ParentPage();

Notice how this is passed into the constructors of the child page objects. This allows for seamless access to the parent page object's elements or methods if required.

The structure of a child Page Object would resemble:

var ChildPage1 = function (parent) {
    // definitions for elements and methods go here
    this.someOtherElement = element(by.id("someotherid"));
}

module.exports = ChildPage1;

Implementing this type of page object makes usage straightforward. By requiring the parent page object, you can easily access sub page objects using dot notation:

var parentPage = requirePO("parent");

describe("Test Something", function () {
    it("should test something", function () {
        // accessing parent
        parentPage.someElement.click();

        // accessing nested page object
        parentPage.childPage1.someOtherElement.sendKeys("test");
    });
});

The requirePO() function serves as a helpful import utility.


A snapshot of a nested page object directory structure from one of our automation projects:

https://i.sstatic.net/lvyHRm.png

Answer №3

Although I don't utilize Protractor, I have found success using a technique that involves dividing a page into components known as "Component Objects." By assigning scopes to each component, I am able to efficiently search for and add elements based on their specific scopes. This method allows me to easily reuse components across various pages.

For instance, when working with the webpage , I segment it into three parts: https://i.sstatic.net/joh0I.png These parts can be identified as: Header, SearchForm, Footer

The code snippet for each part could resemble the following:

class Header {
   public Header(SearchContext context){
       _context = context;
   }

   WebElement GmailLink {
       get {
           return _context.FindElement(By.CssSelector("[data-pid='23']"));
       }
   }
   WebElement ImagesLink {
       get {
           return _context.FindElement(By.CssSelector("[data-pid='2']"));
       }
   } 

   SearchContext _context;
}

class SearchForm{
   public Header(SearchContext context){
       _context = context;
   }

   WebElement SearchTextBox {
       get {
           return _context.FindElement(By.Name("q")):
       }
   }

   WebElement SearchButton {
       get {
           return _context.FindElement(By.Name("btnK")):
       }
   }

   SearchContext _context;
}
..

The corresponding code for the google.com page would look like this:

class GoogleComPage{
   WebDriver _driver;
   public GoogleCompage(driver){
       _driver = driver;
   }
   public Header Header{
       get {
           return new Header(_driver.FindElement(By.Id("gb")));
       }
   }

   public SearchForm SearchForm{
       get {
           return new SearchForm(_driver.FindElement(By.Id("tsf")));
       }
   }
}

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Stop automatic selection of the last value in ng-options using AngularJS

I'm facing an issue related to AngularJS. The problem I'm encountering is that... In my code, there are two select elements. Whenever I choose an option from the first select, an Ajax call is made using ng-change to populate the second select ba ...

What is the best way to stretch the background image across both the footer and navbar for my app?

Here is the code snippet I am currently working with: <template> <v-app> <AppBar></AppBar> <v-main> <router-view></router-view> </v-main> <Footer></Footer> ...

Updating an array based on a condition and a nested array in Firestore

Image of database I'm struggling to modify the convos array based on certain conditions and unable to update the nested values as well. My two main objectives are: - Change 'connected' to false for a specific socketID. - Insert an object int ...

Is the combination of variable arrays and regular expressions causing issues?

Currently, I am working on a script that transforms ::1:: into an image HTML for a div element. Here is the code: var smileys = { '1': 'http://domain.com/smiley1.gif', '2': 'http://domain.com/smiley2.gif', &a ...

Adjust the appearance of input fields that exclusively have the value "1"

When designing my website, I encountered a problem with the style I chose where the number "1" looks like a large "I" or a small "L." I am wondering if there is a way to use JavaScript to dynamically change the font style for input fields that only contai ...

Require assistance in getting a slider operational

Hello there! I could really use your assistance with this code. I have been trying to make it work using JavaScript, but I'm determined not to incorporate any external JS files. Here is the snippet of JavaScript code I am working with: "use strict"; ...

Tips on traversing a JSON array in AngularJS using the ng-repeat directive

My service returns the following JSON data: {"categories":[{"category_id":"20","name":"Desktops"}, {"category_id":"18","name":"Laptops &amp;"},{"category_id":"25","name":"Components"}, {"category_id":"57","name":"Tablets"}, {"category_id":"17","name": ...

Struggling to Make Div Refresh with jQuery/JS in my Rails Application

I'm currently facing an issue in my Rails app where I am unable to refresh a specific Div element. The Div in question is located in bedsheet_lines_index.html.erb <div id="end_time_partial" class="end_time_partial"> <%= render :partial ...

The array is devoid of any elements, despite having been efficiently mapped

I'm facing some challenges with the _.map function from underscore.js library (http://underscorejs.org). getCalories: function() { var encode = "1%20"; var calSource = "https://api.edamam.com/api/nutrition-data?app_id=#&app_key=#"; _.m ...

Verify record removal without a PHP click

My website features a navigation menu that streamlines the browsing experience: <form action="../"> <select onchange="window.open(this.options[this.selectedIndex].value,'_top')"> <option value="" selected="selected">Navigate< ...

Handling Promise Rejections in Puppeteer for Query Selector with Class条件

I keep receiving a promise rejection even after attempting to use an if/else statement for checking purposes. As I iterate through the divs (const items), every item has the class "description-main", but not all of them have the class "description-option ...

Node.JS function using try catch block

Is the following function suitable for use with Async Node.JS? I am wondering if it is written correctly, whether errors will be handled properly, or if it has been incorrectly implemented in terms of Async Node.JS. If there are issues with the implemen ...

Can you smoothly scroll to an anchor and stop the animation mid-scroll?

I've implemented a Jquery snippet that enables smooth scrolling to an anchor: <li><a href="#home">Home</a></li> which directs you to... <a name="home"></a> . var $root = $('html, body'); $('a&apo ...

Unable to retrieve variable declared externally from Switch Statement

Having an issue with assigning a value to the variable contactNumber inside a switch statement. Upon reaching the assignment line, an error SyntaxError: Unexpected identifier is triggered within the switch statement context. function contactMessageForCo ...

The functionality of Bootstrap 4 tabs seems to be stuck and not responding

I have been working with bootstrap 4 and I recently tried to implement tabs based on the documentation. However, I am facing an issue where the tabs are not switching properly. Here is a snippet of my code: <!DOCTYPE html> <html> <head> ...

Add the item to a fresh array using the Ajax function

Here is an array that I have: var arrayOfResults = []; // Results after like statement After making a database call, I receive a JSON result like this: [{ "id": "{fcb42c9c-3617-4048-b2a0-2600775a4c34}", "pid": "{34214CCB-90C3-4D ...

Understanding the NavigationContainer reference in Typescript and react-navigation

In my current project with react-navigation, I've come across a scenario where I need to navigate from outside of a component (specifically after receiving a push notification). The challenge is that when I use the navigation.navigate method from wit ...

What is the method for adjusting the width of a v-card based on its height?

I have a changing number of v-cards that are displayed based on their quantity. My goal is to maintain an aspect ratio of 16/9, but only adjust the width. To achieve this, I believe I need to make the width dependent on the height, although I am unsure ho ...

Send information to the server using the POST method

Trying to send data on button click using Angular 1.x. Client-side debug shows object set correctly: https://i.sstatic.net/Emjpk.png Server-side debug reveals lost values: https://i.sstatic.net/50l4G.png Here is my POCO: [Serializable] public class I ...

Encountering a problem when trying to find an element with xpath in Selenium 3

I am encountering an issue while attempting to click on a link that resembles a tab button using Xpath. Here is the snippet of html: <div id="tile_8" style="height: 93px; width: 26%; background-color: rgb(45, 87, 19); color: white; position: relativ ...