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

gwen.web.eval.driver.DriverManager.scala Maven / Gradle / Ivy

There is a newer version: 4.0.1
Show newest version
/*
 * 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.driver

import gwen.core._
import gwen.core.state.SensitiveData

import gwen.web.eval.DriverManagerImpl
import gwen.web.eval.WebBrowser
import gwen.web.eval.WebSettings
import gwen.web.eval.WebErrors
import gwen.web.eval.driver.event.WebSessionEventDispatcher
import gwen.web.eval.driver.event.WebSessionEventListener

import scala.jdk.CollectionConverters._
import scala.collection.mutable
import scala.util.chaining._

import com.typesafe.scalalogging.LazyLogging
import io.github.bonigarcia.wdm.WebDriverManager
import org.openqa.selenium.{Dimension, Capabilities, MutableCapabilities, WebDriver, WindowType}
import org.openqa.selenium.chrome.ChromeDriver
import org.openqa.selenium.chrome.ChromeDriverService
import org.openqa.selenium.chrome.ChromeOptions
import org.openqa.selenium.chromium.ChromiumOptions
import org.openqa.selenium.edge.EdgeDriver
import org.openqa.selenium.edge.EdgeOptions
import org.openqa.selenium.firefox.{FirefoxDriver, FirefoxOptions, FirefoxProfile}
import org.openqa.selenium.ie.{InternetExplorerDriver, InternetExplorerOptions}
import org.openqa.selenium.remote.HttpCommandExecutor
import org.openqa.selenium.remote.LocalFileDetector
import org.openqa.selenium.remote.RemoteWebDriver
import org.openqa.selenium.safari.{SafariDriver, SafariOptions}

import java.io.File
import java.net.URL
import java.{time => jt}
import java.util.concurrent.TimeUnit
import java.util.concurrent.Semaphore

/** Driver manager companion. */
object DriverManager extends LazyLogging {

  /** Semaphore to limit number of permitted web drivers to max threads setting. */
  private lazy val totalPermits = GwenSettings.`gwen.parallel.maxThreads`
  private lazy val driverPermits = new Semaphore(totalPermits, true)
  
  def acquireDriverPermit(): Unit = driverPermits.acquire()
  def releaseDriverPermit(): Unit = {
    if (driverPermits.availablePermits() < totalPermits) {
      driverPermits.release()
    }
  }

}

/**
  * Provides and manages access to the web drivers.
  */
class DriverManager() extends LazyLogging {

  /** The current web browser session. */
  private var session = "primary"

  /** Map of web driver instances (keyed by name). */
  private [eval] val drivers: mutable.Map[String, WebDriver] = mutable.Map()

  private val eventDispatcher = new WebSessionEventDispatcher()

  def addWebSessionEventListener(listener: WebSessionEventListener): Unit = {
    eventDispatcher.addListener(listener)
  }

  /** Provides private access to the web driver */
  private def webDriver: WebDriver = drivers.getOrElse(session, {
      val driver = retry {
        loadWebDriver
      }
      drivers += (session -> driver)
      driver
    })

  /** Resets the driver manager. */
  def reset(): Unit = {
    session = "primary"
  }

  /** Quits all browsers and closes the web drivers (if any have loaded). */
  def quit(): Unit = {
    drivers.keys.foreach(quit)
  }

  /** Quits a named browser and associated web driver instance. */
  def quit(name: String): Unit = {
    drivers.remove(name) foreach { driver =>
      try {
        logger.info(s"Closing browser session${ if(name == "primary") "" else s": $name"}")
        driver.quit()
        eventDispatcher.sessionClosed(driver)
      } finally {
        DriverManager.releaseDriverPermit()
      }
    }
    session = "primary"
  }

  def retry[T](body: => T): T = {
    def retry(attempts: Int): T = {
      try {
        body
      } catch {
        case e: Exception if attempts > 0 =>
          Thread.sleep((WebSettings.maxRetries + 1 - attempts) * 1000)
          retry(attempts - 1)
      }
    }
    retry(if (WebSettings.`gwen.web.remote.sessionRetries`) WebSettings.maxRetries else 0)
  }

   /**
    * Invokes a function that performs an operation on the current web driver.
    *
    * @param f the function to perform
    */
  def withWebDriver[T](f: WebDriver => T): T = {
      f(webDriver)
  }

  /** Loads the selenium webdriver. */
  private [eval] def loadWebDriver: WebDriver = withGlobalSettings {
    DriverManager.acquireDriverPermit()
    try {
      (WebSettings.`gwen.web.remote.url` match {
        case Some(addr) =>
          remoteDriver(addr)
        case None =>
          localDriver(WebSettings.`gwen.target.browser`)
      }) tap { driver =>
        eventDispatcher.sessionOpened(driver)
        WebSettings.`gwen.web.browser.size` foreach { case (width, height) =>
          logger.info(s"Resizing browser window to width $width and height $height")
          driver.manage().window().setSize(new Dimension(width, height))
        }
      }
    } catch {
      case e: Throwable =>
        DriverManager.releaseDriverPermit()
        throw e
    }
  }

  private def remoteDriver(addr: String): WebDriver = {
    val caps = WebSettings.`gwen.web.capabilities`
    val browserName = Option(caps.getBrowserName).map(_.trim).filter(_.nonEmpty).getOrElse(WebSettings.`gwen.target.browser`.toString).toLowerCase
    val browser = WebBrowser.parse(browserName)
    val options = browser match {
      case WebBrowser.firefox => firefoxOptions(true)
      case WebBrowser.chrome => chromeOptions(true)
      case WebBrowser.ie => ieOptions()
      case WebBrowser.edge => edgeOptions(true)
      case WebBrowser.safari => safariOptions()
    }
    logger.info(s"Starting remote $browser session${ if(session == "primary") "" else s": $session"}")
    remote(addr, options)
  }

  /**
    * Gets the local web driver for the given name.
    *
    *  @param browser the target browser
    */
  private def localDriver(browser: WebBrowser): WebDriver = {
    logger.info(s"Starting $browser browser session${ if(session == "primary") "" else s": $session"}")
    browser match {
      case WebBrowser.chrome => chrome()
      case WebBrowser.edge => edge()
      case WebBrowser.firefox => firefox()
      case WebBrowser.safari => safari()
      case WebBrowser.ie => ie()
    }
  }

  private def firefoxOptions(remote: Boolean) : FirefoxOptions = {
    val firefoxProfile = new FirefoxProfile() tap { profile =>
      if (!remote) {
        WebSettings.`gwen.web.firefox.prefs` foreach { case (name, value) =>
          try {
            logger.info(s"Setting firefox preference: $name=$value")
            profile.setPreference(name, Integer.valueOf(value.trim))
          } catch {
            case _: Throwable =>
              if (value.matches("(true|false)")) profile.setPreference(name, java.lang.Boolean.valueOf(value.trim))
              else profile.setPreference(name, value)
          }
        }
      }
      WebSettings.`gwen.web.useragent` foreach { agent =>
        logger.info(s"Setting firefox preference: general.useragent.override=$agent")
        profile.setPreference("general.useragent.override", agent)
      }
      if (WebSettings.`gwen.web.authorize.plugins`) {
        logger.info("Setting firefox preference: security.enable_java=true")
        profile.setPreference("security.enable_java", true)
        logger.info("Setting firefox preference: plugin.state.java=2")
        profile.setPreference("plugin.state.java", 2)
      }
      if (WebSettings.`gwen.web.suppress.images`) {
        logger.info("Setting firefox preference: permissions.default.image=2")
        profile.setPreference("permissions.default.image", 2)
      }
    }
    new FirefoxOptions()
      .setProfile(firefoxProfile) tap { options =>
        if (WebSettings.`gwen.web.browser.headless`) {
          logger.info(s"Setting firefox argument: -headless")
          options.addArguments("-headless")
        }
        WebSettings.`gwen.web.firefox.path` foreach { path =>
          logger.info(s"Setting firefox path: $path")
          options.setBinary(path)
        }
        setCapabilities(options)
      }
  }

  private def chromeOptions(remote: Boolean) : ChromeOptions = chromiumOptions(WebBrowser.chrome, remote, new ChromeOptions())

  private def edgeOptions(remote: Boolean) : EdgeOptions = chromiumOptions(WebBrowser.edge, remote, new EdgeOptions())

  private def chromiumOptions[T <: ChromiumOptions[T]](browser: WebBrowser, remote: Boolean, options: T): T = {
    WebSettings.`gwen.web.useragent` foreach { agent =>
      logger.info(s"Setting $browser argument: --user-agent=$agent")
      options.addArguments(s"--user-agent=$agent")
    }
    if (WebSettings.`gwen.web.authorize.plugins`) {
      logger.info(s"Setting $browser argument: --always-authorize-plugins")
      options.addArguments("--always-authorize-plugins")
    }
    options.addArguments("--enable-automation")
    val browserPath = browser match {
      case WebBrowser.chrome => WebSettings.`gwen.web.chrome.path`
      case _ => WebSettings.`gwen.web.edge.path`
    }
    browserPath foreach { path =>
      logger.info(s"Setting $browser path: $path")
      options.setBinary(path)
    }
    val browserArgs = browser match {
      case WebBrowser.chrome => WebSettings.`gwen.web.chrome.args`
      case _ => WebSettings.`gwen.web.edge.args`
    }
    browserArgs foreach { arg =>
      logger.info(s"Setting $browser argument: $arg")
      SensitiveData.withValue(arg) { a =>
        options.addArguments(a)
      }
    }
    if (WebSettings.`gwen.web.browser.headless`) {
      logger.info(s"Setting $browser argument: --headless=new")
      options.addArguments("--headless=new")
    }
    if (!remote) {
      val prefs = new java.util.HashMap[String, Object]()
      val browserPrefs = browser match {
        case WebBrowser.chrome => WebSettings.`gwen.web.chrome.prefs`
        case _ => WebSettings.`gwen.web.edge.prefs`
      }
      browserPrefs foreach { case (name, value) =>
        logger.info(s"Setting $browser preference: $name=$value")
        try {
          prefs.put(name, Integer.valueOf(value.trim))
        } catch {
          case _: Throwable =>
            if (value.matches("(true|false)")) prefs.put(name, java.lang.Boolean.valueOf(value.trim))
            else prefs.put(name, value)
        }
      }
      if (!prefs.isEmpty) {
        options.setExperimentalOption("prefs", prefs)
      }
    }
    val browserExensions = browser match {
      case WebBrowser.chrome => WebSettings.`gwen.web.chrome.extensions`
      case _ => WebSettings.`gwen.web.edge.extensions`
    }
    browserExensions tap { extensions =>
      if (extensions.nonEmpty) {
        logger.info(s"Loading $browser extension${if (extensions.size > 1) "s" else ""}: ${extensions.mkString(",")}")
        options.addExtensions(extensions*)
      }
    }
    val mobileSettings = browser match {
      case WebBrowser.chrome => WebSettings.`gwen.web.chrome.mobile`
      case _ => WebSettings.`gwen.web.edge.mobile`
    }
    if (mobileSettings.nonEmpty) {
      val mobileEmulation = new java.util.HashMap[String, Object]()
      mobileSettings.get("deviceName").fold({
        val deviceMetrics = new java.util.HashMap[String, Object]()
        mobileSettings foreach { case (name, value) =>
          name match {
            case "width" | "height" => deviceMetrics.put(name, java.lang.Integer.valueOf(value.trim))
            case "pixelRatio" => deviceMetrics.put(name, java.lang.Double.valueOf(value.trim))
            case "touch" => deviceMetrics.put(name, java.lang.Boolean.valueOf(value.trim))
            case _ => mobileEmulation.put(name, value)
          }
        }
        mobileEmulation.put("deviceMetrics", deviceMetrics)
      }) { (deviceName: String) =>
        mobileEmulation.put("deviceName", deviceName)
      }
      logger.info(s"$browser mobile emulation options: $mobileEmulation")
      options.setExperimentalOption("mobileEmulation", mobileEmulation)
    }
    setCapabilities(options)
    options
  }

  private def ieOptions(): InternetExplorerOptions = new InternetExplorerOptions() tap { options =>
    setDefaultCapability("requireWindowFocus", java.lang.Boolean.TRUE, options)
    setDefaultCapability("nativeEvents", java.lang.Boolean.TRUE, options);
    setDefaultCapability("unexpectedAlertBehavior", "accept", options);
    setDefaultCapability("ignoreProtectedModeSettings", java.lang.Boolean.TRUE, options);
    setDefaultCapability("disable-popup-blocking", java.lang.Boolean.TRUE, options);
    setDefaultCapability("enablePersistentHover", java.lang.Boolean.TRUE, options);
  }

  private def safariOptions(): SafariOptions = new SafariOptions() tap { options =>
    setCapabilities(options)
  }
  
  private def setCapabilities(capabilities: MutableCapabilities): Unit = {
    WebSettings.`gwen.web.capabilities`.asMap().asScala foreach { case (name, value) =>
      setCapability(name, value, capabilities)
    }
  }

  private def setDefaultCapability(name: String, value: Any, caps: MutableCapabilities): Unit = {
    if (caps.getCapability(name) == null) {
      setCapability(name, value, caps)
    }
  }

  private def setCapability(name: String, value: Any, caps: MutableCapabilities): Unit = {
    logger.info(s"Setting web capability: $name=$value")
    caps.setCapability(name, value)
  }

  private [eval] def chrome(): WebDriver = {
    if (DriverManagerImpl.isWebDriverManager && WebSettings.`webdriver.chrome.driver`.isEmpty) {
      WebDriverManager.chromedriver().setup()
    }
    new ChromeDriver(chromeOptions(false))
  }

  private [eval] def firefox(): WebDriver = {
    if (DriverManagerImpl.isWebDriverManager && WebSettings.`webdriver.gecko.driver`.isEmpty) {
      WebDriverManager.firefoxdriver().setup()
    }
    new FirefoxDriver(firefoxOptions(false))
  }

  private [eval] def ie(): WebDriver = {
    if (DriverManagerImpl.isWebDriverManager && WebSettings.`webdriver.ie.driver`.isEmpty) {
      WebDriverManager.iedriver().setup()
    }
    new InternetExplorerDriver(ieOptions())
  }

  private [eval] def edge(): WebDriver = {
    if (DriverManagerImpl.isWebDriverManager && WebSettings.`webdriver.edge.driver`.isEmpty) {
      WebDriverManager.edgedriver().setup()
    }
    new EdgeDriver(edgeOptions(false))
  }

  private [eval] def safari(): WebDriver = {
    new SafariDriver(safariOptions())
  }

  private [eval] def remote(hubUrl: String, capabilities: Capabilities): WebDriver =
    new RemoteWebDriver(new HttpCommandExecutor(new URL(hubUrl)), capabilities) tap { driver =>
      if (WebSettings`gwen.web.remote.localFileDetector`) {
        driver.setFileDetector(new LocalFileDetector())
      }
    }

  private def withGlobalSettings(driver: WebDriver): WebDriver = {
    logger.info(s"Implicit wait (default locator timeout) = ${WebSettings.`gwen.web.locator.wait.seconds`} second(s)")
    driver.manage().timeouts().implicitlyWait(jt.Duration.ofSeconds(WebSettings.`gwen.web.locator.wait.seconds`))
    if (WebSettings.`gwen.web.maximize`) {
      logger.info(s"Attempting to maximize window")
      try {
        driver.manage().window().maximize()
      } catch {
        case _: Throwable =>
          logger.warn(s"Maximizing window not supported on current platform, attempting to go full screen instead")
          try {
            driver.manage().window().fullscreen()
          } catch {
            case _: Throwable =>
              logger.warn(s"Could not maximise or go full screen on current platform")
          }
      }
    }
    driver
  }

  /**
    * Switches to a tab or window occurance.
    *
    * @param occurrence the tag or window occurrence to switch to (primary is zero, 1st opened is 1, 2nd open is 2, ..)
    */
  def switchToWindow(occurrence: Int): Unit = {
    windows().lift(occurrence) map { handle =>
      switchToWindow(handle, isChild = (occurrence > 0))
    } getOrElse {
      WebErrors.noSuchWindowError(s"Cannot switch to window $occurrence: no such occurrence")
    }
  }

  /** Switches the driver to another tab or window.
    *
    * @param handle the handle of the window to switch to
    * @param isChild true if switching to a child window; false for parent window
    */
  private def switchToWindow(handle: String, isChild: Boolean): Unit = {
    logger.info(s"Switching to ${if (isChild) "child" else "parent"} window ($handle)")
    drivers.get(session).fold(WebErrors.noSuchWindowError("Cannot switch to window: no windows currently open")) { _.switchTo.window(handle) }
  }

  def closeChild(): Unit = {
    windows() match {
      case parent::children if children.nonEmpty =>
        val child = children.last
        webDriver.switchTo.window(child)
        logger.info(s"Closing child window ($child)")
        webDriver.close()
        switchToParent()
      case _ =>
        WebErrors.noSuchWindowError("Cannot close child window: currently at root window which has no child")
    }
  }

  def closeWindow(occurrence: Int): Unit = {
    windows().lift(occurrence) map { handle =>
      switchToWindow(handle, isChild = (occurrence > 0))
      logger.info(s"Closing window occurrence $occurrence ($handle)")
      webDriver.close()
      switchToParent()
    } getOrElse {
      WebErrors.noSuchWindowError(s"Cannot close window $occurrence: no such occurrence")
    }
  }

  /** Switches to the parent window. */
  def switchToParent(): Unit = {
    windows() match {
      case parent::_ =>
        switchToWindow(parent, isChild = false)
      case _ =>
        logger.warn("Bypassing switch to parent window: no child windows open")
    }
  }

  /** Switches to the top window / first frame */
  def switchToDefaultContent(): Unit = {
    webDriver.switchTo().defaultContent()
  }

  /**
    * Switches the web driver session
    *
    * @param session the name of the session to switch to
    */
  def switchToSession(session: String): Unit = {
    logger.info(s"Switching to browser session: $session")
    this.session = session
    webDriver
  }

  /**
    * Starts and switches to a new tab or window.
    *
    * @param winType tab or window
    */
  def switchToNewWindow(winType: WindowType): Unit = {
    logger.info(s"Switching to new browser ${winType.name.toLowerCase}")
    if (noOfSessions() > 0) {
      webDriver.switchTo().newWindow(winType)
    } else {
      switchToSession("primary")
    }
  }

  /** Gets the number of open sesions. */
  def noOfSessions(): Int = drivers.size

  /** Gets the number of open windows. */
  def noOfWindows(): Int = windows().size

  /**
    * Starts a new session if there isn't one or stays in the current one.
    */
  def newOrCurrentSession(): Unit = {
    if (noOfSessions() == 0) {
      switchToSession("primary")
    }
  }

  def windows(): List[String] = withWebDriver(_.getWindowHandles.asScala.toList)

  def getSessionId: Option[String] = {
    withWebDriver { getSessionId }
  }

  def getSessionId(driver: WebDriver): Option[String] = {
    if (driver.isInstanceOf[RemoteWebDriver]) {
      Some(driver.asInstanceOf[RemoteWebDriver].getSessionId.toString)
    } else {
      None
    }
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy