Quickstart
Chekhov is a ZIO-first Playwright client: pinned protocol.yml becomes a typed Scala AST,
a Node driver speaks the channel, and suites compose browser / context / page as layers.
If the UI shows a control, a test should be able to fire it.
A suite in one screenful
Add the suite stack, install this Chekhov version's Playwright, then run the tests:
// build.sbt
libraryDependencies ++= Seq(
"rocks.earlyeffect" %% "chekhov-zio-test" % "<version>" % Test,
"rocks.earlyeffect" %% "chekhov-driver" % "<version>" % Test,
)
// project/plugins.sbt (optional, for chekhovInstall + JSEnv)
addSbtPlugin("rocks.earlyeffect" % "sbt-chekhov" % "<version>")
sbt chekhovInstall
sbt 'Test/testOnly …'
import chekhov.*
import chekhov.ziotest.ChekhovSuite
import zio.test.*
object TodoSpec extends ChekhovSuite:
def spec = suite("todo")(
test("add") {
for
page <- Chekhov.page
_ <- page.goto("/")
_ <- page.fill("input.new-todo", "milk")
_ <- page.press("input.new-todo", "Enter")
text <- page.innerText(".todo-list")
yield assertTrue(text.contains("milk"))
}
)
No consumer package.json. A ChekhovSuite in a module just runs when you test that
module. Keep browser suites in their own sbt project if core/test must stay cheap
(see Browsers and E2E). chekhovInstall extracts Playwright
1.62.1 into the Chekhov cache and installs matching
Chromium / Firefox / WebKit. A leftover npx CLI or a different Playwright on
NODE_PATH is a hard error, not a silent driver.
ChekhovSuite wires config + driver + a fresh page per test (fresh context + page; the browser
process is shared across the spec, so concurrent tests never share a page). Override
chekhovConfig when you need a different browser, baseUrl, or artifactsDir. Point baseUrl
at a running app (or use StaticFileServer or AppServer.serve; see Serving the app).
How the stack fits
There is no com.microsoft.playwright JAR. Chekhov claims a curated set of channel methods
and interprets them over the official Node driver. Scala.js DOM tests take a different path
(ChekhovJSEnv + chekhov-dom); see JSEnv and chekhov-dom.
Claimed surface stays honest
ProtocolSurface is generated from the pinned protocol inventory. Navigation and input are
Frame channel methods on the wire (public algebra still looks like page.goto /
page.click). Coverage gates assert the allowlist against this inventory.
(
ProtocolSurface.has("Frame", "goto"),
ProtocolSurface.has("Frame", "click"),
ProtocolSurface.has("Frame", "fill"),
ProtocolSurface.has("Page", "screenshot"),
)(true,true,true,true)