Skip to main content

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 inventoryItem by driver.classNames("inventory_item")

init {
check(headerContainer.isDisplayed) {
"This is not the Inventory Page, current page is: " + driver.currentUrl
}
}

fun getProducts() = inventoryItem.map { webElement ->
Product(webElement)
}

fun getProduct(condition: (Product) -> Boolean): Product {
return getProducts()
.asSequence()
.filter(condition) // Filter by product name or price
.first()
}
}

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
}
}
note

For simplicity, BasePage and BaseComponent classes are skipped from the implementation.

The className calls in the Product class are invoked on the root WebElement. Similar to Selenium's findElement method, locator delegate functions can also perform searches on WebElements, as they are defined as extension functions on SearchContext, the superclass of both WebDriver and WebElement.

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.