All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.iodesystems.selenium.jQuery.kt Maven / Gradle / Ivy

Go to download

SeleniumJQuery is a tool for writing more effective Selenium tests with the power of jQuery selectors and Kotlin's expressiveness

There is a newer version: 3.0.2
Show newest version
package com.iodesystems.selenium

import org.openqa.selenium.*
import org.openqa.selenium.interactions.Actions
import org.openqa.selenium.interactions.MoveTargetOutOfBoundsException
import org.openqa.selenium.remote.RemoteWebDriver
import org.openqa.selenium.remote.RemoteWebElement
import org.openqa.selenium.support.ui.FluentWait
import org.openqa.selenium.support.ui.Select
import java.io.ByteArrayOutputStream
import java.time.Duration


data class jQuery(
    val driver: RemoteWebDriver,
    val timeout: Duration = Duration.ofSeconds(5),
    val logQueriesToBrowser: Boolean = false,
    val logQueriesToStdout: Boolean = false
) {
    class RetryException(
        message: String, cause: Throwable? = null
    ) : Exception(message, cause)

    @Suppress("unused")
    data class Either(
        private val el: IEl, private val left: Boolean
    ) : IEl by el {
        fun  left(fn: IEl.() -> T): T? {
            return if (left) fn(el)
            else null
        }

        fun  right(fn: IEl.() -> T): T? {
            return if (left) null
            else fn(el)
        }
    }

    interface IEl {
        fun actions(): Actions
        fun atLeast(): Int?
        fun atMost(): Int?
        fun click(): IEl
        fun clear(): IEl
        fun blur(): IEl
        fun text(): String
        fun sendKeys(text: CharSequence, rateMillis: Int? = null): IEl
        fun contains(text: String): IEl
        fun value(): String
        fun gone()
        fun exists()
        fun visible(): IEl
        fun ensureEnabled(): IEl
        fun ensureDisabled(): IEl
        fun maybeExists(): Boolean
        fun  ifExists(fn: IEl.() -> T): T?
        fun element(): RemoteWebElement
        fun elementsUnChecked(): List
        fun elements(): List
        fun renderScript(): String
        fun renderSelector(): String
        fun scrollIntoView(): IEl

        // Generic child finder
        fun findAll(childSelector: String, atLeast: Int? = 1, atMost: Int? = null): IEl
        fun findAll(childSelector: List, atLeast: Int? = 1, atMost: Int? = null): IEl

        // Single child finders
        fun  find(childSelector: List, fn: IEl.() -> T): T
        fun  find(childSelector: String, fn: IEl.() -> T): T
        fun find(childSelector: List): IEl
        fun find(childSelector: String): IEl

        // Parent finders
        fun parent(parentSelector: String, atLeast: Int? = 1, atMost: Int? = 1): IEl
        fun parent(parentSelector: List, atLeast: Int? = 1, atMost: Int? = 1): IEl


        fun either(left: String, right: String): Either
        fun either(left: IEl, right: IEl): Either
        fun first(first: IEl, vararg rest: IEl): IEl?
        fun escape(string: String): String
        fun enabled(): IEl
        fun first(): IEl
        fun selectValue(value: String): IEl
        fun last(): IEl
        fun reroot(selector: String? = null): IEl
        fun  withFrame(selector: String, fn: IEl.() -> T): T
        fun waitUntil(message: String = "condition to be true", fn: IEl.() -> Boolean): IEl
        fun  waitFor(message: String = "expression to be nonnull", fn: IEl.() -> T?): T
        fun js(script: String, vararg args: Any): Any
    }

    data class El(
        val jq: jQuery,
        val selector: List,
        val atLeast: Int? = null,
        val atMost: Int? = null,
    ) : IEl {

        private fun  safely(
            el: RemoteWebElement,
            fn: RemoteWebElement.() -> T
        ): T {
            try {
                return fn(el)
            } catch (e: ElementNotInteractableException) {
                throw RetryException("Element not interactable", e)
            } catch (e: StaleElementReferenceException) {
                throw RetryException("Stale element reference", e)
            } catch (e: MoveTargetOutOfBoundsException) {
                try {
                    Actions(jq.driver as WebDriver).moveToElement(el).perform()
                } catch (e: MoveTargetOutOfBoundsException) {
                    jq.driver.executeScript("arguments[0].scrollIntoView()", el)
                    throw RetryException("Element cannot be scrolled to", e)
                }
                throw RetryException("Element requires scrolling to", e)
            }
        }

        override fun actions(): Actions {
            return Actions(jq.driver)
        }

        override fun atLeast(): Int? {
            return atLeast
        }

        override fun atMost(): Int? {
            return atMost
        }

        override fun click(): IEl {
            jq.waitFor("could not click") {
                val element = element()
                try {
                    try {
                        safely(element) {
                            click()
                        }
                    } catch (e: ElementClickInterceptedException) {
                        jq.driver.executeScript("arguments[0].click()", element)
                    }
                } catch (e: StaleElementReferenceException) {
                    throw RetryException("Stale element", e)
                }
            }
            return this
        }

        override fun clear(): IEl {
            jq.waitFor("could not clear element") {
                safely(element()) {
                    clear()
                    val length = getAttribute("value").length
                    if (length > 0) sendKeys((0..(getAttribute("value").length)).joinToString("") {
                        Keys.BACK_SPACE
                    })
                }
            }
            return this
        }

        override fun blur(): IEl {
            actions().sendKeys(Keys.TAB).perform()
            return this
        }

        override fun text(): String {
            return element().text
        }

        override fun sendKeys(text: CharSequence, rateMillis: Int?): IEl {
            if (rateMillis == null) {
                jq.waitFor("Could not send keys") {
                    safely(element()) {
                        sendKeys(text)
                    }
                }
            } else {
                val script = renderScript()
                jq.waitFor(
                    "Could not send keys",
                    retry = Duration.ofMillis(text.length * rateMillis.toLong()),
                    timeOut = Duration.ofMillis(text.length * rateMillis.toLong() * 100),
                ) {
                    val elements = elementsUnChecked()
                    if (elements.size != 1) {
                        throw RetryException("Elements for $script not 1, but ${elements.size}")
                    }
                    safely(elements[0]) {
                        text.fold(
                            Actions(jq.driver as WebDriver)
                                .click(this)
                        ) { a, c ->
                            a.pause(Duration.ofMillis(rateMillis.toLong())).sendKeys(c.toString())
                        }.perform()
                    }
                }
            }
            return this
        }

        override fun contains(text: String): IEl {
            val textEncoded = jq.escape(text)
            return copy(selector = selector.map { "$it:contains($textEncoded)" })
        }

        override fun value(): String {
            return element().getAttribute("value")
        }

        override fun gone() {
            copy(atMost = 0, atLeast = null).elements()
        }

        override fun exists() {
            element()
        }

        override fun ensureEnabled(): El {
            copy(
                selector = selector(":enabled")
            ).element()
            return this
        }

        override fun ensureDisabled(): El {
            copy(
                selector = selector(":disabled")
            ).element()
            return this
        }

        fun selector(extension: String? = null): List {
            return if (extension != null)
                selector.take(selector.size - 1) +
                        (selector.last() + extension)
            else
                selector
        }

        override fun visible(): IEl {
            return copy(
                selector = selector(":visible")
            )
        }

        override fun maybeExists(): Boolean {
            return elementsUnChecked().isNotEmpty()
        }

        override fun  ifExists(fn: IEl.() -> T): T? {
            return if (maybeExists()) return null
            else fn(this)
        }

        override fun element(): RemoteWebElement {
            return elements().first()
        }

        override fun elementsUnChecked(): List {
            return jq.search(
                listOf(this.copy(atLeast = null, atMost = null))
            ).firstOrNull() ?: emptyList()
        }

        override fun elements(): List {
            return jq.search(listOf(this)).first()!!
        }

        override fun renderSelector(): String {
            return selector.joinToString(", ")
        }

        override fun js(script: String, vararg args: Any): Any {
            return jq.driver.executeScript(script, *args)
        }

        override fun scrollIntoView(): IEl {
            jq.driver.executeScript(
                "arguments[0].scrollIntoView({block:'end', inline:'end', behavior:'instant'})",
                element()
            )
            return this
        }

        override fun renderScript(): String {
            return """
                jQuery(${escape(renderSelector())})
            """.trimIndent()
        }

        override fun findAll(childSelector: List, atLeast: Int?, atMost: Int?): IEl {
            return copy(
                selector = selector.map { parent ->
                    childSelector.map { child ->
                        "$parent $child".trim()
                    }
                }.flatten(),
                atLeast = atLeast,
                atMost = atMost,
            )
        }

        override fun findAll(childSelector: String, atLeast: Int?, atMost: Int?): IEl {
            return findAll(listOf(childSelector), atLeast, atMost)
        }

        override fun  find(childSelector: List, fn: IEl.() -> T): T {
            return find(childSelector).run(fn)
        }

        override fun find(childSelector: List): IEl {
            return findAll(childSelector, 1, 1)
        }

        override fun find(childSelector: String): IEl {
            return find(listOf(childSelector))
        }

        override fun  find(childSelector: String, fn: IEl.() -> T): T {
            return fn(find(childSelector))
        }

        override fun parent(parentSelector: String, atLeast: Int?, atMost: Int?): IEl {
            return parent(listOf(parentSelector), atLeast, atMost)
        }

        override fun parent(parentSelector: List, atLeast: Int?, atMost: Int?): IEl {
            return copy(
                selector = parentSelector.map { parent ->
                    selector.map { child ->
                        "$parent:has(${child}):last"
                    }
                }.flatten(),
                atLeast = atLeast,
                atMost = atMost
            )
        }

        override fun enabled(): IEl {
            return copy(selector = selector.map { "$it:enabled" })
        }

        override fun reroot(selector: String?): IEl {
            return copy(
                selector = if (selector != null) listOf(selector) else emptyList(),
                atLeast = 1,
                atMost = null
            )
        }

        override fun escape(string: String): String {
            return jq.escape(string)
        }

        override fun first(first: IEl, vararg rest: IEl): IEl? {
            val all = listOf(first) + rest.toList()
            val results = jq.search(all)
            val found = results.find { it != null }
            if (found != null) {
                return all[results.indexOf(found)]
            }
            return null
        }

        override fun first(): IEl {
            return copy(selector = selector.map { "$it:first" }, atLeast = 1, atMost = null)
        }

        override fun last(): IEl {
            return copy(selector = selector.map { "$it:last" }, atLeast = 1, atMost = null)
        }

        override fun selectValue(value: String): IEl {
            Select(element()).selectByValue(value)
            return this
        }

        override fun  withFrame(selector: String, fn: IEl.() -> T): T {
            val dr = (jq.driver as WebDriver)
            val frame = find(selector).element()
            dr.switchTo().frame(frame)
            val result = fn(copy(selector = listOf("")))
            dr.switchTo().defaultContent()
            return result
        }

        override fun waitUntil(message: String, fn: IEl.() -> Boolean): IEl {
            val msg = "Timeout waiting for $message on ${renderScript()}"
            jq.waitFor(msg) {
                if (!fn(this)) {
                    throw RetryException(msg)
                }
            }
            return this
        }

        override fun  waitFor(message: String, fn: IEl.() -> T?): T {
            return jq.waitFor(message) {
                fn(this) ?: throw RetryException("Timeout waiting for $message on ${renderScript()}")
            }
        }


        override fun either(
            left: String, right: String
        ): Either {
            return either(find(left), find(right))
        }

        override fun either(left: IEl, right: IEl): Either {
            return jq.waitFor("Either left or right not found, or both found") {
                val results = jq.search(listOf(left, right))
                val leftElements = results[0] ?: emptyList()
                val rightElements = results[1] ?: emptyList()
                if (leftElements.size == 1) {
                    Either(left, left = true)
                } else if (rightElements.size == 1) {
                    Either(right, left = false)
                } else {
                    val script = """
                        left: ${left.renderScript()}
                        right: ${right.renderScript()}
                        
                    """.trimIndent()
                    if (leftElements.isEmpty()) {
                        throw RetryException(
                            "Either found no elements:\n${script}"
                        )
                    } else {
                        throw RetryException(
                            "Either found both elements:\n${script}"
                        )
                    }

                }
            }
        }
    }

    fun install() {
        mapOf(
            "jQuery" to "jquery-3.6.3.min.js",
            "SeleniumJQuery" to "selenium-jquery-helpers.js"
        ).map { entry ->
            if (logQueriesToStdout) {
                println("Installing ${entry.key} from ${entry.value}")
            }
            if (driver.executeScript("return typeof window.${entry.key}") == "undefined") {
                val jQueryStream = javaClass.getResourceAsStream("/${entry.value}")
                val jQueryStreamBuffer = ByteArrayOutputStream()
                jQueryStream?.transferTo(jQueryStreamBuffer)
                val jQueryContent = jQueryStreamBuffer.toString()
                driver.executeScript(jQueryContent)
            }
        }
    }

    fun escape(string: String): String {
        return '"' + string.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r") + '"'
    }

    fun search(els: List): List?> {
        return waitFor("Search query to return stable results") {
            try {
                if (logQueriesToStdout) {
                    println(els.joinToString("; ") { it.renderScript() } + ";")
                }
                @Suppress("UNCHECKED_CAST")
                driver.executeAsyncScript(
                    "SeleniumJQuery.search(arguments[0],arguments[1],arguments[2],arguments[3],arguments[4])",
                    logQueriesToBrowser,
                    els.map { it.renderSelector() },
                    els.map { it.atLeast() },
                    els.map { it.atMost() }
                ) as List?>
            } catch (e: StaleElementReferenceException) {
                throw RetryException("Stale element returned", e)
            } catch (e: ScriptTimeoutException) {
                throw RetryException("Script timeout", e)
            }
        }
    }

    fun  waitFor(
        failureMessage: String,
        retry: Duration? = null,
        timeOut: Duration? = null,
        t: () -> T
    ): T {
        return FluentWait(driver)
            .withTimeout(timeOut ?: timeout)
            .pollingEvery(retry ?: Duration.ofMillis(10))
            .withMessage(failureMessage)
            .ignoring(RetryException::class.java).until {
                try {
                    t()
                } catch (e: JavascriptException) {
                    install()
                    t()
                }
            }
    }

    fun  page(url: String, fn: El.() -> T): T {
        driver.get(url)
        return fn(find())
    }

    fun find(
        selector: String = "html",
        atLeast: Int? = 1,
        atMost: Int? = null,
    ): El = find(listOf(selector), atLeast, atMost)

    fun find(
        selector: List,
        atLeast: Int? = 1,
        atMost: Int? = null,
    ): El {
        return El(
            jq = this, selector = selector, atLeast = atLeast, atMost = atMost
        )
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy