@douglance/play
v0.2.0
Published
Run Playwright scripts with zero boilerplate
Maintainers
Readme
@douglance/play
@douglance/play is a zero-boilerplate Playwright runner with persistent sessions and real page debugger control built in.
You can still run one-shot scripts, but the primary workflow is now:
- open a named browser session
- execute small action batches with
play do - trigger or hit a real page-side
debugger; - inspect and control the paused browser debugger from the CLI
- continue or step without leaving the
playsession
Why play
- No imports, fixtures, or browser lifecycle code
- Built-in globals like
goto(),click(), andtext()are auto-awaited - Smart selector resolution for roles, labels, placeholders, text, and test IDs
goto()can auto-detect common local dev servers- Assertions produce CI-friendly exit codes
- Persistent named sessions for agent iteration
- Real Chromium page debugger control through CDP
- Debugger queries over paused browser state
- Raw Playwright escape hatches are still available through
page,browser, andcontext
Install
npm install --save-dev @douglance/playThen run it with the bundled play binary:
npx play 'goto("https://example.com"); log(text("h1"))'For one-off usage without adding it to your project:
npx @douglance/play 'goto("https://example.com"); log(title())'Quick Start
Inline script:
play 'goto("https://example.com"); log(text("h1"))'Script file:
play check-homepage.play.tsFrom stdin:
echo 'goto("https://example.com"); screenshot("homepage.png")' | playPersistent agent loop:
play open agent1 --headed --devtools
play do agent1 'goto("https://example.com")'
play do agent1 'evaluate(() => { debugger; })'
play debug status agent1
play debug step-over agent1
play debug continue agent1
play close agent1Modes
play now has two modes:
One-shot mode:
play 'goto("https://example.com"); log(title())'Each invocation launches a browser, runs the script, prints output, and exits.
Session mode:
play open agent1+play do agent1 '...'The browser, context, page, and page debugger state stay alive across commands until
play close.
Built-in globals are auto-awaited, so you can write:
goto("https://example.com")
click("Sign in")
log(text("h1"))If you use raw Playwright objects like page, browser, or context, write normal Playwright code and use await yourself.
TypeScript syntax is accepted for inline code and .ts script files. Type annotations are stripped before execution.
Persistent Session Workflow
Open a session:
play open agent1Run actions inside the same live browser:
play do agent1 'goto("https://example.com"); log(title())'
play do agent1 'click("More information")'Inspect or list sessions:
play status
play status agent1Open a visible Chromium debug session:
play open agent1 --headed --devtoolsRun actions in the live page:
play do agent1 'goto("https://example.com"); click("More information")'Inspect the real page debugger:
play debug status agent1
play debug query agent1 'frames()'
play debug query agent1 'frame(0)'Pause or continue the browser debugger:
play debug pause agent1
play debug step-over agent1
play debug step-into agent1
play debug step-out agent1
play debug continue agent1Close the session:
play close agent1Assertions and Exit Codes
assert(condition, message?) records pass/fail results without stopping the rest of the script. If any assertion fails, play exits with code 1.
play 'goto("https://example.com"); assert(text("h1") === "Example Domain", "heading correct")'In session mode, assertions and output are still reported batch by batch.
Debugging and Queries
play debug now means only one thing: real page debugger control.
The debugger surface is Chromium/CDP-based and is intended to drive actual paused page JavaScript, not a synthetic automation-step pause:
play open agent1 --headed --devtools
play do agent1 'evaluate(() => { debugger; })'
play debug status agent1
play debug step-over agent1
play debug continue agent1play debug query evaluates against a restricted paused-debugger query root with helpers such as:
status()frames()frame(0)frame(0).scopeChain()frame(0).vars()frame(0).var("name").props()scripts()script("pattern").lines(start, end)breakpoints()
play debug eval evaluates in the paused call frame when the debugger is stopped, and falls back to runtime evaluation otherwise.
Smart Selectors
Selector handling is intentionally forgiving:
- CSS selectors are used directly for inputs like
.cta,#main, or[data-testid="hero"] - XPath selectors are used directly for inputs like
//button - Plain strings fall through a smart cascade that tries roles, labels, placeholders, exact text, partial text, and test IDs
fill()prioritizes labels and placeholders before role and text matching
That means commands like click("Submit"), fill("Email", "..."), and wait(".results") work without writing verbose Playwright locators most of the time.
Local Dev Server Detection
If you call goto() without a URL, play tries to find a local app automatically.
It reads package.json, inspects scripts.dev, and infers ports for common setups such as:
vitenext devnuxt devastro devwebpack serve
Explicit --port or -p flags take priority. If no port can be inferred, play falls back to scanning common local ports.
You can always override detection:
play --port 3000 'goto(); log(title())'
play --url "http://localhost:8080" 'goto(); log(url())'Session Commands
| Command | Description |
| --- | --- |
| play open <name> [--headed] [--devtools] | Start a persistent named browser session |
| play status [name] | List sessions or inspect one session |
| play do <name> '<actions>' | Run a batch of actions in an existing session |
| play close <name> | Close the session |
| play debug status <name> | Show real page debugger status |
| play debug pause <name> | Ask the page debugger to pause |
| play debug continue <name> | Resume the paused page debugger |
| play debug step-over <name> | Step over one paused debugger line |
| play debug step-into <name> | Step into one paused debugger line |
| play debug step-out <name> | Step out of the current paused frame |
| play debug eval <name> '<expr>' | Evaluate against the paused call frame or runtime |
| play debug source <name> [file start end] | Show source around the current or named script |
| play debug query <name> '<expr>' | Query paused debugger frames/scripts/breakpoints |
One-Shot Options
| Flag | Short | Description |
| --- | --- | --- |
| --headed | -h | Show the browser window |
| --slow <ms> | -s | Add slow motion delay between Playwright actions |
| --port <port> | -p | Base local port for goto() |
| --url <url> | -u | Base URL for goto() |
| --timeout <ms> | | Set Playwright action timeout and page default timeout |
| --browser <name> | | Choose chromium, firefox, or webkit |
| --device <name> | | Emulate a Playwright device profile such as iPhone 14 |
| --devtools | | Auto-open Chromium DevTools for one-shot runs |
Built-In Globals
The following globals are available in every script:
| Category | Globals |
| --- | --- |
| Navigation | goto(url?), reload(), back(), forward() |
| Interaction | click(sel), dblclick(sel), fill(sel, value), type(sel, text), press(key), check(sel), uncheck(sel), select(sel, value), hover(sel), focus(sel), clear(sel), upload(sel, path), drag(from, to) |
| Extraction | text(sel), texts(sel), attr(sel, name), html(sel), value(sel), count(sel), visible(sel), title(), url(), content(), table(sel?), evaluate(fn) |
| Waiting | wait(target), waitFor(target), sleep(ms) |
| Media | screenshot(path?), pdf(path?) |
| Network | mock(url, handler), waitForResponse(url), waitForRequest(url) |
| Output | log(...args) |
| Assertions | assert(condition, message?) |
| Escape hatches | page, browser, context |
wait() and waitFor() treat values starting with * or / as URL patterns. Everything else is treated as an element target.
Advanced Example
Use the built-ins for the common path and drop to raw Playwright when needed:
goto("https://example.com")
const response = await page.request.get("https://example.com")
log(await response.text())JS API
import {
closePlaySession,
continuePlayDebug,
getPlayDebugStatus,
openPlaySession,
queryPlayDebug,
runPlayActions,
stepOverPlayDebug,
} from "@douglance/play";
await openPlaySession("agent1", { headed: true, devtools: true });
await runPlayActions("agent1", 'goto("https://example.com")');
await runPlayActions("agent1", 'evaluate(() => { debugger; })');
await getPlayDebugStatus("agent1");
await stepOverPlayDebug("agent1");
await queryPlayDebug("agent1", "frames()");
await continuePlayDebug("agent1");
await closePlaySession("agent1");License
MIT
