Selenium Page Objects 2 of 2

So in part 2 I'm introducing the chaining of pages, this is all about how one object (page) seamlessly hands responsibility to another page. By now you need to almost think of pages and objects as one and the same.

One of the biggest problems in any GUI type testing is that often you need to visit a few different dialogues in an app to set up initial system state. Even before you can run your actual test check. Something that is a few steps or pages and becomes a mini-test in itself, or in unit testing parlance a "fixture".

Please be sure to read part 1 first where I talk about the simple problem, driving web pages or dialogs as objects in your Python script with helpers that make all of the fields in pages. Dynamic variables in the object (Page) that correspond to each field on the page lets you simply get or set in order to populate form fields or click on buttons.

Part 2 chaining pages together.

This is exactly the same functionality as all the objects in main.py, but this time they support chaining together as well. Here you see this in Action, we have 

  1. a login page for a username
  2. another page for the password
  3. the main app page
  4. a profile page (with only one button, logout)


So I represent these pages as 4 objects, and if you want to routinely navigate to the profile page in a test suite, you want to write that code once only just like the "Page" code is only written once.
All you do is string them all along in one method.

    @next_page("DemoProfilePageV2")
    def login_to_profile(self, username, password):
        """
        Does the whole shebang in one go - show how to make sure we always return the object the decorator
        says we should do.
        :param username: The user name
        :param password: The password
        :return: the profile page
        """
        return self.next(username).next(password).next()

You might not notice that the Page class DemoProfilePageV2 was undefined at this time, that's because it's declared elsewhere, and we don't want a dependency. This is solved by every class adding itself to a MAP of pages. And by using a "string". 
The decorator next_page() then neatly passes any parameters you give the method through, and always returns you a object that matches the page you expect. Note that we are also relying on each page pulling some state information from the prior page object, namely a ref to the selenium webdriver. So each page helps to spawn it's successor in the chain.

This decorator contract makes it easy for someone to know that they actually are getting an object (Page) of the kind they are expecting every single time that they call login_to_profile() . The only way not to, is if the method raises an exception. 

    def next(self):
        raise NotImplemented("Must be implemented to return the Chained PageObject!")

In the example, I've mandated that every page has a next() method, this is not strictly necessary. I also experimented with how well it works to pass all the parameters into the constructor to simplify needing to pass data along the chain, but decide against that as harder to maintain over time in practise.

Finally just so you can see what this test does (aside from the animation graphic above) this is the brief output with the dummy WebServer traces the demo requires stripped off.

[WDM] - Current google-chrome version is 86.0.4240
[WDM] - Get LATEST driver version for 86.0.4240
[WDM] - Driver [C:\Users\cb\.wdm\drivers\chromedriver\win32\86.0.4240.22\chromedriver.exe] found in cache
[16:13:17][POM] pre_navigate:http://localhost:8080/loginuser.html
[16:13:17][POM] __setattr__ editUserName = 'user'
[16:13:17][POM] __getattr__ btnContinue
[16:13:18][POM] __setattr__ editPassword = 'pass'
[16:13:19][POM] __getattr__ btnLogin
[16:13:20][POM] __getattr__ btnProfile
[16:13:20][POM] __getattr__ btnLogout
[16:13:21][PB] teardown: quit chromedriver

[WDM] is the webdriver Manager which keeps chromedriver up to date

[POM] is page object model base class

[PB] is the test fixture class

Probably in a real implementation, you might want to not print passwords, even test passwords. So any values that perhaps match "pass" or "password", you replace with ****. I'll leave that up to you the reader to add.

Comments