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: - * - * - * 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: + * + * 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 +}