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
- Gradle
- Maven
dependencies {
implementation("dev.kolibrium:kolibrium-selenium:<version>")
}
tasks.test {
useJUnitPlatform()
}
<dependency>
<groupId>dev.kolibrium</groupId>
<artifactId>kolibrium-selenium</artifactId>
<version><!-- latest version --></version>
</dependency>
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- whentrue(default), the element is looked up once and cached. Set tofalsefor dynamic elements.waitConfig- controls polling interval and timeout. Defaults toWaitConfig.Default(200ms polling, 10s timeout).readyWhen- predicate that determines when the element is ready for interaction. Defaults toisDisplayed.
// 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:
| Preset | Polling | Timeout |
|---|---|---|
Default | 200ms | 10s |
Quick | 100ms | 2s |
Patient | 500ms | 30s |
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 interactionLoggerDecorator- logs element interactionsElementStateCacheDecorator- 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"
}
}
}