Ninja QA

Selenium Automation - Strongly typed classes

A while back on Stack Overflow, I was asked for examples of my work where I used custom wrapper classes to manage and enhance the functionality of Selenium.
I am going to briefly describe what I did, and how I did it.

Selenium as a tool, considers everything as an IWebElement.
While not inherently wrong, it is somewhat deceptive. While buttons and textboxes are DOM elements, they are not however alike, and executing SendKeys to set the text on a button or click on a textbox may not be what we want.
Why would you want to click on a non-hyperlink type object etc.

From a black box perspective, you could look at IWebElement as being a 'God Class' - that metaphysical term used in code quality analysis where a developer has crammed all the functionality into a singular class or interface.

Within my framework, I worked in a different fashion.

First, I recognize that yes, all objects on a website are indeed DOM objects, or as I like to call them 'BaseElement' type classes.
So in my selenium library, I created the following class structure

 

BaseElement is essentially my 'God Class' - where I define as much functionality as I could possibly want.
Before we get as far as showing the functionality, first we want to streamline the grabbing or acquiring of objects from on-screen.

Normally, you would have to do

IWebElement textbox = Driver.FindElement(By.Id("textbox1"));
textbox.SendKeys("testing 123123");

The issue is that you are having to repeatedly reference the Driver object and tell it to 'FindElement'.
This can be a little annoying to repeat that step over and over. It also looks more like a Factory / Script design approach, as opposed to object oriented.

What I do instead is have the constructor of my BaseElement class look like this:

        protected IWebElement InnerObject;
        protected By OriginalByData;


        /// <summary>
        ///  Constructor for the base element 
        /// </summary>
        /// <param name="locator"></param>
        /// <param name="checkExists"></param>
        public BaseElement(By locator, bool checkExists = false)
        {
            OriginalByData = locator;
            if (checkExists)
            {
                FindObject();
            }
        }

First thing you can see is that I am storing both the By information for the object, while also having a field for holding the actual IWebElement object itself.
When I instantiate a BaseElement, I will only need to provide the By information and optionally a boolean to say whether or not to check for existence of the object at when the class is instantiated. There may be instances where you want to not check for existence at instantiation time. Perhaps the object is not meant to be on screen at all.

        /// <summary>
        /// Private method that attempts to find the object
        /// Can be used for lazy / late loading of the object
        /// Eg: Define the By data, but then search for the object at a later time.
        /// </summary>
        private void FindObject()
        {
            if (this.InnerObject == null)
            {
                WebDriverWait wait = new WebDriverWait(GetWebDriver(), TimeSpan.FromSeconds(Constants.WaitTime));
                IWebElement obj = wait.Until(ExpectedConditions.ElementExists((OriginalByData)));
                InnerObject = obj;
                Highlight();
            }
        }

The FindObject method is the one that does the heavy lifting. For such a small method in my framework, it is responsible for acquiring the objects on screen and facilitating all the functionality we are about to go into.

Essentially - once the FindObject code executes, it will store a reference to the object in the IWebElement field on the BaseElement.
The Highlight method can be ignored - it is a method I have setup to optionally highlight the object on screen when it locates it.

I am about to describe to you how my click method works. Before we start, you are probably laughing, thinking....
Surely... it is just 
webObject.click();

Well......er... umm...

        /// <summary>
        /// Used for links, buttons, images etc
        /// </summary>
        protected void Click()
        {
            //Check to see if the object is still on screen
            FindObject();

//Scroll to object - this prevents the 'Object is not within view, or not clickable' issue.     GetWebDriver().ExecuteScript("arguments[0].scrollIntoView();", this.InnerObject);
            WebDriverWait wait = new WebDriverWait(GetWebDriver(),
                TimeSpan.FromSeconds(Constants.MediumWait));
            if (OriginalByData != null)
            {
                IWebElement obj = wait.Until(ExpectedConditions.ElementToBeClickable(OriginalByData));
            }
            if (InnerObject != null)
            {
                IWebElement obj = wait.Until(ExpectedConditions.ElementToBeClickable(InnerObject));
            }

            try
            {
                this.InnerObject.Click();
            }
            catch (InvalidOperationException e)
            {
                if (e.Message.Contains("Other element would receive the click"))
                {
                    //Try javascript click instead?
                    Console.WriteLine("===============================================");
                    Console.WriteLine("Warning: ATTEMPTING JAVASCRIPT CLICK INSTEAD");
                    Console.WriteLine("A soft exception warning that another element would receive the click was detected.");
                    Console.WriteLine("Javascript may be able to bypass this exception and progress the test.");
                    Console.WriteLine("===============================================");
                  GetWebDriver().ExecuteScript("arguments[0].click();", this.InnerObject);
                }
                else
                {
                    throw;
                }
            }
        }

 

Alot of code for something that only needs to 'click' an object.
Firstly, this method will recheck to see if the object is still on screen with FindObject. With a dynamic page, objects can vanish as quickly as they appear. We do not simply want to acquire our object and then trust that it will exist for the duration of our test script. If we keep a single variable, and never refresh it via 'FindObject' we are opening ourselves up to StaleElementExceptions. Refresh your objects frequently, if not every time you try to use them.

After the element is confirmed as being on screen and refreshed, it will then perform a web driver wait upto X seconds for the object to become clickable.
You can see that I am performing this check on both the stored WebElement and on the By data. This is intentional.
Remember, instantiating the BaseElement with a false for existence will not acquire a IWebElement on instantiation. So it may not have the IWebElement, so we can perform the check on the By data instead - assuming it is stored.

Now comes the actual click
You see I am doing it in a try and catch.
I could catch all exceptions, but I don't want to - I only want a specific type of exception here.
InvalidOperationExceptions are thrown on situations where DOM elements and hidden objects can sometimes overlay the object you want to click on. Selenium tries to be smart and warns you that the object is hidden, therefore should not receive the click.
I say - so what...
If I say 'Click on that damn object, I mean.. click on that damn object!!'
So what I do is check for the message on the exception, if it is the expected exception message, I then perform the 'click' using javascript instead of the traditional Selenium functionality.

Javascript is a powerful tool that can augment your automation capabilities, but you need to be aware that using it can be risky.
If your intent is to simulate human interactions, then Javascript is the wrong direction for you. 
If you use Javascript to set a textbox value, it will NOT trigger event handlers on those textboxes.
This can be important for things like Registration pages which have onchange event handlers.

We have all seen those registration pages where you type in a username and it magically tells us if the username is available. It typically does this when you keyup or change the text value of the box. When we type via keyboard, the javascript event is triggered, but if javascript itself changes the value, it generally does not trigger the event. This is possibly to avoid circular execution calls. Eg: Javascript changes box value, triggers event, event changes the box value which then further triggers the event... recursive etc..

In the case of our click method, I think it is an acceptable risk. It does however meant that you are going to have a more reliable click method, but it just wont be behaving 100% like a human being.

You should try to write as much methods in your BaseElement class as possible, ranging from SetText, SendKeys, DropDown interactions etc. I wont walk you through all of those, I am sure you can expand on what I have done above.

I should have mentioned before... why this method is protected. Well... that's because we are going to be inheriting this class.

Create a new class, call it Link.cs or something similar.

About the constructor here: Because we are inheriting the BaseElement class, we need to emulate its constructor. Instead of providing a false for the default behavior of bCheckExist I thought it should be true.
There is also another constructor available, for feeding an IWebElement into the constructor directly. Ignore this, as this is for some advanced functionality I have that casts from one class type to another. (Link to Image etc)

public class Link : BaseElement
    {

        public Link(By locator, bool bCheckExist = true)
            : base(locator, bCheckExist)
        {
            
        }

        public Link(IWebElement wb)
            : base(wb)
        {
            
        }

        public new void VerifyText(string arg, bool ignoreCase = false)
        {
            base.VerifyText(arg, ignoreCase);
        }
        public new void Click()
        {
            base.Click();
        }

    }

The only two methods declared here are 
VerifyText and Click

Because our 'Click' method in the BaseElement was declared as protected, it means that it cannot be accessed by the tester directly from a derived type. In order to provide access, we create the public new void Click() and have it call base.Click();

What is the value of this? 
In this particular case, not much, however, lets imaging you have a new type of object that your company has developed specially. Your click function no longer works, you need to do something special, but don't want to change it for all other buttons / links. You could then write a class that inherits from Link.cs and then does its own special Click functionality.

How does this look at test design time?

Link btn = new Link(By.XPath(p0));
btn.Click();

or

new Link(By.Id("myButton")).Click();

You can see that I am no longer having to worry about interacting with the WebDriver - it is handled in the background as a static variable that only gets used in the BaseElement class.

When we use this in a Specflow test project, it could end up looking like this: (note - this is just a dummy step)

        [When(@"the user clicks on button '(.*)'")]
        public void WhenTheUserClicksOnButtonI(string p0)
        {
            ButtonLink btn = new ButtonLink(By.XPath(p0));
            btn.Click();
        }

Of course, the recommended approach I would usually try to sell would be to implement a page object model approach. Where each object on screen is handled by either a method that instantiates and returns the object, or a field that does the same.
It might look like:

public class LoginPage : Page
{
     public TextBox UsernameTextBox(){
                return new TextBox(By.Id("username"));

     }
}

You can see I am using another special class that I inherit from for my page objects. This is to help synchronize on pages to improve reliability of tests.
I might cover that in a future blog post.

 

 

Comments (2) -

  • Add Hunters

    5/8/2020 5:06:54 PM |

    Grate article. New to Addhunters? It's incredibly easy to become a member of our community, and FREE to list your classified ads to interact with all members. Every day, hundreds of listings get listed for free by our Addhunters members. You can always upgrade your membership and get ahead of the crowd. With Addhunters you're going to benefit from our international usages!Items listed on Add Hunters include electronics, pets, cars and, vehicles and other categories including land and property. The categories with the highest volume on the site are vehicles and property. For more details please visit our web site http://www.addhunters.com <a href="http://www.addhunters.com";>rent a car service in qatar</a>

Add comment

Loading