diff --git a/libs/scalajslib/api/src/mill/scalajslib/worker/api/JsEnvConfig.scala b/libs/scalajslib/api/src/mill/scalajslib/worker/api/JsEnvConfig.scala index 4b142907930c..5a103dcaab46 100644 --- a/libs/scalajslib/api/src/mill/scalajslib/worker/api/JsEnvConfig.scala +++ b/libs/scalajslib/api/src/mill/scalajslib/worker/api/JsEnvConfig.scala @@ -38,4 +38,67 @@ private[scalajslib] object JsEnvConfig { case class FirefoxOptions(headless: Boolean) extends Capabilities case class SafariOptions() extends Capabilities } + + final case class Playwright( + capabilities: Playwright.Capabilities + ) extends JsEnvConfig + object Playwright { + sealed trait Capabilities + case class ChromeOptions( + headless: Boolean = true, + showLogs: Boolean = false, + debug: Boolean = false, + launchOptions: List[String] = List( + "--disable-extensions", + "--disable-web-security", + "--allow-running-insecure-content", + "--disable-site-isolation-trials", + "--allow-file-access-from-files", + "--disable-gpu" + ) + ) extends Capabilities: + + def addLaunchOptions(options: List[String]): ChromeOptions = + copy(launchOptions = (launchOptions ++ options).distinct) + + object ChromeOptions: + val default = ChromeOptions() + + case class FirefoxOptions( + headless: Boolean = true, + showLogs: Boolean = false, + debug: Boolean = false, + firefoxUserPrefs: Map[String, String | Double | Boolean] = Map( + "security.mixed_content.block_active_content" -> false, + "security.mixed_content.upgrade_display_content" -> false, + "security.file_uri.strict_origin_policy" -> false + ) + ) extends Capabilities: + + def addFirefoxUserPrefs(options: Map[String, String | Double | Boolean]) + : FirefoxOptions = + copy(firefoxUserPrefs = (firefoxUserPrefs ++ options)) + + object FirefoxOptions: + val default = FirefoxOptions() + + case class WebkitOptions( + headless: Boolean = true, + showLogs: Boolean = false, + debug: Boolean = false, + launchOptions: List[String] = List( + "--disable-extensions", + "--disable-web-security", + "--allow-running-insecure-content", + "--disable-site-isolation-trials", + "--allow-file-access-from-files" + ) + ) extends Capabilities: + + def addLaunchOptions(options: List[String]): WebkitOptions = + copy(launchOptions = (launchOptions ++ options).distinct) + + object WebkitOptions: + val default = WebkitOptions() + } } diff --git a/libs/scalajslib/package.mill b/libs/scalajslib/package.mill index 7b7a596ad5b9..28e8cc3deedc 100644 --- a/libs/scalajslib/package.mill +++ b/libs/scalajslib/package.mill @@ -35,6 +35,7 @@ object `package` extends MillStableScalaModule with BuildInfo { ), BuildInfo.Value("scalajsEnvPhantomJs", formatDep(Deps.Scalajs_1.scalajsEnvPhantomjs)), BuildInfo.Value("scalajsEnvSelenium", formatDep(Deps.Scalajs_1.scalajsEnvSelenium)), + BuildInfo.Value("scalajsEnvPlaywright", formatDep(Deps.Scalajs_1.scalajsEnvPlaywright)), BuildInfo.Value("scalajsImportMap", formatDep(Deps.Scalajs_1.scalajsImportMap)) ) } @@ -60,6 +61,7 @@ object `package` extends MillStableScalaModule with BuildInfo { Deps.Scalajs_1.scalajsEnvExoegoJsdomNodejs, Deps.Scalajs_1.scalajsEnvPhantomjs, Deps.Scalajs_1.scalajsEnvSelenium, + Deps.Scalajs_1.scalajsEnvPlaywright, Deps.Scalajs_1.scalajsImportMap ) } diff --git a/libs/scalajslib/src/mill/scalajslib/ScalaJSModule.scala b/libs/scalajslib/src/mill/scalajslib/ScalaJSModule.scala index a2b1b6dad000..a6d7f2d4a843 100644 --- a/libs/scalajslib/src/mill/scalajslib/ScalaJSModule.scala +++ b/libs/scalajslib/src/mill/scalajslib/ScalaJSModule.scala @@ -68,6 +68,8 @@ trait ScalaJSModule extends scalalib.ScalaModule with ScalaJSModuleApi { outer = mvn"${ScalaJSBuildInfo.scalajsEnvPhantomJs}" case _: JsEnvConfig.Selenium => mvn"${ScalaJSBuildInfo.scalajsEnvSelenium}" + case _: JsEnvConfig.Playwright => + mvn"${ScalaJSBuildInfo.scalajsEnvPlaywright}" } Seq(dep) diff --git a/libs/scalajslib/src/mill/scalajslib/api/JsEnvConfig.scala b/libs/scalajslib/src/mill/scalajslib/api/JsEnvConfig.scala index 1ec615fe606a..869c941013f6 100644 --- a/libs/scalajslib/src/mill/scalajslib/api/JsEnvConfig.scala +++ b/libs/scalajslib/src/mill/scalajslib/api/JsEnvConfig.scala @@ -11,6 +11,7 @@ object JsEnvConfig { implicit def rwExoegoJsDomNodeJs: RW[ExoegoJsDomNodeJs] = macroRW implicit def rwPhantom: RW[Phantom] = macroRW implicit def rwSelenium: RW[Selenium] = macroRW + implicit def rwPlaywright: RW[Playwright] = macroRW implicit def rw: RW[JsEnvConfig] = macroRW private given Root_JsEnvConfig: Mirrors.Root[JsEnvConfig] = @@ -116,4 +117,149 @@ object JsEnvConfig { new SafariOptions() } } + + final class Playwright private (val capabilities: Playwright.Capabilities) extends JsEnvConfig + object Playwright { + implicit def rwCapabilities: RW[Capabilities] = macroRW + + private given Root_Capabilities: Mirrors.Root[Capabilities] = + Mirrors.autoRoot[Capabilities] + + def apply(capabilities: Capabilities): Playwright = + new Playwright(capabilities = capabilities) + + sealed trait Capabilities + + /** + * Default launch options for Chrome, directly derived from the scala-js-env-playwright implementation: https://github.com/ThijsBroersen/scala-js-env-playwright/blob/main/src/main/scala/jsenv/playwright/PlaywrightJSEnv.scala + */ + val defaultChromeLaunchOptions = List( + "--disable-extensions", + "--disable-web-security", + "--allow-running-insecure-content", + "--disable-site-isolation-trials", + "--allow-file-access-from-files", + "--disable-gpu" + ) + + def chrome( + headless: Boolean = true, + showLogs: Boolean = false, + debug: Boolean = false, + launchOptions: List[String] = defaultChromeLaunchOptions + ): Playwright = + val options = ChromeOptions( + headless = headless, + showLogs = showLogs, + debug = debug, + launchOptions = launchOptions + ) + new Playwright(options) + + case class ChromeOptions( + headless: Boolean = true, + showLogs: Boolean = false, + debug: Boolean = false, + launchOptions: List[String] = defaultChromeLaunchOptions + ) extends Capabilities { + def withHeadless(value: Boolean): ChromeOptions = copy(headless = value) + def withShowLogs(value: Boolean): ChromeOptions = copy(showLogs = value) + def withDebug(value: Boolean): ChromeOptions = copy(debug = value) + def withLaunchOptions(value: List[String]): ChromeOptions = copy(launchOptions = value) + } + object ChromeOptions: + implicit def rw: RW[ChromeOptions] = macroRW + + /** + * Default Firefox user prefs, directly derived from the scala-js-env-playwright implementation: https://github.com/ThijsBroersen/scala-js-env-playwright/blob/main/src/main/scala/jsenv/playwright/PlaywrightJSEnv.scala + */ + val defaultFirefoxUserPrefs: Map[String, String | Double | Boolean] = + Map( + "security.mixed_content.block_active_content" -> false, + "security.mixed_content.upgrade_display_content" -> false, + "security.file_uri.strict_origin_policy" -> false + ) + + def firefox( + headless: Boolean = true, + showLogs: Boolean = false, + debug: Boolean = false, + firefoxUserPrefs: Map[String, String | Double | Boolean] = defaultFirefoxUserPrefs + ): Playwright = + val options = FirefoxOptions( + headless = headless, + showLogs = showLogs, + debug = debug, + firefoxUserPrefs = firefoxUserPrefs + ) + new Playwright(options) + case class FirefoxOptions( + headless: Boolean = true, + showLogs: Boolean = false, + debug: Boolean = false, + firefoxUserPrefs: Map[String, String | Double | Boolean] = defaultFirefoxUserPrefs + ) extends Capabilities { + def withHeadless(value: Boolean): FirefoxOptions = copy(headless = value) + def withShowLogs(value: Boolean): FirefoxOptions = copy(showLogs = value) + def withDebug(value: Boolean): FirefoxOptions = copy(debug = value) + def withFirefoxUserPrefs(value: Map[String, String | Double | Boolean]): FirefoxOptions = + copy(firefoxUserPrefs = value) + } + object FirefoxOptions: + given upickle.default.ReadWriter[String | Double | Boolean] = + upickle.default.readwriter[ujson.Value].bimap[String | Double | Boolean]( + { + case v: Boolean => upickle.default.writeJs(v) + case v: Double => upickle.default.writeJs(v) + case v: String => upickle.default.writeJs(v) + }, + json => + json.boolOpt + .orElse( + json.numOpt + ).orElse( + json.strOpt.map(_.toString) + ).getOrElse(throw new Exception("Invalid value")) + ) + given rw: RW[FirefoxOptions] = macroRW + + /** + * Default launch options for Webkit, directly derived from the scala-js-env-playwright implementation: https://github.com/ThijsBroersen/scala-js-env-playwright/blob/main/src/main/scala/jsenv/playwright/PlaywrightJSEnv.scala + */ + val defaultWebkitLaunchOptions = List( + "--disable-extensions", + "--disable-web-security", + "--allow-running-insecure-content", + "--disable-site-isolation-trials", + "--allow-file-access-from-files" + ) + + def webkit( + headless: Boolean = true, + showLogs: Boolean = false, + debug: Boolean = false, + launchOptions: List[String] = defaultWebkitLaunchOptions + ): Playwright = + val options = WebkitOptions( + headless = headless, + showLogs = showLogs, + debug = debug, + launchOptions = launchOptions + ) + new Playwright(options) + + case class WebkitOptions( + headless: Boolean = true, + showLogs: Boolean = false, + debug: Boolean = false, + launchOptions: List[String] = defaultWebkitLaunchOptions + ) extends Capabilities { + def withHeadless(value: Boolean): WebkitOptions = copy(headless = value) + def withShowLogs(value: Boolean): WebkitOptions = copy(showLogs = value) + def withDebug(value: Boolean): WebkitOptions = copy(debug = value) + def withLaunchOptions(value: List[String]): WebkitOptions = copy(launchOptions = value) + } + object WebkitOptions: + implicit def rw: RW[WebkitOptions] = macroRW + } } diff --git a/libs/scalajslib/src/mill/scalajslib/worker/ScalaJSWorker.scala b/libs/scalajslib/src/mill/scalajslib/worker/ScalaJSWorker.scala index 27e7847b12b1..f16f4d94b66c 100644 --- a/libs/scalajslib/src/mill/scalajslib/worker/ScalaJSWorker.scala +++ b/libs/scalajslib/src/mill/scalajslib/worker/ScalaJSWorker.scala @@ -109,6 +109,30 @@ private[scalajslib] class ScalaJSWorker(jobs: Int) workerApi.JsEnvConfig.Selenium.SafariOptions() } ) + case config: api.JsEnvConfig.Playwright => + val options = config.capabilities match + case options: api.JsEnvConfig.Playwright.ChromeOptions => + workerApi.JsEnvConfig.Playwright.ChromeOptions( + headless = options.headless, + showLogs = options.showLogs, + debug = options.debug, + launchOptions = options.launchOptions + ) + case options: api.JsEnvConfig.Playwright.FirefoxOptions => + workerApi.JsEnvConfig.Playwright.FirefoxOptions( + headless = options.headless, + showLogs = options.showLogs, + debug = options.debug, + firefoxUserPrefs = options.firefoxUserPrefs + ) + case options: api.JsEnvConfig.Playwright.WebkitOptions => + workerApi.JsEnvConfig.Playwright.WebkitOptions( + headless = options.headless, + showLogs = options.showLogs, + debug = options.debug, + launchOptions = options.launchOptions + ) + workerApi.JsEnvConfig.Playwright(options) } } diff --git a/libs/scalajslib/worker/1/src/mill/scalajslib/worker/ScalaJSWorkerImpl.scala b/libs/scalajslib/worker/1/src/mill/scalajslib/worker/ScalaJSWorkerImpl.scala index 8638c796f73a..3ec4bf735a76 100644 --- a/libs/scalajslib/worker/1/src/mill/scalajslib/worker/ScalaJSWorkerImpl.scala +++ b/libs/scalajslib/worker/1/src/mill/scalajslib/worker/ScalaJSWorkerImpl.scala @@ -374,6 +374,8 @@ class ScalaJSWorkerImpl extends ScalaJSWorkerApi { Phantom(config) case config: JsEnvConfig.Selenium => Selenium(config) + case config: JsEnvConfig.Playwright => + Playwright(config) } def jsEnvInput(report: Report): Seq[Input] = { diff --git a/libs/scalajslib/worker/1/src/mill/scalajslib/worker/jsenv/Playwright.scala b/libs/scalajslib/worker/1/src/mill/scalajslib/worker/jsenv/Playwright.scala new file mode 100644 index 000000000000..4b7129b394fa --- /dev/null +++ b/libs/scalajslib/worker/1/src/mill/scalajslib/worker/jsenv/Playwright.scala @@ -0,0 +1,28 @@ +package mill.scalajslib.worker.jsenv + +import mill.scalajslib.worker.api._ + +object Playwright { + def apply(config: JsEnvConfig.Playwright) = config.capabilities match + case options: JsEnvConfig.Playwright.ChromeOptions => + io.github.thijsbroersen.jsenv.playwright.PlaywrightJSEnv.chrome( + headless = options.headless, + showLogs = options.showLogs, + debug = options.debug, + launchOptions = options.launchOptions + ) + case options: JsEnvConfig.Playwright.FirefoxOptions => + io.github.thijsbroersen.jsenv.playwright.PlaywrightJSEnv.firefox( + headless = options.headless, + showLogs = options.showLogs, + debug = options.debug, + firefoxUserPrefs = options.firefoxUserPrefs + ) + case options: JsEnvConfig.Playwright.WebkitOptions => + io.github.thijsbroersen.jsenv.playwright.PlaywrightJSEnv.webkit( + headless = options.headless, + showLogs = options.showLogs, + debug = options.debug, + launchOptions = options.launchOptions + ) +} diff --git a/mill-build/src/millbuild/Deps.scala b/mill-build/src/millbuild/Deps.scala index 1af600c87762..de6371702d98 100644 --- a/mill-build/src/millbuild/Deps.scala +++ b/mill-build/src/millbuild/Deps.scala @@ -32,6 +32,8 @@ object Deps { mvn"org.scala-js::scalajs-env-phantomjs:1.0.0".withDottyCompat(scalaVersion) val scalajsEnvSelenium = mvn"org.scala-js::scalajs-env-selenium:1.1.1".withDottyCompat(scalaVersion) + val scalajsEnvPlaywright = + mvn"io.github.thijsbroersen::scala-js-env-playwright:0.2.3" val scalajsSbtTestAdapter = mvn"org.scala-js::scalajs-sbt-test-adapter:${scalaJsVersion}".withDottyCompat(scalaVersion) val scalajsLinker =