feat: add browser automation commands (v0.6.0)
Testing / test (push) Successful in 24s
Package Extension / package-extension (push) Successful in 9s
Build & Publish Package / publish (push) Successful in 21s

Navigation: open-wait (open + block until loaded)
DOM: key, hover, check/uncheck, clear, focus, submit, poll, scroll, select, eval, wait-for
Tabs: pin/unpin, screenshot, watch-url (block until URL matches regex)
New command groups: page info, storage get/set, cookies list/get/set
Extension: add cookies permission
This commit is contained in:
2026-04-16 14:21:19 +02:00
parent fc4ce8f74d
commit 1c5fd0ffee
11 changed files with 922 additions and 9 deletions
+232
View File
@@ -122,6 +122,45 @@ class BrowserCLI:
"""Navigate a specific tab to *url*."""
self._cmd("navigate.to", {"tabId": tab_id, "url": url})
def open_wait(
self,
url: str,
*,
timeout: float = 30.0,
background: bool = False,
window: str | None = None,
group: str | None = None,
) -> "Tab":
"""Open URL in a new tab and block until fully loaded. Returns the Tab."""
data = self._cmd("navigate.open_wait", {
"url": url, "timeout": int(timeout * 1000),
"background": background, "window": window, "group": group,
})
return self._make_tab(data) if isinstance(data, dict) and "id" in data else data
def wait_for_load(
self,
tab_id: int | None = None,
*,
timeout: float = 30.0,
ready_state: str = "complete",
) -> Tab:
"""Block until the tab finishes loading. Returns the Tab when ready.
Args:
tab_id: Tab to watch. Defaults to the active tab.
timeout: Max seconds to wait before raising ``RuntimeError``.
ready_state: ``"complete"`` (default) or ``"interactive"``.
"""
data = self._cmd("navigate.wait", {
"tabId": tab_id,
"timeout": int(timeout * 1000),
"readyState": ready_state,
})
if not isinstance(data, dict) or "id" not in data:
raise RuntimeError("navigate.wait returned unexpected data")
return self._make_tab(data)
# ── Search ────────────────────────────────────────────────────────────
def search(
@@ -190,6 +229,44 @@ class BrowserCLI:
result = self._cmd("tabs.unmute", {"tabId": tab_id})
return result.get("tabId", tab_id) if isinstance(result, dict) else int(tab_id or 0)
def tabs_pin(self, tab_id: int | None = None) -> int:
"""Pin the active tab or a specific tab. Returns the target tab ID."""
result = self._cmd("tabs.pin", {"tabId": tab_id})
return result.get("tabId", tab_id) if isinstance(result, dict) else int(tab_id or 0)
def tabs_unpin(self, tab_id: int | None = None) -> int:
"""Unpin the active tab or a specific tab. Returns the target tab ID."""
result = self._cmd("tabs.unpin", {"tabId": tab_id})
return result.get("tabId", tab_id) if isinstance(result, dict) else int(tab_id or 0)
def tabs_watch_url(
self,
pattern: str,
*,
tab_id: int | None = None,
timeout: float = 30.0,
) -> "Tab":
"""Block until the tab URL matches regex pattern. Returns the Tab."""
data = self._cmd("tabs.watch_url", {"pattern": pattern, "tabId": tab_id, "timeout": int(timeout * 1000)})
return self._make_tab(data) if isinstance(data, dict) and "id" in data else data
def tabs_screenshot(
self,
tab_id: int | None = None,
*,
format: str = "png",
quality: int | None = None,
) -> str:
"""Capture the visible area of a tab. Returns a base64 data URL.
Args:
tab_id: Tab to capture. Defaults to the active tab.
format: ``"png"`` (default) or ``"jpeg"``.
quality: JPEG quality 0-100 (ignored for PNG).
"""
result = self._cmd("tabs.screenshot", {"tabId": tab_id, "format": format, "quality": quality})
return result.get("dataUrl", "") if isinstance(result, dict) else str(result)
def window_active_tab(self, window_id: int) -> Tab:
"""Return active tab for a specific browser window."""
data = self._cmd("tabs.active_in_window", {"windowId": window_id})
@@ -346,6 +423,161 @@ class BrowserCLI:
def dom_exists(self, selector: str) -> bool:
return self._cmd("dom.exists", {"selector": selector})
def dom_scroll(self, selector: str | None = None, *, x: int | None = None, y: int | None = None) -> None:
"""Scroll to a CSS selector or to pixel coordinates."""
self._cmd("dom.scroll", {"selector": selector, "x": x, "y": y})
def dom_select(self, selector: str, value: str) -> None:
"""Set the value of a <select> element."""
self._cmd("dom.select", {"selector": selector, "value": value})
def dom_eval(self, code: str, tab_id: int | None = None):
"""Evaluate JavaScript in the page's main world and return the result."""
return self._cmd("dom.eval", {"code": code, "tabId": tab_id})
def dom_key(self, key: str, selector: str | None = None) -> None:
"""Dispatch a keyboard event. key examples: 'Enter', 'Tab', 'Escape', 'ArrowDown'."""
self._cmd("dom.key", {"key": key, "selector": selector})
def dom_hover(self, selector: str) -> None:
"""Dispatch mouseover/mouseenter on an element."""
self._cmd("dom.hover", {"selector": selector})
def dom_check(self, selector: str) -> None:
"""Check a checkbox."""
self._cmd("dom.check", {"selector": selector})
def dom_uncheck(self, selector: str) -> None:
"""Uncheck a checkbox."""
self._cmd("dom.uncheck", {"selector": selector})
def dom_clear(self, selector: str) -> None:
"""Clear the value of an input element."""
self._cmd("dom.clear", {"selector": selector})
def dom_focus(self, selector: str) -> None:
"""Focus an element."""
self._cmd("dom.focus", {"selector": selector})
def dom_submit(self, selector: str) -> None:
"""Submit the form containing the matched element."""
self._cmd("dom.submit", {"selector": selector})
def dom_poll(
self,
selector: str,
pattern: str,
*,
attr: str | None = None,
timeout: float = 30.0,
interval: float = 0.5,
tab_id: int | None = None,
) -> dict:
"""Poll selector's text/value until it matches regex pattern.
Returns ``{"selector": ..., "value": ..., "pattern": ...}`` when matched.
"""
return self._cmd("dom.poll", {
"selector": selector,
"pattern": pattern,
"attr": attr,
"timeout": int(timeout * 1000),
"interval": int(interval * 1000),
"tabId": tab_id,
})
def dom_wait_for(
self,
selector: str,
*,
timeout: float = 10.0,
visible: bool = False,
hidden: bool = False,
tab_id: int | None = None,
) -> dict:
"""Wait until a CSS selector appears (or disappears) in the DOM.
Args:
selector: CSS selector to watch.
timeout: Max seconds to wait before raising ``RuntimeError``.
visible: Wait until the element has non-zero dimensions.
hidden: Wait until the element is absent or has ``offsetParent == null``.
tab_id: Tab to watch. Defaults to the active tab.
"""
return self._cmd("dom.wait_for", {
"selector": selector,
"timeout": int(timeout * 1000),
"visible": visible,
"hidden": hidden,
"tabId": tab_id,
})
# ── Page ─────────────────────────────────────────────────────────────
def page_info(self) -> dict:
"""Return title, URL, readyState, lang, and meta tags of the active tab."""
return self._cmd("page.info", {}) or {}
# ── Storage ───────────────────────────────────────────────────────────
def storage_get(
self,
key: str | None = None,
*,
type: str = "local",
tab_id: int | None = None,
) -> str | dict | None:
"""Get a localStorage/sessionStorage entry (or all entries if key omitted)."""
return self._cmd("storage.get", {"key": key, "type": type, "tabId": tab_id})
def storage_set(
self,
key: str,
value: str,
*,
type: str = "local",
tab_id: int | None = None,
) -> None:
"""Set a localStorage/sessionStorage entry."""
self._cmd("storage.set", {"key": key, "value": value, "type": type, "tabId": tab_id})
# ── Cookies ───────────────────────────────────────────────────────────
def cookies_list(
self,
*,
url: str | None = None,
domain: str | None = None,
name: str | None = None,
) -> list[dict]:
"""List cookies, optionally filtered by url, domain, or name."""
return self._cmd("cookies.list", {"url": url, "domain": domain, "name": name}) or []
def cookies_get(self, url: str, name: str) -> dict | None:
"""Get a single cookie by url and name."""
return self._cmd("cookies.get", {"url": url, "name": name})
def cookies_set(
self,
url: str,
name: str,
value: str,
*,
domain: str | None = None,
path: str | None = None,
secure: bool | None = None,
http_only: bool | None = None,
expiration_date: float | None = None,
same_site: str | None = None,
) -> dict:
"""Set a cookie. Returns the created cookie dict."""
return self._cmd("cookies.set", {
"url": url, "name": name, "value": value,
"domain": domain, "path": path,
"secure": secure, "httpOnly": http_only,
"expirationDate": expiration_date, "sameSite": same_site,
})
# ── Extract ───────────────────────────────────────────────────────────
def extract_links(self) -> list[dict]: