keb-kebish / keb

Keb - Kebish. Selenium wrapper with Page Object Pattern implementation written in Kotlin.
http://kebish.org
MIT License
20 stars 4 forks source link

Keb

Web Testing, Browser Automation, Page Object Pattern and more

http://kebish.org/

Content

  1. Page object pattern
  2. Modules
  3. Navigation
  4. Waiting
  5. Browser Management
  6. Installation
  7. Reporters
  8. Usage
  9. About project
    1. Committers
    2. Contributors
    3. Change log

Page object pattern

Keb is a https://gebish.org inspired Selenium wrapper written in Kotlin that allows you to modularize pages of your web application into logic units represented by Kotlin classes.

class KotlinHomePage : Page() {
    override fun url() = "https://kotlinlang.org"
}

Page elements

To select web element on your page use the following methods.

css(".my-selector") // returns single DOM element by CSS selector
cssList(".my-select") // returns list of all elements found by CSS selector
// following selectors are also available
html("h1")
htmlList("h1")
xpath("/html/body/h1")
xpathList("/html/body/h1")
by(MyCustomBy()) // can be used with custom implementation of org.openqa.selenium.By
byList(MyCustomBy())

For details see https://www.selenium.dev/documentation/en/getting_started_with_webdriver/locating_elements/

Module "nuc-bobril" adds support for Bobril selectors.

Content

In order to lazily access page content use the content delegate.

class KotlinHomePage : Page() {
    override fun url() = "https://kotlinlang.org"

    val header by content { css(".global-header-logo") }
    val headerText by content { header.text }
}

There are multiple options how to handle the selected content:

Page verifier

In order to verify, that you successfully landed on your page, you can use at method.

class KotlinHomePage : Page() {
    override fun url() = "https://kotlinlang.org"
    override fun at() = header

    val header by content { css(".global-header-logo") }
}

Whether every page has to define at verifier can be controlled by configuration property atVerifierRequired. If no verifier is defined and neither is required, no page verification is performed.

Modules

If you want to reuse page content present on multiple pages, you can use modules. Module has an optional constructor parameter scope, which can be used as a search root for all the module's content. To initialize module with use the module method.

class NavMenuModule(scope: WebElement) : Module(scope) {
    val menuItems by content { htmlList("a") }
}

class KotlinHomePage : Page() {
    override fun url() = "https://kotlinlang.org"

    // either initialize the module by passing the module object into the 'module' method
    val menu by content { module(NavMenuModule(css(".nav-links"))) }

    // or initialize the module by calling the 'module' method on a WebElement and pass reference to your module
    val menu2 by content { css(".nav-links").module(::NavMenuModule) }
}

Navigation

at(::MyPage) create instance of page and wait until validator at() is satisfied

to(::MyPage) write url of page into browser and call method at()

at(::MyPage) { methodOnMyPage() } methods can have closure. Inside closure this is created page

at(::MyPage) { clickMainLink() } return result of closure

All the methods defined above can be used with constructor reference to(::MyPage) or with instance of a page to(MyPage()). Please note that constructor reference variant can only be used if the required page has no primary constructor parameters.

Both to and at methods can wait for the transition/verification of page to happen. Wait definition is same as for the content method.

val myPage = to(::MyPage, wait = true)
val myPage = to(::MyPage, wait = 30)
val myPage = to(::MyPage, wait = "quick")
val myPage = to(::MyPage, waitTimeout = 10, waitRetryInterval = 1)
// same for the 'at' methods

Navigating pages using fluent API

val joeContact = to(::MyPage)
    .via { clickOnContacts() }
    .via { findContact("joe") } 
// Same as method above, but explicitly validate, that method is called on correct page
val joeContact = to(::MyPage)
    .via(MyPage::class) { clickOnContacts() }
    .via(ContactsPage::class) { findContact("joe") } 

Waiting

Waiting for some page element to be present, you can use waitFor method.

css(".form-input").click()
val result = waitFor {
    css(".result")
}

Timeout duration and retry interval can be defined either by custom wait preset or directly through waitFor method.

waitFor(preset = "quick") { css(".result") }
waitFor(timeout = 2, retryInterval = 0.1) { css(".result") }

Custom wait presets can be defined using Kotlin DSL in the Configuration. Default values when no custom configuration is defined are 15 seconds for timeout and 1 second for retry interval.

kebConfig {
    waiting {
        timeout = 15
        retryInterval = 1
        "quick" {
            timeout = 2
            retryInterval = 0.1
        }
        "slow" {
            timeout = 60        
        }
    }
}

Browser Management

For creating, providing and quiting browser is responsible BrowserProvider Which can be set in configuration e.g.

kebConfig {
   browserProvider = NewBrowserForEachTestProvider(this) 
}

Prepared providers:

Anybody can write BrowserProvider, but for most use cases prepared implementation will be sufficient. If you need implement for example Provider, which share browser per thread (it would be suitable for parallel execution of tests) You can do it on your own or do not hesitate to ask us, and we will add this feature.

Reporters

Reporters work like plugins. A custom reporter can be configured or prepared Reporter can be used.

e.g - save png and html after each failed test:

    kebConfig  {
        reports {
            reporterDir = File("keb-reports")
            testFailReporters.add(ScreenshotReporter())
            testFailReporters.add(PageSourceReporter())
        }
    }

Installation

Released jar files are available in mavenCentral().

Gradle setup:

repositories {
    mavenCentral()
}

val kebVersion = "1.1"
dependencies {
  testImplementation("org.kebish:keb-core:$kebVersion")
  testImplementation("org.kebish:keb-junit5:$kebVersion")
  testImplementation("org.kebish:keb-bobril:$kebVersion")

  // For downloading correct WebDriver we recommend to use WebDriverManager
  // https://github.com/bonigarcia/webdrivermanager  
  testImplementation("io.github.bonigarcia:webdrivermanager:<insert_webdrivermanager_version>")

}

If you use Keb in tests you will probably use configuration testImplementation instead of implementation

Full usage - keb + JUnit

package org.kebish.usage

import io.github.bonigarcia.wdm.WebDriverManager
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import org.kebish.core.config.kebConfig
import org.kebish.core.module.Module
import org.kebish.core.page.Page
import org.kebish.junit5.KebTest
import org.openqa.selenium.WebElement
import org.openqa.selenium.firefox.FirefoxDriver

class KotlinSiteKebTest : KebTest(kebConfig {
    WebDriverManager.firefoxdriver().setup()
    driver = { FirefoxDriver() }
    baseUrl = "https://kotlinlang.org"
}) {

    @Test
    fun `testing kotlin lang page`() {
        // given
        val homePage = to(::KotlinHomePage)

        // when
        val title = homePage.header

        // then
        Assertions.assertEquals("Kotlin", title.text)
        Assertions.assertTrue(homePage.licensedUnderApacheLicense())

        // when
        val docsPage = homePage.openDocumentation()

        // then
        Assertions.assertEquals("Kotlin docs", docsPage.title.text)
    }

}

class KotlinHomePage : Page() {
    override fun url() = "/"
    override fun at() = header

    val header by content { css(".global-header-logo") }
    val menu by content { module(NavMenuModule(css(".nav-links"))) }
    val footer by content { module(FooterModule(html("footer"))) }

    fun openDocumentation(): KotlinDocumentationPage {
        menu.menuItems.first { it.text.contains("Docs", ignoreCase = true) }.click()
        return at(::KotlinDocumentationPage)
    }

    fun licensedUnderApacheLicense() = footer.licenseNotice.text.contains("apache", ignoreCase = true)

}

class KotlinDocumentationPage : Page() {
    override fun url() = "/docs/home.html"
    override fun at() = css("ul.toc")

    val title by content { html("h1") }

}

class NavMenuModule(scope: WebElement) : Module(scope) {
    val menuItems by content { htmlList("a") }
}

class FooterModule(scope: WebElement) : Module(scope) {
    val licenseNotice by content { css(".terms-foundation_link[href*=LICENSE]") }
    val sponsor by content { css(".terms-sponsor") }
}

For full usage example please refer to /keb-core/src/test/kotlin/org/kebish/usage.

About project

Any questions, issues, consultation ... ?

Do not hesitate to contact us at info@kebish.org

Committers

Contributors

Change log