Skip to main content

Selenium

This guide walks through Kolibrium's Selenium module from first principles to a fully structured test suite. Each section builds on the previous one.

We'll be writing tests against the SauceDemo site: https://www.saucedemo.com.

1. Add the dependency

dependencies {
implementation("dev.kolibrium:kolibrium-selenium:<version>")
}

tasks.test {
useJUnitPlatform()
}

2. Locator delegates

Kolibrium uses Kotlin's delegation pattern to locate elements lazily. The element is found when you first access the property, not when you declare it.

Available locator functions: id, name, className, cssSelector, xpath, linkText, partialLinkText, tagName, idOrName, dataTest, dataTestId, dataQa, and more. Each has a plural variant (e.g. cssSelectors) that returns a list of elements.

Every locator accepts optional parameters:

  • cacheLookup - when true (default), the element is looked up once and cached. Set to false for dynamic elements.
  • waitConfig - controls polling interval and timeout. Defaults to WaitConfig.Default (200ms polling, 10s timeout).
  • readyWhen - predicate that determines when the element is ready for interaction. Defaults to isDisplayed.
// Single element - cached by default
private val usernameInput by name("user-name")

// With a custom wait
private val slowElement by id("lazy-load", waitConfig = WaitConfig.Patient)

// Uncached - fresh lookup every access
private val dynamicButton by cssSelector(".dynamic-btn", cacheLookup = false)

// Multiple elements
private val items by dataTests("inventory-item")

3. Define a Site

A Site is the central configuration object for your application under test. It holds the base URL, default wait behavior, element readiness conditions, cookies, and decorators.

object SauceDemo : Site(baseUrl = "https://www.saucedemo.com") {
override val elementReadyCondition: WebElement.() -> Boolean = { isClickable }
override val waitConfig: WaitConfig = WaitConfig.Quick
}

WaitConfig ships with three presets:

PresetPollingTimeout
Default200ms10s
Quick100ms2s
Patient500ms30s

You can also derive custom configs: WaitConfig.Default.copy(timeout = 5.seconds).

4. Define Page Objects

Page objects extend Page<S> where S is your Site type. Locator delegates are declared as properties - no constructor parameters, no driver passing. The driver is resolved from Kolibrium's context automatically.

class LoginPage : Page<SauceDemo>() {
private val usernameInput by name("user-name")
private val passwordInput by idOrName("password")
private val loginButton by name("login-button")

fun login(user: User = User.Standard) {
usernameInput.sendKeys(user.username)
passwordInput.sendKeys(user.password)
loginButton.click()
}
}

Pages can declare a path for automatic navigation. When a page defines a non-empty path, calling on(::PageClass) will automatically navigate to baseUrl + path:

class InventoryPage : Page<SauceDemo>() {
override val path = "inventory.html"

private val title by className("title")

fun titleText(): String = title.text
}

5. Write a test with seleniumTest

seleniumTest is the test harness. It manages the full lifecycle: creates a driver (defaults to ChromeDriver), navigates to the site's base URL, applies cookies, runs your test, and tears down the session.

@Test
fun `login should succeed with default credentials`() = seleniumTest(
site = SauceDemo
) {
on(::LoginPage) {
login()
}
}

Inside the seleniumTest block you get a SiteScope receiver. Use on(::PageClass) { ... } to navigate to a page (if it has a path) and run actions on it. Chain pages with on on the returned PageScope:

on(::LoginPage) {
login()
}.on(::InventoryPage) {
titleText() shouldBe "Products"
}

6. Configure the driver with DSLs

Instead of the default ChromeDriver, use Kolibrium's DSL to configure browser options, arguments, and driver services declaratively:

private val sauceDemoDriver: DriverFactory = {
chromeDriver {
options {
arguments {
+disable_search_engine_choice_screen
+incognito
windowSize {
width = 1920
height = 1080
}
}
}
}
}

Kolibrium also provides predefined factories for common setups: headlessChrome, incognitoChrome, headlessFirefox, incognitoFirefox, headlessEdge, inPrivateEdge.

@Test
fun `headless test`() = seleniumTest(
site = SauceDemo,
driverFactory = headlessChrome,
) {
on(::LoginPage) {
login()
}
}

7. Use the setUp/tearDown lifecycle

seleniumTest supports a three-phase lifecycle: setUp runs before the browser session is created (useful for preparing test data), block is the test body, and tearDown runs after the test even if it fails.

@Test
fun `test with setup and teardown`() = seleniumTest(
site = SauceDemo,
setUp = {
// Prepare data before the browser opens
"standard_user"
},
tearDown = { username ->
// Clean up after the test
},
) { username ->
on(::LoginPage) {
login(User.valueOf(username))
}
}

8. Add decorators for cross-cutting concerns

Decorators compose transparently over element interactions. Define them on your Site and they apply to every element lookup in every page:

object SauceDemo : Site(baseUrl = "https://www.saucedemo.com") {
override val elementReadyCondition: WebElement.() -> Boolean = { isClickable }
override val waitConfig: WaitConfig = WaitConfig.Quick

override val decorators: List<AbstractDecorator> = listOf(
HighlighterDecorator(
style = BorderStyle.Dashed,
color = Color.Green,
),
SlowMotionDecorator(wait = 300.milliseconds),
LoggerDecorator(),
)
}

Built-in decorators:

  • HighlighterDecorator - visually highlights elements before interaction (useful for demos and debugging)
  • SlowMotionDecorator - adds a delay before each interaction
  • LoggerDecorator - logs element interactions
  • ElementStateCacheDecorator - caches element state to reduce redundant queries

9. Manage cookies declaratively

Static cookies can be declared on the Site. Kolibrium applies them at the right time - after establishing origin but before your test flow runs:

object MySite : Site(baseUrl = "https://example.com") {
override val cookies = setOf(
cookie(name = "locale", value = "en-US"),
cookie(name = "ab_test", value = "variant-b"),
)
}

For dynamic, session-specific cookies, such as injecting an authentication token to bypass login, use the cookies DSL inside seleniumTest:

@Test
fun `user should be able to add products to cart after bypassing login`() = seleniumTest(
site = SauceDemo
) {
cookies {
add(name = "session-username", value = "standard_user")
}

on(::InventoryPage) {
// Now on inventory page without manual login
titleText() shouldBe "Products"
}
}

Putting it all together

// Site definition
object SauceDemo : Site(baseUrl = "https://www.saucedemo.com") {
override val elementReadyCondition: WebElement.() -> Boolean = { isClickable }
override val waitConfig: WaitConfig = WaitConfig.Quick
override val decorators: List<AbstractDecorator> = listOf(
HighlighterDecorator(style = BorderStyle.Dashed, color = Color.Green),
LoggerDecorator(),
)
}

// Page objects
class LoginPage : Page<SauceDemo>() {
private val usernameInput by name("user-name")
private val passwordInput by idOrName("password")
private val loginButton by name("login-button")

fun login(user: User = User.Standard) {
usernameInput.sendKeys(user.username)
passwordInput.sendKeys(user.password)
loginButton.click()
}
}

class InventoryPage : Page<SauceDemo>() {
override val path = "inventory.html"
private val title by className("title")

fun titleText(): String = title.text
}

// Test
class SauceDemoTest {
@Test
fun `bypassing login with cookies`() = seleniumTest(
site = SauceDemo,
driverFactory = headlessChrome,
) {
cookies {
add(name = "session-username", value = "standard_user")
}

on(::InventoryPage) {
titleText() shouldBe "Products"
}
}
}