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 assumes that:
- You are familiar with TestProject
- You can package TestProject tests and addons with Gradle
- You can write custom TestProject addons
- You can input and output parameters to TestProject actions
- You can run TestProject tests on a local development environment
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.
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.
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 theexecute()
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:
- Split the method parameter in two parts by using the string: "of" as the delimiting regular expression and store the returned
String
array in thelabelParts
variable. - If the
labelParts
array has more than two items, return an emptyOptional
object. - 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:
- Create a new
ActionReportHelper
object. We will use this object for reporting the outcome of ourWebElementAction
. - 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
. - If the total item count label wasn't found, report this error by using the
ActionReportHelper
object and returnExecutionResult.FAILED
. - Parse the total number of items displayed by the Kendo UI grid.
- If the total number of displayed items cannot be parsed, report this error by using the
ActionReportHelper
object and returnExecutionResult.FAILED
. - Store the total number of displayed item in the
totalItemCount
field. - Report the total number of displayed items by using the
ActionReportHelper
object and returnExecutionResult.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 toAutomatedBrowserType.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"; }
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:
- Create a new
Runner
object. We will use this object for running our web element action. - Create a new
KendoUIGridTotalItemCountAction
object. - Get a reference to a
WebDriver
object and open the HTML page that display the Kendo UI grid. - Run our web element action by invoking the
run()
method of theRunner
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 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.