feat: add browser automation commands (v0.6.0)
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:
@@ -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]:
|
||||
|
||||
Reference in New Issue
Block a user