How to Create Selenium Reports with EventListeners

In this post, we will review EventListeners – one of Selenium WebDriver‘s functionalities that enables us to create reports and logs while avoiding code duplication.

Let’s assume that we’re running a test case with Selenium that includes a few steps, such as: navigation to a certain website, taping a button, entering text into a field, etc. We want all of these actions to be reported to some sort of log system. So, of course, we can add a line of code after each step that will take care of reporting, but that will be an overhead making the code less readable, as described in the example below (printing to console window):

driver.get("https://www.ebay.com/");
System.out.println("Go to Site: Ebay.com");
driver.findElement(By.id("gh-ac")).sendKeys("GoPro HERO6");
System.out.println("Insert Text into field");
driver.findElement(By.id("gh-btn")).click();
System.out.println("click on search button");

Not such a clean code, right?
So, how can we improve it?

Well, there are several more elegant solutions to solve those code duplications (3 lines of code turned out to be 6 in our example). One of the selenium native capabilities is EventListeners.
Don’t get confused with TestNG Listener which also solves this issue, but in this post we will focus on the built-in WebDriver solution.

EvenListener (WebDriverEventListener) is an interface in the WebDriver code libraries that includes (as of today) a list of 23 methods that capture different events. You can see the full list in the official documentation of this project.

Another term we need to know is: EventFiringWebDriver. It is a class that wraps the WebDriver object (How can a class wrap an object? You will see in the example below), and it is designed to dispatch different events during the program runtime. In fact, EventFiringWebDriver also implements the WebDriver, which means it will have all the functionalities of, for example, ChromeDriver (Such as: switchTo, getTitle, findElement, etc) in addition to 2 more actions: register and unregister.

The register action enables listening to events in the WebDriverEventListener, and the unregister action stops the listening.
To summarize: EventFiringWebDriver dispatches events and the WebDriverEventListener catches them.

You can see the hierarchy of the interfaces/classes here:

 

The implementation will include several steps:

1. Initialize our WebDriver driver object in the usual way, for example: Initialize it to ChromeDriver so it can work with Google Chrome browser.

WebDriver  chDriver = new ChromeDriver();

2. Create a new EventFiringWebDriver object – This is the class that implements the WebDriver. Let’s call it driver and send to its constructor the chDriver object we have created earlier (in other words, we are wrapping the driver object with a new class – EventFiringWebDriver):

EventFiringWebDriver driver  = new EventFiringWebDriver(chDriver);

3. Instance an object from a class (let’s call it eventImplementations). We will use it to implement all of the WebDriverEventListener methods (we will see the class and the implementations below, now we will just create its object):

eventImplementations ei  = new eventImplementations();

4. Register the WebDriverEventListener object (which we created in the previous section) by calling the register method (which belongs to the EventFiringWebDriver class):

driver.register(ei);

Now, let’s see the class that implements the WebDriverEventListener methods. The implementation in this example refers to the actions: beforeClickOn, afterClickOn, beforeNavigateTo, afterNavigateTo, onException, afterChangeValueOf (I decided to implement only 6 out of the 23 methods).

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.events.WebDriverEventListener;
 
public class eventImplementations implements WebDriverEventListener 
{
  public void beforeClickOn(WebElement arg0, WebDriver arg1) {
     System.out.println("Before Clicking On " + arg0.getAttribute("value"));
   }
  
  public void afterClickOn(WebElement arg0, WebDriver arg1) {
     System.out.println("After Clicking On Element");
   }	 
   
   public void beforeNavigateTo(String arg0, WebDriver arg1) {
     System.out.println("Before Navigate To " + arg0);
   }
   
   public void afterNavigateTo(String arg0, WebDriver arg1) {
     System.out.println("After Navigate To " + arg0);
   }
   
   public void afterChangeValueOf(WebElement arg0, WebDriver arg1, CharSequence[] ch) {
    System.out.println("Element: \"" + arg0.getAttribute("placeholder") + "\" was updated to: " + arg0.getAttribute("value"));
   }	
   
   public void afterAlertDismiss(WebDriver arg1) {
        // TODO Implement this Method
   }
   
   public void beforeAlertDismiss(WebDriver arg1) {
    // TODO Implement this Method
   }
   
   public void afterAlertAccept(WebDriver arg1) {
    // TODO Implement this Method
   }
   
   public void beforeAlertAccept(WebDriver arg1) {
    // TODO Implement this Method
   }
  
   public void afterFindBy(By arg0, WebElement arg1, WebDriver arg2) {
    // TODO Implement this Method
   }
 
   public void afterNavigateBack(WebDriver arg0) {
    // TODO Implement this Method
   }
 
   public void afterNavigateForward(WebDriver arg0) {
    // TODO Implement this Method
   }
 
   public void afterNavigateRefresh(WebDriver arg0) {
    // TODO Implement this Method
   }	 
 
   public void afterScript(String arg0, WebDriver arg1) {
         // TODO Auto-generated method stub
   }
 
   public void beforeChangeValueOf(WebElement arg0, WebDriver arg1, CharSequence[] ch) {
    // TODO Implement this Method
   }
 
   public void beforeFindBy(By arg0, WebElement arg1, WebDriver arg2) {
    // TODO Implement this Method
   }
   
   public void beforeNavigateBack(WebDriver arg0) {
    // TODO Implement this Method
   }
   
   public void beforeNavigateForward(WebDriver arg0) {
    // TODO Implement this Method
   }
   
   public void beforeNavigateRefresh(WebDriver arg0) {
    // TODO Implement this Method
   }
 
   public void beforeScript(String arg0, WebDriver arg1) {
    // TODO Implement this Method
   }
   
   public void onException(Throwable arg0, WebDriver arg1) {
     System.out.println("See Error: " + arg0.getMessage());
   }
}

This means that when we activate our driver, these following actions will be performed:

  • Before every mouse click, this text will be printed to the console: Before Clicking On, with a string of the button’s name.
  • After every mouse click, this text will be printed to the console: After Clicking On Element.
  • Before navigating to a website, this text will be printed to the console: Before Navigate To, with a string of the website URL.
  • After navigating to a website, this text will be printed to the console: After Navigate To, with a string of the website URL.
  • After sending a text to an element (the sendKeys action we are familiar with), a text will be printed to the console accordingly.
  • Once the test fails and an exception will be dispatched, an exception message will be printed to the console.

Now, all that’s left to do is to implement everything together under our tests class. Our test case is: Navigate to ebay website, enter the value: GoPro HERO6 in the search field and click on the search button:

import java.util.concurrent.TimeUnit;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.support.events.EventFiringWebDriver;
 
public class testCases extends init
{
  private static WebDriver chDriver;
  private static EventFiringWebDriver driver;
  private static eventImplementations ei;
  
  @BeforeClass
  public static void openBrowser()
  {
    ei = new eventImplementations();
    System.setProperty("webdriver.chrome.driver", "C:/chromedriver.exe"); 
    chDriver = new ChromeDriver(); 
    driver = new EventFiringWebDriver(chDriver);
    driver.register(ei);
    driver.manage().window().maximize();
    driver.manage().timeouts().implicitlyWait(60, TimeUnit.SECONDS); 
    driver.get("https://www.ebay.com/");
  }
 
  @Test
  public void myScenario1() throws InterruptedException
  {    		
    driver.findElement(By.id("gh-ac")).sendKeys("GoPro HERO6");
    driver.findElement(By.id("gh-btn")).click();		
  }
  
  @AfterClass
  public static void closeBrowser()
  {
      driver.quit();
  }	
}

These are the actions that actually happened behind the scenes:

  • The driver.get command raised an event that was handled by the beforeNavigateTo and in the afterNavigateTo (and the printing to the console was accordingly).
  • The sendKeys command raised an event that was handled by afterChangeValueOf  (and the printing to the console was accordingly).
  • The click command raised an event that was handled by the beforeClickOn and in the afterClickOn (and the printing to the console was accordingly).

 

 

 

It is also possible to expand the report’s capabilities, and instead of printing to the console (because who actually prints to the console in a real automation project?), you can print to the reporting system by using an external API such as: Allure or ExtentReports, which means we will have to initialize the reports’ objects in a smarter way than with our eventImplementations class.

For example, we can inherit the base or the init from a different class, and there we will take care of the objects’ initialization:

public class eventImplementations extends init implements WebDriverEventListener

Our init class could look like this for example (in this case I’m demonstrating an example for printing into an external file):

import java.io.FileNotFoundException;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
 
public class init
{
  private static String logFile = "D:/test/MyLogFile.log";
  protected static PrintWriter writer;
  
  
  protected static void CreateLog() throws FileNotFoundException, UnsupportedEncodingException
  {
    writer = new PrintWriter(logFile, "UTF-8");
  }
  
  protected static void CloseLog() throws FileNotFoundException, UnsupportedEncodingException
  {
    writer.close();
  }
  
  protected void WriteToLog(String value) throws FileNotFoundException, UnsupportedEncodingException
  {		
    writer.println(value);
    
  }
}

What about our tests class? Will this really be the way we write it in our project? Where is the Page Objects, for example? Where is the abstraction? Or in other words, do EventListeners support proper framework writing? The answers is: Of course! Eventually, we will dispatch some event, it doesn’t matter from where in the code or from which class (of a framework or a business).

For example, we can write the exact same test case we wrote earlier with Page Objects, and the printing to the console will still work like a charm:

The pages class:

import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.How;

public class mainPage
{
  @FindBy(how = How.ID, using = "gh-ac") 
  public WebElement searchField;
  
  @FindBy(how = How.ID, using = "gh-btn") 
  public WebElement searchButton;
  
  public void search(String item)
  {
    searchField.sendKeys(item);
    searchButton.click();
  }
}

The tests class:

import java.util.concurrent.TimeUnit;

import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.support.PageFactory;
import org.openqa.selenium.support.events.EventFiringWebDriver;

public class testCases extends init
{
  private static WebDriver chDriver;
  public static EventFiringWebDriver driver;
  public static eventImplementations ei;
  public static mainPage mp;
  
  @BeforeClass
  public static void openBrowser()
  {
    ei = new eventImplementations();
    System.setProperty("webdriver.chrome.driver", "C:/chromedriver.exe"); 
    chDriver = new ChromeDriver(); 
    driver = new EventFiringWebDriver(chDriver);
    driver.register(ei);
    driver.manage().window().maximize();
    driver.manage().timeouts().implicitlyWait(60, TimeUnit.SECONDS); 
    driver.get("https://www.ebay.com/");
    
    mp = PageFactory.initElements(driver, mainPage.class);
  }

  @Test
  public void myScenario1() throws InterruptedException
  {   
    mp.search("GoPro HERO6");		
  }
  
  @AfterClass
  public static void closeBrowser()
  {
      driver.quit();
  }	
}

The bottom line is that working with Listeners is intelligent, clean and saves us a lot of headaches caused by working with various other report implementations. On the other hand, as already mentioned above, today there are implementations for 23 methods in the WebDriverEventListener (from Selenium’s 3.10 version,  two more actions were added: beforeSwitchToWindow and afterSwitchToWindow), which is good but still not enough. There are many other actions that do not have any implementations, such as: choosing a value from a drop-down, or dragging an object with Drag N Drop, etc. This means that for these actions we will need to create our own implementations, and this is kind of a downside…

WebDriverEventListener documentation.
EventFiringWebDriver documentation.

What is your opinion on Selenium WebDriver EventListeners? How do you solve code duplications in reports? Please share your thoughts below!  🙂