gwen.web.eval.WebContext.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of gwen-web Show documentation
Show all versions of gwen-web Show documentation
Web automation engine for Gwen
/*
* Copyright 2015-2021 Brady Wood, Branko Juric
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package gwen.web.eval
import WebErrors._
import gwen.web.eval.binding._
import gwen.web.eval.driver.DriverManager
import gwen.web.eval.driver.event.WebSessionEventListener
import gwen.core._
import gwen.core.Errors._
import gwen.core.eval.ComparisonOperator
import gwen.core.eval.EvalContext
import gwen.core.eval.binding.BindingType
import gwen.core.eval.binding.JavaScriptBinding
import gwen.core.eval.binding.TextBinding
import gwen.core.node.gherkin.Step
import gwen.core.state.StateLevel
import gwen.core.state.EnvState
import gwen.core.state.SensitiveData
import gwen.core.status.Failed
import scala.concurrent.duration.Duration
import scala.io.Source
import scala.jdk.CollectionConverters._
import scala.util.{Failure, Success, Try}
import scala.util.chaining._
import com.typesafe.scalalogging.LazyLogging
import org.openqa.selenium._
import org.openqa.selenium.interactions.Actions
import org.openqa.selenium.support.ui.ExpectedCondition
import org.openqa.selenium.support.ui.ExpectedConditions
import org.openqa.selenium.support.ui.FluentWait
import org.openqa.selenium.support.ui.Select
import java.io.File
import org.openqa.selenium.remote.RemoteWebDriver
import gwen.web.eval.driver.event.WebSessionEvent
/**
* The web evaluatioin context.
*/
class WebContext(options: GwenOptions, envState: EnvState, driverManager: DriverManager) extends EvalContext(options, envState) with LazyLogging with WebSessionEventListener {
Try(logger.info(s"GWEN_CLASSPATH = ${sys.env("GWEN_CLASSPATH")}"))
Try(logger.info(s"SELENIUM_HOME = ${sys.env("SELENIUM_HOME")}"))
private val locatorBindingResolver = new LocatorBindingResolver(this)
private var lastScreenshotSize: Option[Long] = None
driverManager.addWebSessionEventListener(this)
override def sessionOpened(event: WebSessionEvent): Unit = {
val videoEnabled = Settings.getOpt("gwen.web.capability.enableVideo").map(_.toBoolean).getOrElse(false)
if (videoEnabled) {
driverManager.getSessionId(event.driver) foreach { sessionId =>
addVideo(new File(GwenSettings.`gwen.video.dir`, s"$sessionId.mp4"))
}
}
}
def locator = new WebElementLocator(this)
/** Resets the driver context. */
def reset(): Unit = {
driverManager.reset()
lastScreenshotSize = None
}
/** Resets the context for the given state level. */
override def reset(level: StateLevel): Unit = {
super.reset(level)
reset()
close()
}
/** Closes the context and all browsers and associated web drivers (if any have loaded). */
override def close(): Unit = {
closeDriverSession(None)
super.close()
}
/** Closes a named browser and associated web driver. */
def close(name: String): Unit = {
closeDriverSession(Some(name))
}
private def closeDriverSession(name: Option[String]): Unit = {
perform {
name match {
case Some(n) => driverManager.quit(n)
case _ => driverManager.quit()
}
}
}
/**
* Adds web engine dsl steps to super implementation. The entries
* returned by this method are used for tab completion in the REPL.
*/
override def dsl: List[String] =
Source.fromInputStream(getClass.getResourceAsStream("/gwen-web.dsl")).getLines().toList ++ super.dsl
/**
* Appends a return keyword in front of the given javascript expression in preparation for execute-with-return
* (since web driver requires return prefix).
*
* @param javascript the javascript function
*/
override def formatJSReturn(javascript: String) = s"return $javascript"
/**
* Executes a javascript expression on the current page through the web driver.
*
* @param javascript the script expression to execute
* @param params optional parameters to the script
*/
override def evaluateJS(javascript: String, params: Any*): Any =
executeJS(javascript, params.map(_.asInstanceOf[AnyRef])*)
/**
* Gets a bound value from memory. A search for the value is made in
* the following order and the first value found is returned:
* - Web element text on the current page
* - Currently active page scope
* - The top scope
* - Settings
*
* @param name the name of the bound value to find
*/
override def getBoundReferenceValue(name: String): String = {
if (name == "the current URL") {
val url = captureCurrentUrl
topScope.set(name, url)
}
(getLocatorBinding(name, optional = true) match {
case Some(binding) =>
Try(getElementText(binding)) match {
case Success(text) => text.getOrElse(getAttribute(name))
case Failure(e) => throw e
}
case _ => getAttribute(name)
}) tap { value =>
logger.debug(s"getBoundReferenceValue($name)='$value'")
}
}
/**
* Resolves a bound attribute value from the visible scope.
*
* @param name the name of the bound attribute to find
*/
def getAttribute(name: String): String = {
getCachedWebElement(s"${JavaScriptBinding.key(name)}/param/webElement") map { webElement =>
val javascript = interpolate(scopes.get(JavaScriptBinding.key(name)))
val jsFunction = s"return (function(element) { return $javascript })(arguments[0])"
Option(executeJS(jsFunction, webElement)).map(_.toString).getOrElse("")
} getOrElse {
Try(super.getBoundReferenceValue(name)) match {
case Success(value) => value
case Failure(e) => e match {
case _: UnboundAttributeException =>
Try(getLocatorBinding(name).selectors.map(_.expression).mkString(",")).getOrElse(unboundAttributeError(name))
case _ => throw e
}
}
}
}
def boundAttributeOrSelection(element: String, selection: Option[DropdownSelection]): () => String = () => {
selection match {
case None => getBoundReferenceValue(element)
case Some(sel) =>
try {
getBoundReferenceValue(s"$element $sel")
} catch {
case _: UnboundAttributeException =>
getElementSelection(element, sel).getOrElse(getBoundReferenceValue(element))
case e: Throwable => throw e
}
}
}
/**
* Gets a named locator binding.
*
* @param name the name of the web element
*/
def getLocatorBinding(name: String): LocatorBinding = {
locatorBindingResolver.getBinding(name, optional = false).get
}
/**
* Gets a named locator binding.
*
* @param name the name of the web element
* @param optional true to return None if not found; false to throw error
*/
def getLocatorBinding(name: String, optional: Boolean): Option[LocatorBinding] = {
locatorBindingResolver.getBinding(name, optional)
}
/**
* Add a list of error attachments to the given step including the current
* screenshot and all current error attachments.
*
* @param failure the failed status
*/
override def addErrorAttachments(step: Step, failure: Failed): Step = {
(if (failure.isTechError) {
super.addErrorAttachments(step, failure)
} else {
step
}) tap { _ =>
if (!failure.isLicenseError) {
captureScreenshot(true)
}
}
}
/**
* Binds the given element and value to a given action (element/action=value)
* and then waits for any bound post conditions to be satisfied.
*
* @param element the element to bind the value to
* @param action the action to bind the value to
* @param value the value to bind
*/
def bindAndWait(element: String, action: String, value: String): Unit = {
scopes.set(s"$element/$action", value)
// sleep if wait time is configured for this action
scopes.getOpt(s"$element/$action/wait") foreach { secs =>
logger.info(s"Waiting for $secs second(s) (post-$action wait)")
Thread.sleep(secs.toLong * 1000)
}
// wait for javascript post condition if one is configured for this action
scopes.getOpt(s"$element/$action/condition") foreach { condition =>
val javascript = scopes.get(JavaScriptBinding.key(condition))
logger.info(s"waiting until $condition (post-$action condition)")
logger.debug(s"Waiting for script to return true: $javascript")
waitUntil(s"waiting for true return from javascript: $javascript") {
evaluateJSPredicate(javascript)
}
}
}
/**
* Gets the actual value of an attribute and compares it with an expected value or condition.
*
* @param name the name of the attribute being compared
* @param expected the expected value, regex, xpath, or json path
* @param actual the actual value of the element
* @param operator the comparison operator
* @param negate true to negate the result
* @param nameSuffix optional name suffix
* @param message optional error message to use
* @return true if the actual value matches the expected value
*/
def compare(name: String, expected: String, actual: () => String, operator: ComparisonOperator, negate: Boolean, nameSuffix: Option[String], message: Option[String]): Unit = {
var result = false
var error: Option[String] = None
var actualValue = actual()
var polled = false
try {
waitUntil(s"waiting for $name to ${if(negate) "not " else ""}$operator '$expected'") {
if (polled) {
actualValue = actual()
}
polled = true
result = if (actualValue != null) {
super.compare(name, expected, actualValue, operator, negate) match {
case Success(condition) => condition
case Failure(e) =>
error = Some(e.getMessage)
false
}
} else false
result
}
} catch {
case _: WaitTimeoutException => result = false
}
error match {
case Some(msg) =>
assert(assertion = false, message getOrElse msg)
case None =>
if (!polled) {
result = super.compare(name, expected, actualValue, operator, negate).getOrElse(result)
}
val binding = Try(
getLocatorBinding(name.substring(0, name.length - nameSuffix.map(_.length).getOrElse(0)), optional = true) getOrElse {
getBinding(name)
}
).map(_.toString).getOrElse(name)
assert(result, message getOrElse s"Expected $binding to ${if(negate) "not " else ""}$operator '$expected' but got '$actualValue'")
}
}
/**
* Invokes a function that performs an operation on the current web driver
* session and conditionally captures the current screenshot if the specified
* takeScreenShot is true.
*
* @param function the function to perform
* @param takeScreenShot true to take screenshot after performing the function
*/
def withWebDriver[T](function: WebDriver => T)(implicit takeScreenShot: Boolean = false): Option[T] = {
evaluate(None.asInstanceOf[Option[T]]) {
driverManager.withWebDriver { driver =>
Option(function(driver)) tap { _ =>
if (takeScreenShot) {
captureScreenshot(false)
}
}
}
}
}
/**
* Gets a cached web element.
*
* @param element the name of the web element
* @return the cached web element
*/
def getCachedWebElement(element: String): Option[WebElement] = topScope.getObject(element) match {
case Some(we: WebElement) =>
highlightElement(we)
Some(we)
case _ => None
}
/**
* Locates a web element and highlights it.
*
* @param binding the locator binding
*/
def locateAndHighlight(binding: LocatorBinding): Unit = {
withDriverAndElement(binding, s"trying to locate $binding") { (driver, webElement) =>
createActions(driver).moveToElement(webElement).perform()
}
}
/**
* Locates a web element and performs an operation on it.
*
* @param binding the locator binding
* @param reason a description of what action is being performed
* @param operation the operation to perform on the element
*/
private def withWebElement[T](binding: LocatorBinding, reason: String)(operation: WebElement => T): Option[T] =
evaluate(None.asInstanceOf[Option[T]]) {
val selector = binding.selectors.head
val wHandle = selector.relative.flatMap(_ => withWebDriver(_.getWindowHandle))
try {
var result: Option[Try[T]] = None
val start = System.nanoTime()
try {
var lapsed = 0L
waitUntil(reason) {
try {
val webElement = binding.resolve()
tryMoveTo(webElement)
if (!selector.isContainer) {
highlightElement(webElement)
}
val res = operation(webElement)
result = Some(Success(res))
true
} catch {
case e: Throwable =>
lapsed = Duration.fromNanos(System.nanoTime() - start).toSeconds
if (e.isInstanceOf[InvalidElementStateException] || e.isInstanceOf[NoSuchElementException] || e.isInstanceOf[NotFoundOrInteractableException]) {
if (lapsed >= binding.timeoutSeconds) {
result = if (e.isInstanceOf[WebElementNotFoundException]) {
Some(Failure(e))
} else {
Some(Try(elementNotInteractableError(binding, e)))
}
true
} else {
result = Some(Failure(e))
false
}
} else {
result = Some(Failure(e))
false
}
}
}
} catch {
case _: WaitTimeoutException if result.exists(_.isFailure) =>
waitTimeoutError(WebSettings.`gwen.web.wait.seconds`, reason, result.get.failed.get)
}
result.map {
case Success(res) =>
res tap { _ =>
if (WebSettings.`gwen.web.capture.screenshots.enabled`) {
captureScreenshot(false)
}
}
case Failure(e) =>
throw e
}
} finally {
wHandle foreach { handle =>
withWebDriver { driver =>
driver.switchTo().window(handle)
}
}
}
}
def tryMoveTo(webElement: WebElement): Unit = {
if (WebSettings.`gwen.web.implicit.element.moveTo` || (!webElement.isDisplayed && !isInViewport(webElement))) {
withWebDriver { driver =>
createActions(driver).moveToElement(webElement).perform()
}
}
}
/** Captures the current screenshot and adds it to the attachments list. */
def captureScreenshot(unconditional: Boolean, name: String = "Screenshot"): Option[File] = {
evaluate(Option(new File("$[dryRun:screenshotFile]"))) {
Try(
driverManager.withWebDriver { driver =>
Thread.sleep(150) // give browser time to render
driver.asInstanceOf[TakesScreenshot].getScreenshotAs(OutputType.FILE)
}
) match {
case Success(screenshot) =>
val keep = unconditional || WebSettings.`gwen.web.capture.screenshots.duplicates` || lastScreenshotSize.fold(true) { _ != screenshot.length}
if (keep) {
if (!WebSettings.`gwen.web.capture.screenshots.duplicates`) lastScreenshotSize = Some(screenshot.length())
addAttachment(name, screenshot)
Some(screenshot)
} else {
None
}
case Failure(_) => None
}
}
}
/** Captures an element screenshot and adds it to the attachments list. */
def captureElementScreenshot(binding: LocatorBinding, name: String = "Element Screenshot"): Option[File] = {
evaluate(Option(new File("$[dryRun:elementScreenshotFile]"))) {
withWebElement(binding, s"trying to capture element screenshot of $binding") { webElement =>
Thread.sleep(150) // give element time to render
webElement.getScreenshotAs(OutputType.FILE) tap { elementshot =>
addAttachment(name, elementshot)
}
}
}
}
/**
* Injects and executes a javascript on the current page through web driver.
*
* @param javascript the script expression to execute
* @param params optional parameters to the script
* @param takeScreenShot true to take screenshot after performing the function
*/
def executeJS(javascript: String, params: Any*)(implicit takeScreenShot: Boolean = false): Any =
withWebDriver { webDriver =>
try {
webDriver.asInstanceOf[JavascriptExecutor].executeScript(javascript, params.map(_.asInstanceOf[AnyRef])*) tap { result =>
if (takeScreenShot && WebSettings.`gwen.web.capture.screenshots.enabled`) {
captureScreenshot(false)
}
logger.debug(s"Evaluated javascript: $javascript, result='$result'")
if (result.isInstanceOf[Boolean] && result.asInstanceOf[Boolean]) {
Thread.sleep(150) // observed volatile results for booleans without wait
}
}
} catch {
case e: Throwable => javaScriptError(javascript, e)
}
} getOrElse {
if (options.dryRun) s"$$[${BindingType.javascript}:$javascript]"
else null //js returned null
}
/**
* Waits for a given condition to be true. Errors on time out
* after "gwen.web.wait.seconds" (default is 10 seconds)
*
* @param reason a description of what is being waited on
* @param condition the boolean condition to wait for (until true)
*/
def waitUntil(reason: String)(condition: => Boolean): Unit = {
waitUntil(WebSettings.`gwen.web.wait.seconds`, reason) { condition }
}
/**
* Waits until a given condition is ready for an optional number of seconds.
* Errors on given timeout out seconds.
*
* @param timeoutSecs optional number of seconds to wait before timing out
* @param reason a description of what is being waited on
* @param condition the boolean condition to wait for (until true)
*/
def waitUntil(timeoutSecs: Option[Long], reason: String)(condition: => Boolean): Unit = {
timeoutSecs match {
case (Some(secs)) => waitUntil(secs, reason) { condition }
case _ => waitUntil(reason) { condition }
}
}
/**
* Waits until a given condition is ready for a given number of seconds.
* Errors on given timeout out seconds.
*
* @param timeoutSecs the number of seconds to wait before timing out
* @param reason a description of what is being waited on
* @param condition the boolean condition to wait for (until true)
*/
override def waitUntil(timeoutSecs: Long, reason: String)(condition: => Boolean): Unit = {
waitUntil(None, Some(timeoutSecs), reason)(condition)
}
/**
* Waits until a given condition is ready for a given number of seconds using a given polling delay.
* Errors on given timeout out seconds.
*
* @param delayMsecs the polling delay (milliseconds)
* @param timeoutSecs the number of seconds to wait before timing out
* @param reason a description of what is being waited on
* @param condition the boolean condition to wait for (until true)
*/
def waitUntil(delayMsecs: Option[Long], timeoutSecs: Option[Long], reason: String)(condition: => Boolean): Unit = {
val timeout = timeoutSecs.getOrElse(WebSettings.`gwen.web.wait.seconds`)
try {
withWebDriver { webDriver =>
delayMsecs match {
case Some(delay) =>
new FluentWait(webDriver)
.withTimeout(java.time.Duration.ofSeconds(timeout))
.pollingEvery(java.time.Duration.ofMillis(delay))
.until { driver => condition }
case _ =>
new FluentWait(webDriver)
.withTimeout(java.time.Duration.ofSeconds(timeout))
.until { driver => condition }
}
}
} catch {
case e: TimeoutException =>
waitTimeoutError(timeout, reason, e)
}
}
def waitUntil[T](reason: String, condition: ExpectedCondition[T]): Unit = {
val timeout = WebSettings.`gwen.web.wait.seconds`
try {
withWebDriver { webDriver =>
new FluentWait(webDriver)
.withTimeout(java.time.Duration.ofSeconds(timeout))
.until(condition)
}
} catch {
case e: TimeoutException =>
waitTimeoutError(timeout, reason, e)
}
}
/**
* Highlights and then un-highlights a browser element.
* Uses pure javascript, as suggested by https://github.com/alp82.
* The duration of the highlight lasts for `gwen.web.throttle.msecs`.
* The look and feel of the highlight is controlled by the
* `gwen.web.highlight.style` setting.
*
* @param element the element to highlight
*/
def highlightElement(element: WebElement): Unit = {
perform {
val msecs = WebSettings`gwen.web.throttle.msecs`; // need semi-colon (compiler bug?)
if (msecs > 0) {
val style = WebSettings.`gwen.web.highlight.style`
val origStyle = executeJS(s"element = arguments[0]; type = element.getAttribute('type'); if (('radio' == type || 'checkbox' == type) && element.parentElement.getElementsByTagName('input').length == 1) { element = element.parentElement; } original_style = element.getAttribute('style'); element.setAttribute('style', original_style + '; $style'); return original_style;", element)(WebSettings.`gwen.web.capture.screenshots.highlighting`)
try {
if (!WebSettings.`gwen.web.capture.screenshots.highlighting` || !WebSettings.`gwen.web.capture.screenshots.enabled`) {
Thread.sleep(msecs)
}
} finally {
executeJS(s"element = arguments[0]; type = element.getAttribute('type'); if (('radio' == type || 'checkbox' == type) && element.parentElement.getElementsByTagName('input').length == 1) { element = element.parentElement; } element.setAttribute('style', '$origStyle');", element)(false)
}
}
}
}
/**
* Checks the current state of an element.
*
* @param binding the locator binding of the element
* @param state the state to check
* @param negate whether or not to negate the check
* @param message optional assertion error message
*/
def checkElementState(binding: LocatorBinding, state: ElementState, negate: Boolean, message: Option[String]): Unit = {
perform {
val result = isElementState(binding.jsEquivalent, state, negate)
assert(result, message getOrElse s"$binding should${if(negate) " not" else ""} be $state")
}
}
/**
* Checks the current state of an element.
*
* @param binding the locator binding of the element
* @param state the state to check
* @param negate whether or not to negate the check
*/
private def isElementState(binding: LocatorBinding, state: ElementState, negate: Boolean): Boolean = {
var result = false
perform {
// use fast binding selector if checking for element absence so we don't have to wait for timeout
val parsedBinding = state match {
case ElementState.displayed if negate =>
binding.withFastTimeout
case ElementState.hidden if !negate =>
binding.withFastTimeout
case _ =>
binding
}
try {
withWebElement(parsedBinding, s"waiting for $binding to${if (negate) " not" else ""} be $state") { webElement =>
result = state match {
case ElementState.displayed =>
if (!negate) isDisplayed(webElement)
else !isDisplayed(webElement)
case ElementState.hidden =>
if (!negate) !isDisplayed(webElement)
else isDisplayed(webElement)
case ElementState.checked =>
if (!negate) webElement.isSelected
else !webElement.isSelected
case ElementState.ticked =>
if (!negate) webElement.isSelected
else !webElement.isSelected
case ElementState.unchecked =>
if (!negate) !webElement.isSelected
else webElement.isSelected
case ElementState.unticked =>
if (!negate) !webElement.isSelected
else webElement.isSelected
case ElementState.enabled =>
if (!negate) webElement.isEnabled
else!webElement.isEnabled
case ElementState.disabled =>
if (!negate) !webElement.isEnabled
else webElement.isEnabled
}
}
} catch {
case e @ (_ : NoSuchElementException | _ : NotFoundOrInteractableException | _ : WaitTimeoutException) =>
if (state == ElementState.displayed) result = negate
else if (state == ElementState.hidden) result = !negate
else throw e
}
}
result
}
/**
* Waits the state of an element.
*
* @param binding the locator binding of the element
* @param state the state to wait for
* @param negate whether or not to negate the check
*/
def waitForElementState(binding: LocatorBinding, state: ElementState, negate: Boolean): Unit =
waitUntil(s"waiting for $binding to${if (negate) " not" else""} be $state") {
isElementState(binding, state, negate)
}
/** Gets the title of the current page in the browser.*/
def getTitle: String =
withWebDriver { driver =>
driver.getTitle tap { title =>
bindAndWait("page", "title", title)
}
}.getOrElse("$[dryRun:title]")
/**
* Sends a value to a web element.
*
* @param binding the locator binding
* @param value the value to send
* @param clickFirst true to click field first (if element is a text field)
* @param clearFirst true to clear field first (if element is a text field)
* @param sendEnterKey true to send the Enter key after sending the value
*/
def sendValue(binding: LocatorBinding, value: String, clearFirst: Boolean, clickFirst: Boolean, sendEnterKey: Boolean): Unit = {
val element = binding.name
withDriverAndElement(binding, s"trying to send value to $element") { (driver, webElement) =>
createActions(driver)
if (clickFirst) {
webElement.click()
}
if (clearFirst) {
webElement.clear()
}
SensitiveData.withValue(value) { plainValue =>
if ("file" == webElement.getAttribute("type")) {
createActions(driver).moveToElement(webElement).perform()
webElement.sendKeys(plainValue)
} else {
createActions(driver).moveToElement(webElement).sendKeys(plainValue).perform()
}
}
bindAndWait(element, ElementAction.`type`.toString, value)
if (sendEnterKey) {
createActions(driver).sendKeys(webElement, Keys.RETURN).perform()
bindAndWait(element, ElementAction.enter.toString, "true")
}
}
}
private [eval] def createSelect(webElement: WebElement): Select = new Select(webElement)
/**
* Selects a value in a dropdown (select control) by visible text.
*
* @param binding the locator binding
* @param value the value to select
*/
def selectByVisibleText(binding: LocatorBinding, value: String): Unit = {
withWebElement(binding, s"trying to select option in $binding by visible text") { webElement =>
logger.debug(s"Selecting '$value' in ${binding.name} by text")
createSelect(webElement).selectByVisibleText(value)
bindAndWait(binding.name, ElementAction.select.toString, value)
}
}
/**
* Selects a value in a dropdown (select control) by value.
*
* @param binding the locator binding
* @param value the value to select
*/
def selectByValue(binding: LocatorBinding, value: String): Unit = {
withWebElement(binding, s"trying to select option in $binding by value") { webElement =>
logger.debug(s"Selecting '$value' in ${binding.name} by value")
createSelect(webElement).selectByValue(value)
bindAndWait(binding.name, ElementAction.select.toString, value)
}
}
/**
* Selects a value in a dropdown (select control) by index.
*
* @param binding the locator binding
* @param index the index to select (first index is 0)
*/
def selectByIndex(binding: LocatorBinding, index: Int): Unit = {
withWebElement(binding, s"trying to select option in $binding at index $index") { webElement =>
logger.debug(s"Selecting option in ${binding.name} by index: $index")
val select = createSelect(webElement)
select.selectByIndex(index)
bindAndWait(binding.name, ElementAction.select.toString, select.getOptions.get(index).getText)
}
}
/**
* Deselects a value in a dropdown (select control) by visible text.
*
* @param binding the locator binding
* @param value the value to select
*/
def deselectByVisibleText(binding: LocatorBinding, value: String): Unit = {
withWebElement(binding, s"trying to deselect option in $binding by visible text") { webElement =>
logger.debug(s"Deselecting '$value' in ${binding.name} by text")
createSelect(webElement).deselectByVisibleText(value)
bindAndWait(binding.name, ElementAction.deselect.toString, value)
}
}
/**
* Deselects a value in a dropdown (select control) by value.
*
* @param binding the locator binding
* @param value the value to select
*/
def deselectByValue(binding: LocatorBinding, value: String): Unit = {
withWebElement(binding, s"trying to deselect option in $binding by value") { webElement =>
logger.debug(s"Deselecting '$value' in ${binding.name} by value")
createSelect(webElement).deselectByValue(value)
bindAndWait(binding.name, ElementAction.deselect.toString, value)
}
}
/**
* Deselects a value in a dropdown (select control) by index.
*
* @param binding the locator binding
* @param index the index to select (first index is 0)
*/
def deselectByIndex(binding: LocatorBinding, index: Int): Unit = {
withWebElement(binding, s"trying to deselect option in $binding at index $index") { webElement =>
logger.debug(s"Deselecting option in ${binding.name} by index: $index")
val select = createSelect(webElement)
select.deselectByIndex(index)
bindAndWait(binding.name, ElementAction.deselect.toString, select.getOptions.get(index).getText)
}
}
private [web] def createActions(driver: WebDriver): Actions = new Actions(driver)
def performAction(action: ElementAction, binding: LocatorBinding): Unit = {
val actionBinding = scopes.getOpt(JavaScriptBinding.key(s"${binding.name}/action/$action"))
actionBinding match {
case Some(javascript) =>
performScriptAction(action, javascript, binding, s"trying to $action $binding")
case None =>
withDriverAndElement(binding, s"trying to $action $binding") { (driver, webElement) =>
if (action != ElementAction.`move to`) {
moveToAndCapture(driver, webElement)
}
action match {
case ElementAction.click =>
webElement.click()
case ElementAction.`right click` =>
createActions(driver).contextClick(webElement).perform()
case ElementAction.`double click` =>
createActions(driver).doubleClick(webElement).perform()
case ElementAction.`move to` =>
moveToAndCapture(driver, webElement)
case ElementAction.submit => webElement.submit()
case ElementAction.check | ElementAction.tick =>
if (!webElement.isSelected) webElement.click()
if (!webElement.isSelected)
createActions(driver).sendKeys(webElement, Keys.SPACE).perform()
case ElementAction.uncheck | ElementAction.untick =>
if (webElement.isSelected) webElement.click()
if (webElement.isSelected)
createActions(driver).sendKeys(webElement, Keys.SPACE).perform()
case ElementAction.clear =>
webElement.clear()
case _ => WebErrors.invalidActionError(action)
}
}
bindAndWait(binding.name, action.toString, "true")
}
}
def moveToAndCapture(driver: WebDriver, webElement: WebElement): Unit = {
createActions(driver).moveToElement(webElement).perform()
if (WebSettings.`gwen.web.capture.screenshots.enabled`) {
captureScreenshot(false)
}
}
def dragAndDrop(sourceBinding: LocatorBinding, targetBinding: LocatorBinding): Unit = {
withWebDriver { driver =>
withWebElement(sourceBinding, s"trying to drag $sourceBinding to $targetBinding") { source =>
withWebElement(targetBinding, s"trying to drag $sourceBinding to $targetBinding") { target =>
createActions(driver).clickAndHold(source)
.moveToElement(target)
.release(target)
.build().perform()
}
}
}
}
def holdAndClick(modifierKeys: Array[String], clickAction: ElementAction, binding: LocatorBinding): Unit = {
val keys = modifierKeys.map(_.trim).map(key => Try(Keys.valueOf(key.toUpperCase)).getOrElse(unsupportedModifierKeyError(key)))
withDriverAndElement(binding, s"trying to $clickAction $binding") { (driver, webElement) =>
moveToAndCapture(driver, webElement)
var actions = createActions(driver)
keys.foreach { key => actions = actions.keyDown(key) }
actions = clickAction match {
case ElementAction.click => actions.click(webElement)
case ElementAction.`right click` => actions.contextClick(webElement)
case ElementAction.`double click` => actions.doubleClick(webElement)
case _ => WebErrors.invalidClickActionError(clickAction)
}
keys.reverse.foreach { key => actions = actions.keyUp(key) }
actions.build().perform()
}
bindAndWait(binding.name, clickAction.toString, "true")
}
def sendKeys(keysToSend: Array[String]): Unit = {
sendKeys(None, keysToSend)
}
def sendKeys(binding: LocatorBinding, keysToSend: Array[String]): Unit = {
sendKeys(Some(binding), keysToSend)
}
def sendKeys(elementBindingOpt: Option[LocatorBinding], keysToSend: Array[String]): Unit = {
val keys = keysToSend.map(_.trim).map(key => Try(Keys.valueOf(key.toUpperCase)).getOrElse(key))
elementBindingOpt match {
case Some(binding) =>
withDriverAndElement(binding, s"trying to send key(s) to $binding") { (driver, webElement) =>
if (keys.size > 1) {
webElement.sendKeys(Keys.chord(keys: _*))
} else {
var actions = createActions(driver).moveToElement(webElement)
keys.foreach { key => actions = actions.sendKeys(webElement, key) }
actions.build().perform()
}
}
case None =>
withWebDriver { driver =>
var actions = createActions(driver)
if (keys.size > 1) {
actions = actions.sendKeys(Keys.chord(keys: _*))
} else {
keys.foreach { key => actions = actions.sendKeys(key) }
}
actions.build().perform()
}
}
}
private def withDriverAndElement(binding: LocatorBinding, reason: String)(doActions: (WebDriver, WebElement) => Unit): Unit = {
withWebDriver { driver =>
withWebElement(binding, reason) { webElement =>
if (WebSettings.`gwen.web.implicit.element.focus`) {
executeJS("(function(element){element.focus();})(arguments[0]);", webElement)
}
doActions(driver, webElement)
}
}
}
private def performScriptAction(action: ElementAction, javascript: String, binding: LocatorBinding, reason: String): Unit = {
withDriverAndElement(binding, reason) { (driver, webElement) =>
if (action != ElementAction.`move to`) {
moveToAndCapture(driver, webElement)
}
executeJS(s"(function(element) { $javascript })(arguments[0])", webElement)
bindAndWait(binding.name, action.toString, "true")
}
}
/**
* Performs and action on a web element in the context of another element.
*
* @param action description of the action
* @param element the name of the element to perform the action on
* @param context the name of the context element binding to find the element in
*/
def performActionInContext(action: ElementAction, element: String, context: String): Unit = {
try {
val contextBinding = getLocatorBinding(context)
val binding = getLocatorBinding(element)
performActionIn(action, binding, contextBinding)
} catch {
case e1: LocatorBindingException =>
try {
val binding = getLocatorBinding(s"$element of $context")
performAction(action, binding)
} catch {
case e2: LocatorBindingException =>
throw new LocatorBindingException(s"${e1.getMessage}. ${e2.getMessage}.")
}
}
}
private def performActionIn(action: ElementAction, binding: LocatorBinding, contextBinding: LocatorBinding): Unit = {
def perform(webElement: WebElement, contextElement: WebElement)(buildAction: Actions => Actions): Unit = {
withWebDriver { driver =>
val moveTo = createActions(driver).moveToElement(contextElement).moveToElement(webElement)
buildAction(moveTo).build().perform()
if (WebSettings.`gwen.web.capture.screenshots.enabled`) {
captureScreenshot(false)
}
}
}
val reason = s"trying to $action $binding"
withWebElement(contextBinding, reason) { contextElement =>
withWebElement(binding, reason) { webElement =>
action match {
case ElementAction.click => perform(webElement, contextElement) { _.click() }
case ElementAction.`right click` => perform(webElement, contextElement) { _.contextClick() }
case ElementAction.`double click` => perform(webElement, contextElement) { _.doubleClick() }
case ElementAction.check | ElementAction.tick =>
if (!webElement.isSelected) perform(webElement, contextElement) { _.click() }
if (!webElement.isSelected) perform(webElement, contextElement) { _.sendKeys(Keys.SPACE) }
case ElementAction.uncheck | ElementAction.untick =>
if (webElement.isSelected) perform(webElement, contextElement) { _.click() }
if (webElement.isSelected) perform(webElement, contextElement) { _.sendKeys(Keys.SPACE) }
case ElementAction.`move to` => perform(webElement, contextElement) { action => action }
case _ => WebErrors.invalidContextActionError(action)
}
bindAndWait(binding.name, action.toString, "true")
}
}
}
/**
* Waits for text to appear in the given web element.
*
* @param binding the locator binding
*/
def waitForText(binding: LocatorBinding): Boolean =
getElementText(binding).map(_.length()).getOrElse {
scopes.set(TextBinding.key(binding.name), BindingType.text.toString)
0
} > 0
/**
* Scrolls an element into view.
*
* @param binding the locator binding
* @param scrollTo scroll element into view, options are: top or bottom
*/
def scrollIntoView(binding: LocatorBinding, scrollTo: ScrollTo): Unit = {
withWebElement(binding, s"trying to scroll to $scrollTo of $binding") { scrollIntoView(_, scrollTo) }
}
/**
* Scrolls the given web element into view.
*
* @param webElement the web element to scroll to
* @param scrollTo scroll element into view, options are: top or bottom
*/
def scrollIntoView(webElement: WebElement, scrollTo: ScrollTo): Unit = {
executeJS(s"var elem = arguments[0]; if (typeof elem !== 'undefined' && elem != null) { elem.scrollIntoView(${scrollTo == ScrollTo.top}); }", webElement)
}
/**
* Resizes the browser window to the given dimensions.
*
* @param width the width
* @param height the height
*/
def resizeWindow(width: Int, height: Int): Unit = {
withWebDriver { driver =>
logger.info(s"Resizing browser window to width $width and height $height")
driver.manage().window().setSize(new Dimension(width, height))
}
}
/**
* Maximizes the browser window.
*/
def maximizeWindow(): Unit = {
withWebDriver { driver =>
logger.info("Maximising browser window")
driver.manage().window().maximize()
}
}
def captureCurrentUrl: String = {
withWebDriver { driver =>
driver.getCurrentUrl
} getOrElse {
"$[dryRun:currentUrl]"
}
}
/**
* Gets the text value of a web element on the current page.
* A search for the text is made in the following order and the first value
* found is returned:
* - Web element text
* - Web element text attribute
* - Web element value attribute
* If a value is found, its value is bound to the current page
* scope as `name/text`.
*
* @param binding the locator binding
*/
def getElementText(binding: LocatorBinding): Option[String] =
withWebElement(binding, s"trying to get text of $binding") { webElement =>
(Option(webElement.getText) match {
case None | Some("") =>
Option(webElement.getAttribute("text")) match {
case None | Some("") =>
Option(webElement.getAttribute("value")) match {
case None | Some("") =>
val value = executeJS("return (function(element){return element.innerText || element.textContent || ''})(arguments[0]);", webElement).asInstanceOf[String]
if (value != null) value else ""
case Some(value) => value
}
case Some(value) => value
}
case Some(value) => value
}) tap { text =>
bindAndWait(binding.name, BindingType.text.toString, text)
}
} tap { value =>
logger.debug(s"getElementText(${binding.name})='$value'")
}
/**
* Gets the selected text of a dropdown web element on the current page.
* If a value is found, its value is bound to the current page
* scope as `name/selectedText`.
*
* @param name the web element name
*/
private def getSelectedElementText(name: String): Option[String] = {
val binding = getLocatorBinding(name)
withWebElement(binding, s"trying to get selected text of $binding") { webElement =>
(getElementSelectionByJS(webElement, DropdownSelection.text) match {
case None =>
Try(createSelect(webElement)) map { select =>
Option(select.getAllSelectedOptions.asScala.map(_.getText()).mkString(",")) match {
case None | Some("") =>
select.getAllSelectedOptions.asScala.map(_.getAttribute("text")).mkString(",")
case Some(value) => value
}
} getOrElse null
case Some(value) => value
}) tap { text =>
bindAndWait(binding.name, "selectedText", text)
}
} tap { value =>
logger.debug(s"getSelectedElementText(${binding.name})='$value'")
}
}
/**
* Gets the selected value of a dropdown web element on the current page.
* If a value is found, its value is bound to the current page
* scope as `name/selectedValue`.
*
* @param name the web element name
*/
private def getSelectedElementValue(name: String): Option[String] = {
val binding = getLocatorBinding(name)
withWebElement(binding, s"trying to get selected value of $binding") { webElement =>
getElementSelectionByJS(webElement, DropdownSelection.value) match {
case None =>
Try(createSelect(webElement)) map { select =>
select.getAllSelectedOptions.asScala.map(_.getAttribute("value")).mkString(",") tap { value =>
bindAndWait(binding.name, "selectedValue", value)
}
} getOrElse null
case Some(value) => value
}
} tap { value =>
logger.debug(s"getSelectedElementValue(${binding.name})='$value'")
}
}
private def getElementSelectionByJS(webElement: WebElement, by: DropdownSelection): Option[String] = {
Option(executeJS(s"""return (function(select){try{var byText=${by == DropdownSelection.text};var result='';var options=select && select.options;if(!!options){var opt;for(var i=0,iLen=options.length;i0){result=result+',';}if(byText){result=result+opt.text;}else{result=result+opt.value;}}}return result;}else{return null;}}catch(e){return null;}})(arguments[0])""", webElement).asInstanceOf[String])
}
/**
* Gets an element's selected value(s).
*
* @param name the name of the element
* @param selection `text` to get selected option text, `value` to get
* selected option value
* @return the selected value or a comma seprated string containing all
* the selected values if multiple values are selected.
*/
def getElementSelection(name: String, selection: DropdownSelection): Option[String] = {
if (selection == DropdownSelection.text) {
getSelectedElementText(name)
} else {
getSelectedElementValue(name)
}
}
/**
* Switches the web driver session
*
* @param session the name of the session to switch to
*/
def switchToSession(session: String): Unit = {
perform {
driverManager.switchToSession(session)
}
}
/**
* Starts and switches to a new tab or window.
*
* @param winType tab or window
*/
def switchToNewWindow(winType: WindowType): Unit = {
perform {
driverManager.switchToNewWindow(winType)
}
}
/**
* Starts a new session if there isn't one or stays in the current one.
*/
def newOrCurrentSession(): Unit = {
perform {
driverManager.newOrCurrentSession()
}
}
/** Gets the number of open sesions. */
def noOfSessions(): Int = driverManager.noOfSessions()
/** Gets the number of open windows. */
def noOfWindows(): Int = driverManager.noOfWindows()
/**
* Switches to the first child window if one was just opened.
*/
def switchToChild(): Unit = {
switchToWindow(1)
}
/**
* Switches to a tab or window occurrence.
*
* @param occurrence the tag or window occurrence to switch to (primary is 0, first opened is occurrence 1, 2nd is 2, ..)
*/
def switchToWindow(occurrence: Int): Unit = {
waitUntil(s"trying to switch to tab/window occurrence $occurrence") {
driverManager.windows().lift(occurrence).nonEmpty
}
driverManager.switchToWindow(occurrence)
}
/**
* Closes the last child window.
*/
def closeChild(): Unit = {
perform {
driverManager.closeChild()
}
}
/**
* Closes the tab or window occurrence.
*
* @param occurrence the tag or window occurrence to close (primary is 0, first opened is occurrence 1, 2nd is 2, ..)
*/
def closeWindow(occurrence: Int): Unit = {
perform {
driverManager.closeWindow(occurrence)
}
}
/** Switches to the parent window. */
def switchToParent(): Unit = {
perform {
driverManager.switchToParent()
}
}
/** Switches to the top window / first frame */
def switchToDefaultContent(): Unit = {
perform {
driverManager.switchToDefaultContent()
}
}
/** Refreshes the current page.*/
def refreshPage(): Unit = {
withWebDriver { driver =>
driver.navigate().refresh()
}
}
/**
* Handles an alert (pop-up)
*
* @param accept true to accept; false to dismiss
*/
def handleAlert(accept: Boolean): Unit = {
withWebDriver { driver =>
waitUntil("waiting for alert popup", ExpectedConditions.alertIsPresent())
if (accept) {
driver.switchTo().alert().accept()
} else {
driver.switchTo().alert().dismiss()
}
}
}
/**
* Navigates the browser the given URL
*
* @param url the URL to navigate to
*/
def navigateTo(url: String): Unit = {
withWebDriver { driver =>
driver.get(url)
} (WebSettings.`gwen.web.capture.screenshots.enabled`)
}
/**
* Gets the message of an alert of confirmation popup.
*
* @return the alert or confirmation message
*/
def getPopupMessage: String = {
withWebDriver { driver =>
waitUntil("waiting for alert popup", ExpectedConditions.alertIsPresent())
driver.switchTo().alert().getText
} getOrElse "$[dryRun:popupMessage]"
}
/** Checks if an element is displayed. */
def isDisplayed(webElement: WebElement): Boolean = {
if (!isDisplayedAndInViewport(webElement)) {
Try(
withWebDriver { driver =>
createActions(driver).moveToElement(webElement).perform()
}
) match {
case Success(_) => isDisplayedAndInViewport(webElement)
case Failure(_) => false
}
} else {
true
}
}
private def isDisplayedAndInViewport(webElement: WebElement): Boolean = {
webElement.isDisplayed && isInViewport(webElement)
}
/** Checks if an element is not in the view port. */
private def isInViewport(webElement: WebElement): Boolean = {
executeJS("return (function(elem){var b=elem.getBoundingClientRect(); return b.top>=0 && b.left>=0 && b.bottom<=(window.innerHeight || document.documentElement.clientHeight) && b.right<=(window.innerWidth || document.documentElement.clientWidth);})(arguments[0])", webElement).asInstanceOf[Boolean]
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy