Driver facade
The browser driver of prettyplay. For engineers reading or hand-writing step code and integrators reasoning about where step code executes.
Generated step code works through the standard Playwright sync API on the
genuine Page — prettyplay ships no page API of its own. What the library
owns is the boundary that code crosses: the whole step executes inside the
driver worker thread as one unit, and the calling thread never touches
Playwright. This page documents the internal page handle, the worker
boundary and the generated-code contour.
The worker boundary
The whole Playwright session — start, browser, contexts, pages — lives in one dedicated driver thread owned by the library. The only crossing point of that boundary is the run primitive of the internal page handle:
result = page.run(action) # action(page) executes wholly inside the worker
- The action receives the genuine sync
Pageand its outcome returns as-is; an exception inside the action propagates to the caller untouched - The calling thread never adopts the Playwright event loop — hand-written code stays safe in interactive hosts (IPython, Jupyter)
- The callable runs sequentially with every other unit: one unit at a time
- Playwright objects never cross back through the result — plain data only
The internal page handle
PageFacade (exported from prettyplay.driver) is the runtime plumbing
handle of a single test page — not the API of step code. Exactly five
members:
| Call | Purpose |
|---|---|
page.run(action) |
execute the callable inside the worker thread; receives the genuine sync Page |
page.url |
the current URL — an immediate read executed as one unit inside the worker thread |
page.aria_snapshot() |
accessibility-tree page state — the primary LLM input |
page.screenshot() |
full-page PNG bytes |
page.close() |
close this page's isolated context |
No member proxies, delegates or re-exports of page capabilities exist: the
handle is plumbing, not a managed surface. Everything step code does is a
standard Playwright call on the genuine page inside run. Each context
opens with the screen mode configured in the browser group — the handle and
the contour are identical in every mode.
The generated-code contour
Step code is one function of the fixed form def step(page) -> None: —
compiled and resolved on the calling thread, then executed as one worker
unit against the genuine page:
from playwright.sync_api import expect
def step(page):
page.goto("https://example.com/login")
page.get_by_label("Username").fill("user")
page.get_by_role("button", name="Sign in").click()
expect(page.get_by_text("Welcome back")).to_be_visible()
assert "dashboard" in page.url
The contour:
- Standard Playwright sync API — locator factories (
get_by_role,get_by_label,get_by_text,locator, ...), actions,expectchains for dynamic content, plain Python asserts on immediate reads (assert videos.count() > 1) - Imports from
playwright.sync_apionly — a prompt rule, no hard gate; every generation and regeneration request carries the compact cheat sheet as guidance, never an allowlist - Safety core: no
time.sleep, no fixed delays — waits live in locators and expectations; neverpage.close()orcontext.close()— the runtime owns the page lifecycle - Stateful actions stay out of generated code:
page.route,page.clock,add_init_script, tracing, HAR, CDP are the author's explicit tools (see the escape hatch below) - No provider constructs, no direct driver imports
Dialogs
Step code captures a dialog through the stock Playwright event form:
with page.expect_event("dialog") as info:
page.get_by_role("button", name="Delete").click()
dialog = info.value
assert dialog.type == "confirm"
assert dialog.message == "Delete the item?"
dialog.accept()
A captured dialog is step-controlled. Dialogs no in-step capture claims are
resolved by the resolver of last resort: the driver registers one
record-only routing handler per page of the context before any step code
runs (registering a dialog listener disables Playwright's implicit
auto-dismiss), and at the tail of every driver-thread unit — each run
action and each plumbing call of the handle alike — every unclaimed dialog
is resolved exactly once, chained dialogs included: accepted when the
accept_dialogs browser setting is on, explicitly dismissed when off. The
drain of one unit is bounded: a page that fires a fresh dialog for every
resolution — an endless while (true) alert() loop — leaves the excess
pending for the next unit tail instead of holding the unit forever, so the
failure surfaces through the regular timeouts. The resolver never touches
a dialog an in-step capture handled and never masks the outcome of the
action — see
Configuration.
Popups, frames, scrolling — stock means
Popups and new tabs:
with page.expect_popup() as popup_info:
page.get_by_role("link", name="Open docs").click()
popup = popup_info.value
popup.bring_to_front()
expect(popup.get_by_role("heading", name="Documentation")).to_be_visible()
Iframes:
checkout = page.frame_locator("#checkout")
checkout.get_by_role("button", name="Pay").click()
inner = page.frame_locator("#outer").frame_locator("#inner")
expect(inner.get_by_text("Nested")).to_be_visible()
Scrolling:
page.get_by_role("button", name="More").scroll_into_view_if_needed()
page.mouse.wheel(0, 600)
No fixed delays around scrolls — the scrolled state is awaited through locators and expectations.
The author escape hatch — run_on_page
title = t.run_on_page(lambda page: page.title())
t.run_on_page(action) executes the author action wholly inside the driver
worker thread against the genuine page — the same primitive step code
crosses through, sequential with every step. The stateful actions excluded
from generated code (page.route, page.clock, add_init_script,
tracing, HAR, CDP) are the author's explicit tools here; the prompt rules
of generated code do not bind the author.
- Requires an opened page — run a step first: the page opens lazily on the
first step, and a missing page raises a loud actionable
PrettyplayError - The action must use the page API only — calling back into the test
object (
t.step, screenshots, a nestedrun_on_page) marshals into the same worker thread the action runs on; the worker rejects the re-entrant crossing with a loud error instead of a deadlock - The outcome returns as-is — plain data only; an exception inside the action propagates to the caller untouched
Pollable failure kinds
The driver ships a fixed pollable map, exported as is_pollable_failure(exc)
from prettyplay.driver: it returns True when the exception kind is
transient page state — the settle window may re-execute the same code — and
False when the failure is deterministic or unknown, so it goes straight to
classification. Pollable: timeouts (Timeout NNNms exceeded), element state
(not visible, not enabled, outside the viewport, detached/stale), navigation
and context races (Execution context was destroyed, Target closed), and
plain AssertionErrors of failed checks — a failed expect(...) chain or a
plain Python assert on an immediate read alike. Not pollable: locator
ambiguity (strict mode violation — deterministic), Python-level errors of
the step code itself, and unrecognized failures. The map is fixed in code: it
never reads settings and never asks an LLM. See
Settle polling.
Rules
- One browser process per test: each test owns its browser through its runtime; contexts stay isolated
- Every run unit executes in the library's driver thread and returns when done: driving is strictly sequential, and the calling thread never adopts the Playwright event loop
- Auto-wait everywhere: no
time.sleep, no fixed delays in step code — the scrolled and loaded states are awaited through locators and expectations - A locator resolving to several elements fails an action or expectation
with the strict-mode violation — narrow positionally (
first,last,nth) to address one match; over anor_composition the positional narrowing is the canonical guard when both branches may match - The screen mode of the browser group (empty, WxH, fullscreen, device name) changes only how the context opens — never the contour; step code is identical in every mode
- Dialogs: an in-step stock capture claims its dialog; dialogs outside a
capture follow the
accept_dialogsbrowser setting - No
page.route, nopage.clock, noadd_init_script, no CDP, no HAR, no tracing in generated code — the author performs them explicitly throughrun_on_page - Never put secrets into step actions — step texts and code land in the repository cache
!!! warning Never put secrets into step actions — step texts and code land in the repository cache.