The TypeScript/JavaScript API is async by default — every call is a promise, every step an await. The Python API takes the same object model and fits it to the language that QA engineers actually reach for: synchronous by default, snake_case throughout, and as readable in a test file as it is in a REPL. Same browser, same power, different idiom.
new_context · pages
on_page · on_popup
stop · remove_all_listeners
set_cookies · clear_cookies
storage · set_storage
clear_storage · add_init_script
reload · url · title
content
el.find · el.find_all
type · press · clear
check · uncheck · select_option
hover · focus · drag_to
tap · scroll_into_view
dispatch_event · set_files
value · attr · get_attribute
bounds · bounding_box
is_visible · is_hidden
is_enabled · is_checked
is_editable · role · label
screenshot · wait_until
keyboard.down · keyboard.up
mouse.click · mouse.move
mouse.down · mouse.up
mouse.wheel · touch.tap
emulate_media · set_content
set_geolocation · set_window
window · scroll
bring_to_front · close
evaluate · eval
add_script · add_style
expose · a11y_tree
frames · frame
main_frame
wait_until.url
wait_until.loaded
capture.navigation
capture.response
capture.request
capture.download
capture.event
on_request · on_response
on_web_socket · on_dialog
on_console · console_messages
on_error · errors
on_download
remove_all_listeners
run_for · pause_at
resume · set_fixed_time
set_system_time · set_timezone
start_chunk · stop_chunk
start_group · stop_group
default_value · accept
dismiss
abort
request_id · post_data
request_id · body · json
save_as · path
on_close · is_closed
Starting up
Install with pip install vibium, then import and start. browser.start()
returns a Browser instance synchronously. No event loop to manage, no
async def, no asyncio.run() — just a function call that blocks
until the browser is ready.
bro = browser.start()
page = bro.new_page()
page.go('https://github.com/login')
inp = page.find(role='textbox', label='Username or email address')
inp.fill('your-username')
bro.stop()
The naming follows Python conventions throughout: new_page,
find_all, is_visible, set_content,
inner_text. If you've used any Python test automation tool before,
the method names will feel familiar before you finish the import line.
Pass headless=True to browser.start() for CI.
The default is headed. Pass executable_path to point at a specific
browser binary, or headers to set default request headers for every
page in the session.
Finding elements
page.find() returns a single Element. page.find_all()
returns a list. Both accept keyword arguments: role, label,
text, placeholder, selector. The most durable pattern
is role + label — it survives layout changes and refactors because it targets semantics,
not structure.
btn = page.find(role='button', text='Sign in')
field = page.find(role='textbox', label='Email')
# Text content
msg = page.find(text='Welcome back')
# CSS selector — last resort
badge = page.find('.notification-badge')
# All matching elements
rows = page.find_all(role='row')
assert len(rows) == 5
Find once, act many times. Once you hold an Element reference, every
subsequent method call — .click(), .fill(),
.text() — goes directly to that element without re-querying the DOM.
This makes long interaction sequences both faster and more explicit about what
the script is operating on.
Find once, act many. The element handle is the stable reference — don't re-query what you already have.
Element interactions
Nineteen methods on every Element. The split between write and read
methods is deliberate: fill() clears then types; type()
types character-by-character without clearing first. Use fill for
form inputs, type when you need to trigger per-keystroke event handlers.
email.fill('user@example.com')
search.type('pyt') # triggers autocomplete after each char
# State checks — good assertion primitives
assert submit.is_enabled()
assert checkbox.is_checked() is False
assert field.is_editable()
# Read
label_text = heading.text() # raw text content (includes hidden)
visible = heading.inner_text() # rendered visible text only
href = link.get_attribute('href')
rect = btn.bounds() # rect.x, rect.y, rect.width, rect.height
is_visible(), is_enabled(), is_checked(),
and is_editable() return plain Python booleans. They are the cleanest
assertion primitives in the API — no wrapping, no helpers, no matcher libraries needed.
Use them directly in assert statements.
bounds() returns a BoundingBox object with x,
y, width, and height as attributes — access them
as rect.x, rect.width, not dict-style. It's the right tool
for layout assertions — checking that a tooltip appears above its trigger, that a modal
is centred, or that two elements don't overlap.
Keyboard and mouse
When the element API doesn't reach — global keyboard shortcuts, drag operations,
coordinate-based clicks in canvas elements — the page.keyboard and
page.mouse sub-objects give you direct input control.
page.keyboard.press('Control+a') # select all
page.keyboard.press('Escape') # dismiss modals
page.keyboard.press('Tab') # advance focus
# Hold a modifier while clicking
page.keyboard.down('Shift')
page.mouse.click(400, 300)
page.keyboard.up('Shift')
# Drag via mouse primitives
page.mouse.move(100, 200)
page.mouse.down()
page.mouse.move(300, 200)
page.mouse.up()
page.keyboard.type(text) types a full string with realistic per-character
timing. Use it when element.type() isn't an option — for example, when the
target element is a canvas-based input or a custom rich-text editor that intercepts
keyboard events at the window level.
Waiting
Vibium's synchronous model means most actions already wait for the browser to settle. The explicit wait API is for cases where settling isn't defined by a single navigation — polling for an element to appear, waiting for a URL change after an async redirect, or confirming that a full SPA hydration is complete.
page.wait(1000) # 1 second
# Wait for URL to match a glob pattern
page.wait_until.url('**/dashboard')
# Wait for full page load
page.wait_until.loaded()
# Conditional wait — JS expression polled until truthy
page.wait_until('!!document.querySelector(".toast.success")')
page.wait(ms) pauses execution for the given number of milliseconds.
For waiting on element state, page.find() already auto-waits — the element
wait is built into the find, not a separate step.
page.wait_until(expr) takes a JavaScript expression string. It evaluates
the expression inside the page every 100ms until it returns a truthy value. This is the
escape hatch for anything the built-in wait methods don't cover — custom DOM conditions,
cross-element checks, or anything requiring a JS predicate before deciding the page is ready.
Capturing output and network
Dialog capture in Python follows the same fire-and-forget rule as TypeScript/JavaScript and MCP: the action that triggers the dialog must be called after starting the capture, and the call to the triggering action must not block before the dialog handler has run. In Python's synchronous model, this means starting the capture context manager, then calling the triggering action inside it.
with page.capture.dialog() as dlg:
btn.click() # triggers the alert; auto-dismissed on exit
assert dlg.value['message'] == 'Are you sure?'
# Intercept a network response
with page.capture.response('**/api/user') as resp:
page.reload()
assert resp.value['status'] == 200
body = resp.value['body'] # plain string — parse with json.loads()
# Mock a route
page.route('**/api/flags', lambda r: r.fulfill(
body='{"dark_mode": true}', content_type='application/json'
))
page.go('https://app.example.com')
page.route(url_pattern, handler) intercepts matching requests before
they leave the browser. The handler receives a Route object with
fulfill(), continue_(), and abort() — use it
to stub APIs, inject feature flags, or simulate network errors without touching the
server.
Route mocking at the browser level means no server changes, no test doubles — just intercept and respond.
page.console_messages() returns a list of console entries captured since
the last call. Filter by type — "error", "warning",
"log" — to assert that a feature runs without producing console errors.
Browser contexts and the virtual clock
A browser context is an isolated session: separate cookies, separate storage, separate
authentication state. Use bro.new_context() when you need to run tests
as different users, or when parallel test workers must not share state.
ctx_a = bro.new_context()
ctx_b = bro.new_context()
page_a = ctx_a.new_page()
page_b = ctx_b.new_page()
# Read cookies + storage from a logged-in context
state = ctx_a.storage() # returns StorageState dict
cookies = ctx_a.cookies()
# Virtual clock — control Date.now() inside the page
page.set_content('<div id="ts"></div><script>setInterval(()=>{document.getElementById("ts").textContent=Date.now()},100)</script>')
page.clock.install()
page.clock.set_fixed_time('2026-01-01T00:00:00Z')
ts = page.find('#ts').text()
assert ts == '1735689600000'
# Advance time without waiting in real time
page.clock.fast_forward(3_600_000) # jump one hour (ms)
page.clock.run_for(5000) # run 5 seconds of timers
ctx.storage() and ctx.cookies() read the current session
state from a context where you've completed a login flow. Use them for inspection or
to seed a new context via ctx.set_cookies() — keeping authentication
state portable across contexts without repeating the login flow for every test.
The virtual clock intercepts Date, setTimeout,
setInterval, and requestAnimationFrame inside the page.
clock.install() freezes time at the current moment.
clock.set_fixed_time() pins it to a specific ISO timestamp.
clock.fast_forward() jumps time forward without blocking the test process —
a timer that would take an hour to fire in production fires immediately in the test.
That's the Python API: 157 methods, synchronous by default, snake_case throughout. The same object model as the JS API — browser → context → page → element — but written for the language that QA engineers spend most of their time in. No boilerplate, no event loop ceremony, assertions that read like plain English.
The full docs with per-method demos is at daisyladybug.com/vibium/python/. Next up: the Java API — the same surface one more time, typed, fluent, and built for teams running on the JVM.