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
}
}
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 WebElement
s, 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.