Skip to main content

Selenium module

Kolibrium's Selenium module provides a type-safe, Kotlin-first approach to browser automation using Selenium WebDriver. This guide explains how to use Kolibrium's locator strategies and configuration options for reliable web element interaction.

Locator strategies

A locator is a mechanism used to identify elements on a page. It serves as an argument for element-finding methods. In this section, we will explore how to use locator delegates.

Add the Selenium module to your project

To get started, add the following dependency and configure JUnit in your Gradle project build file (build.gradle.kts):

dependencies { 
implementation("dev.kolibrium:kolibrium-selenium:0.4.0")
// other dependencies
}

tasks.test {
useJUnitPlatform()
}

Finding elements with locator delegate functions

Kolibrium's Selenium module provides support for the following traditional locator strategies:

LocatorDescriptionMultiple elements supported?
classNameLocates elements with a class name containing the search value (compound class names are not allowed).Yes
cssSelectorLocates elements that match a CSS selector.Yes
idLocates elements with an ID attribute that matches the search value.No
idOrNameFirst, attempts to locate an element by ID and falls back to using the name if the ID is not found.No
linkTextLocates anchor elements with visible text that matches the search value.Yes
nameLocates elements with a NAME attribute that matches the search value.Yes
partialLinkTextLocates anchor elements with visible text that contains the search value; if multiple elements match, only the first one is selected.Yes
tagNameLocates elements with a tag name that matches the search value.Yes
xPathLocates elements that match an XPath expression.Yes

Creating locators

To interact with a web element using Selenium, first we need to locate it on the web page. Selenium offers several methods to locate elements on the page. To learn and create locators, we will refer to the following HTML snippet.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Selenium Locators Test Page</title>
</head>
<body>
<h1>Selenium Locators Test Page</h1>

<div class="locator" id="single-locators">
<h2>Single Element Locators</h2>
<div class="by-class-name">Locate by Class Name</div><br>
<div data-test="css-selector">Locate by CSS Selector</div><br>
<div id="by-id">Locate by ID</div><br>
<a href="#" id="by-link-text">Locate by Link Text</a><br><br>
<button name="by-name">Locate by Name</button><br><br>
<a href="#" id="by-partial-link">Locate by Partial Link Text Example</a><br>
<p>Locate by Tag Name</p>
<div id="by-xpath">Locate by XPath</div>
</div>

<div class="locator" id="multiple-locators">
<h2>Multiple Element Locators</h2>
<div class="multiple">Multiple Locate by Class Name</div><br>
<div class="multiple">Multiple Locate by Class Name</div><br>
<div data-test="multiple">Multiple Locate by CSS Selector</div><br>
<div data-test="multiple">Multiple Locate by CSS Selector</div><br>
<a href="#">Multiple Locate by Link Text</a><br><br>
<a href="#">Multiple Locate by Link Text</a><br><br>
<button name="multiple-by-name">Multiple Locate by Name</button><br><br>
<button name="multiple-by-name">Multiple Locate by Name</button><br><br>
<a href="#">Locate by Partial Link Example</a><br><br>
<a href="#">Locate by Partial Link Example</a><br><br>
<span>Multiple Locate by Tag Name</span><br><br>
<span>Multiple Locate by Tag Name</span><br><br>
<div class="multiple-by-xpath">Multiple Locate by XPath</div><br>
<div class="multiple-by-xpath">Multiple Locate by XPath</div>
</div>
</body>
</html>

className locator strategy

HTML web elements can have a class attribute. These elements can be identified using the class name locator provided by Selenium.

Locating single element

val className by driver.className("by-class-name")

Locating multiple elements

val classNames by driver.classNames("multiple")

cssSelector locator strategy

We can use the css selector locator strategy to identify elements on the page. If the element has an ID, the locator is defined as css=#id. Otherwise, the format used is css=[attribute=value].

Locating single element

val cssSelector by driver.cssSelector("[data-test='css-selector']")

Locating multiple elements

val cssSelectors by driver.cssSelectors("[data-test='multiple']")

id selector locator strategy

The ID attribute of an element on a web page can be used to locate it. Typically, the ID property is unique for each element on the page.

Locating single element

val id by driver.id("by-id")

Locating multiple elements with ID attribute is not supported.

idOrName selector locator strategy

This locator strategy first tries to locate the element by ID. If the element is not found, it then tries by name.

Locating single element

val idOrName by driver.idOrName("by-name")

Locating multiple elements with ID or name attribute is not supported.

linkText locator strategy

To locate an element that is a link, we can use the link text locator. The link text refers to the visible text displayed for the link on the web page.

Locating single element

val linkText by driver.linkText("Locate by Link Text")

Locating multiple elements

val linkTexts by driver.linkTexts("Multiple Locate by Link Text")

name locator strategy

The name attribute of an element on a web page can be used to locate it.

Locating single element

val name by driver.name("by-name")

Locating multiple elements

val names by driver.names("multiple-by-name")

partialLinkText locator strategy

To locate an element that is a link, we can use the partial link text locator on the web page. The link text refers to the visible text of the link, and we can provide a portion of this text as the value.

Locating single element

val partialLinkText by driver.partialLinkText("Partial Link Text Example")

Locating multiple elements

val partialLinkTexts by driver.partialLinkTexts("Partial Link Example")

tagName locator strategy

The HTML tag itself can be used as a locator to identify a web element on the page.

Locating single element

val tagName by driver.tagName("p")

Locating multiple elements

val tagNames by driver.tagNames("span")

xPath locator strategy

An HTML document can be treated as an XML document, allowing us to use XPath to navigate the path to the desired element for locating it. XPath can be absolute, starting from the root of the document. For example, /html/body/h1 refers to the "Single Element Locators" heading. Alternatively, XPath can be relative, such as //button[@name='by-name'], which identifies the "Locate by Name" button.

Locating single element

val xPath by driver.xPath("//div[@id='by-xpath']")

Locating multiple elements

val xPaths by driver.xPaths("//div[@class='multiple-by-xpath']")

Configuring synchronization behavior

Single elements

By default, Kolibrium checks if an element is displayed using the WebElement's isDisplayed property. In some cases, relying solely on the isDisplayed property may be insufficient. Common practice is to wait until the element is displayed and enabled so it can be clicked. In order to facilitate this waiting strategy, we can specify what WebElement properties should be evaluated to be true before performing an action on it. This is done by providing a predicate that determines when the found element is considered ready for use:

val button by id("button") { isDisplayed && isEnabled }

Or, by using Kolibrium's extension property on WebElement, it can be simplified to:

val button by id("button") { clickable }

where clickable is defined as:

public val WebElement.clickable: Boolean
get() = isDisplayed && isEnabled

If you are using IntelliJ IDEA or Aqua, and have enabled Inlay Hints (Settings -> Editor -> Inlay Hints -> Lambdas -> Kotlin -> Implicit receivers and parameters), you will see a this: WebElement hint after the opening bracket:

intellij_inlay_hint

This indicates that we have access to the WebElement, meaning we can use other properties as well, such as isSelected to determine when the element is ready for use.

In fact, we can write any code in between the bracket as long as it's evaulated to a Boolean value.

Multiple elements

When working with multiple elements, we usually wait until all elements in the collection are loaded:

val links by classNames("link")

We can also wait until all elements with the same attribute value appear:

val images by names("kodee") {
size == 9
}

Caching elements

Caching elements reduces test execution time by eliminating repeated searches for frequently accessed elements.

By default, Kolibrium looks up elements only once and caches them for subsequent accesses. If cacheLookup is set to false, a new lookup will be performed each time the element is accessed.

val className by driver.className("by-class-name", cacheLookup = true)
val classNames by driver.classNames("multiple", cacheLookup = true)

Configuring waiting behavior

There is an option to configure the waiting behavior when looking up elements by specifying polling interval, timeout, error message, and which exceptions to ignore during the wait.

val className by driver.className(
locator = "by-class-name",
wait = Wait(
pollingInterval = 250.milliseconds,
timeout = 5.seconds,
message = "Element could not be found",
ignoring = arrayOf(NoSuchElementException::class, StaleElementReferenceException::class),
)
)

Under the hood, a FluentWait instance is being configured for the element lookup.

⚠️ Note: Ensure that org.openqa.selenium.NoSuchElementException is imported. If it isn't listed among the imports, it means the java.util.NoSuchElementException class has been imported instead. This results in a wait configuration that fails to work as intended, as it waits for the wrong exception.

Using locator delegate functions with WebElement

Similar to Selenium's findElement method, locator delegate functions also work with SearchContext (in fact, all of them are defined as extension functions on SearchContext). This means we can issue searches with WebElement as well, not only with WebDriver.

val singleLocators by id("single-locators")
val byClassName by singleLocators.className("by-class-name")

Creating your own locator delegate functions

You can leverage existing locator delegate functions to create your own. For example, the following functions find locators using the data-test attribute.

fun SearchContext.dataTest(
locator: String,
cacheLookup: Boolean = true,
wait: Wait = defaultWait,
syncConfig: WebElement.() -> Boolean = { isDisplayed },
) = xPath("//*[@data-test='$locator']", cacheLookup, wait, syncConfig)

fun SearchContext.dataTests(
locator: String,
cacheLookup: Boolean = true,
wait: Wait = defaultWait,
syncConfig: WebElements.() -> Boolean = { isDisplayed },
) = xPaths("//*[@data-test='$locator']", cacheLookup, wait, syncConfig)

Calling dataTest and dataTest in tests or Page Objects:

 private val sortMenu by dataTest("product-sort-container")
private val products by dataTests("inventory-item")

Define project level configuration

Kolibrium is flexible and has many ways to configure the locator delegate functions. Sometimes you may want to set configuration at a global level and for that you need to use project level configuration.

Project level configuration can be created by implementing an object that extends from AbstractSeleniumProjectConfiguration class:

object SeleniumConfiguration : AbstractSeleniumProjectConfiguration() {
override val elementReadyWhen: (WebElement.() -> Boolean) = { clickable }

override val wait = Wait(
pollingInterval = 100.milliseconds,
timeout = 5.seconds,
message = "Element could not be found",
ignoring = arrayOf(WebDriverException::class),
)
}

Once the configuration is created, we need to also configure metadata for the java.util.ServiceLoader by registering our implementation under META-INF/services folder.

Create resources/META-INF/services/dev.kolibrium.selenium.configuration.AbstractSeleniumProjectConfiguration file with content of

FQN.SeleniumConfiguration

where FQN is the fully-qualified name of the object implementing AbstractSeleniumProjectConfiguration.

At runtime, Kolibrium scans for objects that extend AbstractSeleniumProjectConfiguration and instantiate them, using any configuration values defined in those objects.
With the above configuration, by default clickable is used to determine if an element is a ready for use, and the waiting uses a 100 miliseconds polling interval with 5 seconds of timeout. It will also ignore any exception that implements WebDriverException class.

Use AutoService for generating ServiceLoader metadata

To prevent manual service descriptor maintenance errors, we can leverage on a more robust solution: AutoService generates the metadata for any class annotated with @AutoService, avoiding typos, providing resistance to errors from refactoring, etc.

First, need to register the Kotlin Symbol Processing (KSP) plugin and add the AutoService dependency:

plugins {
id("com.google.devtools.ksp") version "2.0.21-1.0.26"
// other plugins
}

dependencies {
ksp("dev.zacsweers.autoservice:auto-service-ksp:1.2.0")
// other dependencies
}

After that, we just annotate our SeleniumConfiguration object as follows:

@AutoService(AbstractSeleniumProjectConfiguration::class)
object SeleniumConfiguration : AbstractSeleniumProjectConfiguration() {
override val elementReadyWhen: (WebElement.() -> Boolean) = { clickable }

override val wait = Wait(
pollingInterval = 100.milliseconds,
timeout = 5.seconds,
message = "Element could not be found",
ignoring = arrayOf(WebDriverException::class),
)
}

Boost productivity by generating Page Objects with Aqua templates

If you're using Aqua from Jetbrains, you can create a custom template for Kolibrium Page Objects. Just follow the guide and place the following content below to Custom Kotlin Page Object.kt and Custom Kotlin Locator.kt templates.

Content of Custom Kotlin Page Object.kt:

#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")
package ${PACKAGE_NAME}

#end
import dev.kolibrium.selenium.*
import org.openqa.selenium.WebDriver

context(WebDriver)
class $PAGE_NAME {
#foreach($field in $PAGE_FIELDS)
$field
#end
}

Content of Custom Kotlin Locator.kt:

#set ($item = $PAGE_ELEMENT)
#if (${item.locatorType} == "css")
private val ${item.fieldName} by cssSelector("${item.css}")
#elseif (${item.locatorType} == "id")
private val ${item.fieldName} by id("${item.id}")
#elseif (${item.locatorType} == "name")
private val ${item.fieldName} by name("${item.name}")
#elseif (${item.locatorType} == "tag-with-classes")
private val ${item.fieldName} by className("${item.tagWithClasses}")
#elseif (${item.locatorType} == "data")
private val ${item.fieldName} by dataTest("${item.dataAttributeValue}")
#elseif (${item.locatorType} == "text")
private val ${item.fieldName} by xpath("//*[text()='${item.text}']")
#elseif (${item.locatorType} == "aria-label")
private val ${item.fieldName} by cssSelector("[aria-label='${item.ariaLabel}']")
#else
private val ${item.fieldName} by xpath("${item.xpath}")
#end

Click "Apply" button to save changes and close the Settings panel.

From now on, Aqua will generate the properties using Kolibrium's locator delegate functions in your Page Object classes. Simply create a new file by selecting "Selenium Page Object" with "Kotlin" Language and "Custom" Framework.

Then open up the Web Inspector, select an element in the built in browser. Right click in the element in the Locators Evaulator tree and click on "Add Element to Code".