playwright-dsl
v0.2.0
Published
Minimal DSL + MCP server for AI agents to drive Playwright — one action per line, snapshot-first workflow
Downloads
75
Maintainers
Readme
playwright-dsl
Minimal DSL for AI agents to drive Playwright. Use it as a library, a CLI, an MCP server, or embed it in your own toolkit (e.g. mypry for fullstack debugging).
npm install playwright-dsl playwrightThree ways to use it: as a library (execute/Session), as a CLI
(playwright-dsl run), or as an MCP server (playwright-dsl-mcp).
import { execute, connect, dispose } from 'playwright-dsl';
const handle = await connect({ cdpEndpoint: 'http://localhost:9222' });
const result = await execute(`
fill "textbox Email" "[email protected]"
fill "textbox Password" "s3cret"
click "button Sign in"
wait "#dashboard" visible
expect url contains "/dashboard"
extract "#handle" as handle
`, { page: handle.page, refMap: {} });
await dispose(handle);Philosophy
- Snapshot-first. The agent takes a snapshot (ARIA tree), sees what's on the page, and copies selectors directly. No guessing.
- One action per line. No structures, no indentation, no YAML. Cheapest format in tokens for any LLM to generate and humans to read.
- No
if/for. If the agent needs to branch, it executes a chunk, takes a snapshot, receives the new state, and emits the next chunk. - Source-code selectors welcome. If the agent already knows selectors
from the codebase (
#email,.btn-submit), use them directly — no snapshot needed, saves tokens.
Targeting
Every verb that acts on an element receives a target as its first argument. All targets must be quoted strings.
Snapshot refs (recommended — copy from snapshot)
Take a snapshot, see what's there, copy the role + name into quotes:
| Snapshot shows | Write in script |
|---|---|
| button "Sign In" | click "button Sign In" |
| textbox "Email" | fill "textbox Email" "alice" |
| link "Dashboard" | click "link Dashboard" |
| heading "Welcome" | expect "heading Welcome" visible |
| checkbox "Accept" | check "checkbox Accept" |
| combobox | select "combobox" "Spain" |
All ARIA roles are supported: button, textbox, link, heading, checkbox, combobox, listitem, tab, dialog, menuitem, radio, slider, switch, treeitem, etc.
Prefixed selectors
Explicit prefixes that map to Playwright's user-facing locators:
| You write | Playwright API | When to use |
|---|---|---|
| "role:button=Sign in" | getByRole('button', { name: 'Sign in' }) | Explicit role (same as snapshot ref) |
| "label:Email" | getByLabel('Email') | Inputs by their <label> |
| "placeholder:Search" | getByPlaceholder('Search') | Inputs by placeholder |
| "text:Welcome" | getByText('Welcome') | Visible text |
| "testid:submit" | getByTestId('submit') | If your app has data-testid |
| "alt:Logo" | getByAltText('Logo') | Images by alt text |
| "title:Close" | getByTitle('Close') | By title attribute |
CSS / native selectors (fallback)
If you know selectors from the source code, use them directly:
click "#submit"
fill "#email" "[email protected]"
fill "input[name=username]" "alice"
click ".btn-primary"
expect "nav >> text=Logout" visiblePriority: snapshot refs →
role:→label:→text:→testid:→ CSS →xpath:. The first ones survive style and structure changes; the last ones are fragile.
Full syntax
# comment until end of line
# ── navigation ──
goto https://example.com
goto https://example.com networkidle # optional waitUntil
back
forward
reload
# ── interactions (target = "quoted selector") ──
click "button Save"
click "role:button=Save"
dblclick ".row"
rclick "#item" # right-click
fill "textbox Email" "[email protected]"
clear "#email"
type "#search" "playwright" # types char by char (fires key events)
type "Enter" # no target → types on the focused element
press "#search" Enter # key on an element
press Escape # global key
press "Control+A" # combos
select "combobox" "España"
check "checkbox Accept terms"
uncheck "#newsletter"
hover ".menu"
focus "#email"
upload "#file" "/tmp/report.pdf"
# ── scroll ──
scroll "#footer" into # scrollIntoView
scroll page down 800 # wheel: down|up by N px
scroll page to 0 # to absolute Y coordinate
# ── waits ──
wait "button Submit" visible # states: visible|hidden|enabled|attached|detached
wait "#spinner" hidden
wait 500ms # absolute time (use sparingly)
waitload networkidle # load|domcontentloaded|networkidle
waiturl "/dashboard" # wait for URL to match
# ── assertions (auto-retry until timeout) ──
expect "heading Welcome" visible
expect "#status" contains "OK"
expect ".item" count 10
expect "#email" equals "[email protected]"
expect url contains "/dashboard"
expect title matches "Inbox \\(\\d+\\)"
# ── extraction to variables ──
extract "#username" as user # textContent by default
extract "#email" value as email # value of an input
extract ".link" href as url # href attribute
extract "#card" html as snippet # innerHTML
extract ".item" count as howMany # number of matches
extract "#card" attr "data-id" as cardId # arbitrary attribute
# ── interpolation (in strings and barewords) ──
fill "#email" "${username}@example.com"
goto https://x.com/${path}
# ── dialogs (alert/confirm/prompt) ──
ondialog accept # accept the NEXT dialog
ondialog accept "text for prompt"
ondialog dismiss # cancel
ondialog default # revert to default behavior
# ── downloads ──
download "#export" as "data.csv" # click and save to downloadDir
download "role:button=Download" # uses the suggested filename
# ── multi-tab / popups ──
tab open second https://other.com # open and switch to tab "second"
tab switch main # switch back
tab close second
tab list # returns {tabs, active}
popup as checkout # capture the popup
# ── screenshots ──
screenshot "full.png" full # full page
screenshot "#card" "card.png" # element screenshot
screenshot # auto name: step-N.png
# ── control break for the agent ──
snapshot # runtime returns; re-snapshot externally
# ── escape hatch ──
eval "document.title = 'patched'"
eval "window.scrollTo(0, 0)" as _ # run JS, discard result
eval "someGlobalVar" as val # run JS, save resultVerb reference
| Verb | Form |
|---|---|
| goto | goto <url> [load\|domcontentloaded\|networkidle\|commit] |
| back / forward / reload | history navigation |
| click / dblclick / rclick | click <target> |
| fill | fill <target> "<text>" |
| clear | clear <target> |
| type | type [<target>] "<text>" |
| press | press [<target>] <Key> (e.g. Enter, Control+A) |
| select | select <target> "<value>" |
| check / uncheck | check <target> |
| hover / focus | hover <target> |
| upload | upload <target> "<path>" |
| scroll | scroll <target> into / scroll page <up\|down\|to> [N] |
| wait | wait <target> [state] / wait <N>ms |
| waitload | waitload [load\|domcontentloaded\|networkidle] |
| waiturl | waiturl "<substring-or-regex>" |
| expect | see table below |
| extract | extract <target> [prop\|attr "<n>"] as <name> |
| ondialog | ondialog accept ["text"] / dismiss / default |
| download | download <target> [as "<file>"] |
| tab | tab open <id> [url] / switch <id> / close [id] / list |
| popup | popup as <id> |
| screenshot | screenshot [<target>] ["<file>"] [full] |
| snapshot | snapshot — control break |
| eval | eval "<js>" [as <var>] |
expect operators
| On | Operators |
|---|---|
| <target> | visible, hidden, enabled, disabled, checked, contains "x", equals "x", count N |
| url | equals, contains, matches (regex) |
| title | equals, contains, matches |
Variables
extract and eval ... as save variables. Use ${var} in any string:
extract "#price" as price
fill "#total" "${price}"
goto https://api.example.com/${path}CLI
# Inline:
playwright-dsl run -e 'goto https://example.com' -e 'screenshot "out.png"' --headless
# From file:
playwright-dsl run script.as --cdp-endpoint http://localhost:9222
# From stdin:
echo 'goto https://example.com' | playwright-dsl run - --headlessFull help: playwright-dsl --help.
playwright-dsl run [<script.as>|-] [options]
playwright-dsl run -e "<line>" [-e "<line>"...] [options]
playwright-dsl parse [<script.as>|-]
Options:
--cdp-endpoint <url> Connect to existing Chrome via CDP
--headless Headless when launching (no CDP)
--page-url <regex> With CDP: pick page whose URL matches
--vars <file> JSON file with initial ${vars}
--session <file> Persist vars across runs
--timeout <ms> Per-step timeout (default 10000)
--output <file> Write RunResult as JSON to file
--json Print RunResult as JSON to stdout
--verbose, -v Log each stepCDP flow (attach to your real browser)
# 1. Open Chrome with remote debugging
google-chrome --remote-debugging-port=9222 --user-data-dir=/tmp/profile
# 2. Run against the page already open (preserves logins, cookies)
playwright-dsl run script.as --cdp-endpoint http://localhost:9222 -vPersistent session (multiple scripts, one browser)
google-chrome --remote-debugging-port=9222 &
# Script 1 — login
playwright-dsl run --cdp-endpoint http://localhost:9222 --session s.json \
-e 'fill "label:Email" "[email protected]"' \
-e 'fill "label:Password" "s3cret"' \
-e 'click "button Sign in"'
# Script 2 — same browser, same cookies, same ${vars}
playwright-dsl run --cdp-endpoint http://localhost:9222 --session s.json \
-e 'goto https://site/settings' \
-e 'fill "#token" "${token}"'MCP server
AgentScript ships an MCP server: playwright-dsl-mcp. Your agent sees the
page via browser_snapshot (ARIA tree) and acts via browser_run.
Registration
Claude Code:
claude mcp add playwright-dsl -- npx -y playwright-dsl-mcpAntigravity / Cursor / Codex (mcp.json):
{
"mcpServers": {
"playwright-dsl": {
"command": "npx",
"args": ["-y", "playwright-dsl-mcp"],
"env": { "AGENTSCRIPT_CDP": "http://localhost:9222" }
}
}
}Tools
| Tool | Description |
|---|---|
| browser_connect | Attach (CDP) or launch a browser |
| browser_snapshot | ARIA snapshot as YAML — how the agent "sees" the page |
| browser_run | Execute a script. Returns steps, failure, and extracted vars |
| browser_syntax | Full DSL reference (call once) |
| browser_disconnect | Close/detach and clear state |
Agent loop
browser_connect { cdpEndpoint: "http://localhost:9222" }
browser_snapshot → YAML with roles + names
browser_run { script: "fill \"textbox Email\" \"alice\"\nclick \"button Sign in\"" }
browser_snapshot → re-snapshot after navigation
browser_run { script: "extract \"#balance\" as balance" }
→ { ok: true, vars: { balance: "€1,234" }, needsSnapshot: false }
browser_disconnectEmbeddable Toolkit (BrowserToolKit)
Import the toolkit into your own MCP server to add browser tools without running a separate process. This is how mypry adds browser interaction to its debugger:
import { BrowserToolKit } from 'playwright-dsl/toolkit';
const kit = new BrowserToolKit();
// kit.tools → MCP tool definitions (JSON Schema)
// kit.call(name, args) → execute a tool
// kit.page → Playwright Page (for direct page.evaluate())
// kit.dispose() → cleanup
// Register in your MCP server:
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [...yourTools, ...kit.tools],
}));
server.setRequestHandler(CallToolRequestSchema, async (req) => {
if (kit.handles(req.params.name)) {
return kit.call(req.params.name, req.params.arguments);
}
// ...your tools
});Programmatic API
execute(source, opts)
import { execute } from 'playwright-dsl';
import { chromium } from 'playwright';
const browser = await chromium.launch();
const page = await browser.newPage();
const result = await execute(script, {
page,
refMap: {},
vars: { username: 'alice' },
timeoutMs: 10_000,
onStep: (step, status, detail) => console.log(status, step.raw),
});
// result: { completed, failed?, vars, parseErrors }connect(opts) / dispose(handle)
import { connect, dispose } from 'playwright-dsl';
// Attach to running Chrome (preserves logins)
const handle = await connect({
cdpEndpoint: 'http://localhost:9222',
pageUrlMatch: 'github\\.com',
});
// Or launch fresh
const handle = await connect({ headless: true });
await dispose(handle);parse(source) (no browser needed)
import { parse } from 'playwright-dsl/parser';
const { steps, errors } = parse(source);Structure
src/
parser.ts # tokenizer + parser → Step[]
runtime.ts # executor on Playwright Page
connect.ts # CDP / launch helpers
session.ts # Session state management
toolkit.ts # BrowserToolKit — embeddable MCP tools
cli.ts # `playwright-dsl` binary
mcp.ts # `playwright-dsl-mcp` MCP server
index.ts # public API
playground/ # Vite test app (localhost:5199)
examples/ # login.as, search.as, etc.
test/ # parser + integration testsLicense
MIT
