Modern, Playwright-style web automation on a pure Selenium engine, with BotCity RPA reach.
Project description
Visus Web
Modern, Playwright-style web automation on a pure Selenium engine — semantic locators, auto-waiting actions, auto-retrying assertions, BotCity-style RPA reach, and an OCR + image-matching vision plugin.
Visus Web automates the browser the way a developer thinks about it: semantic locators
(get_by_role, get_by_label, get_by_text), auto-waiting actions (no sleep, ever), and
web-first assertions that auto-retry until the page settles. It pairs Playwright's developer
experience with Selenium's compatibility (Grid, corporate browsers, Edge IE-mode) — written from
scratch, fully typed, with no Playwright dependency. On top of the test-automation core it adds
RPA reach (frames, popups, dialogs, cookies, downloads, file upload, PDF) and an optional
OCR + image-matching vision plugin for captchas.
Part of the Visus family.
visus-web(import rootvisus.web) sits alongsidevisus-desktop(visus.desktop, GUI automation) under one planned automation ecosystem.
Table of contents
- Features
- Requirements
- Installation
- Quickstart
- Drive it from any MCP agent
- Drive it from the shell — the
visusCLI - Python usage guide
rpa()— batteries-included automationlaunch()— full control- Locators
- Paste-an-element locator
- Actions
- Web-first waits
- Assertions (
expect) - Field enumeration (act by number)
- Frames & iframes
- Tabs & popups
- Dialogs
- Downloads
- Cookies & contexts
- Storage state — persist logins across runs
- Screenshots, PDF & JavaScript
- Network controls
- Network & console capture
- Emulation & init scripts
- Low-level mouse & keyboard
- Vision — OCR & image matching
- Tracing & HTML reports
- Async API
- pytest plugin
- Engines
- Configuration & environment variables
- Error handling
- Project layout
- Development & testing
- License
Features
- 🎭 Semantic locators —
get_by_role / text / label / placeholder / test_id / alt_text / title,locator(css|xpath),first / last / nth / filter, plusframe_locatorfor cross-origin and nested iframes. Find elements the way a user perceives them, not by brittle CSS chains. Every text matcher also accepts a compiledre.Pattern(get_by_text(re.compile(r"Sub\w+", re.I))). - 🧮 Locator composition —
filter(has=, has_not=, has_text=, has_not_text=)and theor_()/and_()operators disambiguate without dropping to XPath. - ⏱️ Web-first waits —
locator.wait_for(state=...),page.wait_for_url(glob/regex/predicate),page.wait_for_load_state,page.wait_for_function— synchronize on SPA redirects and async renders without hand-rolled polling. - 📋 Paste-an-element locator — paste an element straight from DevTools ("Copy element") into
page.locator('<input name="email" class="…">'); Visus derives an ordered set of selectors (id →data-*→ name → aria → class → xpath, Tailwind classes escaped) and tries each until one matches — resilient to a changed id/class. - ⏳ Auto-waited actions —
click / dblclick / fill / press / hover / check / uncheck / set_checked / select_option / drag_to / focus / blur / clear / set_input_filesall wait for the element to become actionable. Nosleep, ever. - ✅ Web-first assertions —
expect(loc).to_be_visible / to_have_text / to_have_value / to_have_count / to_have_role / to_be_focused / to_be_in_viewport / to_have_css / …with.not_negation; every matcher auto-retries. Page-levelexpect(page).to_have_url / to_have_title, custom failure messages (expect(loc, "context")), and soft assertions (expect.soft(...)+expect.verify_soft()). - 🌍 Network & console capture (Chromium) —
page.network_requests(),page.wait_for_response("*api*")(with.body()),page.expect_response(...)around an action, andpage.console_messages()— assert on the API call behind a click, not just the UI. - 🪪 Storage state & profiles —
context.storage_state(path=...)/restore_storage_state(...)persist logins across runs;launch(user_data_dir=...)reuses a persistent browser profile;launch(remote_url=...)drives a Selenium Grid. - 📱 Emulation (Chromium) —
set_device_metrics,set_geolocation+grant_permissions,set_viewport_size(all engines), andadd_init_scriptto seed pages before they boot. - 🧪 pytest plugin —
visus_browser/visus_context/visus_pagefixtures with screenshot-on-failure (--visus-output),--visus-engine/--visus-headedoptions, and automatic soft-assert verification. - 🤖 Batteries-included RPA —
rpa()launches the browser, records the run, writes a single-file HTML report (even on failure), and prints a summary — you write only the automation. - 🗂️ RPA reach — frames,
expect_popup,expect_dialog,expect_download, cookies, contexts,evaluate, element/full-page screenshots, and PDF. - 🔢 Field enumerator —
list_fields()enumerates every interactive field (across open Shadow DOM and same-origin iframes), draws a numbered overlay, and lets you act by index (page.field(7).fill(...)) — the same model the CLI uses. - 👁️ Vision plugin (
[vision]) —read_text/solve_captcha(RapidOCR) andfind_image/find_all_images(OpenCV), pluslocator.ocr_text()/locator.find_image()/page.solve_captcha(). - 🧩 MCP server (
[mcp]) —visus-web-mcpexposes 45 tools (navigation, accessibility snapshot, all actions, tabs, dialogs, cookies, network/console capture, screenshot, evaluate, vision) to AI agents. - ⌨️ Full-featured CLI (
[cli]) — thevisuscommand mirrors the whole surface in the shell with a persistent browser daemon,--jsoneverywhere, and an interactiveconsole. - 🔌 Async API —
visus.web.async_apiis a 100% mirror of the sync surface (same names, same parameters) wrapped inasyncio.to_thread; builders stay sync (recipe-only), reads and actions areawait-able. - 🌐 Selenium compatibility — Chrome, Edge, Firefox, and Edge IE-mode (Trident) for legacy corporate apps.
- 🏷️ Fully typed — ships a
py.typedmarker; noselenium.*type ever crosses the public API.
Requirements
- Python 3.10+
- A supported browser installed (Chrome, Edge, or Firefox). Drivers are resolved automatically by
Selenium Manager — no manual driver download needed (
visus installforces a one-time resolve). - Core dependency installed automatically:
selenium>=4.38,<5. - Optional extras pull in their own deps:
[vision](RapidOCR + OpenCV + Pillow + NumPy),[mcp](themcppackage),[cli](Typer + Rich).
Installation
pip install visus-web # core (Selenium engine + Python API)
pip install "visus-web[vision]" # + OCR (RapidOCR) + image matching (OpenCV)
pip install "visus-web[mcp]" # + MCP server for AI agents (visus-web-mcp)
pip install "visus-web[cli]" # + the `visus` CLI (incl. codegen)
Install several extras at once:
pip install "visus-web[vision,mcp,cli]"
Verify a browser + driver launch correctly:
visus doctor --engine chrome # OK: chrome launches and navigates ...
Quickstart
Batteries included — write only the automation; Visus launches the browser, records the run, writes an HTML report (even on failure), and prints a summary:
from visus.web import rpa, expect
with rpa("login", engine="chrome") as page: # "chrome" | "edge" | "firefox" | "edge_ie"
page.goto("https://example.com/login")
page.get_by_label("Email").fill("ada@example.com")
page.get_by_role("button", name="Sign in").click() # auto-waits until actionable
expect(page.get_by_text("Welcome")).to_be_visible() # auto-retries until true
# → ./visus-runs/login-<timestamp>/report.html (+ run.zip, + a printed summary)
Need full control (multiple pages, custom contexts)? Drop down to launch:
from visus.web import launch, expect
with launch(headless=True) as browser:
page = browser.new_page()
page.goto("https://example.com")
page.get_by_role("button", name="Sign in").click()
expect(page.get_by_text("Welcome")).to_be_visible()
Drive it from any MCP agent
visus-web ships a Model Context Protocol server exposing 45 tools so an AI agent
(Claude Code / Desktop, Cursor) can drive the browser end to end. Each tool carries a docstring that
teaches the agent when and how to use it, so it never has to read source.
pip install "visus-web[mcp]"
Wire it into your MCP client:
// claude_desktop_config.json (or ~/.claude.json / .mcp.json for Claude Code)
{
"mcpServers": {
"visus-web": {
"command": "visus-web-mcp",
"env": {
"VISUS_WEB_ENGINE": "chrome", // chrome | edge | firefox | edge_ie
"VISUS_WEB_HEADLESS": "1" // "0" to watch the browser drive
}
}
}
}
Tool groups (45 tools):
| Group | Tools |
|---|---|
| Navigation | browser_navigate, browser_navigate_back, browser_navigate_forward, browser_reload |
| Inspect | browser_snapshot ★, browser_list_fields, browser_clear_highlights, browser_translate_element, browser_title, browser_url, browser_get_text, browser_get_attribute, browser_count |
| Actions | browser_click, browser_dblclick, browser_fill, browser_press, browser_hover, browser_check, browser_uncheck, browser_select_option, browser_drag, browser_focus, browser_clear, browser_set_input_files |
| Wait / expect | browser_wait_for, browser_expect_text |
| Tabs | browser_tab_list, browser_tab_new, browser_tab_select, browser_tab_activate, browser_tab_close, browser_set_tab_follow |
| Dialogs | browser_handle_dialog |
| Network / console (Chromium) | browser_network_requests, browser_console_messages |
| Cookies | browser_get_cookies, browser_add_cookies, browser_clear_cookies |
| Media / JS | browser_screenshot, browser_evaluate |
Vision ([vision]) |
browser_read_text, browser_solve_captcha, browser_find_image |
| Lifecycle | browser_close |
Recommended agent flow: browser_navigate → browser_snapshot (read the role/name table) or
browser_list_fields (numbered, with ready-to-use locators) → act with browser_click /
browser_fill / … targeting by role+name, text, or selector → browser_wait_for /
browser_expect_text to synchronize.
Every action tool accepts the same target parameters — provide whichever you have:
| Param | Meaning |
|---|---|
selector |
A CSS selector, xpath=…, or a pasted DevTools element (<…>). |
role + name |
ARIA role and accessible name (e.g. role="button", name="Sign in"). |
text |
Visible text content. |
exact |
Match name/text exactly instead of as a substring. |
frame |
CSS/XPath of an <iframe> to resolve the target inside. |
New tabs/popups are manual by default (browser_set_tab_follow(False)): a click that opens a tab
does not change your context — steer explicitly with browser_tab_list / browser_tab_select. Set
browser_set_tab_follow(True) for auto-follow.
A runnable example of the exact calls an agent makes lives in
examples/login_mcp.py.
Drive it from the shell — the visus CLI
The visus command (install the [cli] extra) mirrors the automation surface in the terminal,
backed by a persistent browser daemon so commands share one live session. Every read command
takes --json.
pip install "visus-web[cli]"
visus session start https://practicetestautomation.com/practice-test-login/ # start the daemon
visus list-fields # enumerate + number the page's fields
visus fill 1 student # act by index, like the MCP list_fields flow
visus fill 2 Password123
visus click --role button --name Submit # or target by role/name/selector/text
visus expect-text --role heading "Logged In Successfully"
visus session stop # close the browser
Run visus --help (or visus <command> --help) for inline help. Use visus console for an
interactive REPL attached to the session.
Full command reference (click to expand)
Targeting: most commands accept a positional field index (from the last list-fields) or
--selector/-s, --role, --name, --text. Nearly every read command accepts --json.
Session & lifecycle
| Command | What it does |
|---|---|
visus session start [URL] --engine <e> --headless |
Start the persistent browser daemon (headed by default). |
visus session stop |
Stop the session (closes the browser). |
visus session status [--json] |
Show the running session's status. |
visus console [URL] --engine <e> --headless |
Open an interactive REPL attached to the session. |
Navigation
| Command | What it does |
|---|---|
visus goto <URL> |
Navigate the session's page to a URL. |
visus back / visus forward / visus reload |
History navigation / reload. |
Inspect / search the DOM
| Command | What it does |
|---|---|
visus snapshot [--json] |
List interactive elements as a role/name table. |
visus list-fields --kind <k> --all --no-highlight [--json] |
Enumerate interactive fields, numbered + highlighted. |
visus get-text [idx] [target] |
Print an element's text content. |
visus get-attribute <attr> [idx] [target] |
Print an element's attribute value. |
visus count [idx] [target] |
Count elements matching the target. |
visus title / visus url |
Print the page title / URL. |
visus translate [html] --file <f> [--json] |
Convert a pasted DevTools element into css/xpath/id/class selectors (reads the clipboard if omitted). |
Actions
| Command | What it does |
|---|---|
visus click [idx] [target] |
Click a field. |
visus dblclick [idx] [target] |
Double-click a field. |
visus fill [idx] <text> / … -s <sel> --value <v> |
Fill an input. |
visus clear-input [idx] [target] |
Clear an input's value. |
visus check / visus uncheck [idx] [target] |
Check / uncheck a checkbox or radio. |
visus select [idx] <value> |
Select an option on a <select>. |
visus press [idx] <key> |
Press a key/chord (e.g. Enter, Control+a). |
visus hover / visus focus [idx] [target] |
Hover / focus a field. |
visus drag <src> <dst> / … --to-selector |
Drag a source element onto a target. |
visus upload [idx] <paths…> |
Set file(s) on a file input. |
Wait / assert
| Command | What it does |
|---|---|
visus wait [idx] [target] --state <s> --timeout <ms> |
Wait for visible/hidden/attached/detached. |
visus expect-text [idx] <text> --timeout <ms> |
Assert an element contains text (PASSED/FAILED). |
Tabs / windows
| Command | What it does |
|---|---|
visus tabs [--json] |
List open tabs/windows (active marked *). |
visus tab [idx] |
Switch to a tab by index (newest if omitted). |
visus tab-new [URL] |
Open a new tab and switch to it. |
visus tab-close [idx] |
Close a tab (active if omitted). |
Dialogs / cookies / JS
| Command | What it does |
|---|---|
visus dialog --accept/--dismiss --prompt-text <t> |
Handle the next alert/confirm/prompt. |
visus cookies [--json] |
List cookies. |
visus add-cookies '<json>' / visus clear-cookies |
Add / clear cookies. |
visus eval "<fn>" [arg] [--json] |
Evaluate JS in the page (e.g. '() => document.title'). |
Capture / vision / utilities
| Command | What it does |
|---|---|
visus screenshot <URL> -o <png> --full-page --headless --engine <e> |
Screenshot a URL to a PNG (one-off browser). |
visus session-screenshot [idx] -o <png> --full-page |
Screenshot the session's page or a single element. |
visus pdf <URL> -o <pdf> |
Print a URL to a PDF (Chromium). |
visus open <URL> --engine <e> |
Open a headed browser at URL until you press Enter. |
visus clear |
Remove the numbered field-highlight overlay. |
visus read-text [idx] [target] ([vision]) |
OCR an element's screenshot → recognized text. |
visus solve-captcha [idx] [target] ([vision]) |
OCR-solve a text CAPTCHA. |
visus find-image <template.png> [idx] [target] -c <conf> [--json] ([vision]) |
Find a template image inside the page/element. |
Tooling
| Command | What it does |
|---|---|
visus codegen <URL> -o <file> --engine <e> |
Record interactions in a headed browser and generate visus.web code. |
visus doctor --engine <e> |
Check a browser + driver launch correctly. |
visus install --engine <e> |
Resolve/download the driver via Selenium Manager. |
visus mcp |
Start the MCP server over stdio (same as visus-web-mcp). |
visus run <script.py> |
Run a Python script with the active interpreter. |
visus version |
Print the visus.web version. |
Python usage guide
Conventions:
- Timeouts are in milliseconds; omit to use the defaults (action/navigation 30 000 ms,
expect5 000 ms). - Locators are lazy recipes —
get_by_*/locatoronly build a selector chain; the live page is touched only when you call an action or read. backtrack(on actions andgoto) replays the previous step(s) and retries on failure — passTrueor an integer count.
rpa() — batteries-included automation
from visus.web import rpa, expect
with rpa(
"checkout", # run label (used in the output folder name)
engine="chrome", # "chrome" | "edge" | "firefox" | "edge_ie"
headless=False, # watch it drive; True for CI
outdir=None, # default: ./visus-runs/<name>-<timestamp>/
report=True, # render report.html on exit (even if a step fails)
summary=True, # print a one-block run summary on exit
open_report=False, # open report.html in the default browser
reraise=False, # True: re-raise VisusWebError for programmatic handling
) as page:
page.goto("https://shop.example.com")
page.get_by_role("button", name="Add to cart").click()
expect(page.get_by_text("1 item")).to_be_visible()
# → run.zip + report.html written automatically; summary printed
launch() — full control
from visus.web import launch
with launch("firefox", headless=True) as browser:
page = browser.new_page()
page.goto("https://example.com")
# ... multiple pages / contexts available via browser.new_context()
Launch options for real-world setups:
launch(slow_mo=300) # watch each action land (debug/demo)
launch(user_data_dir="~/profiles/erp") # persistent profile: logins survive runs
launch("chrome", remote_url="http://grid:4444/wd/hub") # run on a Selenium Grid
Per-page/context default timeouts (per-call timeout= still wins):
page.set_default_timeout(10_000) # actions + navigations on this page
page.set_default_navigation_timeout(60_000) # navigations only
ctx.set_default_timeout(10_000) # pages created by this context afterwards
Locators
page.get_by_role("button", name="Sign in") # ARIA role + accessible name
page.get_by_role("link", name="Docs", exact=True)
page.get_by_text("Welcome back") # visible text (substring)
page.get_by_label("Email") # form control by its <label>
page.get_by_placeholder("Search…")
page.get_by_test_id("submit") # data-testid
page.get_by_alt_text("Company logo")
page.get_by_title("Close")
page.locator("#username") # CSS
page.locator("xpath=//button[@type='submit']") # XPath
page.locator(".row").locator("css=button") # chain
# Regex — every text matcher takes a compiled re.Pattern
import re
page.get_by_text(re.compile(r"order #\d+", re.I))
page.get_by_role("button", name=re.compile("^(OK|Confirm)$"))
# Refinement
page.get_by_role("listitem").filter(has_text="In stock").first()
page.get_by_role("row").nth(2)
page.locator(".card").last()
page.locator("#shadow-host").locator("button", deep=True) # pierce open Shadow DOM
# Composition — disambiguate without XPath
page.locator("tr").filter(has=page.get_by_role("button", name="Edit")) # rows WITH an Edit button
page.locator("tr").filter(has_not_text="archived")
page.locator("#save").or_(page.locator("#submit")).click() # whichever exists
page.locator("input").and_(page.locator(".touched")) # intersection
# Reads
loc = page.locator("#total")
loc.count(); loc.is_visible(); loc.is_enabled(); loc.is_checked()
loc.text_content(); loc.inner_text(); loc.inner_html(); loc.input_value()
loc.get_attribute("href"); loc.bounding_box() # {x, y, width, height} | None
loc.all_text_contents(); loc.all_inner_texts()
# Utilities
loc.scroll_into_view_if_needed(); loc.highlight() # debug box around the element
loc.dispatch_event("change") # synthetic DOM event
loc.press_sequentially("4242424242424242", delay=50) # char-by-char typing
Paste-an-element locator
Copy an element in DevTools ("Copy element") and paste its outerHTML straight in — Visus derives an
ordered candidate list (id → data-* → name → aria → class → xpath) and tries each:
page.locator('<input name="email" class="form-control w-full" type="email">').fill("ada@x.com")
Preview what it would generate (no live page needed):
from visus.web.api._htmlsel import translate
translate('<button id="go" class="btn">Go</button>') # {id, css, xpath, class, candidates_*}
The same is available as visus translate '<…>' and the browser_translate_element MCP tool.
Actions
Every action auto-waits for actionability. All accept timeout=<ms>, most accept force=True
(skip checks) and backtrack:
loc.click(); loc.dblclick(); loc.hover(); loc.focus(); loc.blur()
loc.fill("hello"); loc.clear(); loc.press("Enter"); loc.press("Control+a")
loc.check(); loc.uncheck(); loc.set_checked(True)
loc.select_option(value="US") # or label="United States" / index=0
loc.set_input_files("invoice.pdf") # or ["a.png", "b.png"]
src.drag_to(target)
Web-first waits
Synchronize on navigations and async renders without polling:
import re
page.get_by_role("button", name="Sign in").click()
page.wait_for_url("*dashboard*") # glob — also re.Pattern or a predicate
page.wait_for_url(re.compile(r"/orders/\d+$"))
page.wait_for_load_state() # "load" (default) or "domcontentloaded"
page.locator("#toast").wait_for() # visible (default)
page.locator("#spinner").wait_for(state="hidden") # also: attached / detached
ready = page.wait_for_function("() => window.app && window.app.ready") # poll JS until truthy
Assertions (expect)
Web-first matchers auto-retry until they pass or time out; negate with .not_:
from visus.web import expect
expect(page.get_by_role("heading", name="Dashboard")).to_be_visible()
expect(page.get_by_label("Email")).to_have_value("ada@example.com")
expect(page.get_by_role("listitem")).to_have_count(3)
expect(page.get_by_text("Error")).not_.to_be_visible()
# Page-level assertions (glob string or re.Pattern)
expect(page).to_have_url("*checkout*")
expect(page).to_have_title(re.compile("Dashboard"))
# Custom failure message — prefixed onto the AssertionError
expect(page.locator("#cart"), "cart should appear after adding an item").to_be_visible()
# Soft assertions — collect failures, fail once at the end
expect.soft(page.locator("#price")).to_have_text("$10")
expect.soft(page.locator("#qty")).to_have_value("2")
expect.verify_soft() # raises ONE combined error if anything failed
# (the pytest plugin calls verify_soft() automatically at the end of each test)
# Full set:
# to_be_visible / to_be_hidden / to_be_enabled / to_be_disabled / to_be_checked / to_be_editable
# to_be_attached / to_be_focused / to_be_empty / to_be_in_viewport
# to_have_text(exact=True) / to_contain_text / to_have_value / to_have_values / to_have_count
# to_have_attribute(name, value) / to_have_class / to_contain_class / to_have_role
# to_have_css(name, value) / to_have_id
# text/value/attribute/css matchers all accept re.Pattern
Field enumeration (act by number)
list_fields() enumerates every interactive field (across open Shadow DOM and same-origin iframes),
draws a numbered overlay, and lets you act by index — ideal for quick/interactive scripts and the
exact model the CLI and MCP use:
fields = page.list_fields(kinds=["input", "button"], include_hidden=False, highlight=True)
for f in fields:
print(f.index, f.kind, f.name, f.locator)
page.field(0).fill("student") # act by number (from the latest list_fields)
page.field_locator(fields[1]).fill("…") # or by the Field object (resolves frame/shadow chain)
page.clear_highlights()
Each Field carries index, kind, tag, type, role, name, label, placeholder, value, checked, disabled, visible, frame, shadow, locator, locator_kind, css, xpath, code, deep and a .to_dict().
For durable automation prefer a stable selector over the positional index.
Frames & iframes
frame = page.frame_locator("#checkout-iframe")
frame.get_by_label("Card number").fill("4242 4242 4242 4242")
frame.frame_locator("#nested").get_by_role("button", name="Pay").click() # nested iframes
Tabs & popups
with page.expect_popup() as popup:
page.get_by_role("link", name="Open in new tab").click()
new_page = popup.value
new_page.goto("https://…")
# Enumerate / focus tabs without dropping to launch:
for p in page.context.pages:
print(p.handle, p.url)
p.bring_to_front() # alias: p.activate()
Dialogs
with page.expect_dialog(accept=True, prompt_text="yes") as dlg:
page.get_by_role("button", name="Delete").click()
print(dlg.value.message, dlg.value.type) # "alert" | "confirm" | "prompt" | "beforeunload"
Downloads
with page.expect_download() as dl:
page.get_by_role("button", name="Export CSV").click()
dl.value.save_as("./reports/export.csv") # also: dl.value.path, dl.value.suggested_filename
Cookies & contexts
ctx = browser.new_context() # isolated cookies/cache
page = ctx.new_page()
ctx.add_cookies([{"name": "session", "value": "abc", "url": "https://example.com"}])
print(ctx.cookies())
ctx.clear_cookies()
Storage state — persist logins across runs
Snapshot cookies + localStorage/sessionStorage once, restore in later runs and skip the login:
# Run 1 — log in once, save the state
page.goto("https://app.example.com")
page.get_by_label("User").fill("ada"); page.get_by_role("button", name="Sign in").click()
page.context.storage_state(path="auth.json")
# Run N — restore instead of logging in
page.goto("https://app.example.com") # be at the origin first (cookies are per-domain)
page.context.restore_storage_state("auth.json")
page.reload() # app boots already authenticated
For a fully persistent browser (extensions, saved passwords, real profile), use
launch(user_data_dir="…") instead — the profile directory is reused and never deleted.
Screenshots, PDF & JavaScript
page.screenshot(path="page.png", full_page=True)
page.locator("#chart").screenshot(path="chart.png")
page.pdf(path="page.pdf") # Chromium only
host = page.evaluate("() => window.location.hostname")
doubled = page.evaluate("(x) => x * 2", 21)
Network controls
Chromium-only, via CDP:
page.block_urls(["*.ads.com", "*ga.js"])
page.set_extra_http_headers({"X-Debug": "1"})
page.set_offline(True); page.set_offline(False)
Network & console capture
Chromium-only. Assert on the API call behind a UI action — usually more reliable than waiting for the UI itself — and read the browser console when something misbehaves:
# Wait for the response a click triggers
with page.expect_response("*api/login*") as info:
page.get_by_role("button", name="Sign in").click()
assert info.value.ok # .url, .status, .method, .resource_type
print(info.value.body()) # response body via CDP
page.wait_for_response("*api/orders*") # or wait for the next matching response
for r in page.network_requests(): # everything captured so far
print(r.status, r.url)
for m in page.console_messages(): # console.* and uncaught errors
print(m.level, m.text) # SEVERE / WARNING / INFO
The MCP server exposes the same data to agents via browser_network_requests and
browser_console_messages.
Emulation & init scripts
page.set_viewport_size(1280, 720) # exact viewport, all engines
page.set_device_metrics(390, 844, device_scale_factor=3.0, mobile=True) # Chromium
page.grant_permissions(["geolocation"]); page.set_geolocation(-23.55, -46.63)
page.add_init_script("window.__env = 'test'") # runs before every document from now on
Low-level mouse & keyboard
page.mouse.move(100, 200); page.mouse.click(100, 200); page.mouse.dblclick(100, 200)
page.mouse.down(); page.mouse.up(); page.mouse.wheel(0, 300)
page.keyboard.press("Control+a"); page.keyboard.type("hello"); page.keyboard.insert_text("raw")
Vision — OCR & image matching
Requires the [vision] extra (pip install "visus-web[vision]").
# Locator / page hooks
text = page.locator("#captcha").ocr_text()
solution = page.solve_captcha(page.get_by_role("img", name="CAPTCHA"))
match = page.locator("#canvas").find_image("button.png", confidence=0.9)
if match:
page.mouse.click(match.center_x, match.center_y)
# Standalone functions on any image (path, bytes, PIL, numpy, base64)
from visus.web.vision import read_text, solve_captcha, find_image, find_all_images, Match
read_text(page.screenshot())
m = find_image(page.screenshot(), "logo.png", confidence=0.85) # -> Match | None
ms = find_all_images(page.screenshot(), "row.png") # -> list[Match]
Tracing & HTML reports
rpa() does this for you, but you can record any launch() block too:
from visus.web import tracing, launch
with tracing.record("run.zip", report="report.html"): # report written even on exception
with launch(headless=True) as browser:
page = browser.new_page()
page.goto("https://example.com")
page.get_by_label("Search").fill("visus")
# Or render a report from a saved zip later:
tracing.render_report("run.zip", "report.html")
# Global toggles: tracing.enable() / tracing.disable() / tracing.is_enabled()
Async API
visus.web.async_api is a 100% mirror of the synchronous surface — same method names,
same parameter names — so porting sync code is a mechanical await insertion. The rule:
- Builders stay sync (
get_by_*/locator/first/last/nth/filter/frame_locator) and so do zero-I/O accessors (page.handle/page.is_closed/page.context/browser.contexts/page.mouse/page.keyboard/page.field/page.field_locator) — they never touch the browser. - Everything that reaches the live browser is
await-able and runs in a thread, so the event loop never blocks.
import asyncio
from visus.web.async_api import launch, expect
async def main():
async with await launch(headless=True) as browser:
page = await browser.new_page()
await page.goto("https://example.com/login")
await page.get_by_label("Email").fill("ada@example.com") # builder sync, fill async
await page.get_by_role("button", name="Sign in").click()
await expect(page.get_by_text("Welcome")).to_be_visible()
asyncio.run(main())
Everything the sync API offers is here too — RPA, tracing, the field enumerator, low-level mouse/keyboard, captcha solving, popup/dialog/download capture, and multi-tab navigation:
from visus.web.async_api import rpa
async def login():
# batteries-included: launch + record + report + summary, all handled
async with rpa("login", engine="chrome", headless=True) as page:
await page.goto("https://example.com/login")
# field enumerator — act by number, like the CLI
for f in await page.list_fields():
if f.name == "Username":
await page.field(f.index).fill("ada")
# low-level input devices
await page.mouse.click(120, 240)
await page.keyboard.press("Control+a")
# event capture (context managers propagate errors just like sync)
async with page.expect_popup() as popup:
await page.get_by_role("link", name="Terms").click()
await popup.value.close()
# multi-tab: page.context reaches the other tabs
handles = {p.handle for p in await page.context.pages()}
Exports: launch, rpa, expect, Engine, errors, tracing, Field, Dialog,
Download, ConsoleMessage, AsyncBrowser, AsyncContext, AsyncPage, AsyncLocator,
AsyncFrameLocator, AsyncLocatorAssertions, AsyncPageAssertions, AsyncNetworkResponse,
AsyncMouse, AsyncKeyboard.
pytest plugin
Installing visus-web registers a pytest plugin with ready-made fixtures — no conftest needed:
def test_login(visus_page): # fresh page per test, isolated context
visus_page.goto("https://example.com/login")
visus_page.get_by_label("User").fill("ada")
visus_page.get_by_role("button", name="Sign in").click()
# on failure a screenshot lands in visus-results/<test-id>.png
pytest --visus-engine=firefox --visus-headed --visus-output=artifacts
visus_browser(session-scoped),visus_contextandvisus_page(per-test, isolated)- Screenshot on failure saved to
--visus-output(defaultvisus-results/) - Soft assertions (
expect.soft) are verified automatically at the end of each test
Engines
Pass a string or the Engine enum to launch() / rpa() / the CLI --engine / the MCP
VISUS_WEB_ENGINE env var.
| String | Engine member |
Browser | Notes |
|---|---|---|---|
"chrome" |
Engine.CHROME |
Google Chrome / Chromium | Default. Full feature set (PDF, CDP network controls). |
"edge" |
Engine.EDGE |
Microsoft Edge | Chromium-based; same features as Chrome. |
"firefox" |
Engine.FIREFOX |
Mozilla Firefox | No CDP-only features (PDF / network controls). |
"edge_ie" |
Engine.EDGE_IE |
Edge in IE mode (Trident) | Windows-only; for legacy corporate apps. See env vars below. |
Drivers are resolved automatically by Selenium Manager.
Configuration & environment variables
Default timeouts (visus.web.config.Defaults, all in ms): action_timeout_ms = 30_000,
navigation_timeout_ms = 30_000, expect_timeout_ms = 5_000. Override per call with timeout=<ms>.
| Variable | Used by | Default | Purpose |
|---|---|---|---|
VISUS_WEB_ENGINE |
MCP & CLI session | chrome |
Browser engine for the daemon/MCP session. |
VISUS_WEB_HEADLESS |
MCP & CLI session | 1 (MCP) |
"0" to run headed. |
VISUS_WEB_URL |
CLI session | — | Initial URL opened when the daemon starts. |
VISUS_WEB_TOKEN |
CLI session | auto | Auth token for the local daemon socket. |
VISUS_WEB_TRACING |
tracing | off | Enable tracing globally without code. |
VISUS_WEB_EDGE_BINARY |
edge_ie |
default install path | Path to msedge.exe. |
VISUS_WEB_IE_DRIVER |
edge_ie |
auto | Explicit IEDriverServer path (wins over auto-resolve). |
VISUS_WEB_IE_DRIVER_VERSION |
edge_ie |
4.0.0 |
Pinned IEDriverServer version (4.8+ can't attach to modern Edge). |
VISUS_WEB_IE_TIMEOUT |
edge_ie |
45 |
Seconds before the launch watchdog fails fast with a clear error. |
Error handling
All exceptions live in visus.web.errors and subclass VisusWebError:
from visus.web import rpa, errors
try:
with rpa("flow", reraise=True) as page: # reraise=True surfaces the error
page.goto("https://example.com")
page.get_by_role("button", name="Nope").click()
except errors.VisusTimeoutError:
print("element never became actionable")
except errors.VisusWebError as exc:
print(f"automation failed: {exc}")
| Exception | Raised when |
|---|---|
VisusWebError |
Base class for all visus.web errors. |
UnsupportedEngineError |
An unknown engine string was requested. |
VisusTimeoutError |
An operation exceeded its deadline (action/navigation/expect). |
NavigationError |
A navigation failed. |
TargetClosedError |
The page/context/browser is already closed. |
StrictModeViolation |
A strict locator matched more than one element. |
ElementNotFoundError |
A locator resolved to zero elements where one was required. |
Without reraise=True, rpa() catches a failed step, writes the report, prints a friendly summary,
and exits non-zero — so a failure is always debuggable from report.html.
Project layout
visus-framework-web/
├── src/visus/web/ # library code (PEP 420 namespace: no __init__.py in src/visus/)
│ ├── __init__.py # public API: launch, rpa, expect, Engine, Browser, Context, Page, Field
│ ├── rpa.py # rpa() batteries-included context manager
│ ├── engine.py # Engine enum (chrome / edge / firefox / edge_ie)
│ ├── config.py # Defaults (timeouts)
│ ├── errors.py # exception hierarchy
│ ├── tracing.py # tracing.record / enable / disable / render_report
│ ├── api/ # engine-agnostic public surface
│ │ ├── page.py # Page
│ │ ├── locator.py # Locator
│ │ ├── frame_locator.py # FrameLocator
│ │ ├── assertions.py # expect() + LocatorAssertions
│ │ ├── browser.py # Browser
│ │ ├── context.py # Context
│ │ ├── fields.py # Field dataclass
│ │ ├── input.py # Mouse, Keyboard
│ │ └── events.py # Dialog, Download
│ ├── async_api/ # async facade (AsyncPage, AsyncLocator, …)
│ ├── vision/ # OCR + image matching ([vision] extra)
│ ├── backends/ # Selenium engine (all selenium.* confined here) + browser drivers
│ ├── cli/ # the `visus` CLI + persistent session daemon
│ ├── mcp/ # the visus-web-mcp server (45 tools)
│ └── observability/ # run recording + HTML report rendering
├── examples/ # demo_rpa.py, login.py, login_rpa.py, login_mcp.py, dolar.py
├── skills/ # using-visus-web, developing-visus-web (agent/contributor guides)
├── tests/ # pytest suite (parallel, real headless-Chrome integration)
├── TESTING.md
├── LICENSE
└── pyproject.toml
Development & testing
git clone https://github.com/matheuszwilk/visus-framework-web.git
cd visus-framework-web
uv venv && uv pip install -e ".[dev,vision,mcp,cli]"
uv run pytest -q # parallel (xdist), real headless-Chrome integration tests
uv run ruff check src tests && uv run mypy
See TESTING.md for the test matrix (including the Edge IE-mode end-to-end tests) and
the skills/ directory for the agent-facing usage playbook (using-visus-web) and contributor guide
(developing-visus-web).
License
Proprietary — all rights reserved. You may install and use visus-web as a dependency in your
own projects, but copying the source, redistributing, modifying, or reverse engineering it is not
permitted. See LICENSE for the full terms. For commercial or redistribution licenses,
contact the author.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file visus_web-0.1.0.tar.gz.
File metadata
- Download URL: visus_web-0.1.0.tar.gz
- Upload date:
- Size: 242.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
25cf9b58dbc6b514c69054eb8ce5790e5c6f2d0905e8c7d55e08c15fa71ee8af
|
|
| MD5 |
c02d5be6b9efa2de73f6f40aafd7e2ee
|
|
| BLAKE2b-256 |
2ccb7d8ec59f471c29a0ea2343f1df9dc2371af72081bb27134d6a3b181337dd
|
Provenance
The following attestation bundles were made for visus_web-0.1.0.tar.gz:
Publisher:
publish.yml on botvisus/visus-framework-web
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
visus_web-0.1.0.tar.gz -
Subject digest:
25cf9b58dbc6b514c69054eb8ce5790e5c6f2d0905e8c7d55e08c15fa71ee8af - Sigstore transparency entry: 2163760968
- Sigstore integration time:
-
Permalink:
botvisus/visus-framework-web@6ef3260a3797f61639374665f430bf2bfb4ec218 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/botvisus
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@6ef3260a3797f61639374665f430bf2bfb4ec218 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file visus_web-0.1.0-py3-none-any.whl.
File metadata
- Download URL: visus_web-0.1.0-py3-none-any.whl
- Upload date:
- Size: 154.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1ecb1ff09f948f199074b24876bdeb5056e169d4c8f0b3cd54c9191547851521
|
|
| MD5 |
233274547651efa211a7452441dc73da
|
|
| BLAKE2b-256 |
cf8d52e73a97507477eb185eaf4e731cdfa0941e6e813e8b2ad725d821dd2454
|
Provenance
The following attestation bundles were made for visus_web-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on botvisus/visus-framework-web
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
visus_web-0.1.0-py3-none-any.whl -
Subject digest:
1ecb1ff09f948f199074b24876bdeb5056e169d4c8f0b3cd54c9191547851521 - Sigstore transparency entry: 2163760989
- Sigstore integration time:
-
Permalink:
botvisus/visus-framework-web@6ef3260a3797f61639374665f430bf2bfb4ec218 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/botvisus
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@6ef3260a3797f61639374665f430bf2bfb4ec218 -
Trigger Event:
workflow_dispatch
-
Statement type: