diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4063397..7f1321c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,10 +12,10 @@ jobs: strategy: fail-fast: false matrix: - scalaversion: ["2.11.12", "2.12.10", "2.13.1"] + scalaversion: ["2.12.20", "2.13.15"] browser: ["chrome"] include: - - scalaversion: "2.12.10" + - scalaversion: "2.12.20" browser: "firefox" env: SJS_TEST_BROWSER: ${{ matrix.browser }} @@ -23,16 +23,16 @@ jobs: - uses: actions/checkout@v2 - uses: olafurpg/setup-scala@v10 with: - java-version: "adopt@1.8" + java-version: "adopt@1.11" - uses: coursier/cache-action@v5 - name: Scalastyle run: > sbt "++${{ matrix.scalaversion }}" seleniumJSEnv/scalastyle - seleniumJSEnv/test:scalastyle + seleniumJSEnv/Test/scalastyle seleniumJSEnvTest/scalastyle - seleniumJSHttpEnvTest/test:scalastyle - seleniumJSEnvTest/test:scalastyle + seleniumJSHttpEnvTest/Test/scalastyle + seleniumJSEnvTest/Test/scalastyle - name: MiMa run: sbt "++${{ matrix.scalaversion }}" seleniumJSEnv/mimaReportBinaryIssues - name: Unit tests diff --git a/README.md b/README.md index bcab6df..68bb8ab 100644 --- a/README.md +++ b/README.md @@ -5,34 +5,30 @@ ## Usage Simply add the following line to your `project/plugins.sbt` (note that this line must be placed before `addSbtPlugin("org.scala-js" % "sbt-scalajs" % )`; otherwise you may get errors such as `java.lang.NoSuchMethodError: com.google.common.base.Preconditions.checkState` when you run tests): ```scala -// For Scala.js 0.6.x -libraryDependencies += "org.scala-js" %% "scalajs-env-selenium" % "0.3.0" - -// For Scala.js 1.x -libraryDependencies += "org.scala-js" %% "scalajs-env-selenium" % "1.1.1" +libraryDependencies += "org.scala-js" %% "scalajs-env-selenium" % "2.0.0" ``` and the following line to your sbt settings: ```scala // Apply to the 'run' command -jsEnv := new org.scalajs.jsenv.selenium.SeleniumJSEnv(capabilities) +jsEnv := new org.scalajs.jsenv.selenium.SeleniumJSEnv(driverFactory) // Apply to tests -jsEnv in Test := new org.scalajs.jsenv.selenium.SeleniumJSEnv(capabilities) +(Test / jsEnv) := new org.scalajs.jsenv.selenium.SeleniumJSEnv(driverFactory) +``` +where `driverFactory` is an implementation of the `org.scalajs.jsenv.selenium.DriverFactory` function type: + +```scala +type DriverFactory = () => WebDriver ``` -where `capabilities` is one of the members of -[`org.openqa.selenium.remote.DesiredCapabilities`]( -https://seleniumhq.github.io/selenium/docs/api/java/org/openqa/selenium/remote/DesiredCapabilities.html). For example for Firefox: ```scala jsEnv := new org.scalajs.jsenv.selenium.SeleniumJSEnv( - new org.openqa.selenium.firefox.FirefoxOptions()) + () => new org.openqa.selenium.firefox.FirefoxDriver()) ``` -You are responsible for installing the [drivers]( -http://docs.seleniumhq.org/download/#thirdPartyDrivers) needed for the browsers -you request. +Selenium will download the appropriate binaries for you based on the driver your factory creates. When executing the program with `run` a new browser window will be created, the code will be executed in it and finally the browser will close itself. @@ -44,7 +40,7 @@ If you wish to keep the browser window opened after the execution has terminated add the option `withKeepAlive` on the environment: ``` scala -new SeleniumJSEnv(capabilities, SeleniumJSEnv.Config().withKeepAlive(true)) +new SeleniumJSEnv(driverFactory, SeleniumJSEnv.Config().withKeepAlive(true)) ``` It is recommend to use this with a `run` and not `test` because the latter tends @@ -53,14 +49,38 @@ to leave too many browser windows open. #### Debugging tests on a single window By default tests are executed in their own window for parallelism. When debugging tests with `withKeepAlive` it is possible to disable this option -using the `sbt` setting `parallelExecution in Test := false`. +using the `sbt` setting `(Test / parallelExecution) := false`. ### Headless Usage It is often desirable to run Selenium headlessly. This could be to run tests on a server without graphics, or to just prevent browser windows popping up when running locally. -##### xvfb +#### Driver options +**Note:** the availability and behavior of this configuration depends on the driver you choose. See the [noteworthy post from Selenium developers themselves][selenium-headless-going-away]. + +When creating a driver in your `DriverFactory`, you can provide additional options to specify behavior +such as headless mode. + +For example, when using Firefox: + +```scala +new FirefoxDriver( + new FirefoxOptions() + .addArguments("--headless") +) +``` + +For Chrome: + +```scala +new ChromeDriver( + new ChromeOptions() + .addArguments("--headless") +) +``` + +#### xvfb A common approach on Linux and Mac OSX, is to use `xvfb`, "X Virtual FrameBuffer". It starts an X server headlessly, without the need for a graphics driver. @@ -82,3 +102,5 @@ long as there isn't another X server running that's already associated with it. ## Contributing Follow the [contributing guide](./CONTRIBUTING.md). + +[selenium-headless-going-away]: https://www.selenium.dev/blog/2023/headless-is-going-away/ diff --git a/build.sbt b/build.sbt index e6cde68..867d282 100644 --- a/build.sbt +++ b/build.sbt @@ -5,18 +5,19 @@ import org.scalajs.sbtplugin.ScalaJSCrossVersion import org.openqa.selenium.Capabilities import org.scalajs.jsenv.selenium.SeleniumJSEnv -import org.scalajs.jsenv.selenium.TestCapabilities +import org.scalajs.jsenv.selenium.TestDrivers -val previousVersion: Option[String] = Some("1.1.1") +// we're breaking bincompat with the bump to selenium 4 +val previousVersion: Option[String] = None val newScalaBinaryVersionsInThisRelease: Set[String] = Set() val commonSettings: Seq[Setting[_]] = Seq( - version := "1.1.2-SNAPSHOT", + version := "2.0.0-SNAPSHOT", organization := "org.scala-js", - scalaVersion := "2.11.12", - crossScalaVersions := Seq("2.11.12", "2.12.10", "2.13.1"), + scalaVersion := "2.12.20", + crossScalaVersions := Seq("2.12.20", "2.13.15"), scalacOptions ++= Seq("-deprecation", "-feature", "-Xfatal-warnings"), homepage := Some(url("http://scala-js.org/")), @@ -55,12 +56,8 @@ val previousArtifactSetting = Def.settings( } ) -val jsEnvCapabilities = settingKey[org.openqa.selenium.Capabilities]( - "Capabilities of the SeleniumJSEnv") - val testSettings: Seq[Setting[_]] = commonSettings ++ Seq( - jsEnvCapabilities := TestCapabilities.fromEnv, - jsEnv := new SeleniumJSEnv(jsEnvCapabilities.value), + jsEnv := new SeleniumJSEnv(TestDrivers.fromEnv), scalaJSUseMainModuleInitializer := true ) @@ -77,7 +74,7 @@ lazy val seleniumJSEnv: Project = project. * It pulls in "closure-compiler-java-6" which in turn bundles some old * guava stuff which in turn makes selenium fail. */ - "org.seleniumhq.selenium" % "selenium-server" % "3.141.59", + "org.seleniumhq.selenium" % "selenium-java" % "4.25.0", "org.scala-js" %% "scalajs-js-envs" % "1.1.1", "com.google.jimfs" % "jimfs" % "1.1", "org.scala-js" %% "scalajs-js-envs-test-kit" % "1.1.1" % "test", @@ -117,7 +114,7 @@ lazy val seleniumJSEnv: Project = project. pomIncludeRepository := { _ => false }, // The chrome driver seems to not deal with parallelism very well (#47). - parallelExecution in Test := false + (Test / parallelExecution) := false ) lazy val seleniumJSEnvTest: Project = project. @@ -132,7 +129,7 @@ lazy val seleniumJSHttpEnvTest: Project = project. settings( jsEnv := { new SeleniumJSEnv( - jsEnvCapabilities.value, + TestDrivers.fromEnv, SeleniumJSEnv.Config() .withMaterializeInServer("tmp", "http://localhost:8080/tmp/") ) diff --git a/project/build.properties b/project/build.properties index c0bab04..0b699c3 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.2.8 +sbt.version=1.10.2 diff --git a/project/build.sbt b/project/build.sbt index 147d851..8e03d9d 100644 --- a/project/build.sbt +++ b/project/build.sbt @@ -1,6 +1,6 @@ -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.2.0") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.17.0") -addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "0.1.18") +addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.4") addSbtPlugin("org.scalastyle" % "scalastyle-sbt-plugin" % "1.0.0") @@ -9,14 +9,14 @@ addSbtPlugin("org.scalastyle" % "scalastyle-sbt-plugin" % "1.0.0") * guava stuff which in turn makes selenium fail. */ libraryDependencies ~= - ("org.seleniumhq.selenium" % "selenium-server" % "3.141.59" +: _) + ("org.seleniumhq.selenium" % "selenium-java" % "4.25.0" +: _) -unmanagedSourceDirectories in Compile ++= { +Compile / unmanagedSourceDirectories ++= { val root = baseDirectory.value.getParentFile Seq(root / "seleniumJSEnv/src/main/scala") } -sources in Compile += { +Compile / sources += { val root = baseDirectory.value.getParentFile - root / "seleniumJSEnv/src/test/scala/org/scalajs/jsenv/selenium/TestCapabilities.scala" + root / "seleniumJSEnv/src/test/scala/org/scalajs/jsenv/selenium/TestDrivers.scala" } diff --git a/seleniumJSEnv/src/main/scala/org/scalajs/jsenv/selenium/SeleniumJSEnv.scala b/seleniumJSEnv/src/main/scala/org/scalajs/jsenv/selenium/SeleniumJSEnv.scala index a2f1179..7ecd0e1 100644 --- a/seleniumJSEnv/src/main/scala/org/scalajs/jsenv/selenium/SeleniumJSEnv.scala +++ b/seleniumJSEnv/src/main/scala/org/scalajs/jsenv/selenium/SeleniumJSEnv.scala @@ -1,25 +1,22 @@ package org.scalajs.jsenv.selenium -import org.openqa.selenium._ -import org.openqa.selenium.remote.DesiredCapabilities -import org.openqa.selenium.remote.server._ import org.scalajs.jsenv._ import java.net.URL import java.nio.file.{Path, Paths} +import org.openqa.selenium.Capabilities +import org.openqa.selenium.remote.DesiredCapabilities +import org.openqa.selenium.WebDriver +import org.openqa.selenium.JavascriptExecutor +import org.openqa.selenium.Platform +import SeleniumJSEnv.DriverFactory -final class SeleniumJSEnv(capabilities: Capabilities, config: SeleniumJSEnv.Config) extends JSEnv { - def this(capabilities: Capabilities) = - this(capabilities, SeleniumJSEnv.Config()) +final class SeleniumJSEnv(driverFactory: DriverFactory, config: SeleniumJSEnv.Config) extends JSEnv { - private val augmentedCapabilities = { - val x = new DesiredCapabilities(capabilities) - x.setJavascriptEnabled(true) - x - } + def this(driverFactory: DriverFactory) = this(driverFactory, SeleniumJSEnv.Config()) - val name: String = s"SeleniumJSEnv ($capabilities)" + val name: String = s"SeleniumJSEnv ($config)" def start(input: Seq[Input], runConfig: RunConfig): JSRun = SeleniumRun.start(newDriver _, input, config, runConfig) @@ -29,7 +26,7 @@ final class SeleniumJSEnv(capabilities: Capabilities, config: SeleniumJSEnv.Conf private def newDriver() = { val driver: WebDriver = - config.driverFactory.newInstance(augmentedCapabilities) + driverFactory() /* The first `asInstanceOf`s are a fail-fast for the second one, which * scalac partially erases, so that we're sure right now that the last @@ -46,8 +43,9 @@ final class SeleniumJSEnv(capabilities: Capabilities, config: SeleniumJSEnv.Conf } object SeleniumJSEnv { + type DriverFactory = () => WebDriver + final class Config private ( - val driverFactory: DriverFactory, val keepAlive: Boolean, val materialization: Config.Materialization ) { @@ -55,8 +53,7 @@ object SeleniumJSEnv { private def this() = this( keepAlive = false, - materialization = Config.Materialization.Temp, - driverFactory = new DefaultDriverFactory(Platform.getCurrent())) + materialization = Config.Materialization.Temp) /** Materializes purely virtual files into a temp directory. * @@ -113,13 +110,9 @@ object SeleniumJSEnv { def withKeepAlive(keepAlive: Boolean): Config = copy(keepAlive = keepAlive) - def withDriverFactory(driverFactory: DriverFactory): Config = - copy(driverFactory = driverFactory) - private def copy(keepAlive: Boolean = keepAlive, - materialization: Config.Materialization = materialization, - driverFactory: DriverFactory = driverFactory) = { - new Config(driverFactory, keepAlive, materialization) + materialization: Config.Materialization = materialization) = { + new Config(keepAlive, materialization) } } diff --git a/seleniumJSEnv/src/test/scala/org/scalajs/jsenv/selenium/KeepAliveTest.scala b/seleniumJSEnv/src/test/scala/org/scalajs/jsenv/selenium/KeepAliveTest.scala index a05c985..c1afdf4 100644 --- a/seleniumJSEnv/src/test/scala/org/scalajs/jsenv/selenium/KeepAliveTest.scala +++ b/seleniumJSEnv/src/test/scala/org/scalajs/jsenv/selenium/KeepAliveTest.scala @@ -7,12 +7,12 @@ import java.net.URL import org.openqa.selenium._ import org.openqa.selenium.remote.DesiredCapabilities -import org.openqa.selenium.remote.server._ import org.junit._ import org.junit.Assert._ import org.scalajs.jsenv._ +import org.scalajs.jsenv.selenium.SeleniumJSEnv.DriverFactory class KeepAliveTest { private final class MockWebDriver extends WebDriver with JavascriptExecutor { @@ -56,23 +56,19 @@ class KeepAliveTest { private final class MockInjector(driver: WebDriver) extends DriverFactory { var used = false - def newInstance(caps: Capabilities): WebDriver = { + def apply(): WebDriver = { require(!used) used = true driver } - - def hasMappingFor(caps: Capabilities): Boolean = true - def registerDriverProvider(p: DriverProvider): Unit = ??? } private def setup(keepAlive: Boolean) = { val driver = new MockWebDriver val factory = new MockInjector(driver) val config = SeleniumJSEnv.Config() - .withDriverFactory(factory) .withKeepAlive(keepAlive) - val env = new SeleniumJSEnv(new DesiredCapabilities, config) + val env = new SeleniumJSEnv(factory, config) (driver, factory, env) } diff --git a/seleniumJSEnv/src/test/scala/org/scalajs/jsenv/selenium/SeleniumJSEnvSuite.scala b/seleniumJSEnv/src/test/scala/org/scalajs/jsenv/selenium/SeleniumJSSuite.scala similarity index 68% rename from seleniumJSEnv/src/test/scala/org/scalajs/jsenv/selenium/SeleniumJSEnvSuite.scala rename to seleniumJSEnv/src/test/scala/org/scalajs/jsenv/selenium/SeleniumJSSuite.scala index ec321fd..dfc18a2 100644 --- a/seleniumJSEnv/src/test/scala/org/scalajs/jsenv/selenium/SeleniumJSEnvSuite.scala +++ b/seleniumJSEnv/src/test/scala/org/scalajs/jsenv/selenium/SeleniumJSSuite.scala @@ -9,8 +9,10 @@ import org.junit.runner.Runner import org.junit.runners.Suite import org.junit.runner.manipulation.Filter import org.junit.runner.Description +import org.openqa.selenium.firefox.FirefoxDriver +import org.openqa.selenium.firefox.FirefoxOptions @RunWith(classOf[JSEnvSuiteRunner]) class SeleniumJSSuite extends JSEnvSuite( - JSEnvSuiteConfig(new SeleniumJSEnv(TestCapabilities.fromEnv)) + JSEnvSuiteConfig(new SeleniumJSEnv(TestDrivers.fromEnv)) ) diff --git a/seleniumJSEnv/src/test/scala/org/scalajs/jsenv/selenium/TestCapabilities.scala b/seleniumJSEnv/src/test/scala/org/scalajs/jsenv/selenium/TestDrivers.scala similarity index 52% rename from seleniumJSEnv/src/test/scala/org/scalajs/jsenv/selenium/TestCapabilities.scala rename to seleniumJSEnv/src/test/scala/org/scalajs/jsenv/selenium/TestDrivers.scala index 726e06b..b406a3a 100644 --- a/seleniumJSEnv/src/test/scala/org/scalajs/jsenv/selenium/TestCapabilities.scala +++ b/seleniumJSEnv/src/test/scala/org/scalajs/jsenv/selenium/TestDrivers.scala @@ -5,20 +5,28 @@ import org.openqa.selenium.firefox.{FirefoxOptions, FirefoxDriverLogLevel} import org.openqa.selenium.chrome.ChromeOptions import java.util.logging.{Logger, Level} +import org.openqa.selenium.WebDriver +import org.openqa.selenium.firefox.FirefoxDriver +import org.openqa.selenium.chrome.ChromeDriver +import org.scalajs.jsenv.selenium.SeleniumJSEnv.DriverFactory -object TestCapabilities { +object TestDrivers { // Lower the logging level for Selenium to avoid spam. Logger.getLogger("org.openqa.selenium").setLevel(Level.WARNING) - def fromEnv: Capabilities = nameFromEnv match { + val fromEnv: DriverFactory = () => nameFromEnv match { case "firefox" => - new FirefoxOptions() - .setHeadless(true) - .setLogLevel(FirefoxDriverLogLevel.ERROR) + new FirefoxDriver( + new FirefoxOptions() + .addArguments("--headless") + .setLogLevel(FirefoxDriverLogLevel.ERROR) + ) case "chrome" => - new ChromeOptions() - .setHeadless(true) + new ChromeDriver( + new ChromeOptions() + .addArguments("--headless") + ) case name => throw new IllegalArgumentException(s"Unknown browser $name")