Skip to main content

Getting Started

Write your first test with locator delegates

In this tutorial, we’ll cover the basics of Kolibrium and explore several project configurations to help you get started quickly.

We'll be writing tests for the login functionality on the Sauce Labs demo e-commerce website: https://www.saucedemo.com.

All the examples can be found in the kolibrium-demo project.

1. 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()
}

View the full file here

2. Use locator delegate functions from the Selenium module

Create a test class, such as a JUnit test class, and use the locator delegate functions from the selenium module to locate elements:

@Test  
fun loginTest() {
with(driver) {
val username by name("user-name")
val password by id("password")
val button by name("login-button")

username.sendKeys("standard_user")
password.sendKeys("secret_sauce")
button.click()

val shoppingCart by className("shopping_cart_link")

shoppingCart.isDisplayed shouldBe true
}
}

View the full file here

Note: shouldBe is an assertion function from kotest, but you may use any assertion library you prefer.

Kolibrium utilizes the Delegation pattern, natively supported in Kotlin, to locate elements lazily with locator strategies.

For instance, the following code locates the WebElement with the "user-name" attribute when it’s accessed (in this case, when the sendKeys command is issued):

val username by name("user-name")  
username.sendKeys("standard_user")

3. Implement Page Object models

To introduce a layer of abstraction, let's create a Page Object class for the login page:

class LoginPage(driver: WebDriver) {  
private val username: WebElement by driver.name("user-name")

private val password: WebElement by driver.idOrName("password")

private val button: WebElement by driver.name("login-button")

fun login(username: String, password: String) {
this.username.sendKeys(username)
this.password.sendKeys(password)
button.click()
}
}

Similarly, for the inventory page:

class InventoryPage(driver: WebDriver) { 
private val shoppingCart by driver.className("shopping_cart_link")

fun isShoppingCartDisplayed() = shoppingCart.isDisplayed
}

View the full files here

Now, let's utilize these Page Objects in our test:

@Test  
fun loginTest() {
LoginPage(driver).login(
username = "standard_user",
password = "secret_sauce"
)

InventoryPage(driver).isShoppingCartDisplayed() shouldBe true
}

View the full file here

4. Use Context Receivers to inject the driver instance into the Page Objects

Context receivers offer a streamlined way to provide context (such as a WebDriver instance) to functions without passing it explicitly as an argument. In this example, we'll use context receivers to simplify access to the WebDriver instance in our Page Objects.

⚠️ Note: Context receivers, also known as context parameters, are still an experimental feature in Kotlin and are not enabled by default.

To enable them, add the following configuration to your build.gradle.kts file:

tasks.withType<KotlinCompile> { 
compilerOptions.freeCompilerArgs = listOf(
"-Xcontext-receivers",
)
}

View the full file here

After reloading the Gradle configuration, we can add context(WebDriver) to our Page Objects and remove the constructor and driver instance when calling the delegate functions:

context(WebDriver)  
class LoginPage {
private val username: WebElement by name("user-name")

private val password: WebElement by idOrName("password")

private val button: WebElement by name("login-button")

fun login(username: String, password: String) {
this.username.sendKeys(username)
this.password.sendKeys(password)
button.click()
}
}

context(WebDriver)
class InventoryPage {
private val shoppingCart by className("shopping_cart_link")

fun isShoppingCartDisplayed() = shoppingCart.isDisplayed
}

View the full files here

To make the test compile after introducing context receivers, we need to provide a driver context by using the with scope function:

@Test  
fun loginTest() {
with(driver) {
LoginPage().login(
username = "standard_user",
password = "secret_sauce"
)

InventoryPage().isShoppingCartDisplayed() shouldBe true
}
}

View the full file here

Generate repository classes for locators with Kolibrium code generation

In this section, we will begin using the ksp module to generate part of our Page Objects.

1. Add KSP module to the build file

First, update your Gradle project build file by adding the following configuration:

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

dependencies {
implementation("dev.kolibrium:kolibrium-annotations:0.4.0")
implementation("dev.kolibrium:kolibrium-selenium:0.4.0")
ksp("dev.kolibrium:kolibrium-ksp:0.4.0")
ksp("dev.zacsweers.autoservice:auto-service-ksp:1.2.0")
// other dependencies
}

View the full file here

2. Store locators in a single file

Create a file named Locators.kt and add the following enum classes to the dev.kolibrium.demo.ksp._01.locators package:

package dev.kolibrium.demo.ksp._01.locators

@Locators
enum class LoginPageLocators {
@Id("user-name")
username,

password,

@Name("login-button")
loginButton
}

@Locators
enum class InventoryPageLocators {
@ClassName("shopping_cart_link")
shoppingCart
}

View the full file here

3. Generate files

Now, build the project and navigate to the build/generated/ksp/main/kotlin/dev/kolibrium/demo/ksp/_01/locators/generated/ directory. You will notice that two files have been created:

LoginPageLocators.kt:

package dev.kolibrium.demo.ksp._01.locators.generated  

context(WebDriver)
public class LoginPageLocators {
public val username: WebElement by id("user-name")

public val password: WebElement by idOrName("password")

public val loginButton: WebElement by name("login-button")
}

InventoryPageLocators:

package dev.kolibrium.demo.ksp._01.locators.generated  

context(WebDriver)
public class InventoryPageLocators {
public val shoppingCart: WebElement by className("shopping_cart_link")
}

4. Create Page Objects and use locators from the generated files

Now, let's create Page Object classes that incorporate a locators property.

LoginPage.kt:

context(WebDriver)  
class LoginPage {
private val locators = LoginPageLocators()

fun login(username: String = "standard_user", password: String = "secret_sauce") = with(locators) {
this.username.sendKeys(username)
this.password.sendKeys(password)
loginButton.submit()
}
}

InventoryPage:

context(WebDriver)  
class InventoryPage {
private val locators = InventoryPageLocators()

fun isShoppingCartDisplayed() = locators.shoppingCart.isDisplayed
}

View the full files here

Next, update the test accordingly:

@Test  
fun loginTest() {
with(driver) {
LoginPage().login()

InventoryPage().isShoppingCartDisplayed() shouldBe true
}
}

View the full file here

Use DSL functions for creating and configuring WebDriver instances

In this section, we will leverage DSLs to create WebDriver instances in our tests.

1. Add the DSL module to the build file

As always, begin by updating your Gradle configuration:

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

View the full file here

2. Create driver instance using the DSL module

Let's create a Chrome driver instance with the following configuration:

@BeforeEach  
fun setUp() {
driver = chromeDriver {
driverService {
logFile = "chrome.log"
appendLog = true
readableTimestamp = true
}
options {
arguments {
+incognito
windowSize {
width = 1920
height = 1080
}
}
}
}.apply {
get("https://www.saucedemo.com")
}
}

View the full file here

While it's intuitive to understand what the chromeDriver function call does, let's break it down further:

  • It creates a DriverService (specifically, a ChromeDriverService) with appendLog and readableTimestamp enabled.
  • It sets up an Options object (in this case, a ChromeOptions) with incognito mode enabled and a window size of 1920 x 1080 pixels.
  • Finally, it utilizes both objects to instantiate the actual driver.

Inject driver instances into your tests with the JUnit module

It's time to unlock the full potential of Kolibrium by enabling it to inject drivers into your JUnit 5 tests.

1. Add the JUnit module to the build file

As you may expect, we begin with the Gradle configurations:

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

dependencies {
implementation("dev.kolibrium:kolibrium-junit:0.4.0")
implementation("dev.kolibrium:kolibrium-selenium:0.4.0")
testImplementation("io.kotest:kotest-assertions-core-jvm:5.9.1")
testImplementation("org.junit.jupiter:junit-jupiter-api:5.11.3")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.11.3")
}

View the full file here

2. Annotate your test with @Kolibrium

Next, let's enhance the test class by adding context(WebDriver) and the @Kolibrium annotation at the top. This allows us to simplify the test code:

context(WebDriver)  
@Kolibrium
class JUnitTest {
@BeforeEach
fun setUp() {
this@WebDriver["https://www.saucedemo.com"]
}

@Test
fun loginTest() {
LoginPage().login(
username = "standard_user",
password = "secret_sauce"
)

InventoryPage().isShoppingCartDisplayed() shouldBe true
}
}

View the full file here

3. Customize the injected driver

You can tailor the injected driver by creating a custom Kolibrium configuration.

First, add the AutoService dependency:

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

View the full file here

Next, create a project level configuration by extending AbstractJUnitProjectConfiguration and overriding the baseUrl and chromeDriver properties:

@AutoService(AbstractJUnitProjectConfiguration::class)
object JUnitConfiguration : AbstractJUnitProjectConfiguration() {
override val baseUrl = "https://www.saucedemo.com"

override val chromeDriver = {
chromeDriver {
options {
arguments {
+incognito
+start_maximized
}
experimentalOptions {
excludeSwitches {
+enable_automation
}
localState {
browserEnabledLabsExperiments {
+same_site_by_default_cookies
+cookies_without_same_site_must_be_secure
}
}
}
}
}
}
}

View the full file here

After that, remove the @BeforeEach block from the test:

context(WebDriver)  
@Kolibrium
class JUnitTest {
@Test
fun loginTest() {
LoginPage().login(
username = "standard_user",
password = "secret_sauce"
)

InventoryPage().isShoppingCartDisplayed() shouldBe true
}
}

View the full file here

With this configuration, the @Kolibrium annotation will instruct JUnit to inject a ChromeDriver with the specified experimental options into the tests. It will also navigate to https://www.saucedemo.com before executing the tests.