Skip to content

Commit 2f07969

Browse files
feat: support Playwright testing for Scala-JS (#5777)
I could not find any existing JSEnv which allows me to properly test against some browser features and am using [scala-js-env-playwright](https://github.com/ThijsBroersen/scala-js-env-playwright) for a while now. The current version is still a snapshot so this PR is not ready to be merged, but I would love some feedback on what is required to get this feature as part of Mill. Currently I am using a custom build mill-assembly for my projects. Here a draft of Playwright support for Scala-js testing. I was also wondering why isn't Mill supporting just a raw JSEnv? Any custom JSEnv implemention now first requires Mill to include it.. --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 187c187 commit 2f07969

File tree

8 files changed

+269
-0
lines changed

8 files changed

+269
-0
lines changed

libs/scalajslib/api/src/mill/scalajslib/worker/api/JsEnvConfig.scala

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,67 @@ private[scalajslib] object JsEnvConfig {
3838
case class FirefoxOptions(headless: Boolean) extends Capabilities
3939
case class SafariOptions() extends Capabilities
4040
}
41+
42+
final case class Playwright(
43+
capabilities: Playwright.Capabilities
44+
) extends JsEnvConfig
45+
object Playwright {
46+
sealed trait Capabilities
47+
case class ChromeOptions(
48+
headless: Boolean = true,
49+
showLogs: Boolean = false,
50+
debug: Boolean = false,
51+
launchOptions: List[String] = List(
52+
"--disable-extensions",
53+
"--disable-web-security",
54+
"--allow-running-insecure-content",
55+
"--disable-site-isolation-trials",
56+
"--allow-file-access-from-files",
57+
"--disable-gpu"
58+
)
59+
) extends Capabilities:
60+
61+
def addLaunchOptions(options: List[String]): ChromeOptions =
62+
copy(launchOptions = (launchOptions ++ options).distinct)
63+
64+
object ChromeOptions:
65+
val default = ChromeOptions()
66+
67+
case class FirefoxOptions(
68+
headless: Boolean = true,
69+
showLogs: Boolean = false,
70+
debug: Boolean = false,
71+
firefoxUserPrefs: Map[String, String | Double | Boolean] = Map(
72+
"security.mixed_content.block_active_content" -> false,
73+
"security.mixed_content.upgrade_display_content" -> false,
74+
"security.file_uri.strict_origin_policy" -> false
75+
)
76+
) extends Capabilities:
77+
78+
def addFirefoxUserPrefs(options: Map[String, String | Double | Boolean])
79+
: FirefoxOptions =
80+
copy(firefoxUserPrefs = (firefoxUserPrefs ++ options))
81+
82+
object FirefoxOptions:
83+
val default = FirefoxOptions()
84+
85+
case class WebkitOptions(
86+
headless: Boolean = true,
87+
showLogs: Boolean = false,
88+
debug: Boolean = false,
89+
launchOptions: List[String] = List(
90+
"--disable-extensions",
91+
"--disable-web-security",
92+
"--allow-running-insecure-content",
93+
"--disable-site-isolation-trials",
94+
"--allow-file-access-from-files"
95+
)
96+
) extends Capabilities:
97+
98+
def addLaunchOptions(options: List[String]): WebkitOptions =
99+
copy(launchOptions = (launchOptions ++ options).distinct)
100+
101+
object WebkitOptions:
102+
val default = WebkitOptions()
103+
}
41104
}

libs/scalajslib/package.mill

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ object `package` extends MillStableScalaModule with BuildInfo {
3535
),
3636
BuildInfo.Value("scalajsEnvPhantomJs", formatDep(Deps.Scalajs_1.scalajsEnvPhantomjs)),
3737
BuildInfo.Value("scalajsEnvSelenium", formatDep(Deps.Scalajs_1.scalajsEnvSelenium)),
38+
BuildInfo.Value("scalajsEnvPlaywright", formatDep(Deps.Scalajs_1.scalajsEnvPlaywright)),
3839
BuildInfo.Value("scalajsImportMap", formatDep(Deps.Scalajs_1.scalajsImportMap))
3940
)
4041
}
@@ -60,6 +61,7 @@ object `package` extends MillStableScalaModule with BuildInfo {
6061
Deps.Scalajs_1.scalajsEnvExoegoJsdomNodejs,
6162
Deps.Scalajs_1.scalajsEnvPhantomjs,
6263
Deps.Scalajs_1.scalajsEnvSelenium,
64+
Deps.Scalajs_1.scalajsEnvPlaywright,
6365
Deps.Scalajs_1.scalajsImportMap
6466
)
6567
}

libs/scalajslib/src/mill/scalajslib/ScalaJSModule.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ trait ScalaJSModule extends scalalib.ScalaModule with ScalaJSModuleApi { outer =
6868
mvn"${ScalaJSBuildInfo.scalajsEnvPhantomJs}"
6969
case _: JsEnvConfig.Selenium =>
7070
mvn"${ScalaJSBuildInfo.scalajsEnvSelenium}"
71+
case _: JsEnvConfig.Playwright =>
72+
mvn"${ScalaJSBuildInfo.scalajsEnvPlaywright}"
7173
}
7274

7375
Seq(dep)

libs/scalajslib/src/mill/scalajslib/api/JsEnvConfig.scala

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ object JsEnvConfig {
1111
implicit def rwExoegoJsDomNodeJs: RW[ExoegoJsDomNodeJs] = macroRW
1212
implicit def rwPhantom: RW[Phantom] = macroRW
1313
implicit def rwSelenium: RW[Selenium] = macroRW
14+
implicit def rwPlaywright: RW[Playwright] = macroRW
1415
implicit def rw: RW[JsEnvConfig] = macroRW
1516

1617
private given Root_JsEnvConfig: Mirrors.Root[JsEnvConfig] =
@@ -116,4 +117,149 @@ object JsEnvConfig {
116117
new SafariOptions()
117118
}
118119
}
120+
121+
final class Playwright private (val capabilities: Playwright.Capabilities) extends JsEnvConfig
122+
object Playwright {
123+
implicit def rwCapabilities: RW[Capabilities] = macroRW
124+
125+
private given Root_Capabilities: Mirrors.Root[Capabilities] =
126+
Mirrors.autoRoot[Capabilities]
127+
128+
def apply(capabilities: Capabilities): Playwright =
129+
new Playwright(capabilities = capabilities)
130+
131+
sealed trait Capabilities
132+
133+
/**
134+
* 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
135+
*/
136+
val defaultChromeLaunchOptions = List(
137+
"--disable-extensions",
138+
"--disable-web-security",
139+
"--allow-running-insecure-content",
140+
"--disable-site-isolation-trials",
141+
"--allow-file-access-from-files",
142+
"--disable-gpu"
143+
)
144+
145+
def chrome(
146+
headless: Boolean = true,
147+
showLogs: Boolean = false,
148+
debug: Boolean = false,
149+
launchOptions: List[String] = defaultChromeLaunchOptions
150+
): Playwright =
151+
val options = ChromeOptions(
152+
headless = headless,
153+
showLogs = showLogs,
154+
debug = debug,
155+
launchOptions = launchOptions
156+
)
157+
new Playwright(options)
158+
159+
case class ChromeOptions(
160+
headless: Boolean = true,
161+
showLogs: Boolean = false,
162+
debug: Boolean = false,
163+
launchOptions: List[String] = defaultChromeLaunchOptions
164+
) extends Capabilities {
165+
def withHeadless(value: Boolean): ChromeOptions = copy(headless = value)
166+
def withShowLogs(value: Boolean): ChromeOptions = copy(showLogs = value)
167+
def withDebug(value: Boolean): ChromeOptions = copy(debug = value)
168+
def withLaunchOptions(value: List[String]): ChromeOptions = copy(launchOptions = value)
169+
}
170+
object ChromeOptions:
171+
implicit def rw: RW[ChromeOptions] = macroRW
172+
173+
/**
174+
* 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
175+
*/
176+
val defaultFirefoxUserPrefs: Map[String, String | Double | Boolean] =
177+
Map(
178+
"security.mixed_content.block_active_content" -> false,
179+
"security.mixed_content.upgrade_display_content" -> false,
180+
"security.file_uri.strict_origin_policy" -> false
181+
)
182+
183+
def firefox(
184+
headless: Boolean = true,
185+
showLogs: Boolean = false,
186+
debug: Boolean = false,
187+
firefoxUserPrefs: Map[String, String | Double | Boolean] = defaultFirefoxUserPrefs
188+
): Playwright =
189+
val options = FirefoxOptions(
190+
headless = headless,
191+
showLogs = showLogs,
192+
debug = debug,
193+
firefoxUserPrefs = firefoxUserPrefs
194+
)
195+
new Playwright(options)
196+
case class FirefoxOptions(
197+
headless: Boolean = true,
198+
showLogs: Boolean = false,
199+
debug: Boolean = false,
200+
firefoxUserPrefs: Map[String, String | Double | Boolean] = defaultFirefoxUserPrefs
201+
) extends Capabilities {
202+
def withHeadless(value: Boolean): FirefoxOptions = copy(headless = value)
203+
def withShowLogs(value: Boolean): FirefoxOptions = copy(showLogs = value)
204+
def withDebug(value: Boolean): FirefoxOptions = copy(debug = value)
205+
def withFirefoxUserPrefs(value: Map[String, String | Double | Boolean]): FirefoxOptions =
206+
copy(firefoxUserPrefs = value)
207+
}
208+
object FirefoxOptions:
209+
given upickle.default.ReadWriter[String | Double | Boolean] =
210+
upickle.default.readwriter[ujson.Value].bimap[String | Double | Boolean](
211+
{
212+
case v: Boolean => upickle.default.writeJs(v)
213+
case v: Double => upickle.default.writeJs(v)
214+
case v: String => upickle.default.writeJs(v)
215+
},
216+
json =>
217+
json.boolOpt
218+
.orElse(
219+
json.numOpt
220+
).orElse(
221+
json.strOpt.map(_.toString)
222+
).getOrElse(throw new Exception("Invalid value"))
223+
)
224+
given rw: RW[FirefoxOptions] = macroRW
225+
226+
/**
227+
* 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
228+
*/
229+
val defaultWebkitLaunchOptions = List(
230+
"--disable-extensions",
231+
"--disable-web-security",
232+
"--allow-running-insecure-content",
233+
"--disable-site-isolation-trials",
234+
"--allow-file-access-from-files"
235+
)
236+
237+
def webkit(
238+
headless: Boolean = true,
239+
showLogs: Boolean = false,
240+
debug: Boolean = false,
241+
launchOptions: List[String] = defaultWebkitLaunchOptions
242+
): Playwright =
243+
val options = WebkitOptions(
244+
headless = headless,
245+
showLogs = showLogs,
246+
debug = debug,
247+
launchOptions = launchOptions
248+
)
249+
new Playwright(options)
250+
251+
case class WebkitOptions(
252+
headless: Boolean = true,
253+
showLogs: Boolean = false,
254+
debug: Boolean = false,
255+
launchOptions: List[String] = defaultWebkitLaunchOptions
256+
) extends Capabilities {
257+
def withHeadless(value: Boolean): WebkitOptions = copy(headless = value)
258+
def withShowLogs(value: Boolean): WebkitOptions = copy(showLogs = value)
259+
def withDebug(value: Boolean): WebkitOptions = copy(debug = value)
260+
def withLaunchOptions(value: List[String]): WebkitOptions = copy(launchOptions = value)
261+
}
262+
object WebkitOptions:
263+
implicit def rw: RW[WebkitOptions] = macroRW
264+
}
119265
}

libs/scalajslib/src/mill/scalajslib/worker/ScalaJSWorker.scala

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,30 @@ private[scalajslib] class ScalaJSWorker(jobs: Int)
111111
workerApi.JsEnvConfig.Selenium.SafariOptions()
112112
}
113113
)
114+
case config: api.JsEnvConfig.Playwright =>
115+
val options = config.capabilities match
116+
case options: api.JsEnvConfig.Playwright.ChromeOptions =>
117+
workerApi.JsEnvConfig.Playwright.ChromeOptions(
118+
headless = options.headless,
119+
showLogs = options.showLogs,
120+
debug = options.debug,
121+
launchOptions = options.launchOptions
122+
)
123+
case options: api.JsEnvConfig.Playwright.FirefoxOptions =>
124+
workerApi.JsEnvConfig.Playwright.FirefoxOptions(
125+
headless = options.headless,
126+
showLogs = options.showLogs,
127+
debug = options.debug,
128+
firefoxUserPrefs = options.firefoxUserPrefs
129+
)
130+
case options: api.JsEnvConfig.Playwright.WebkitOptions =>
131+
workerApi.JsEnvConfig.Playwright.WebkitOptions(
132+
headless = options.headless,
133+
showLogs = options.showLogs,
134+
debug = options.debug,
135+
launchOptions = options.launchOptions
136+
)
137+
workerApi.JsEnvConfig.Playwright(options)
114138
}
115139
}
116140

libs/scalajslib/worker/1/src/mill/scalajslib/worker/ScalaJSWorkerImpl.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,8 @@ class ScalaJSWorkerImpl(jobs: Int) extends ScalaJSWorkerApi {
369369
Phantom(config)
370370
case config: JsEnvConfig.Selenium =>
371371
Selenium(config)
372+
case config: JsEnvConfig.Playwright =>
373+
Playwright(config)
372374
}
373375

374376
def jsEnvInput(report: Report): Seq[Input] = {
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package mill.scalajslib.worker.jsenv
2+
3+
import mill.scalajslib.worker.api._
4+
5+
object Playwright {
6+
def apply(config: JsEnvConfig.Playwright) = config.capabilities match
7+
case options: JsEnvConfig.Playwright.ChromeOptions =>
8+
io.github.thijsbroersen.jsenv.playwright.PlaywrightJSEnv.chrome(
9+
headless = options.headless,
10+
showLogs = options.showLogs,
11+
debug = options.debug,
12+
launchOptions = options.launchOptions
13+
)
14+
case options: JsEnvConfig.Playwright.FirefoxOptions =>
15+
io.github.thijsbroersen.jsenv.playwright.PlaywrightJSEnv.firefox(
16+
headless = options.headless,
17+
showLogs = options.showLogs,
18+
debug = options.debug,
19+
firefoxUserPrefs = options.firefoxUserPrefs
20+
)
21+
case options: JsEnvConfig.Playwright.WebkitOptions =>
22+
io.github.thijsbroersen.jsenv.playwright.PlaywrightJSEnv.webkit(
23+
headless = options.headless,
24+
showLogs = options.showLogs,
25+
debug = options.debug,
26+
launchOptions = options.launchOptions
27+
)
28+
}

mill-build/src/millbuild/Deps.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ object Deps {
3232
mvn"org.scala-js::scalajs-env-phantomjs:1.0.0".withDottyCompat(scalaVersion)
3333
val scalajsEnvSelenium =
3434
mvn"org.scala-js::scalajs-env-selenium:1.1.1".withDottyCompat(scalaVersion)
35+
val scalajsEnvPlaywright =
36+
mvn"io.github.thijsbroersen::scala-js-env-playwright:0.2.3"
3537
val scalajsSbtTestAdapter =
3638
mvn"org.scala-js::scalajs-sbt-test-adapter:${scalaJsVersion}".withDottyCompat(scalaVersion)
3739
val scalajsLinker =

0 commit comments

Comments
 (0)