diff --git a/.scalafix.conf b/.scalafix.conf
new file mode 100644
index 0000000..5011d85
--- /dev/null
+++ b/.scalafix.conf
@@ -0,0 +1,3 @@
+rules = [OrganizeImports]
+
+OrganizeImports.removeUnused = false
diff --git a/.scalafmt.conf b/.scalafmt.conf
index 8134e97..9aa8794 100644
--- a/.scalafmt.conf
+++ b/.scalafmt.conf
@@ -1,2 +1,63 @@
-version = "3.7.15"
-runner.dialect = scala3
\ No newline at end of file
+version = 3.7.17
+
+runner.dialect = scala3
+
+maxColumn = 96
+
+includeCurlyBraceInSelectChains = true
+includeNoParensInSelectChains = true
+
+optIn {
+ breakChainOnFirstMethodDot = false
+ forceBlankLineBeforeDocstring = true
+}
+
+binPack {
+ literalArgumentLists = true
+ parentConstructors = Never
+}
+
+danglingParentheses {
+ defnSite = false
+ callSite = false
+ ctrlSite = false
+
+ exclude = []
+}
+
+newlines {
+ beforeCurlyLambdaParams = multilineWithCaseOnly
+ afterCurlyLambda = squash
+ implicitParamListModifierPrefer = before
+ sometimesBeforeColonInMethodReturnType = true
+}
+
+align.preset = none
+align.stripMargin = true
+
+assumeStandardLibraryStripMargin = true
+
+docstrings {
+ style = Asterisk
+ oneline = unfold
+}
+
+project.git = true
+
+trailingCommas = never
+
+rewrite {
+ // RedundantBraces honestly just doesn't work, otherwise I'd love to use it
+ rules = [PreferCurlyFors, RedundantParens, SortImports]
+
+ redundantBraces {
+ maxLines = 1
+ stringInterpolation = true
+ }
+}
+
+rewriteTokens {
+ "⇒": "=>"
+ "→": "->"
+ "←": "<-"
+}
diff --git a/build.sbt b/build.sbt
index c84016c..78afefb 100644
--- a/build.sbt
+++ b/build.sbt
@@ -41,7 +41,7 @@ lazy val root = (project in file("."))
"com.google.jimfs" % "jimfs" % "1.2",
"com.outr" %% "scribe" % "3.13.0",
"org.typelevel" %% "cats-effect" % "3.5.2",
- "org.scala-js" %% "scalajs-js-envs-test-kit" % "1.1.1" % Test,
+ "org.scala-js" %% "scalajs-js-envs-test-kit" % "1.4.0" % Test,
"com.novocode" % "junit-interface" % "0.11" % Test
),
releaseProcess := Seq[ReleaseStep](
@@ -65,7 +65,7 @@ lazy val root = (project in file("."))
},
// For all Sonatype accounts created on or after February 2021
sonatypeCredentialHost := "s01.oss.sonatype.org",
- Test / parallelExecution := false,
+ Test / parallelExecution := true,
Test / publishArtifact := false,
usePgpKeyHex("F7E440260BAE93EB4AD2723D6613CA76E011F638")
)
diff --git a/project/plugins.sbt b/project/plugins.sbt
index dfba2f1..513f021 100644
--- a/project/plugins.sbt
+++ b/project/plugins.sbt
@@ -1,5 +1,9 @@
+val sbtTypelevelVersion = "0.6.4"
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.1.1")
addSbtPlugin("com.github.sbt" % "sbt-release" % "1.3.0")
addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.2.1")
addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.10.0")
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.9")
+addSbtPlugin("org.typelevel" % "sbt-typelevel" % sbtTypelevelVersion)
+addSbtPlugin("org.typelevel" % "sbt-typelevel-scalafix" % sbtTypelevelVersion)
+addSbtPlugin("org.typelevel" % "sbt-typelevel-site" % sbtTypelevelVersion)
diff --git a/src/main/java/jsenv/DriverJar.java b/src/main/java/jsenv/DriverJar.java
index 9f1e7a3..7b06ece 100644
--- a/src/main/java/jsenv/DriverJar.java
+++ b/src/main/java/jsenv/DriverJar.java
@@ -129,7 +129,7 @@ void extractDriverToTempDir() throws URISyntaxException, IOException {
URI uri = maybeExtractNestedJar(originalUri);
// Create zip filesystem if loading from jar.
- try (FileSystem fileSystem = "jar".equals(uri.getScheme()) ? initFileSystem(uri) : null) {
+ FileSystem fileSystem = "jar".equals(uri.getScheme()) ? initFileSystem(uri) : null;
Path srcRoot = Paths.get(uri);
// jar file system's .relativize gives wrong results when used with
// spring-boot-maven-plugin, convert to the default filesystem to
@@ -159,6 +159,8 @@ void extractDriverToTempDir() throws URISyntaxException, IOException {
throw new RuntimeException("Failed to extract driver from " + uri + ", full uri: " + originalUri, e);
}
});
+ if (fileSystem != null) {
+ fileSystem.close();
}
}
@@ -173,7 +175,8 @@ private URI maybeExtractNestedJar(final URI uri) throws URISyntaxException {
}
String innerJar = String.join(JAR_URL_SEPARATOR, parts[0], parts[1]);
URI jarUri = new URI(innerJar);
- try (FileSystem fs = FileSystems.newFileSystem(jarUri, Collections.emptyMap())) {
+ try {
+ FileSystem fs = FileSystems.newFileSystem(jarUri, Collections.emptyMap());
Path fromPath = Paths.get(jarUri);
Path toPath = driverTempDir.resolve(fromPath.getFileName().toString());
Files.copy(fromPath, toPath);
diff --git a/src/main/scala/jsenv/playwright/CEComRun.scala b/src/main/scala/jsenv/playwright/CEComRun.scala
new file mode 100644
index 0000000..85d6b3f
--- /dev/null
+++ b/src/main/scala/jsenv/playwright/CEComRun.scala
@@ -0,0 +1,36 @@
+package jsenv.playwright
+
+import cats.effect.IO
+import cats.effect.unsafe.implicits.global
+import jsenv.playwright.PWEnv.Config
+import org.scalajs.jsenv.Input
+import org.scalajs.jsenv.JSComRun
+import org.scalajs.jsenv.RunConfig
+
+import scala.concurrent._
+
+// browserName, headless, pwConfig, runConfig, input, onMessage
+class CEComRun(
+ override val browserName: String,
+ override val headless: Boolean,
+ override val pwConfig: Config,
+ override val runConfig: RunConfig,
+ override val input: Seq[Input],
+ onMessage: String => Unit
+) extends JSComRun
+ with Runner {
+ scribe.debug(s"Creating CEComRun for $browserName")
+ // enableCom is false for CERun and true for CEComRun
+ // send is called only from JSComRun
+ override def send(msg: String): Unit = sendQueue.offer(msg)
+ // receivedMessage is called only from JSComRun. Hence its implementation is empty in CERun
+ override protected def receivedMessage(msg: String): Unit = onMessage(msg)
+
+ lazy val future: Future[Unit] =
+ jsRunPrg(browserName, headless, isComEnabled = true, None)
+ .use(_ => IO.unit)
+ .unsafeToFuture()
+
+}
+
+private class WindowOnErrorException(errs: List[String]) extends Exception(s"JS error: $errs")
diff --git a/src/main/scala/jsenv/playwright/CERun.scala b/src/main/scala/jsenv/playwright/CERun.scala
index 91c220d..8dd50fd 100644
--- a/src/main/scala/jsenv/playwright/CERun.scala
+++ b/src/main/scala/jsenv/playwright/CERun.scala
@@ -1,152 +1,27 @@
package jsenv.playwright
+import cats.effect.IO
import cats.effect.unsafe.implicits.global
-import cats.effect.{IO, Resource}
-import com.microsoft.playwright.BrowserType.LaunchOptions
import jsenv.playwright.PWEnv.Config
-import jsenv.playwright.PageFactory._
-import jsenv.playwright.ResourcesFactory._
-import org.scalajs.jsenv.{Input, JSComRun, JSRun, RunConfig}
+import org.scalajs.jsenv.Input
+import org.scalajs.jsenv.JSRun
+import org.scalajs.jsenv.RunConfig
-import java.util.concurrent.ConcurrentLinkedQueue
-import java.util.concurrent.atomic.AtomicBoolean
import scala.concurrent._
-import scala.concurrent.duration.DurationInt
class CERun(
- browserName: String,
- headless: Boolean,
- pwConfig: Config,
- runConfig: RunConfig,
- input: Seq[Input]
-) extends JSRun {
-
- implicit val ec: scala.concurrent.ExecutionContext =
- scala.concurrent.ExecutionContext.global
-
- // enableCom is false for CERun and true for CEComRun
- protected val enableCom = false
- protected val intf = "this.scalajsPlayWrightInternalInterface"
- protected val sendQueue = new ConcurrentLinkedQueue[String]
- // receivedMessage is called only from JSComRun. Hence its implementation is empty in CERun
- protected def receivedMessage(msg: String): Unit = ()
-
- /** A Future that completes if the run completes.
- *
- * The future is failed if the run fails.
- *
- * Note that a JSRun is not required to ever terminate on it's own. That
- * means even if all code is executed and the event loop is empty, the run
- * may continue to run. As a consequence, it is *not* correct to rely on
- * termination of a JSRun without any external means of stopping it (i.e.
- * calling [[close]]).
- */
- var wantToClose = new AtomicBoolean(false)
- // List of programs
- // 1. isInterfaceUp()
- // Create PW resource if not created. Create browser,context and page
- // 2. Sleep
- // 3. wantClose
- // 4. sendAll()
- // 5. fetchAndProcess()
- // 6. Close diver
- // 7. Close streams
- // 8. Close materializer
- // Flow
- // if interface is down and dont want to close wait for 100 milliseconds
- // interface is up and dont want to close sendAll(), fetchAndProcess() Sleep for 100 milliseconds
- // If want to close then close driver, streams, materializer
- // After future is completed close driver, streams, materializer
-
- def jsRunPrg(
- browserName: String,
- headless: Boolean,
- isComEnabled: Boolean,
- launchOptions: Option[LaunchOptions]
- ): Resource[IO, Unit] = for {
- _ <- Resource.pure(
- scribe.info(
- s"Begin Main with isComEnabled $isComEnabled " +
- s"and browserName $browserName " +
- s"and headless is $headless "
- )
- )
- pageInstance <- createPage(
- browserName,
- headless,
- launchOptions
- )
- _ <- preparePageForJsRun(
- pageInstance,
- materializer(pwConfig),
- input,
- isComEnabled
- )
- connectionReady <- isConnectionUp(pageInstance, intf)
- _ <-
- if (!connectionReady) Resource.pure[IO, Unit](IO.sleep(100.milliseconds))
- else Resource.pure[IO, Unit](IO.unit)
- _ <- isConnectionUp(pageInstance, intf)
- out <- outputStream(runConfig)
- _ <- processUntilStop(
- wantToClose,
- pageInstance,
- intf,
- sendQueue,
- out,
- receivedMessage,
- isComEnabled
- )
- } yield ()
-
+ override val browserName: String,
+ override val headless: Boolean,
+ override val pwConfig: Config,
+ override val runConfig: RunConfig,
+ override val input: Seq[Input]
+) extends JSRun
+ with Runner {
+ scribe.debug(s"Creating CERun for $browserName")
lazy val future: Future[Unit] =
- jsRunPrg(browserName, headless, enableCom, None)
+ jsRunPrg(browserName, headless, isComEnabled = false, None)
.use(_ => IO.unit)
.unsafeToFuture()
- /** Stops the run and releases all the resources.
- *
- * This must be called to ensure the run's resources are
- * released.
- *
- * Whether or not this makes the run fail or not is up to the implementation.
- * However, in the following cases, calling [[close]] may not fail the run:
- *
- [[future]] is already completed when [[close]] is called.
- *
- This is a [[CERun]] and the event loop inside the VM is empty.
- *
- *
- * Idempotent, async, nothrow.
- */
-
- override def close(): Unit = {
- wantToClose.set(true)
- scribe.info(s"StopSignal is ${wantToClose.get()}")
- }
-
+ override protected def receivedMessage(msg: String): Unit = ()
}
-// browserName, headless, pwConfig, runConfig, input, onMessage
-class CEComRun(
- browserName: String,
- headless: Boolean,
- pwConfig: Config,
- runConfig: RunConfig,
- input: Seq[Input],
- onMessage: String => Unit
-) extends CERun(
- browserName,
- headless,
- pwConfig,
- runConfig,
- input
- )
- with JSComRun {
- // enableCom is false for CERun and true for CEComRun
- override protected val enableCom = true
- // send is called only from JSComRun
- override def send(msg: String): Unit = sendQueue.offer(msg)
- // receivedMessage is called only from JSComRun. Hence its implementation is empty in CERun
- override protected def receivedMessage(msg: String): Unit = onMessage(msg)
-}
-
-private class WindowOnErrorException(errs: List[String])
- extends Exception(s"JS error: $errs")
diff --git a/src/main/scala/jsenv/playwright/CEUtils.scala b/src/main/scala/jsenv/playwright/CEUtils.scala
index 64fbfef..f3bcbd8 100644
--- a/src/main/scala/jsenv/playwright/CEUtils.scala
+++ b/src/main/scala/jsenv/playwright/CEUtils.scala
@@ -1,7 +1,15 @@
package jsenv.playwright
-import org.scalajs.jsenv.{Input, UnsupportedInputException}
-import scribe.format.{FormatterInterpolator, dateFull, level, mdc, messages, methodName, threadName}
+import org.scalajs.jsenv.Input
+import org.scalajs.jsenv.UnsupportedInputException
+import scribe.format.FormatterInterpolator
+import scribe.format.classNameSimple
+import scribe.format.dateFull
+import scribe.format.level
+import scribe.format.mdc
+import scribe.format.messages
+import scribe.format.methodName
+import scribe.format.threadName
import java.nio.file.Path
@@ -12,6 +20,8 @@ object CEUtils {
): String = {
val tags = fullInput.map {
case Input.Script(path) => makeTag(path, "text/javascript", materializer)
+ case Input.CommonJSModule(path) =>
+ makeTag(path, "text/javascript", materializer)
case Input.ESModule(path) => makeTag(path, "module", materializer)
case _ => throw new UnsupportedInputException(fullInput)
}
@@ -36,8 +46,10 @@ object CEUtils {
def setupLogger(showLogs: Boolean, debug: Boolean): Unit = {
val formatter =
- formatter"$dateFull [$threadName] $level $methodName - $messages$mdc"
- scribe.Logger.root
+ formatter"$dateFull [$threadName] $classNameSimple $level $methodName - $messages$mdc"
+ scribe
+ .Logger
+ .root
.clearHandlers()
.withHandler(
formatter = formatter
diff --git a/src/main/scala/jsenv/playwright/FileMaterializers.scala b/src/main/scala/jsenv/playwright/FileMaterializers.scala
index 55521ff..9207561 100644
--- a/src/main/scala/jsenv/playwright/FileMaterializers.scala
+++ b/src/main/scala/jsenv/playwright/FileMaterializers.scala
@@ -4,14 +4,20 @@ import java.net._
import java.nio.file._
import java.util
-abstract class FileMaterializer extends AutoCloseable{
+abstract class FileMaterializer extends AutoCloseable {
private val tmpSuffixRE = """[a-zA-Z0-9-_.]*$""".r
private var tmpFiles: List[Path] = Nil
def materialize(path: Path): URL = {
val tmp = newTmp(path.toString)
+ // if file with extension .map exist then copy it too
+ val mapPath = Paths.get(path.toString + ".map")
Files.copy(path, tmp, StandardCopyOption.REPLACE_EXISTING)
+ if (Files.exists(mapPath)) {
+ val tmpMap = newTmp(mapPath.toString)
+ Files.copy(mapPath, tmpMap, StandardCopyOption.REPLACE_EXISTING)
+ }
toURL(tmp)
}
@@ -48,7 +54,9 @@ object FileMaterializer {
}
}
-/** materializes virtual files in a temp directory (uses file:// schema). */
+/**
+ * materializes virtual files in a temp directory (uses file:// schema).
+ */
private class TempDirFileMaterializer extends FileMaterializer {
override def materialize(path: Path): URL = {
try {
@@ -59,7 +67,8 @@ private class TempDirFileMaterializer extends FileMaterializer {
}
}
- protected def createTmp(suffix: String): Path = Files.createTempFile(null, suffix)
+ protected def createTmp(suffix: String): Path =
+ Files.createTempFile(null, suffix)
protected def toURL(file: Path): URL = file.toUri.toURL
}
diff --git a/src/main/scala/jsenv/playwright/JSSetup.scala b/src/main/scala/jsenv/playwright/JSSetup.scala
index 7a93b83..af40d18 100644
--- a/src/main/scala/jsenv/playwright/JSSetup.scala
+++ b/src/main/scala/jsenv/playwright/JSSetup.scala
@@ -3,90 +3,90 @@ package jsenv.playwright
import com.google.common.jimfs.Jimfs
import java.nio.charset.StandardCharsets
-import java.nio.file.{Files, Path}
+import java.nio.file.Files
+import java.nio.file.Path
private object JSSetup {
def setupFile(enableCom: Boolean): Path = {
val path = Jimfs.newFileSystem().getPath("setup.js")
-// val simpleContents = setupCode(enableCom)
val contents = setupCode(enableCom).getBytes(StandardCharsets.UTF_8)
Files.write(path, contents)
}
private def setupCode(enableCom: Boolean): String = {
s"""
- |(function() {
- | // Buffers for console.log / console.error
- | var consoleLog = [];
- | var consoleError = [];
- |
- | // Buffer for errors.
- | var errors = [];
- |
- | // Buffer for outgoing messages.
- | var outMessages = [];
- |
- | // Buffer for incoming messages (used if onMessage not initalized).
- | var inMessages = [];
- |
- | // Callback for incoming messages.
- | var onMessage = null;
- |
- | function captureConsole(fun, buf) {
- | if (!fun) return fun;
- | return function() {
- | var strs = []
- | for (var i = 0; i < arguments.length; ++i)
- | strs.push(String(arguments[i]));
- |
- | buf.push(strs.join(" "));
- | return fun.apply(this, arguments);
- | }
- | }
- |
- | console.log = captureConsole(console.log, consoleLog);
- | console.error = captureConsole(console.error, consoleError);
- |
- | window.addEventListener('error', function(e) {
- | errors.push(e.message)
- | });
- |
- | if ($enableCom) {
- | this.scalajsCom = {
- | init: function(onMsg) {
- | onMessage = onMsg;
- | window.setTimeout(function() {
- | for (var m in inMessages)
- | onMessage(inMessages[m]);
- | inMessages = null;
- | });
- | },
- | send: function(msg) { outMessages.push(msg); }
- | }
- | }
- |
- | this.scalajsPlayWrightInternalInterface = {
- | fetch: function() {
- | var res = {
- | consoleLog: consoleLog.slice(),
- | consoleError: consoleError.slice(),
- | errors: errors.slice(),
- | msgs: outMessages.slice()
- | }
- |
- | consoleLog.length = 0;
- | consoleError.length = 0;
- | errors.length = 0;
- | outMessages.length = 0;
- |
- | return res;
- | },
- | send: function(msg) {
- | if (inMessages !== null) inMessages.push(msg);
- | else onMessage(msg);
- | }
- | };
- |}).call(this)
+ |(function() {
+ | // Buffers for console.log / console.error
+ | var consoleLog = [];
+ | var consoleError = [];
+ |
+ | // Buffer for errors.
+ | var errors = [];
+ |
+ | // Buffer for outgoing messages.
+ | var outMessages = [];
+ |
+ | // Buffer for incoming messages (used if onMessage not initalized).
+ | var inMessages = [];
+ |
+ | // Callback for incoming messages.
+ | var onMessage = null;
+ |
+ | function captureConsole(fun, buf) {
+ | if (!fun) return fun;
+ | return function() {
+ | var strs = []
+ | for (var i = 0; i < arguments.length; ++i)
+ | strs.push(String(arguments[i]));
+ |
+ | buf.push(strs.join(" "));
+ | return fun.apply(this, arguments);
+ | }
+ | }
+ |
+ | console.log = captureConsole(console.log, consoleLog);
+ | console.error = captureConsole(console.error, consoleError);
+ |
+ | window.addEventListener('error', function(e) {
+ | errors.push(e.message)
+ | });
+ |
+ | if ($enableCom) {
+ | this.scalajsCom = {
+ | init: function(onMsg) {
+ | onMessage = onMsg;
+ | window.setTimeout(function() {
+ | for (var m in inMessages)
+ | onMessage(inMessages[m]);
+ | inMessages = null;
+ | });
+ | },
+ | send: function(msg) { outMessages.push(msg); }
+ | }
+ | }
+ |
+ | this.scalajsPlayWrightInternalInterface = {
+ | fetch: function() {
+ | var res = {
+ | consoleLog: consoleLog.slice(),
+ | consoleError: consoleError.slice(),
+ | errors: errors.slice(),
+ | msgs: outMessages.slice()
+ | }
+ |
+ | consoleLog.length = 0;
+ | consoleError.length = 0;
+ | errors.length = 0;
+ | outMessages.length = 0;
+ |
+ | return res;
+ | },
+ | send: function(msg) {
+ | if (inMessages !== null) inMessages.push(msg);
+ | else onMessage(msg);
+ | }
+ | };
+ |}).call(this)
""".stripMargin
}
diff --git a/src/main/scala/jsenv/playwright/OutputStreams.scala b/src/main/scala/jsenv/playwright/OutputStreams.scala
index 5c12da4..914a777 100644
--- a/src/main/scala/jsenv/playwright/OutputStreams.scala
+++ b/src/main/scala/jsenv/playwright/OutputStreams.scala
@@ -34,8 +34,7 @@ object OutputStreams {
}
}
- private class UnownedOutputStream(out: OutputStream)
- extends FilterOutputStream(out) {
+ private class UnownedOutputStream(out: OutputStream) extends FilterOutputStream(out) {
override def close(): Unit = flush()
}
}
diff --git a/src/main/scala/jsenv/playwright/PWEnv.scala b/src/main/scala/jsenv/playwright/PWEnv.scala
index 9bff58d..f099bb3 100644
--- a/src/main/scala/jsenv/playwright/PWEnv.scala
+++ b/src/main/scala/jsenv/playwright/PWEnv.scala
@@ -3,23 +3,22 @@ package jsenv.playwright
import jsenv.playwright.PWEnv.Config
import org.scalajs.jsenv._
-import java.net.{URI, URL}
-import java.nio.file.{Path, Paths}
+import java.net.URI
+import java.net.URL
+import java.nio.file.Path
+import java.nio.file.Paths
import scala.util.control.NonFatal
class PWEnv(
- browserName: String = "chromium",
- headless: Boolean = true,
- showLogs: Boolean = false,
- debug: Boolean = false,
- pwConfig: Config = Config()
+ browserName: String = "chromium",
+ headless: Boolean = true,
+ showLogs: Boolean = false,
+ debug: Boolean = false,
+ pwConfig: Config = Config()
) extends JSEnv {
private lazy val validator = {
- RunConfig
- .Validator()
- .supportsInheritIO()
- .supportsOnOutputStream()
+ RunConfig.Validator().supportsInheritIO().supportsOnOutputStream()
}
override val name: String = s"CEEnv with $browserName"
System.setProperty("playwright.driver.impl", "jsenv.DriverJar")
@@ -74,56 +73,59 @@ object PWEnv {
materialization = Config.Materialization.Temp
)
- /** Materializes purely virtual files into a temp directory.
- *
- * Materialization is necessary so that virtual files can be referred to by
- * name. If you do not know/care how your files are referred to, this is a
- * good default choice. It is also the default of [[PWEnv.Config]].
- */
+ /**
+ * Materializes purely virtual files into a temp directory.
+ *
+ * Materialization is necessary so that virtual files can be referred to by name. If you do
+ * not know/care how your files are referred to, this is a good default choice. It is also
+ * the default of [[PWEnv.Config]].
+ */
def withMaterializeInTemp: Config =
copy(materialization = Materialization.Temp)
- /** Materializes files in a static directory of a user configured server.
- *
- * This can be used to bypass cross origin access policies.
- *
- * @param contentDir
- * Static content directory of the server. The files will be put here.
- * Will get created if it doesn't exist.
- * @param webRoot
- * URL making `contentDir` accessible thorugh the server. This must have
- * a trailing slash to be interpreted as a directory.
- *
- * @example
- *
- * The following will make the browser fetch files using the http:// schema
- * instead of the file:// schema. The example assumes a local webserver is
- * running and serving the ".tmp" directory at http://localhost:8080.
- *
- * {{{
- * jsSettings(
- * jsEnv := new SeleniumJSEnv(
- * new org.openqa.selenium.firefox.FirefoxOptions(),
- * SeleniumJSEnv.Config()
- * .withMaterializeInServer(".tmp", "http://localhost:8080/")
- * )
- * )
- * }}}
- */
+ /**
+ * Materializes files in a static directory of a user configured server.
+ *
+ * This can be used to bypass cross origin access policies.
+ *
+ * @param contentDir
+ * Static content directory of the server. The files will be put here. Will get created if
+ * it doesn't exist.
+ * @param webRoot
+ * URL making `contentDir` accessible thorugh the server. This must have a trailing slash
+ * to be interpreted as a directory.
+ *
+ * @example
+ *
+ * The following will make the browser fetch files using the http:// schema instead of the
+ * file:// schema. The example assumes a local webserver is running and serving the ".tmp"
+ * directory at http://localhost:8080.
+ *
+ * {{{
+ * jsSettings(
+ * jsEnv := new SeleniumJSEnv(
+ * new org.openqa.selenium.firefox.FirefoxOptions(),
+ * SeleniumJSEnv.Config()
+ * .withMaterializeInServer(".tmp", "http://localhost:8080/")
+ * )
+ * )
+ * }}}
+ */
def withMaterializeInServer(contentDir: String, webRoot: String): Config =
withMaterializeInServer(Paths.get(contentDir), new URI(webRoot).toURL)
- /** Materializes files in a static directory of a user configured server.
- *
- * Version of `withMaterializeInServer` with stronger typing.
- *
- * @param contentDir
- * Static content directory of the server. The files will be put here.
- * Will get created if it doesn't exist.
- * @param webRoot
- * URL making `contentDir` accessible thorugh the server. This must have
- * a trailing slash to be interpreted as a directory.
- */
+ /**
+ * Materializes files in a static directory of a user configured server.
+ *
+ * Version of `withMaterializeInServer` with stronger typing.
+ *
+ * @param contentDir
+ * Static content directory of the server. The files will be put here. Will get created if
+ * it doesn't exist.
+ * @param webRoot
+ * URL making `contentDir` accessible thorugh the server. This must have a trailing slash
+ * to be interpreted as a directory.
+ */
def withMaterializeInServer(contentDir: Path, webRoot: URL): Config =
copy(materialization = Materialization.Server(contentDir, webRoot))
@@ -143,8 +145,7 @@ object PWEnv {
abstract class Materialization private ()
object Materialization {
final case object Temp extends Materialization
- final case class Server(contentDir: Path, webRoot: URL)
- extends Materialization {
+ final case class Server(contentDir: Path, webRoot: URL) extends Materialization {
require(
webRoot.getPath.endsWith("/"),
"webRoot must end with a slash (/)"
diff --git a/src/main/scala/jsenv/playwright/PageFactory.scala b/src/main/scala/jsenv/playwright/PageFactory.scala
index 55eeb40..a846c64 100644
--- a/src/main/scala/jsenv/playwright/PageFactory.scala
+++ b/src/main/scala/jsenv/playwright/PageFactory.scala
@@ -1,8 +1,12 @@
package jsenv.playwright
-import cats.effect.{IO, Resource}
+import cats.effect.IO
+import cats.effect.Resource
+import com.microsoft.playwright.Browser
+import com.microsoft.playwright.BrowserType
import com.microsoft.playwright.BrowserType.LaunchOptions
-import com.microsoft.playwright.{Browser, BrowserType, Page, Playwright}
+import com.microsoft.playwright.Page
+import com.microsoft.playwright.Playwright
import scala.jdk.CollectionConverters.seqAsJavaListConverter
object PageFactory {
@@ -11,29 +15,24 @@ object PageFactory {
val pg = browser.newContext().newPage()
scribe.debug(s"Creating page ${pg.hashCode()} ")
pg
- })(page =>
- IO {page.close()}
- )
+ })(page => IO { page.close() })
}
private def browserBuilder(
- playwright: Playwright,
- browserName: String,
- headless: Boolean,
- launchOptions: Option[LaunchOptions] = None
+ playwright: Playwright,
+ browserName: String,
+ headless: Boolean,
+ launchOptions: Option[LaunchOptions] = None
): Resource[IO, Browser] =
Resource.make(IO {
val browserType: BrowserType = browserName.toLowerCase match {
case "chromium" | "chrome" =>
- playwright
- .chromium()
+ playwright.chromium()
case "firefox" =>
- playwright
- .firefox()
+ playwright.firefox()
case "webkit" =>
- playwright
- .webkit()
+ playwright.webkit()
case _ => throw new IllegalArgumentException("Invalid browser type")
}
launchOptions match {
@@ -42,47 +41,46 @@ object PageFactory {
case None =>
val launchOptions = browserName.toLowerCase match {
case "chromium" | "chrome" =>
- new BrowserType.LaunchOptions()
- .setArgs(
- List(
- "--disable-extensions",
- "--disable-web-security",
- "--allow-running-insecure-content",
- "--disable-site-isolation-trials",
- "--allow-file-access-from-files",
- "--disable-gpu"
- ).asJava
- )
+ new BrowserType.LaunchOptions().setArgs(
+ List(
+ "--disable-extensions",
+ "--disable-web-security",
+ "--allow-running-insecure-content",
+ "--disable-site-isolation-trials",
+ "--allow-file-access-from-files",
+ "--disable-gpu"
+ ).asJava
+ )
case "firefox" =>
- new BrowserType.LaunchOptions()
- .setArgs(
- List(
- "--disable-web-security"
- ).asJava
- )
+ new BrowserType.LaunchOptions().setArgs(
+ List(
+ "--disable-web-security"
+ ).asJava
+ )
case "webkit" =>
- new BrowserType.LaunchOptions()
- .setArgs(
- List(
- "--disable-extensions",
- "--disable-web-security",
- "--allow-running-insecure-content",
- "--disable-site-isolation-trials",
- "--allow-file-access-from-files"
- ).asJava
- )
+ new BrowserType.LaunchOptions().setArgs(
+ List(
+ "--disable-extensions",
+ "--disable-web-security",
+ "--allow-running-insecure-content",
+ "--disable-site-isolation-trials",
+ "--allow-file-access-from-files"
+ ).asJava
+ )
case _ => throw new IllegalArgumentException("Invalid browser type")
}
- val browser = browserType.launch(launchOptions.setHeadless(headless))
- scribe.debug(s"Creating browser $browserName version ${browser.version()} with ${browser.hashCode()}")
+ val browser = browserType.launch(launchOptions.setHeadless(headless))
+ scribe.info(
+ s"Creating browser ${browser.browserType().name()} version ${browser
+ .version()} with ${browser.hashCode()}"
+ )
browser
}
})(browser =>
IO {
scribe.debug(s"Closing browser with ${browser.hashCode()}")
browser.close()
- }
- )
+ })
private def playWrightBuilder: Resource[IO, Playwright] =
Resource.make(IO {
@@ -92,13 +90,12 @@ object PageFactory {
IO {
scribe.debug("Closing playwright")
pw.close()
- }
- )
+ })
def createPage(
- browserName: String,
- headless: Boolean,
- launchOptions: Option[LaunchOptions]
+ browserName: String,
+ headless: Boolean,
+ launchOptions: Option[LaunchOptions]
): Resource[IO, Page] =
for {
playwright <- playWrightBuilder
diff --git a/src/main/scala/jsenv/playwright/ResourcesFactory.scala b/src/main/scala/jsenv/playwright/ResourcesFactory.scala
index 2991a77..bceb04b 100644
--- a/src/main/scala/jsenv/playwright/ResourcesFactory.scala
+++ b/src/main/scala/jsenv/playwright/ResourcesFactory.scala
@@ -1,9 +1,11 @@
package jsenv.playwright
-import cats.effect.{IO, Resource}
+import cats.effect.IO
+import cats.effect.Resource
import com.microsoft.playwright.Page
import jsenv.playwright.PWEnv.Config
-import org.scalajs.jsenv.{Input, RunConfig}
+import org.scalajs.jsenv.Input
+import org.scalajs.jsenv.RunConfig
import java.util
import java.util.concurrent.ConcurrentLinkedQueue
@@ -53,16 +55,17 @@ object ResourcesFactory {
intf: String,
sendQueue: ConcurrentLinkedQueue[String],
outStream: OutputStreams.Streams,
- receivedMessage: String => Unit,
- isComEnabled: Boolean
+ receivedMessage: String => Unit
): Resource[IO, Unit] = {
Resource.pure[IO, Unit] {
+ scribe.debug(s"Started processUntilStop")
while (!stopSignal.get()) {
sendAll(sendQueue, pageInstance, intf)
val jsResponse = fetchMessages(pageInstance, intf)
streamWriter(jsResponse, outStream, Some(receivedMessage))
IO.sleep(100.milliseconds)
}
+ scribe.debug(s"Stop processUntilStop")
}
}
@@ -71,8 +74,11 @@ object ResourcesFactory {
intf: String
): Resource[IO, Boolean] = {
Resource.pure[IO, Boolean] {
- scribe.debug(s"Page instance is ${pageInstance.hashCode()}")
- pageInstance.evaluate(s"!!$intf;").asInstanceOf[Boolean]
+ val status = pageInstance.evaluate(s"!!$intf;").asInstanceOf[Boolean]
+ scribe.debug(
+ s"Page instance is ${pageInstance.hashCode()} with status $status"
+ )
+ status
}
}
@@ -140,12 +146,23 @@ object ResourcesFactory {
): Unit = {
val msg = sendQueue.poll()
if (msg != null) {
- scribe.debug(s"Sending message ${msg.take(100)}")
+ scribe.debug(s"Sending message")
val script = s"$intf.send(arguments[0]);"
val wrapper = s"function(arg) { $script }"
pageInstance.evaluate(s"$wrapper", msg)
+ val pwDebug = sys.env.getOrElse("PWDEBUG", "0")
+ if (pwDebug == "1") {
+ pageInstance.pause()
+ }
sendAll(sendQueue, pageInstance, intf)
}
}
private def consumer[A](f: A => Unit): Consumer[A] = (v: A) => f(v)
+ private def logStackTrace(): Unit = {
+ try {
+ throw new Exception("Logging stack trace")
+ } catch {
+ case e: Exception => e.printStackTrace()
+ }
+ }
}
diff --git a/src/main/scala/jsenv/playwright/Runner.scala b/src/main/scala/jsenv/playwright/Runner.scala
new file mode 100644
index 0000000..f2ee254
--- /dev/null
+++ b/src/main/scala/jsenv/playwright/Runner.scala
@@ -0,0 +1,129 @@
+package jsenv.playwright
+
+import cats.effect.IO
+import cats.effect.Resource
+import com.microsoft.playwright.BrowserType.LaunchOptions
+import jsenv.playwright.PWEnv.Config
+import jsenv.playwright.PageFactory._
+import jsenv.playwright.ResourcesFactory._
+import org.scalajs.jsenv.Input
+import org.scalajs.jsenv.RunConfig
+
+import java.util.concurrent.ConcurrentLinkedQueue
+import java.util.concurrent.atomic.AtomicBoolean
+import scala.concurrent.duration.DurationInt
+
+trait Runner {
+ val browserName: String = "" // or provide actual values
+ val headless: Boolean = false // or provide actual values
+ val pwConfig: Config = Config() // or provide actual values
+ val runConfig: RunConfig = RunConfig() // or provide actual values
+ val input: Seq[Input] = Seq.empty // or provide actual values
+
+ // enableCom is false for CERun and true for CEComRun
+ protected val enableCom = false
+ protected val intf = "this.scalajsPlayWrightInternalInterface"
+ protected val sendQueue = new ConcurrentLinkedQueue[String]
+ // receivedMessage is called only from JSComRun. Hence its implementation is empty in CERun
+ protected def receivedMessage(msg: String): Unit
+ var wantToClose = new AtomicBoolean(false)
+ // List of programs
+ // 1. isInterfaceUp()
+ // Create PW resource if not created. Create browser,context and page
+ // 2. Sleep
+ // 3. wantClose
+ // 4. sendAll()
+ // 5. fetchAndProcess()
+ // 6. Close diver
+ // 7. Close streams
+ // 8. Close materializer
+ // Flow
+ // if interface is down and dont want to close wait for 100 milliseconds
+ // interface is up and dont want to close sendAll(), fetchAndProcess() Sleep for 100 milliseconds
+ // If want to close then close driver, streams, materializer
+ // After future is completed close driver, streams, materializer
+
+ def jsRunPrg(
+ browserName: String,
+ headless: Boolean,
+ isComEnabled: Boolean,
+ launchOptions: Option[LaunchOptions]
+ ): Resource[IO, Unit] = for {
+ _ <- Resource.pure(
+ scribe.info(
+ s"Begin Main with isComEnabled $isComEnabled " +
+ s"and browserName $browserName " +
+ s"and headless is $headless "
+ )
+ )
+ pageInstance <- createPage(
+ browserName,
+ headless,
+ launchOptions
+ )
+ _ <- preparePageForJsRun(
+ pageInstance,
+ materializer(pwConfig),
+ input,
+ isComEnabled
+ )
+ connectionReady <- isConnectionUp(pageInstance, intf)
+ _ <-
+ if (!connectionReady) Resource.pure[IO, Unit] {
+ IO.sleep(100.milliseconds)
+ }
+ else Resource.pure[IO, Unit](IO.unit)
+ _ <-
+ if (!connectionReady) isConnectionUp(pageInstance, intf)
+ else Resource.pure[IO, Unit](IO.unit)
+ out <- outputStream(runConfig)
+ _ <- processUntilStop(
+ wantToClose,
+ pageInstance,
+ intf,
+ sendQueue,
+ out,
+ receivedMessage
+ )
+ } yield ()
+
+ /**
+ * Stops the run and releases all the resources.
+ *
+ * This must be called to ensure the run's resources are released.
+ *
+ * Whether or not this makes the run fail or not is up to the implementation. However, in the
+ * following cases, calling [[close]] may not fail the run: - [[Future]] is already
+ * completed when [[close]] is called.
- This is a [[CERun]] and the event loop inside the
+ * VM is empty.
+ *
+ * Idempotent, async, nothrow.
+ */
+
+ def close(): Unit = {
+ wantToClose.set(true)
+ scribe.debug(s"Received stopSignal ${wantToClose.get()}")
+ }
+
+ def getCaller: String = {
+ val stackTraceElements = Thread.currentThread().getStackTrace
+ if (stackTraceElements.length > 5) {
+ val callerElement = stackTraceElements(5)
+ s"Caller class: ${callerElement.getClassName}, method: ${callerElement.getMethodName}"
+ } else {
+ "Could not determine caller."
+ }
+ }
+
+ def logStackTrace(): Unit = {
+ try {
+ throw new Exception("Logging stack trace")
+ } catch {
+ case e: Exception => e.printStackTrace()
+ }
+ }
+
+}
+
+//private class WindowOnErrorException(errs: List[String])
+// extends Exception(s"JS error: $errs")
diff --git a/src/test/scala/jsenv/playwright/PWSuiteChrome.scala b/src/test/scala/jsenv/playwright/PWSuiteChrome.scala
index 3fe44dd..08d1932 100644
--- a/src/test/scala/jsenv/playwright/PWSuiteChrome.scala
+++ b/src/test/scala/jsenv/playwright/PWSuiteChrome.scala
@@ -4,6 +4,7 @@ import org.junit.runner.RunWith
import org.scalajs.jsenv.test._
@RunWith(classOf[JSEnvSuiteRunner])
-class PWSuiteChrome extends JSEnvSuite(
- JSEnvSuiteConfig(new PWEnv("chrome", debug=true))
-)
+class PWSuiteChrome
+ extends JSEnvSuite(
+ JSEnvSuiteConfig(new PWEnv("chrome", debug = true, headless = true))
+ )
diff --git a/src/test/scala/jsenv/playwright/PWSuiteFirefox.scala b/src/test/scala/jsenv/playwright/PWSuiteFirefox.scala
index 9678ac1..d929c47 100644
--- a/src/test/scala/jsenv/playwright/PWSuiteFirefox.scala
+++ b/src/test/scala/jsenv/playwright/PWSuiteFirefox.scala
@@ -4,6 +4,7 @@ import org.junit.runner.RunWith
import org.scalajs.jsenv.test._
@RunWith(classOf[JSEnvSuiteRunner])
-class PWSuiteFirefox extends JSEnvSuite(
- JSEnvSuiteConfig(new PWEnv("firefox",debug=true))
-)
+class PWSuiteFirefox
+ extends JSEnvSuite(
+ JSEnvSuiteConfig(new PWEnv("firefox", debug = true))
+ )
diff --git a/src/test/scala/jsenv/playwright/RunTests.scala b/src/test/scala/jsenv/playwright/RunTests.scala
index b10173b..d2d6617 100644
--- a/src/test/scala/jsenv/playwright/RunTests.scala
+++ b/src/test/scala/jsenv/playwright/RunTests.scala
@@ -1,10 +1,10 @@
-
package jsenv.playwright
import com.google.common.jimfs.Jimfs
import org.junit.Test
import org.scalajs.jsenv._
-import org.scalajs.jsenv.test.kit.{Run, TestKit}
+import org.scalajs.jsenv.test.kit.Run
+import org.scalajs.jsenv.test.kit.TestKit
import java.io.File
import java.nio.charset.StandardCharsets
@@ -13,7 +13,7 @@ import scala.concurrent.duration.DurationInt
class RunTests {
val withCom = true
- private val kit = new TestKit(new PWEnv("chrome",debug = true), 10.second)
+ private val kit = new TestKit(new PWEnv("chrome", debug = true), 100.second)
private def withRun(input: Seq[Input])(body: Run => Unit): Unit = {
if (withCom) kit.withComRun(input)(body)
@@ -21,7 +21,7 @@ class RunTests {
}
private def withRun(code: String, config: RunConfig = RunConfig())(
- body: Run => Unit
+ body: Run => Unit
): Unit = {
if (withCom) kit.withComRun(code, config)(body)
else kit.withRun(code, config)(body)
@@ -60,24 +60,21 @@ class RunTests {
console.log(e);
}
""") {
- _.expectOut("hello world\n")
- .closeRun()
+ _.expectOut("hello world\n").closeRun()
}
}
@Test // Failed in Phantom - #2053
def utf8Test(): Unit = {
withRun("console.log('\u1234')") {
- _.expectOut("\u1234\n")
- .closeRun()
+ _.expectOut("\u1234\n").closeRun()
}
}
@Test
def allowScriptTags(): Unit = {
withRun("""console.log("");""") {
- _.expectOut("\n")
- .closeRun()
+ _.expectOut("\n").closeRun()
}
}
@@ -99,8 +96,7 @@ class RunTests {
val result = strings.mkString("", "\n", "\n")
withRun(code) {
- _.expectOut(result)
- .closeRun()
+ _.expectOut(result).closeRun()
}
}
@@ -149,12 +145,11 @@ class RunTests {
)
withRun(Input.Script(tmpPath) :: Nil) {
- _.expectOut("test\n")
- .closeRun()
+ _.expectOut("test\n").closeRun()
}
} finally {
tmpFile.delete()
}
}
-}
\ No newline at end of file
+}