Skip to content

IodeSystems/SeleniumJQuery

Repository files navigation

Selenium jQuery

Working with selenium's selector api is a huge pain in the ass.

What technology likes to fix bad web-centric apis for the most developer centric ergonomics? JQUERY!

The technology from the 90s is still saving us from bad design today!

<dependency>
  <groupId>com.iodesystems.selenium-jquery</groupId>
  <artifactId>selenium-jquery</artifactId>
  <version>2.1.3</version>
</dependency>

Overview

SeleniumJQuery rides on Selenium's RemoteWebDriver api and automagically installs jQuery in websites to make automation and testing a breeze.

Combined with kotlin's DSL utilities, PageObject design goes to it's well deserved grave.

Example

Check out jQueryTest.kt for an example of how to bootstrap the jQuery object using webdrivermanager:

<dependency>
    <groupId>io.github.bonigarcia</groupId>
    <artifactId>webdrivermanager</artifactId>
    <version>5.3.2</version>
    <scope>test</scope>
</dependency>

Creating the driver and jQuery instance:

// Use WebDriverManager to ensure full selenium protocol compatibility for performance
WebDriverManager.chromedriver().setup()
// Silence as much noise as possible
val chromeDriverService: ChromeDriverService = ChromeDriverService.Builder()
    .withSilent(true)
    .build()
chromeDriverService.sendOutputTo(NullOutputStream.NULL_OUTPUT_STREAM)

var options = ChromeOptions()
// Disable attempts to save things to the profile
options.setExperimentalOption(
    "prefs", mapOf(
        "autofill.profile_enabled" to false
    )
)
// Don't show popup notifications
options.addArguments("--disable-notifications")
// Ignore prompts
options.setUnhandledPromptBehaviour(UnexpectedAlertBehaviour.DISMISS)
// Don't bother waiting for the entire dom to load, if you can do your thang already
options.setPageLoadStrategy(PageLoadStrategy.EAGER)
if (headless) {
    options = options
        .addArguments("--headless")
        .addArguments("window-size=1920,1080")
}
val driver = ChromeDriver(chromeDriverService, options)
// Set some sane amount of timeout so tests don't hang on issues
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(5))

Using the jQuery object:

jq.page("http://google.com") {
    find("input[name='q']").sendKeys("hello world", 30)
    find("input[value='Google Search']").first().click()
    find("#result-stats").visible()
}

Nested objects, parent and child traversals:

jq.page("http://some-page.com") {
    frame("#frame-id") {
        find(".hidden-in-frame").parent(".most-recent-parent-containing") {
            find(".child-of-that-parent")
        }
    }
    find(".a"){
        // Hierarchy can be nested as far as you like
        find(".b"){
            // Reroot breaks out of hierarchy            
            reroot("body").exists()
        }
    }
}

More aggressive testing for difficult controls:

// Not direct clicking due to bubbling? FORCE IT!
find(".weird-control").clickForce()
// Controlled text input masking things all weird? Put a delay in it!
find(".text-settling-input").clear().sendKeys("important", delayMilis = 100)

Extension

I'll admit, SeleniumJQuery doesn't have it all, pull requests are welcome, however, it's super easy to extend the IEl object with kotlin's extension functions:

// Some examples for the Mui framework and general time savers:
fun jQuery.IEl.inputLabeled(label: String) =
  find(".MuiInputBase-formControl")
      .contains(label)
      .find(":input:first")
fun jQuery.IEl.buttonLabeled(label: String) = 
    find("button")
    .contains(label)
    .enabled()
fun jQuery.IEl.linkWithText(text: String) = 
    find("a")
        .contains(text)
fun jQuery.IEl.buttonWithIcon(icon: String) = 
    find("svg[data-testid='$icon']")
        .parent("button")

Custom domain objects are also a breeze:

// Page Object DSL
data class Dialog(val el:IEl) : IEl by el {
    fun password(password:String) = inputLabeled("Password").first().sendKeys(password)
    fun submit() = find("button[type=submit]").click()
}
// Bootstrap extension:
fun <T> jQuery.IEl.dialog(
    label: String,
    fn: Dialog.()->T
) {
    val el = find(".dialog-container").contains(label)
    return fn(Dialog(el))
}
// Usage:
...
jq.page{
    dialog("Enter Password"){
        password("super secure!") // DSL
        submit() // DSL
        gone() // EL method: Wait until dialog disappears
    }
}
...