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

gwen.web.eval.WebContext.scala Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2015-2024 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.WebSessionEvent
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.DryValueBinding
import gwen.core.eval.binding.JSBinding
import gwen.core.eval.support.BooleanCondition
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.collection.SeqView
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

/**
  * The web evaluatioin context.
  */
class WebContext(options: GwenOptions, envState: EnvState, driverManager: DriverManager) extends EvalContext(options, envState) with LazyLogging with WebSessionEventListener with ImplicitWebValueKeys {

  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 = { 
    driverManager.getSessionId(event.driver) foreach { sessionId =>
      if (WebSettings.videoEnabled) {
        addVideo(new File(GwenSettings.`gwen.video.dir`, s"$sessionId.mp4"))
      }
      envState.topScope.set(`gwen.web.sessionId`, sessionId)
    }
  }

  override def sessionClosed(event: WebSessionEvent): Unit = { 
    envState.topScope.set(`gwen.web.sessionId`, null)
  }

  def webElementlocator = 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 function on the current page through the web driver.
    *
    * @param javascript the script function to execute
    * @param params optional parameters to the web driver
    */
  override def evaluateJS(javascript: String, params: List[Any]): Any = {
    evaluateJSWeb(javascript, params)
  }

  /**
    * Injects and executes a javascript function on the current page through web driver.
    *
    * @param javascript the function to apply
    * @param takeScreenShot true to take screenshot after execting the function
    */
  def executeJS(javascript: String)(implicit takeScreenShot: Boolean = false): Any = {
    evaluateJSWeb(javascript, Nil)
  }

  /**
    * Injects and applies a javascript function to a web element on the current page through web driver.
    *
    * @param javascript the function to apply
    * @param webElement the web element to apply the function to
    * @param takeScreenShot true to take screenshot after execting the function
    */
  def applyJS(javascript: String, webElement: WebElement)(implicit takeScreenShot: Boolean = false): Any = {
    evaluateJSWeb(javascript, List(webElement))
  }

  /**
    * Injects and executes a javascript function on the current page through web driver.
    *
    * @param javascript the function to execute
    * @param params optional objects to apply the function to
    * @param takeScreenShot true to take screenshot after execting the function
    */
  private def evaluateJSWeb(javascript: String, params: List[Any])(implicit takeScreenShot: Boolean = false): Any = {
    withWebDriver { webDriver =>
      try {
        webDriver.asInstanceOf[JavascriptExecutor].executeScript(formatJSReturn(parseJS(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: GwenException => throw e
        case e: Throwable => functionError(javascript, e)
      }
    } getOrElse {
      if (options.dryRun) s"$$[${BindingType.javascript}:$javascript]"
      else null  //js returned null
    }
  }

  /**
    * 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 getBoundValue(name: String): String = getBoundValue(name.trim, None)
  def getBoundValue(name: String, timeout: Option[Duration]): String = {
    if (name == "the current URL") {
      val url = captureCurrentUrl
      topScope.set(name, url)
    }
    val locatorEntry = topScope.namedEntry(name) { _ => true } map { (n, _) => n.startsWith(LocatorKey.baseKey(name)) }
    if (locatorEntry.exists(_ == true) || locatorEntry.isEmpty) {
      getLocatorBinding(name, optional = true).map(_.withTimeout(timeout)) match {
        case Some(binding) =>
          evaluate(new DryValueBinding(binding.name, "webElementText", this).resolve()) {
            Try(getElementText(binding)) match {
              case Success(text) => 
                text.getOrElse(getCachedOrBoundValue(name))
              case Failure(e) => throw e
            }
          }
        case _ => getCachedOrBoundValue(name)
      }
    } else {
      getCachedOrBoundValue(name)
    }

  }

  /**
    * Resolves a bound attribute value from the visible scope.
    *
    * @param name the name of the bound attribute to find
    */
  def getCachedOrBoundValue(name: String): String = {
    getCachedWebElement(s"${JSBinding.key(name)}/param/webElement") map { webElement =>
      val javascript = interpolate(topScope.get(JSBinding.key(name)))
      val jsFunction = jsFunctionWrapper("element", "arguments[0]", s"return $javascript")
      Option(applyJS(jsFunction, webElement)).map(_.toString).getOrElse("")
    } getOrElse {
      super.getBoundValue(name)
    }
  }

  def boundAttributeOrSelection(element: String, selection: Option[DropdownSelection]): String = boundAttributeOrSelection(element, selection, None)
  def boundAttributeOrSelection(element: String, selection: Option[DropdownSelection], timeout: Option[Duration]): String = {
    selection match {
      case None => getBoundValue(element, timeout)
      case Some(sel) =>
        try {
          getBoundValue(s"$element $sel", timeout)
        } catch {
          case _: UnboundAttributeException =>
            getElementSelection(element, sel).getOrElse(getBoundValue(element, timeout))
          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
    */
  def getLocatorBindingOpt(name: String): Option[LocatorBinding] = {
    locatorBindingResolver.getBinding(name, optional = true)
  }

  /**
   * 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)
      }
    }
  }

  /**
    * 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
    * @param timeoutSecs the number of seconds to wait before timing out
    * @param mode the assertion mode
    * @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], timeoutSecs: Option[Long], mode: AssertionMode): Unit = {
    Thread.sleep(WebSettings.`gwen.web.assertions.delayMillisecs`)
    var result = false
    var error: Option[String] = None
    var actualValue = actual()
    var polled = false
    var attempts = 0
    try {
      waitUntil(timeoutSecs, s"waiting for $name to ${if(negate) "not " else ""}$operator '$expected'") {
        if (polled) {
          Thread.sleep(WebSettings.`gwen.web.assertions.delayMillisecs`)
          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
        attempts = attempts + 1
        result || !(attempts < WebSettings.`gwen.web.assertions.maxStrikes`)
      }
    } catch {
      case _: WaitTimeoutException => result = false
    }
    error match {
      case Some(msg) =>
        assertWithError(assertion = false, message, msg, mode)
      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(_.displayName).getOrElse(name)
        assertWithError(
          result, 
          message, 
          Assert.formatFailed(binding, expected, actualValue, negate, operator),
          mode)
    }

  }

  /**
    * 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 { 
      topScope.set(`gwen.web.sessionId`, DryValueBinding.unresolved(`gwen.web.sessionId`))
      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.displayName}") { (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 timeoutSecs = binding.timeoutSeconds
      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(timeoutSecs, 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 >= timeoutSecs) {
                    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(DryValueBinding.unresolved("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())
            addAttachmentFile(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(DryValueBinding.unresolved("elementScreenshotFile")))) {
      withWebElement(binding, s"trying to capture element screenshot of ${binding.displayName}") { webElement =>
        Thread.sleep(150) // give element time to render
        webElement.getScreenshotAs(OutputType.FILE) tap { elementshot =>
          addAttachmentFile(name, elementshot)
        }
      }
    }
  }

  /**
    * 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 = applyJS(
          jsFunctionWrapper("element", "arguments[0]", s"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 {
          applyJS(jsFunctionWrapper("element", "arguments[0]", s"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
    * @param mode the assertion mode
    */
  def checkElementState(binding: LocatorBinding, state: ElementState, negate: Boolean, message: Option[String], mode: AssertionMode): Unit = {
    perform {
        var result = false
        var attempts = 0
        try {
          waitUntil(binding.timeoutSeconds, s"waiting for ${binding.displayName} to ${if(negate) "not " else ""}be '$state'") {
            result = isElementState(binding, state, negate)
            attempts = attempts + 1
            result || !(attempts < WebSettings.`gwen.web.assertions.maxStrikes`)
          }
        } catch {
          case _: WaitTimeoutException =>
            result = false  
        }
        assertWithError(result, message, s"${binding.displayName} should${if(negate) " not" else ""} be $state", mode)
    }
  }

  /**
    * 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
    */
  def isElementState(binding: LocatorBinding, state: ElementState, negate: Boolean): Boolean = {
    var result = false
    perform {
      Thread.sleep(WebSettings.`gwen.web.assertions.delayMillisecs`)
      val fastBinding = binding.withFastTimeout
      state match {
        case ElementState.displayed => 
          result = if (!negate) isDisplayed(fastBinding) else !isDisplayed(fastBinding)
        case ElementState.hidden =>
          result = if (!negate) !isDisplayed(fastBinding) else isDisplayed(fastBinding)
        case _ =>
          try {  
            withWebElement(fastBinding, s"waiting for ${binding.displayName} to${if (negate) " not" else ""} be $state") { webElement =>
              result = state match {
                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
                case _ => false //never
              }
            }
          } 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.displayName} 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
    }.getOrElse(DryValueBinding.unresolved("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) {
        click(webElement)
      }
      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()
        }
      }
      if (sendEnterKey) {
        createActions(driver).sendKeys(webElement, Keys.RETURN).perform()
      }
    }
  }

  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.displayName} by visible text") { webElement =>
      logger.debug(s"Selecting '$value' in $binding by text")
      createSelect(webElement).selectByVisibleText(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.displayName} by value") { webElement =>
      logger.debug(s"Selecting '$value' in $binding by value")
      createSelect(webElement).selectByValue(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.displayName} at index $index") { webElement =>
      logger.debug(s"Selecting option in $binding by index: $index")
      val select = createSelect(webElement)
      select.selectByIndex(index)
    }
  }

  /**
    * 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.displayName} by visible text") { webElement =>
      logger.debug(s"Deselecting '$value' in $binding by text")
      createSelect(webElement).deselectByVisibleText(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.displayName} by value") { webElement =>
      logger.debug(s"Deselecting '$value' in $binding by value")
      createSelect(webElement).deselectByValue(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.displayName} at index $index") { webElement =>
      logger.debug(s"Deselecting option in $binding by index: $index")
      val select = createSelect(webElement)
      select.deselectByIndex(index)
    }
  }

  private [web] def createActions(driver: WebDriver): Actions = new Actions(driver)

  def performAction(action: ElementAction, binding: LocatorBinding): Unit = {
    val actionBinding = topScope.getOpt(JSBinding.key(s"${binding.name}/action/$action"))
    actionBinding match {
      case Some(javascript) =>
        performScriptAction(action, javascript, binding, s"trying to $action ${binding.displayName}")
      case None =>
        withDriverAndElement(binding, s"trying to $action ${binding.displayName}") { (driver, webElement) =>
          if (action != ElementAction.`move to`) {
            moveToAndCapture(driver, webElement)
          }
          action match {
            case ElementAction.click =>
              click(webElement)
            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 =>
              clickCheckbox(webElement, None, true)
            case ElementAction.uncheck | ElementAction.untick =>
              clickCheckbox(webElement, None, false)
            case ElementAction.clear =>
              webElement.clear()
            case _ => WebErrors.invalidActionError(action)
          }
        }
    }
  }

  private def clickCheckbox(webElement: WebElement, contextElement: Option[WebElement], selected: Boolean): Unit = {
    contextElement match {
      case None =>
        if (webElement.isSelected != selected) {
          Try(webElement.click()) match {
            case Failure(e) =>
              jsClick(webElement)
              if (webElement.isSelected != selected) webElement.sendKeys(Keys.SPACE)
              if (webElement.isSelected != selected) throw e
            case _ =>
              if (webElement.isSelected != selected) jsClick(webElement)
              if (webElement.isSelected != selected) webElement.sendKeys(Keys.SPACE)
          }
        }
      case Some(ctxElement) =>
        if (webElement.isSelected != selected) perform(webElement, ctxElement) { _.click() }
        if (webElement.isSelected != selected) jsClick(webElement)
        if (webElement.isSelected != selected) perform(webElement, ctxElement) { _.sendKeys(Keys.SPACE) }
    }
  }

  private def click(webElement: WebElement): Unit = {
    Try(webElement.click()) match {
      case Failure(e) =>
        Try(jsClick(webElement)) match {
          case Failure(e2) => throw e
          case _ =>
        }
      case _ =>
    }
  }

  private def jsClick(webElement: WebElement): Unit = {
    applyJS(jsFunctionWrapper("element", "arguments[0]", "element.click()"), webElement)
  }

  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.displayName}") { (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()
    }
  }

  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.displayName}") { (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`) {
          applyJS(jsFunctionWrapper("element", "arguments[0]", "element.focus()"), 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)
      }
      applyJS(jsFunctionWrapper("element", "arguments[0]", javascript), webElement)
    }
  }

  /**
    * 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 = {
    val reason = s"trying to $action ${binding.displayName}"
    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 =>
            clickCheckbox(webElement, Some(contextElement), true)
          case ElementAction.uncheck | ElementAction.untick =>
            clickCheckbox(webElement, Some(contextElement), false)
          case ElementAction.`move to` => perform(webElement, contextElement) { action => action }
          case _ => WebErrors.invalidContextActionError(action)
        }
      }
    }
  }

  private 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)
      }
    }
  }

  /**
    * Waits for text to appear in the given web element.
    *
    * @param binding the locator binding
    */
  def waitForText(binding: LocatorBinding): Boolean =
    getElementText(binding).map(_.length() > 0).getOrElse(false)

  /**
   * 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.displayName}") { 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
   * @param offset offset to scroll by (default is zero)
   */
  def scrollIntoView(webElement: WebElement, scrollTo: ScrollTo, offset: Int = 0): Unit = {
    applyJS(jsFunctionWrapper("elem", "arguments[0]", s"if (typeof elem !== 'undefined' && elem != null) { elem.scrollIntoView(${scrollTo == ScrollTo.top});${if (offset != 0) s" window.scroll(0, window.scrollY + $offset);" else ""}}"), webElement)
  }

  /**
   * Scrolls to the top of bottom of the current page.
   *
   * @param scrollTo top or bottom
   */
  def scrollPage(scrollTo: ScrollTo): Unit = {
    scrollTo.match {
      case ScrollTo.top => executeJS(s"window.scrollTo(0, 0)")
      case _ => executeJS(s"window.scrollTo(0, document.body.scrollHeight || document.documentElement.scrollHeight);")
    }
  }

  /**
    * 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 {
      DryValueBinding.unresolved("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
    *
    * @param binding the locator binding
    */
  def getElementText(binding: LocatorBinding): Option[String] =
    withWebElement(binding, s"trying to get ${binding.displayName} text") { 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 = applyJS(jsFunctionWrapper("element", "arguments[0]", "return element.innerText || element.textContent || ''"), webElement).asInstanceOf[String]
                  if (value != null) value else ""
                case Some(value) => value
              }
            case Some(value) => value
          }
        case Some(value) => value
      }
    } tap { value =>
      logger.debug(s"getElementText($binding)='$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.displayName}") { 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 { value =>
      logger.debug(s"getSelectedElementText($binding)='$value'")
    }
  }

  def getElementAttribute(binding: LocatorBinding, name: String): String = {
    evaluate(Some(DryValueBinding.unresolved(s"WebElementAttribute"))) {
      withWebElement(binding, s"trying to get $name attribute of ${binding.displayName}") { webElement => 
        Option(webElement.getAttribute(name)) getOrElse ""
      }
    } getOrElse ""
  }

   /**
    * 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.displayName}") { webElement =>
      getElementSelectionByJS(webElement, DropdownSelection.value) match {
        case None =>
          Try(createSelect(webElement)) map { select =>
            select.getAllSelectedOptions.asScala.map(_.getAttribute("value")).mkString(",")
          } getOrElse null
        case Some(value) => value
      }
    } tap { value =>
      logger.debug(s"getSelectedElementValue($binding)='$value'")
    }
  }

  private def getElementSelectionByJS(webElement: WebElement, by: DropdownSelection): Option[String] = {
    Option(applyJS(jsFunctionWrapper("select", "arguments[0]", s"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;}"), 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()
    }
  }

   /** Switches to the top window / first frame */
  def switchToFrame(binding: LocatorBinding): Unit = {
    withDriverAndElement(binding, s"trying to switch to ${binding.displayName} content (frame)") { (driver, frame) =>
      driver.switchTo().frame(frame)
    }
  }

  /** 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 =>
      SensitiveData.withValue(url) { u =>
        driver.get(u)
      }
    } (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 DryValueBinding.unresolved("popupMessage")
  }

  /** Checks if an element is displayed. */
  def isDisplayed(binding: LocatorBinding): Boolean = {
    Try {
      withDriverAndElement(binding, s"trying to locate ${binding.displayName}") { (driver, webElement) =>
        createActions(driver).moveToElement(webElement).perform()
        if (!isInViewport(webElement)) {
          Try(scrollIntoView(webElement, ScrollTo.top, -100))
        }
      }
    } isSuccess
  }

  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 = {
    applyJS(jsFunctionWrapper("elem", "arguments[0]", "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);"), webElement).asInstanceOf[Boolean]
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy