Writing Custom Web Element Actions With TestProject

We can remove duplicate code from our TestProject test suite by using web actions. Even though web actions make our life easier, they have a access to the entire DOM, and this can make our test code more complicated than it could be. This blog post describes how we can solve this problem by using web element actions.

After we have finished this blog post, we:

  • Know what a web element action is.
  • Understand when we should use web element actions.
  • Can write a custom web element action with TestProject.
  • Know how we can run our web element actions on a local development environment.
  • Can configure the root element of our web element action when we upload our addon to the app.testproject.io website.

Let's begin.

This blog post is the tenth part of my TestProject tutorial that is sponsored by TestProject.io. However, the views and opinions expressed in this tutorial are mine.

This blog post assumes that:

By the way, you might want to read the other parts of my TestProject tutorial.

What Is a Web Element Action?

A web element action is an action whose scope is limited to the child elements of the specified root element. In other words, a web element action should process only the child elements of the configured root element.

If you must violate the scope of a web element action (you must access the whole DOM), you can get a WebDriver object by invoking the getDriver() method of the WebAddonHelper class.

For example, let's assume that we want to create a web element action that supports the Kendo UI grid. When we create this web element action, we have to:

  • Identify the root element of the Kendo UI grid.
  • Implement our web element action.
  • Configure the element type which allows the TestProject framework to locate the root element of the Kendo UI grid. An element type identifies the root element by using an XPath expression and multiple web element actions can share the same element type. This means that:
    • If we want to write multiple web element actions which support the Kendo UI grid (or any other component), our web element actions have less code because we don't have to write the code which locates the root element of the target component.
    • We can make our tests a lot simpler by leveraging the web element actions found from the TestProject Addon store. These addons save us a lot of time when we are dealing with complex UIs because the TestProject framework locates the required components for us and the web element actions allow us to interact with the found components. In other words, we can write (or record) our tests without digging into the complex DOM.

When we run our web element action, the TestProject framework locates the root element of the Kendo UI grid and defines the scope of the invoked web element action by passing the root element to the action.

The following figure illustrates the scope of our web element action:

The benefit of this technique is that our web element action doesn't have to know anything about the structure of the displayed HTML document. This means that we can write simple web element actions which have only one purpose: perform an operation X to the component Y or extract information from the component Y. These actions are easy to read, write, and maintain.

A web element action is an extremely useful tool when we have to write tests for a web application that has components which are shared by multiple views. For example, if our web application uses a component library such as Kendo UI or Material-UI, we should use web element actions in our TestProject tests.

Next, we will find out how we can implement a reporter helper class.

Implementing a Reporter Helper Class

As we remember, we should always report the result of our action by using the ActionReporter class. When we want to report the result of our action, we have to invoke the result() method of the ActionReporter class. This method takes a String object as a method parameter.

In other words, if we want to use dynamic result messages, we have to construct these messages in our action class before we report the result of our action. This adds unnecessary clutter to our action classes.

That's why we have to write a helper class which allows us to use dynamic result messages when we report the result of our action. We can write this class by following these steps:

First, we have to create a new ActionReportHelper class. After we have created this class, its source code looks as follows:

public class ActionReportHelper {

}

Second, we have to add a private and final ActionReporter field to the ActionReportHelper class and ensure that the value of this field is provided by using constructor injection. After we have added this field to the ActionReportHelper class, its source code looks as follows:

import io.testproject.java.sdk.v2.reporters.ActionReporter;

public class ActionReportHelper {

    private final ActionReporter reporter;

    public ActionReportHelper(ActionReporter reporter) {
        this.reporter = reporter;
    }
}

Third, we have to write a method which allows us to report result messages to TestProject framework by using the format that's supported by the String.format() method. After we have written this method, the source code of the ActionReportHelper class looks as follows:

import io.testproject.java.sdk.v2.reporters.ActionReporter;

public class ActionReportHelper {

    private final ActionReporter reporter;

    public ActionReportHelper(ActionReporter reporter) {
        this.reporter = reporter;
    }
    
    public void reportResult(String resultTemplate, Object... params) {
        reporter.result(String.format(resultTemplate, params));
    }
}

We can now send dynamic result messages to TestProject framework without adding any clutter to our action classes. Let's move on and implement a custom web element action.

Implementing a Custom Web Element Action

Let's write a custom web element action which extracts the total number of items shown on a Kendo UI grid. The following figure identifies the extracted information:

We can write our web element action by following these steps:

First, we have to create a new action class which implements the WebElementAction interface. After we have created a new action class, its source code looks as follows:

import io.testproject.java.annotations.v2.Action;
import io.testproject.java.sdk.v2.addons.WebElementAction;

@Action(name = "Extracts the total item count of a Kendo UI grid")
public class KendoUIGridTotalItemCountAction implements WebElementAction {
    
}

Second, we have to add an output parameter called: totalItemCount to our action class. This parameter contains the total number of items shown on the Kendo UI grid. After we have added a new output parameter to our action class, the source code of our action class looks as follows:

import io.testproject.java.annotations.v2.Action;
import io.testproject.java.annotations.v2.Parameter;
import io.testproject.java.enums.ParameterDirection;
import io.testproject.java.sdk.v2.addons.WebElementAction;

@Action(name = "Extracts the total item count of a Kendo UI grid")
public class KendoUIGridTotalItemCountAction implements WebElementAction {

    @Parameter(description = "Contains the total item count of a Kendo UI grid",
            direction = ParameterDirection.OUTPUT
    )
    private int totalItemCount;
}

Third, we have to override the execute() method of the WebElementAction interface. This method returns an ExecutionResult enum and it has two method parameters:

  • The WebAddonHelper object allows us to access the TestProject API when we implement the execute() method.
  • The WebElement object is the root element of our web element action. In our case, this object is the root element of the Kendo UI grid.

After we have added the execute() method to our action class, the source code of our action class looks as follows:

import io.testproject.java.annotations.v2.Action;
import io.testproject.java.annotations.v2.Parameter;
import io.testproject.java.enums.ParameterDirection;
import io.testproject.java.sdk.v2.addons.WebElementAction;
import io.testproject.java.sdk.v2.addons.helpers.WebAddonHelper;
import io.testproject.java.sdk.v2.enums.ExecutionResult;
import io.testproject.java.sdk.v2.exceptions.FailureException;
import org.openqa.selenium.WebElement;

@Action(name = "Extracts the total item count of a Kendo UI grid")
public class KendoUIGridTotalItemCountAction implements WebElementAction {

    @Parameter(description = "Contains the total item count of a Kendo UI grid",
            direction = ParameterDirection.OUTPUT
    )
    private int totalItemCount;

    @Override
    public ExecutionResult execute(WebAddonHelper webAddonHelper,
                                   WebElement webElement) throws FailureException {
    }
}

Fourth, we have to write a private method which parses the total number of displayed items from the String object given as a method parameter and returns an Optional object which contains the total number of displayed items. If the total number of displayed items cannot be parsed, this method returns an empty Optional object. Also, this method expects that the String object given as a method parameter uses the format: '1 - 20 of 91 items'.

We can write this method by following these steps:

  1. Split the method parameter in two parts by using the string: "of" as the delimiting regular expression and store the returned String array in the labelParts variable.
  2. If the labelParts array has more than two items, return an empty Optional object.
  3. Parse the total number of displayed items from the string: '91 items' and return an Optional object that contains the total number of displayed items.

After have written the parseTotalItemCount() method, the source code of our action class looks as follows:

import io.testproject.java.annotations.v2.Action;
import io.testproject.java.annotations.v2.Parameter;
import io.testproject.java.enums.ParameterDirection;
import io.testproject.java.sdk.v2.addons.WebElementAction;
import io.testproject.java.sdk.v2.addons.helpers.WebAddonHelper;
import io.testproject.java.sdk.v2.enums.ExecutionResult;
import io.testproject.java.sdk.v2.exceptions.FailureException;
import org.openqa.selenium.WebElement;

import java.util.Optional;

@Action(name = "Extracts the total item count of a Kendo UI grid")
public class KendoUIGridTotalItemCountAction implements WebElementAction {

    @Parameter(description = "Contains the total item count of a Kendo UI grid",
            direction = ParameterDirection.OUTPUT
    )
    private int totalItemCount;
    
    @Override
    public ExecutionResult execute(WebAddonHelper webAddonHelper, 
                                   WebElement webElement) throws FailureException {
    }

    private Optional<Integer> parseTotalItemCount(String totalItemCountLabel) {
        String[] labelParts = totalItemCountLabel.split("of");

        if (labelParts.length != 2) {
            return Optional.empty();
        }

        String totalItemCount = labelParts[1].replace("items", "").trim();
        return Optional.of(Integer.valueOf(totalItemCount));
    }
}

Fifth, we have to implement the execute() method of the WebElementAction interface by following these steps:

  1. Create a new ActionReportHelper object. We will use this object for reporting the outcome of our WebElementAction.
  2. Find the HTML element that contains the total item count label. We can find this HTML element by using the HTML class: k-pager-info.
  3. If the total item count label wasn't found, report this error by using the ActionReportHelper object and return ExecutionResult.FAILED.
  4. Parse the total number of items displayed by the Kendo UI grid.
  5. If the total number of displayed items cannot be parsed, report this error by using the ActionReportHelper object and return ExecutionResult.FAILED.
  6. Store the total number of displayed item in the totalItemCount field.
  7. Report the total number of displayed items by using the ActionReportHelper object and return ExecutionResult.PASSED.

After we have to implemented the execute() method of the WebElementAction interface, the source code of our action class looks as follows:

import io.testproject.java.annotations.v2.Action;
import io.testproject.java.annotations.v2.Parameter;
import io.testproject.java.enums.ParameterDirection;
import io.testproject.java.sdk.v2.addons.WebElementAction;
import io.testproject.java.sdk.v2.addons.helpers.WebAddonHelper;
import io.testproject.java.sdk.v2.enums.ExecutionResult;
import io.testproject.java.sdk.v2.exceptions.FailureException;
import org.openqa.selenium.By;
import org.openqa.selenium.Keys;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;

import java.util.Optional;

@Action(name = "Extracts the total item count of a Kendo UI grid")
public class KendoUIGridTotalItemCountAction implements WebElementAction {

    @Parameter(description = "Contains the total item count of a Kendo UI grid",
            direction = ParameterDirection.OUTPUT
    )
    private int totalItemCount;

    @Override
    public ExecutionResult execute(WebAddonHelper webAddonHelper,
                                   WebElement webElement) throws FailureException {
        ActionReportHelper reporter = new ActionReportHelper(
                webAddonHelper.getReporter()
        );

        WebElement totalItemCountLabel = webElement
                .findElement(By.className("k-pager-info"));
        if (totalItemCountLabel == null) {
            reporter.reportResult("The total item count label wasn't found");
            return ExecutionResult.FAILED;
        }

        Optional<Integer> totalItemCount = parseTotalItemCount(
                totalItemCountLabel.getText()
        );
        if (!totalItemCount.isPresent()) {
            reporter.reportResult(
                    "Couldn't parse the total item count from the text: %s",
                    totalItemCountLabel.getText()
            );
            return ExecutionResult.FAILED;
        }

        this.totalItemCount = totalItemCount.get();
        reporter.reportResult("The total item count is: %d", this.totalItemCount);

        return ExecutionResult.PASSED;
    }

    private Optional<Integer> parseTotalItemCount(String totalItemCountLabel) {
        String[] labelParts = totalItemCountLabel.split("of");

        if (labelParts.length != 2) {
            return Optional.empty();
        }

        String totalItemCount = labelParts[1].replace("items", "").trim();
        return Optional.of(Integer.valueOf(totalItemCount));
    }
}

We have now written a custom web element action that extracts the total number of items shown on a Kendo UI grid. Next, we will find out how we can debug web element actions on our development environment.

Debugging Web Element Actions on Development Environment

When we want to debug a web element action on our local development environment, we have to write a so called runner class which runs our web element action. We can write this class by following these steps:

First, we have to create a new class. After we have created our runner class, its source code looks as follows:

public class KendoUIGridAddonRunner {

}

Second, we have to add two constants to our runner class:

  • The BROWSER constant configures the browser that runs our web element action. Because we want to run our web element action by using the Chrome web browser, we have to set the value of this constant to AutomatedBrowserType.Chrome.
  • The DEVELOPER_KEY constant configures our developer key.

After we have added these constants to our runner class, its source code looks as follows:

import io.testproject.java.enums.AutomatedBrowserType;

public class KendoUIGridAddonRunner {

    private static final AutomatedBrowserType BROWSER = AutomatedBrowserType.Chrome;
    private static final String DEVELOPER_KEY = "PUT_YOUR_DEVELOPER_KEY_HERE";
}
If you don't know how you can get your developer key, you should read my blog post which helps you to run TestProject tests on your local development environment.

Third, we have to add a public and static main() method to our runner class. This method takes a String array as a method parameter and doesn’t return anything. Also, this method can throw an Exception.

After we have added this method to our runner class, the source code of our runner class looks as follows:

import io.testproject.java.enums.AutomatedBrowserType;

public class KendoUIGridAddonRunner {

    private static final AutomatedBrowserType BROWSER = AutomatedBrowserType.Chrome;
    private static final String DEVELOPER_KEY = "PUT_YOUR_DEVELOPER_KEY_HERE";

    public static void main(String[] args) throws Exception {

    }
}

Fourth, we have to implement the main() method by following these steps:

  1. Create a new Runner object. We will use this object for running our web element action.
  2. Create a new KendoUIGridTotalItemCountAction object.
  3. Get a reference to a WebDriver object and open the HTML page that display the Kendo UI grid.
  4. Run our web element action by invoking the run() method of the Runner class. When we invoke this method, we have to pass the following objects as method parameters:
    • The invoked web element action.
    • The root element of the web element action. In our case, this method parameter is the root element of the Kendo UI grid. We can find this HTML element by using the HTML id: grid.

After we have written the main() method, the source code of the KendoUIGridAddonRunner class looks as follows:

import io.testproject.java.enums.AutomatedBrowserType;
import io.testproject.java.sdk.v2.Runner;
import io.testproject.java.sdk.v2.drivers.WebDriver;
import org.openqa.selenium.By;

public class KendoUIGridAddonRunner {

    private static final AutomatedBrowserType BROWSER = AutomatedBrowserType.Chrome;
    private static final String DEVELOPER_KEY = "PUT_YOUR_DEVELOPER_KEY_HERE";

    public static void main(String[] args) throws Exception {
        Runner runner = Runner.createWeb(DEVELOPER_KEY, BROWSER);

        KendoUIGridTotalItemCountAction totalItemCount = 
                new KendoUIGridTotalItemCountAction();

        WebDriver driver = runner.getDriver();
        driver.get("https://demos.telerik.com/kendo-ui/grid/index");

        runner.run(totalItemCount, By.id("grid"));
    }
}

We can now run our web element action by running the main() method of our runner class. If we want to debug our web element action, we can simply add breakpoints to the preferred lines. Let's move on and find out how we can upload our addon to the app.testproject.io website.

Uploading Our Addon to the TestProject Website

Before we can use our web element action in our test classes or in our recorded tests, we have to package our actions in a jar file and upload this file to the app.testproject.io website.

If you want to get more information about the upload process, you should read the blog post: Writing Addons With TestProject.

If the uploaded jar file contains web element actions (classes which implement the WebElementAction interface), we have to configure the element type of each web element action when we review the actions found from the uploaded jar file (our addon). We can configure the element type of a web element action by clicking the 'Select' link found from the 'Element Types' column.

The following figure illustrates the layout of the 'Review Actions' modal dialog:

When we click the 'Select' link, the app.testproject.io website opens the 'Select Element Types' modal dialog. We can now either select the correct element type by using the 'Element Types' combo box or we can create a new element by type by clicking the 'Create element type' link. Let's assume that we have to create a new element type for our web element action.

The following figure illustrates this step:

When we click the 'Create element type' link, the app.testproject.io website starts the 'Create Element Type' wizard. We can complete this wizard by following these steps:

First, we have to configure the target platform of our element type. Because we want to create an element type that supports web applications, we have to click the 'Web' icon and move to the next step of this wizard by clicking the 'Next' button.

The following figure illustrates this step:

Second, we have to configure the created element type. When we configure the created element type, we have to provide the following information:

  • The name of the element type.
  • An optional description of the element type.
  • The XPath locator which is used to locate the HTML element of the created element type. This element is the root element of our web element action. In other words, our web element action can process only the child elements of the specified HTML element. Because we want to locate the root element of a Kendo UI grid, we have to use the XPath locator: //div[@data-role = 'grid' and contains(@class, 'k-grid')].

After have provided the required information, we can create a new element type by clicking the 'Create' button.

The following figure illustrates this step:

After we have created a new element type, the app.testproject.io website opens the 'Select Element Types' modal dialog. We can now configure the element type of our web element action by using the 'Element Types' combo box. After we have selected the element type, we can save the selected element type by clicking the 'Save & Return to Actions' button.

The following figure illustrates this step:

After we have selected the element type of our web element action, the app.testproject.io website opens the 'Review Actions' modal dialog. This dialog displays the number of selected element types of our web element action (1). We can finish the upload process by clicking the 'Finish' button.

The following figure illustrates this step:

We can write custom web element actions with TestProject and we know how we can upload our actions to the app.testproject.io website. Let's summarize what we learned from this blog post.

Summary

This blog post has taught us five things:

  • A web element action is an action whose scope is limited to the child elements of the specified root element.
  • We should use web element actions in our test classes if the tested web application uses a component library such as Kendo UI or Material-UI.
  • We can write a custom web element action by creating a class that implements the WebElementAction interface.
  • When we want to run or debug our web element action in our local development environment, we have to write a runner class that runs our web element action.
  • When we upload a web element action to the app.testproject.io website, we have to configure the XPath locator which identifies the root element of our web element action.

P.S. You can get the example application of this blog post from Github.

0 comments… add one

Leave a Reply