Using locator delegate functions for Page Component Objects
As noted at Selenium's Page object models website, we can create Page Component Objects to represent distinct sections of a page. These component objects can be incorporated into Page Objects by referencing a root element that contains the entire component.
Let's implement the example from the linked Selenium page but in Kotlin.
class ProductsPage(driver: WebDriver) {
private val headerContainer by driver.className("header_container")
private val inventoryItems by driver.classNames("inventory_item")
init {
check(headerContainer.isDisplayed) {
"This is not the Inventory Page, current page is: " + driver.currentUrl
}
}
fun getProducts() = inventoryItems.map { Product(it) }
fun getProduct(predicate: (Product) -> Boolean): Product {
return getProducts().first(predicate) // Filter by product name or price
}
}
The Product component object takes a WebElement as a root and initiates searches starting from there.
class Product(root: WebElement) {
private val name by root.className("inventory_item_name")
private val price by root.className("inventory_item_price")
fun getName() = name.text
fun getPrice(): BigDecimal {
return BigDecimal(price.text.replace("$", "")).setScale(
2,
RoundingMode.UNNECESSARY
) // Sanitation and formatting
}
}
For simplicity, BasePage and BaseComponent classes are skipped from the implementation.
The className calls in the Product class are invoked on the root WebElement. Like Selenium's findElement, Kolibrium's locator delegates are extension functions on SearchContext (the supertype of both WebDriver and WebElement), so you can call them on either a WebDriver or any WebElement to start a scoped search. Single-element delegates cache the located element by default, while multi-element delegates always perform a fresh lookup.
To make the tests work, we need to log in to the application first, which is handled by the LoginPage.
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()
}
}
Now that we have the page objects implemented, let's write the tests.
class ProductsTest {
private lateinit var driver: WebDriver
@BeforeEach
fun setUp() {
driver = ChromeDriver().apply {
get("https://www.saucedemo.com")
LoginPage(this).login(
username = "standard_user",
password = "secret_sauce"
)
}
}
@AfterEach
fun tearDown() {
driver.quit()
}
@Test
fun `there should be 6 products in total`() {
with(ProductsPage(driver)) {
val products = getProducts()
products.size shouldBe 6
}
}
@Test
fun `prices should be correctly displayed`() {
with(ProductsPage(driver)) {
val backpack = getProduct { it.getName().contains("Backpack") }
val bikeLight = getProduct { it.getName().contains("Bike Light") }
backpack.getPrice() shouldBe BigDecimal("29.99")
bikeLight.getPrice() shouldBe BigDecimal("9.99")
}
}
}
The page and its components are each represented by dedicated objects, with functions limited to the specific services they provide. This approach aligns with real-world object-oriented programming principles.