Smart click (part 2)

Smart click (part 2)

Here is the explanation how you can implement your own Click() method and what is behind the scenes. I will describe the method using a pure IWebElement instance. To be able to invoke the method out of an IWebElement, it should be declared as an IWebElement interface extension method. The method will have a couple of overridden versions for different sets of input parameters.

Ok, let’s start. First of all let’s define what the method should do:

  • It checks if the clicking action may be done
  • It checks if all the necessary conditions for successful clicking are met (for example, some other elements are displayed in the specified state)
  • It clicks the element
  • It verifies if the clicking action has been performed and checks the result of the click

How it does it:

  1. It verifies the element exists, is visible and is available for clicking
  2. It starts the loop. The number of iterations is presented by one of the parameters
  3. It invokes a method which is passed as an input parameter and verifies preconditions for successful clicking (checks if some necessary elements are in the correct state, clicks some other elements to meet the expected conditions, and performs all the actions required to prepare your web element for successful clicking)
  4. It tries to click the element with one of the approaches passed as a parameters (list of clicking actions)
  5. It invokes a method passed as a parameter which verifies that the clicking action has been performed and the expected result has been achieved.
  6. If the expected result is not met, it proceeds to the next clicking action from the parameter list

This is the end of the theory, now let’s begin with practice.

Create a simple test aka testing Google Search page:

using System;
using NUnit.Framework;
using OpenQA.Selenium;
using OpenQA.Selenium.Firefox;
using OpenQA.Selenium.Support.UI;

namespace SmartClick
{
   [TestFixture]
   public class TestClass
   {
      IWebDriver driver;

      [SetUp]
      public void Setup()
      {
         driver = new FirefoxDriver();
      }

      [TearDown]
      public void Teardown()
      {
         driver.Quit();
      }

      [Test]
      public void GoogleSearch()
      {
         driver.Navigate().GoToUrl("http://www.google.com ");
         IWebElement query = driver.FindElement(By.Name("q"));
         query.SendKeys("selenium");

         IWebElement btnSearch = driver.FindElement(By.Name("btnG"));
         //assume this method is working fine now but does not work with previous version of your browser (or with another browser) and the customer just eager to run your tests with both versions
         btnSearch.Click();

         WebDriverWait wait = new WebDriverWait(driver, TimeSpan.FromSeconds(5));
         wait.Until((d) => { return d.Title.StartsWith("selenium"); });
         Assert.AreEqual("selenium - Google Search", driver.Title);
      }
   }
}

Now let’s start writing our own clicking method. I named it SmartClick(). Add a static class to the namespace and put a static void method SmartClick() into it:

public static class Extensions
{
   public static void SmartClick(this IWebElement element)
   {
      element.Click();
   }
}

Now you can replace btnSearch.Click(); with btnSearch.SmartClick();

Or you can invoke the method this way:

Extensions.SmartClick(btnSearch);

because it is static.

Now let’s check that you did not pass null as a parameter and the element is displayed and enabled for clicking (step 1)

public static class Extensions
{
   public static void SmartClick(this IWebElement element)
   {
      if (element == null) 
      { 
         //add 'Element is null' error message into your log file or throw NoSuchElement or NullReference exception 
      }
      if(!element.Displayed) 
      { 
         //add 'Element is not displayed and cannot be clicked' error message into your log file 
         //you may add a screenshot here
      }
      if(!element.Enabled) 
      {
         //add 'Element is not displayed and cannot be clicked' error message into your log file
         //plus a screenshot
      }
      element.Click();
   }
}

Step 2 – wrap the clicking action into a loop. There is no sense in it right now but in the future it will be used for multiple attempts to click the element if it is necessary. The number of iterations is one of the method parameters with value 1 as default.

public static class Extensions
{
   public static void SmartClick(this IWebElement element, int iterations = 1)
   {
      int i = iterations;
   
      if (element == null) 
      { 
         //add 'Element is null' error message into your log file or throw NoSuchElement or NullReference exception 
      }
      if(!element.Displayed) 
      { 
         //add 'Element is not displayed and cannot be clicked' error message into your log file 
         //you may add a screenshot here
      }
      if(!element.Enabled) 
      {
         //add 'Element is not displayed and cannot be clicked' error message into your log file
         //plus a screenshot
      }
      
      while (i > 0)
      {
         i--;
         element.Click();
      }      
   }
}

Inside the loop, you can do any precondition, clicking actions and post-clicking verification to make successful click.

Step 3 – invoke a method that checks or does some preparation for successful clicking actions. You can put here almost any method which will be doing almost anything you need. To do this, you have to create a delegate for a method and then to initialize it, pass it as a parameter to the method and invoke it inside the loop. Add the following delegate to the namespace:

public delegate bool CustomFunction(params object[] obj);

and pass the delegate as a parameter to the SmartClick method:

public static void SmartClick(this IWebElement element, CustomFunction precondition = nullint iterations = 1)
   {
      int i = iterations;
      string elementName = element.Text;

      if (precondition == null)
         precondition = x => true; //if you did not specify the method let's assume it has passed (return true)
      
      while (i > 0)
      {
         i--;
         if (!precondition.Invoke())
         {
            //add string.Format("Preconditions for clicking '{0}' web element are not met", elementName) error message into your log file
            //screenshot
            continue; //there is no sense to do clicking action if precondition method has failed. Return back to the beginning of the loop and start again from the very beginning
         }
         if (element == null)   
         {   
            //add 'Element is null' error message into your log file or throw NoSuchElement or NullReference exception   
         }
         if(!element.Displayed)   
         {   
            //add 'Element is not displayed and cannot be clicked' error message into your log file   
            //you may add a screenshot here  
         }  
         if(!element.Enabled)   
        {  
           //add 'Element is not displayed and cannot be clicked' error message into your log file      //plus a screenshot  
         }
         element.Click();
      }      
   }

Now you can invoke the SmartClick method in this way:

btnSearch.SmartClick(x =>
{
   try
   {
      IWebElement query = driver.FindElement(By.Name("q"));
      query.SendKeys("selenium");
      return true;
   }
   catch (Exception e)
   {
      //add "SmartClick preconditions error: " + e.Message error message into your log file
      return false;
   }
});

Above I passed an anonymous method as a parameter but actually there are a lot of ways to use this delegate – and this is your choice how to do it. A couple of examples:

btnSearch.SmartClick();//preconditions are always true because precondition parameter is null by default
btnSearch.SmartClick(x => SomeMethod("blah", "blah-blah", "blah-blah-blah"));//invokes precondition method with parameters
CustomFunction myDelegate = SomeMethod;
btnSearch.SmartClick(myDelegate);//using the delegate instance

Step 4 – clicking the element. I mentioned that the method must perform different clicking actions on the web element according to input parameters. Therefore there must be additional parameter which specifies the action. You are free to implement it as you wish but I used an enumerator for it. I pass its value to the method and then handle it inside the switch operator.

Add a new enumerator inside the namespace

public enum ClickAction
{
   Click,
   Submit,
   DoubleClick,
   LeftMBClick,
   RightMBClick,
   JSClick,
   CursorClick,
   SendKeyEnter,
   SendKeyReturn,
   SendKeySpacebar
}

This is the list of clicking actions I sometimes use in relation to specific web elements. You can use some of them or implement your own ones. Keep it in mind that some of the actions used here require an instance of IWebDriver, therefore you have to pass it to the method as a parameter or to make it available for use inside the method.

Now update the method with new input parameters and inner implementation:

public static void SmartClick(this IWebElement element, IWebDriver driver = null, CustomFunction precondition = nullint iterations = 1, params ClickAction[] clicks)
   {
      int i = iterations;

      //if you do not specify what actions to use for clicking the following default actions will be used
      ClickAction[] clickActions = clicks != null ? clicks : new ClickAction[] { ClickAction.Click, ClickAction.LeftMBClick, ClickAction.JSClick };
   
      string elementName = element.Text;

      if (precondition == null)
         precondition = x => true; //if you did not specify the method let's assume it has passed (return true)
      
      while (i > 0)
      {
         i--;
         if (!precondition.Invoke())
         {
            //add string.Format("Preconditions for clicking '{0}' web element are not met", elementName) error message into your log file
            //screenshot
            continue; //there is no sense to do clicking action if precondition method has failed. Return back to the beginning of the loop and start again from the very beginning
         }
         if (element == null)   
        {
           //add 'Element is null' error message into your log file or throw NoSuchElement or NullReference exception   
        }  
        if(!element.Displayed)   
        {    
           //add 'Element is not displayed and cannot be clicked' error message into your log file      
          //you may add a screenshot here  
        }  
        if(!element.Enabled)   
        {  
           //add 'Element is not displayed and cannot be clicked' error message into your log file  
          //plus a screenshot  
        }
         foreach (var action in clickActions)
         {
            //here you may add string.Format("Clicking '{0}' web element with {1} click action", elementName, action) message into your log file
            switch (action)
            {
               case ClickAction.Click:
                  try
                  {
                      element.Click();
                  }
                  catch (Exception e)
                  {
                      //"SmartClick clicking error: " + e.Message error message
                  }
                  break;
               case ClickAction.Submit:
                  try
                  {
                  }
                  catch (Exception e)
                  {
                     //"SmartClick clicking error: " + e.Message error message
                  }
                  break;
               case ClickAction.DoubleClick:
                  try
                  {
                     var builder = new Actions(driver);
                     builder.DoubleClick().Build().Perform();
                  }
                  catch (Exception e)
                  {
                     //"SmartClick clicking error: " + e.Message error message
                  } 
                  break;
              
               case ClickAction.RightMBClick:
                  if (driver == null)
                  {
                     //"SmartClick clicking error: IWebDriver instance must be passed as a parameter and connot be null if you use clicking with right mouse button action" error message
                     break;
                  }
                  try
                  {
                      var builder = new Actions(driver);
                      builder.MoveToElement(element).ContextClick(element).Build().Perform();
                  }
                  catch (Exception e)
                  {
                     //"SmartClick clicking error: " + e.Message error message
                  }
                  break;
               case ClickAction.JSClick: 
                  if (driver == null)
                  {
                     //"SmartClick clicking error: IWebDriver instance must be passed as a parameter and connot be null if you use clicking with JavaScript action" error message
                     break;
                  }
                  try
                  {
                     ((IJavaScriptExecutor)driver).ExecuteScript("arguments[0].click()", element);
                  }
                  catch (Exception e)
                  {
                     //"SmartClick clicking error: " + e.Message error message
                  }
                  break;
               case ClickAction.CursorClick:
                  try
                  {
                     var X = element.Location.X;
                     var Y = element.Location.Y;
                     SetCursorPos(X, Y);
                     mouse_event(MOUSEEVENTF_LEFTDOWN | MOUSEEVENTF_LEFTUP, 0, 0, 0, 0);
                  }
                  catch (Exception e)
                  {
                     //"SmartClick clicking error: " + e.Message error message
                  }
                  break;
               case ClickAction.SendKeyEnter:
                  try
                  {
                     element.SendKeys(Keys.Enter);
                  }
                  catch (Exception e)
                  {
                     //"SmartClick clicking error: " + e.Message error message
                  }
                  break;
               case ClickAction.SendKeyReturn:
                  try
                  {
                     element.SendKeys(Keys.Return);
                  }
                  catch (Exception e)
                  {
                     //"SmartClick clicking error: " + e.Message error message
                  }
                  break;
               case ClickAction.SendKeySpacebar:
                  try
                  {
                     element.SendKeys(Keys.Space);
                  }
                  catch (Exception e)
                  {
                     //"SmartClick clicking error: " + e.Message error message
                  }
                  break;
            }
            //For now this part of code is placed here just to avoid multiple clicking on the same web element. It will be replaced with Step 5 on the next stage
           if(true)
               break;
         }
      }      
   }

The ClickAction.CursorClick action requires the use of some additional variables. Add them to the Extensions class:

[DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
public static extern void mouse_event(uint dwFlags, int dx, int dy, uint cButtons, uint dwExtraInfo);

[DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
public static extern long SetCursorPos(int X, int Y);

private const int MOUSEEVENTF_LEFTDOWN = 0x02;
private const int MOUSEEVENTF_LEFTUP = 0x04;

Also you need to add reference to System.Drawing library and use following namespaces:

using System;
using NUnit.Framework;
using OpenQA.Selenium;
using OpenQA.Selenium.Support.UI;
using OpenQA.Selenium.Interactions;
using System.Runtime.InteropServices;

Steps 5 – invoking a method passed as a parameter which verifies that the clicking action has been performed and the expected result matches the actual one.

To be sure that a clicking action has been successfully done and the expected result has occurred, you can pass one more function to the method and invoke it after every attempt to click the web element. This function has the same signature as the method which verifies preconditions. Let’s name it ‘postcondition’.

If you specify your own method which will verify post-click conditions or you can not to specify the method itself but pass IWebDriver instance to the method – in this case the method will use default actions defined in the method. If you specify neither post-click verification conditions method nor IWebDriver instance there no verification will be performed. Here is an example of how I implemented it, but you can change the functionality in your own way.

public static void SmartClick(this IWebElement element, IWebDriver driver = null, CustomFunction precondition = null, CustomFunction postcondition = nullint iterations = 1, params ClickAction[] clicks)
   {
      int i = iterations;

      //if you do not specify what actions to use for clicking the following default actions will be used
      ClickAction[] clickActions = clicks != null ? clicks : new ClickAction[] { ClickAction.Click, ClickAction.LeftMBClick, ClickAction.JSClick };
   
      string elementName = element.Text;

      if (precondition == null)
         precondition = x => true; //if you did not specify the method let's assume it has passed (return true)

      if (postcondition == null)
      {
         if (driver == null)
         {
            //"SmartClick: IWebDriver is null, no postcondition verification provided" message to log file
            postcondition = x => true//assume if you do not pass IWebDriver to the mathod you do not need any post-click conditions verification. Therefore, this method always returns true
         }
         else
         {
            //"SmartClick: No click verification conditions are specified. Default verification wil be used." message to log file
            var numOfWindows = driver.WindowHandles.Count;
            var pageHashCode = PageHashCode(driver);
            var pageTitle = driver.Title;
            var pageUrl = driver.Url;
            //the method returns true if any of the following conditions are met, otherwise it returns false
            postcondition = delegate
            {
               //page title is changed
               if (pageTitle != driver.Title)
               {
                  //log message "SmartClick: Click validation - Title of the page has changed"
                  //screenshot
                  return true;
               }
               //page url address is changed
               if (pageUrl != driver.Url)
               {
                  //log message "SmartClick: Click validation - Url address of the page has changed"
                  //screenshot
                  return true;
               }
               //alert pop-up is displayed
               try
               {
                  driver.SwitchTo().Alert();
                  //log message "SmartClick: "Element_ClickWhile: Click validation - Alert pop-up is displayed"
                  //screenshot
                  return true;
               }
               catch {  }
               //number of opened windows or tabs is changed
               if (numOfWindows != driver.WindowHandles.Count)
               {
                  //log message "SmartClick: "Element_ClickWhile: Click validation - Number of opened windows (tabs) has changed"
                  //screenshot
                  return true;
               }
               //page hash code is changed. It indicates that some changes has occurred on the page
               if (pageHashCode != PageHashCode(driver))
               {
                  //log message "SmartClick: "Element_ClickWhile: Click validation - Hash code of the page has changed"
                  //screenshot
                  return true;
               }
               return false;
            }
         }
      }
      
      while (i > 0)
      {
         i--;
         if (!precondition.Invoke())
         {
            //add string.Format("Preconditions for clicking '{0}' web element are not met", elementName) error message into your log file
            //screenshot
            continue; //there is no sense to do clicking action if precondition method has failed. Return back to the beginning of the loop and start again from the very beginning
         }
         if (element == null)   
         {   
            //add 'Element is null' error message into your log file or throw NoSuchElement or NullReference exception   
         }  
         if(!element.Displayed)   
         {   
            //add 'Element is not displayed and cannot be clicked' error message into your log file   
            //you may add a screenshot here  
         }  
         if(!element.Enabled)   
         {  
            //add 'Element is not displayed and cannot be clicked' error message into your log file  
            //plus a screenshot  
         }
         foreach (var action in clickActions)
         {
            //here you may add string.Format("Clicking '{0}' web element with {1} click action", elementName, action) message into your log file
            switch (action)
            {
               case ClickAction.Click:
                  try
                  {
                      element.Click();
                  }
                  catch (Exception e)
                  {
                      //"SmartClick clicking error: " + e.Message error message
                  }
                  break;
               case ClickAction.Submit:
                  try
                  {
                  }
                  catch (Exception e)
                  {
                     //"SmartClick clicking error: " + e.Message error message
                  }
                  break;
               case ClickAction.DoubleClick:
                  try
                  {
                     var builder = new Actions(driver);
                     builder.DoubleClick().Build().Perform();
                  }
                  catch (Exception e)
                  {
                     //"SmartClick clicking error: " + e.Message error message
                  } 
                  break;
              
               case ClickAction.RightMBClick:
                  if (driver == null)
                  {
                     //"SmartClick clicking error: IWebDriver instance must be passed as a parameter and connot be null if you use clicking with right mouse button action" error message
                     break;
                  }
                  try
                  {
                      var builder = new Actions(driver);
                      builder.MoveToElement(element).ContextClick(element).Build().Perform();
                  }
                  catch (Exception e)
                  {
                     //"SmartClick clicking error: " + e.Message error message
                  }
                  break;
               case ClickAction.JSClick: 
                  if (driver == null)
                  {
                     //"SmartClick clicking error: IWebDriver instance must be passed as a parameter and connot be null if you use clicking with JavaScript action" error message
                     break;
                  }
                  try
                  {
                     ((IJavaScriptExecutor)driver).ExecuteScript("arguments[0].click()", element);
                  }
                  catch (Exception e)
                  {
                     //"SmartClick clicking error: " + e.Message error message
                  }
                  break;
               case ClickAction.CursorClick:
                  try
                  {
                     var X = element.Location.X;
                     var Y = element.Location.Y;
                     SetCursorPos(X, Y);
                     mouse_event(MOUSEEVENTF_LEFTDOWN | MOUSEEVENTF_LEFTUP, 0, 0, 0, 0);
                  }
                  catch (Exception e)
                  {
                     //"SmartClick clicking error: " + e.Message error message
                  }
                  break;
               case ClickAction.SendKeyEnter:
                  try
                  {
                     element.SendKeys(Keys.Enter);
                  }
                  catch (Exception e)
                  {
                     //"SmartClick clicking error: " + e.Message error message
                  }
                  break;
               case ClickAction.SendKeyReturn:
                  try
                  {
                     element.SendKeys(Keys.Return);
                  }
                  catch (Exception e)
                  {
                     //"SmartClick clicking error: " + e.Message error message
                  }
                  break;
               case ClickAction.SendKeySpacebar:
                  try
                  {
                     element.SendKeys(Keys.Space);
                  }
                  catch (Exception e)
                  {
                     //"SmartClick clicking error: " + e.Message error message
                  }
                  break;
            }
            if (postcondition())
               return;
            //add string.Format("Post-conditions for clicking '{0}' web element are not met", elementName) error message into your log file
            //screenshot
         }
      }      
   }

Note: In the code above I invoked a method named PageHashCode(). This is a custom method which gets html-code of the current page (IWebDriver.PageSource property) and computes its hash code. The result is returned as a string. Here is the code of the method. Put it into the Extensions class:

static string PageHashCode(IWebDriver driver)
{
   MD5 md5 = MD5.Create();
   byte[] bytesArray = Encoding.ASCII.GetBytes(driver.PageSource);
   byte[] hashCode = md5.ComputeHash(bytesArray);
   var sb = new StringBuilder();
   foreach (byte t in hashCode)
      sb.Append(t.ToString("X2"));
   return sb.ToString();
}

Step 6 – If the expected result is not met, it tries to clicking the elements with the next clicking action from the list.

You can implement it in many ways according to your objectives. For example, you may just add an error message to log file or your can throw an exception. You should add the code for this right after the switch() operator.

Also, I recommend to add one more input parameter to the method: a delay between the clicking attempts. Obviously, after a click on a web element it takes some time for something to happen. Usually, when you click a button, a request is sent to a server, and then the server sends a response which your browser handles and displays the final result. Sometimes it happens almost immediately and sometimes it may be a time-consuming process. So, you can pass a delay parameter (period of time in seconds during which the method should do nothing, assuming this time is enough for performing all the actions described above) with some default value, for example 1 second.

public static void SmartClick(this IWebElement element, IWebDriver driver = null, CustomFunction precondition = null, CustomFunction postcondition = nullint iterations = 1, int delay = 1, params ClickAction[] clicks)
{
   //method code
}

Put its use right after switch() operator

foreach (var action in clickActions)
{        
   ...
   switch (action)
   {
        ... 
   }
   Thread.Sleep(delay * 1000);
   if(postcondition())
      ...
}

Note: you may use global variables instead of the method parameters. For example, you can store such variables as delay or IWebDriver instance in external classes and have them visible all over the project. In that case, you do not need pass them as a parameter. Anyway, you are free to modify the method as you wish. This is just a common point of view, one of possible implementations.

In the next part, I will show how the method may be used in a test.

 

 

One comment

Leave a Reply

Your email address will not be published. Required fields are marked *