diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a3920f7de..6f3ce0146 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,17 +21,11 @@ jobs: - name: Setup Scala uses: japgolly/setup-everything-scala@v1.0 - - name: Build - run: sbt -DCI=1 "++${{ matrix.scalaversion }}" package - - - name: Test generate documentation - run: sbt -DCI=1 "++${{ matrix.scalaversion }}" doc - - - name: Build examples - run: sbt -DCI=1 "++${{ matrix.scalaversion }}" example/compile + - name: Build and test + run: sbt -DCI=1 "++${{ matrix.scalaversion }}" test package doc - name: Validate formatting - run: sbt -DCI=1 "++${{ matrix.scalaversion }}" scalafmtCheck + run: sbt -DCI=1 "++${{ matrix.scalaversion }}" dom/scalafmtCheck - name: Validate api report if: matrix.scalaversion != '2.11.12' && matrix.scalaversion != '3.0.1' diff --git a/build.sbt b/build.sbt index 0d18f1e6b..6f7a9e27e 100644 --- a/build.sbt +++ b/build.sbt @@ -4,8 +4,16 @@ ThisBuild / organization := "org.scala-js" ThisBuild / shellPrompt := ((s: State) => Project.extract(s).currentRef.project + "> ") ThisBuild / versionScheme := Some("early-semver") -val root = Build.root -val scalafixRules = Build.scalafixRules -val dom = Build.dom -val example = Build.example -val readme = Build.readme +val root = Build.root +val scalafixRules = Build.scalafixRules +val dom = Build.dom +val testsShared = Build.testsShared +val testsWebworker = Build.testsWebworker +val testsChrome = Build.testsChrome +val testsFirefox = Build.testsFirefox +val testsNodeJsdom = Build.testsNodeJsdom +val example = Build.example +val readme = Build.readme + +// TODO: Remove after dom project get it's own directory +Global / onLoad ~= (_.andThen("project root" :: _)) diff --git a/prePR.sbt b/prePR.sbt index 515fae1d5..d5ecb479a 100644 --- a/prePR.sbt +++ b/prePR.sbt @@ -4,8 +4,21 @@ addCommandAlias("prePR", "+prePR_nonCross") val prePR_nonCross = taskKey[Unit]("Performs all necessary work required before submitting a PR, for a single version of Scala.") +// Unfortunately we can't just call `root/Test/compile` because it doesn't take aggregation into account :( ThisBuild / prePR_nonCross := Def.sequential( - root / clean, + + Def.task { + (root / clean).value + (scalafixRules / clean).value + (dom / clean).value + (testsShared / clean).value + (testsWebworker / clean).value + (testsChrome / clean).value + (testsFirefox / clean).value + (testsNodeJsdom / clean).value + (example / clean).value + }, + dom / Compile / scalafmt, Def.taskDyn { if (scalaVersion.value.startsWith("2.")) @@ -13,5 +26,21 @@ ThisBuild / prePR_nonCross := Def.sequential( else Def.task[Unit]((dom / Compile / compile).value) }, - root / Compile / compile, + + Def.task { + (testsShared / Test / compile).value + (testsWebworker / Test / compile).value + (testsChrome / Test / compile).value + (testsFirefox / Test / compile).value + (testsNodeJsdom / Test / compile).value + (example / Test / compile).value + }, + + Def.taskDyn { + if (scalaVersion.value.startsWith("2.12.")) + Def.task[Unit]((readme / Compile / compile).value) + else + Def.task(()) + }, + ).value diff --git a/project/Build.scala b/project/Build.scala index de90efa55..bf249b7d4 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1,7 +1,17 @@ import sbt._ import sbt.Keys._ +import java.util.concurrent.TimeUnit +import org.openqa.selenium.{Capabilities, WebDriver} +import org.openqa.selenium.chrome.{ChromeDriver, ChromeOptions} +import org.openqa.selenium.firefox.{FirefoxOptions, FirefoxProfile} +import org.openqa.selenium.remote.server.{DriverFactory, DriverProvider} +import org.scalajs.jsenv.jsdomnodejs.JSDOMNodeJSEnv +import org.scalajs.jsenv.selenium.SeleniumJSEnv +import org.scalajs.sbtplugin.ScalaJSJUnitPlugin import org.scalajs.sbtplugin.ScalaJSPlugin import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._ +import sbtbuildinfo.BuildInfoPlugin +import sbtbuildinfo.BuildInfoPlugin.autoImport._ import scalafix.sbt.ScalafixPlugin import scalafix.sbt.ScalafixPlugin.autoImport._ import scalatex.ScalatexReadme @@ -21,6 +31,11 @@ object Build { .aggregate( scalafixRules, dom, + testsShared, + testsWebworker, + testsChrome, + testsFirefox, + testsNodeJsdom, example, // readme, // This is a Scala 2.12 only module ) @@ -43,6 +58,85 @@ object Build { libraryDependencies += Dep.scalafixCore.value, ) + lazy val testsShared = project + .in(file("tests-shared")) + .dependsOn(dom) + .enablePlugins(ScalaJSPlugin, ScalaJSJUnitPlugin) + .configure(commonSettings, crossScala, preventPublication, moveTestLibsToCompile) + + lazy val testsWebworker = project + .in(file("tests-webworker")) + .dependsOn(testsShared) + .enablePlugins(ScalaJSPlugin, ScalaJSJUnitPlugin, BuildInfoPlugin) + .configure(commonSettings, crossScala, preventPublication, moveTestLibsToCompile) + .settings( + buildInfoKeys := Seq[BuildInfoKey]( + "wwJsPath" -> (Compile / fastOptJS / artifactPath).value.absolutePath, + ), + buildInfoPackage := "org.scalajs.dom.tests.webworker", + scalaJSUseMainModuleInitializer := true, + ) + + def testsWebworkers: Project => Project = _ + .dependsOn(testsWebworker) + .settings( + Test / test := { + val _ = (testsWebworker / Compile / fastOptJS).value + (Test / test).value + }, + ) + + lazy val testsChrome = project + .in(file("tests-chrome")) + .dependsOn(testsShared % Test) + .enablePlugins(ScalaJSPlugin, ScalaJSJUnitPlugin) + .configure(commonSettings, crossScala, preventPublication, testsWebworkers) + .settings( + Test / jsEnv := { + System.setProperty("webdriver.chrome.silentOutput", "true") + val o = new ChromeOptions() + o.setHeadless(true) + o.addArguments("--allow-file-access-from-files") + val df = new DriverFactory { + private[this] val default = SeleniumJSEnv.Config().driverFactory + override def newInstance(c: Capabilities): WebDriver = { + val d = default.newInstance(c).asInstanceOf[ChromeDriver] + d.manage.timeouts.pageLoadTimeout(if (inCI) 10 else 1, TimeUnit.MINUTES) + d.manage.timeouts.setScriptTimeout(if (inCI) 10 else 1, TimeUnit.MINUTES) + d + } + override def registerDriverProvider(p: DriverProvider): Unit = + default.registerDriverProvider(p) + } + new SeleniumJSEnv(o, SeleniumJSEnv.Config().withDriverFactory(df)) + }, + ) + + lazy val testsFirefox = project + .in(file("tests-firefox")) + .dependsOn(testsShared % Test) + .enablePlugins(ScalaJSPlugin, ScalaJSJUnitPlugin) + .configure(commonSettings, crossScala, preventPublication, testsWebworkers) + .settings( + Test / jsEnv := { + val p = new FirefoxProfile() + p.setPreference("privacy.file_unique_origin", false) + val o = new FirefoxOptions() + o.setProfile(p) + o.setHeadless(true) + new SeleniumJSEnv(o) + }, + ) + + lazy val testsNodeJsdom = project + .in(file("tests-node-jsdom")) + .dependsOn(testsShared % Test) + .enablePlugins(ScalaJSPlugin, ScalaJSJUnitPlugin) + .configure(commonSettings, crossScala, preventPublication) + .settings( + Test / jsEnv := new JSDOMNodeJSEnv, + ) + lazy val example = project .dependsOn(dom) .enablePlugins(ScalaJSPlugin) diff --git a/project/Lib.scala b/project/Lib.scala index 26727423e..9ff78c465 100644 --- a/project/Lib.scala +++ b/project/Lib.scala @@ -1,5 +1,6 @@ import sbt._ import sbt.Keys._ +import org.scalajs.sbtplugin.ScalaJSJUnitPlugin import Dependencies._ object Lib { @@ -22,6 +23,7 @@ object Lib { case Some((2, 13)) => "-Wunused:imports,patvars,locals,implicits" :: Nil case _ => Nil }), + testOptions += Tests.Argument(TestFramework("com.novocode.junit.JUnitFramework"), "-v"), ) def crossScala: Project => Project = _ @@ -100,4 +102,13 @@ object Lib { else sourceDir / "scala-old-collections" + def moveTestLibsToCompile: Project => Project = + _.settings( + libraryDependencies ~= { _.map(m => + if (m.configurations.contains(Test.name)) + m.withConfigurations(Some(Compile.name)) + else + m + )}, + ) } diff --git a/project/plugins.sbt b/project/plugins.sbt index 51e7424b4..4cc950a7a 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,4 +1,8 @@ +libraryDependencies += "org.scala-js" %% "scalajs-env-jsdom-nodejs" % "1.1.0" +libraryDependencies += "org.scala-js" %% "scalajs-env-selenium" % "1.1.1" + addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.30") +addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.10.0") addSbtPlugin("com.geirsson" % "sbt-ci-release" % "1.5.7") addSbtPlugin("com.lihaoyi" % "scalatex-sbt-plugin" % "0.3.11") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.5.1") diff --git a/tests-chrome/src/test/scala/org/scalajs/dom/tests/chrome/ChromeTests.scala b/tests-chrome/src/test/scala/org/scalajs/dom/tests/chrome/ChromeTests.scala new file mode 100644 index 000000000..9a4e5e267 --- /dev/null +++ b/tests-chrome/src/test/scala/org/scalajs/dom/tests/chrome/ChromeTests.scala @@ -0,0 +1,6 @@ +package org.scalajs.dom.tests.chrome + +import org.scalajs.dom.tests.shared._ +import org.scalajs.dom.tests.webworker._ + +class ChromeTests extends SharedTests with WebWorkerTests \ No newline at end of file diff --git a/tests-firefox/src/test/scala/org/scalajs/dom/tests/firefox/FirefoxTests.scala b/tests-firefox/src/test/scala/org/scalajs/dom/tests/firefox/FirefoxTests.scala new file mode 100644 index 000000000..1a8735389 --- /dev/null +++ b/tests-firefox/src/test/scala/org/scalajs/dom/tests/firefox/FirefoxTests.scala @@ -0,0 +1,6 @@ +package org.scalajs.dom.tests.firefox + +import org.scalajs.dom.tests.shared._ +import org.scalajs.dom.tests.webworker._ + +class FirefoxTests extends SharedTests with WebWorkerTests \ No newline at end of file diff --git a/tests-node-jsdom/src/test/scala/org/scalajs/dom/tests/node/jsdom/NodeJsdomTests.scala b/tests-node-jsdom/src/test/scala/org/scalajs/dom/tests/node/jsdom/NodeJsdomTests.scala new file mode 100644 index 000000000..47d280102 --- /dev/null +++ b/tests-node-jsdom/src/test/scala/org/scalajs/dom/tests/node/jsdom/NodeJsdomTests.scala @@ -0,0 +1,5 @@ +package org.scalajs.dom.tests.node.jsdom + +import org.scalajs.dom.tests.shared._ + +class NodeJsdomTests extends SharedTests diff --git a/tests-shared/src/main/scala/org/scalajs/dom/tests/shared/AsyncTesting.scala b/tests-shared/src/main/scala/org/scalajs/dom/tests/shared/AsyncTesting.scala new file mode 100644 index 000000000..2a1646097 --- /dev/null +++ b/tests-shared/src/main/scala/org/scalajs/dom/tests/shared/AsyncTesting.scala @@ -0,0 +1,37 @@ +package org.scalajs.dom.tests.shared + +import org.junit.Assert +import scala.concurrent._ +import scala.util._ +import scala.scalajs.js.timers._ + +object AsyncTesting { + + type AsyncResult = Future[Try[Unit]] + + implicit def global: ExecutionContext = + ExecutionContext.global + + def async(run: => Future[Any]): AsyncResult = { + val p = Promise[Try[Unit]]() + val timeout = setTimeout(1200) { + p.tryComplete(Failure(new RuntimeException("Test timed out."))) + } + setTimeout(1) { + run.onComplete { ta => + clearTimeout(timeout) + p.complete(Success(ta.map(_ => ()))) + } + } + p.future + } + + implicit final class AsyncFutureOps[A](private val self: Future[A]) extends AnyVal { + def tap(f: A => Any): Future[A] = + self.map { a => f(a); a } + + def assertEquals(expect: A): Future[A] = + tap(Assert.assertEquals(expect, _)) + } + +} diff --git a/tests-shared/src/main/scala/org/scalajs/dom/tests/shared/SharedTests.scala b/tests-shared/src/main/scala/org/scalajs/dom/tests/shared/SharedTests.scala new file mode 100644 index 000000000..48abbe9c4 --- /dev/null +++ b/tests-shared/src/main/scala/org/scalajs/dom/tests/shared/SharedTests.scala @@ -0,0 +1,31 @@ +package org.scalajs.dom.tests.shared + +import java.util.UUID +import org.junit.Test +import org.scalajs.dom._ +import org.scalajs.dom.raw._ + +trait SharedTests { + import SharedTests._ + + // https://github.com/scala-js/scala-js-dom/issues/411 - console doesn't work in web workers + @Test final def ConsoleLogTest(): Unit = + console.log("Testing console.log") + + // https://github.com/scala-js/scala-js-dom/pull/432 - Avoid forcing evaluation of crypto + @Test final def CryptoNonStrictTest(): Unit = { + val _ = crypto.HashAlgorithm + } + + @Test final def WindowIdbTest(): Unit = + window.indexedDB.foreach(testIdb) + +} + +object SharedTests { + def testIdb(idb: IDBFactory): Unit = { + val open = idb.open(UUID.randomUUID().toString()) + open.onerror = (e: Event) => sys.error("idb open failed: " + e) + // TODO: Test properly in a different PR + } +} diff --git a/tests-webworker/src/main/scala/org/scalajs/dom/tests/webworker/Client.scala b/tests-webworker/src/main/scala/org/scalajs/dom/tests/webworker/Client.scala new file mode 100644 index 000000000..614e61c58 --- /dev/null +++ b/tests-webworker/src/main/scala/org/scalajs/dom/tests/webworker/Client.scala @@ -0,0 +1,43 @@ +package org.scalajs.dom.tests.webworker + +import org.scalajs.dom.raw._ +import scala.concurrent._ +import scala.scalajs.js +import scala.util.Success + +final class Client(worker: Worker) { + import Protocol._ + + private var preInit = new js.Array[Message] + private var promises = new js.Array[Promise[String]] + + worker.onmessage = (e: MessageEvent) => { + val m = e.data.asInstanceOf[Message] + if (m._1 == ServerStarted) { + preInit.foreach(worker.postMessage(_)) + preInit = null + } else + promises(m._1).complete(Success(m._2)) + } + + def send(cmd: WebWorkerCmd): Future[String] = { + val id = promises.length + val p = Promise[String]() + val m = Message(id, cmd.id) + promises.push(p) + if (preInit eq null) + worker.postMessage(m) + else + preInit.push(m) + p.future + } +} + +object Client { + + def workerUrl: String = + "file://" + BuildInfo.wwJsPath + + lazy val global = + new Client(new Worker(workerUrl)) +} diff --git a/tests-webworker/src/main/scala/org/scalajs/dom/tests/webworker/Protocol.scala b/tests-webworker/src/main/scala/org/scalajs/dom/tests/webworker/Protocol.scala new file mode 100644 index 000000000..6f6829468 --- /dev/null +++ b/tests-webworker/src/main/scala/org/scalajs/dom/tests/webworker/Protocol.scala @@ -0,0 +1,14 @@ +package org.scalajs.dom.tests.webworker + +import scala.scalajs.js + +object Protocol { + + type MsgId = Int + type Message = js.Tuple2[MsgId, String] + + def Message(id: MsgId, data: String): Message = + js.Tuple2(id, data) + + final val ServerStarted: MsgId = -1 +} diff --git a/tests-webworker/src/main/scala/org/scalajs/dom/tests/webworker/Server.scala b/tests-webworker/src/main/scala/org/scalajs/dom/tests/webworker/Server.scala new file mode 100644 index 000000000..ba460e84e --- /dev/null +++ b/tests-webworker/src/main/scala/org/scalajs/dom/tests/webworker/Server.scala @@ -0,0 +1,24 @@ +package org.scalajs.dom.tests.webworker + +import org.scalajs.dom.raw.MessageEvent +import org.scalajs.dom.webworkers._ + +object Server extends ServerResponses { + import Protocol._ + + def main(args: Array[String]): Unit = { + val ww = DedicatedWorkerGlobalScope.self + + ww.onmessage = (e: MessageEvent) => { + val msgIn = e.data.asInstanceOf[Message] + val id = msgIn._1 + val cmdId = msgIn._2 + val cmd = WebWorkerCmd.byId(cmdId) + val output = respond(cmd) + val msgOut = Message(id, output) + ww.postMessage(msgOut) + } + + ww.postMessage(Message(ServerStarted, "")) + } +} diff --git a/tests-webworker/src/main/scala/org/scalajs/dom/tests/webworker/WebWorkerTests.scala b/tests-webworker/src/main/scala/org/scalajs/dom/tests/webworker/WebWorkerTests.scala new file mode 100644 index 000000000..cc40a8bd3 --- /dev/null +++ b/tests-webworker/src/main/scala/org/scalajs/dom/tests/webworker/WebWorkerTests.scala @@ -0,0 +1,66 @@ +package org.scalajs.dom.tests.webworker + +import org.junit.Assert._ +import org.junit.Test +import org.scalajs.dom.tests.shared.AsyncTesting._ + +// ===================================================================================================================== +sealed abstract class WebWorkerCmd { + def id = toString +} + +object WebWorkerCmd { + + case object SayHello extends WebWorkerCmd + case object TestConsole extends WebWorkerCmd + case object TestIdb extends WebWorkerCmd + + def byId(id: String): WebWorkerCmd = + id match { + case "SayHello" => SayHello + case "TestConsole" => TestConsole + case "TestIdb" => TestIdb + } +} + +import WebWorkerCmd._ + +// ===================================================================================================================== +trait WebWorkerTests { + import Client.{global => client} + + final protected def webWorkerTest(test: (WebWorkerCmd, String)) = async { + client.send(test._1).assertEquals(test._2) + } + + @Test final def WebWorkerHelloTest() = + webWorkerTest(SayHello -> "hello") + + @Test final def WebWorkerConsoleTest() = + webWorkerTest(TestConsole -> "ok") + + @Test final def WebWorkerIdbTest() = + webWorkerTest(TestIdb -> "ok") +} + +// ===================================================================================================================== +trait ServerResponses { + import org.scalajs.dom._ + import org.scalajs.dom.tests.shared.SharedTests._ + import org.scalajs.dom.webworkers.DedicatedWorkerGlobalScope.self + + final val respond: WebWorkerCmd => String = { + + case SayHello => + "hello" + + case TestConsole => + console.log("WW console.log test") + "ok" + + case TestIdb => + assertTrue(self.indexedDB.isDefined) + testIdb(self.indexedDB.get) + "ok" + } +}