Files
browser-cli/browser_cli/sdk/dom.py
T
daniel156161 914508e2db
Testing / test (push) Successful in 40s
Testing / remote-protocol-compat (0.16.0) (push) Successful in 31s
Testing / remote-protocol-compat (0.15.0) (push) Successful in 39s
feat(read): target any tab for page info and extraction
- Add --tab option to all extract commands and page info
- Thread tab_id through the SDK extract and page namespaces
- Route page.info with a tab_id to tabs.status for cross-tab metadata
- Accept tab_id in the MCP page_info, extract_text, extract_markdown tools
- Forward tabId in the extension page.info and extract.html handlers
- Keep the active tab as default when no tab is given

- Cover tab-scoped reads in API, CLI, and MCP tests
- Bump package and extension version to 0.16.7
- Refresh uv.lock with current dependency versions
2026-08-28 11:19:02 +02:00

172 lines
6.7 KiB
Python

"""DOM, content-extraction, and page-info namespaces: ``b.dom.*``, ``b.extract.*``, ``b.page.*``."""
from __future__ import annotations
from browser_cli.sdk.base import Namespace, sdk_command
def _selector_args(self, selector):
return {"selector": selector}
def _selector_value_args(self, selector, value):
return {"selector": selector, "value": value}
def _extract_markdown(self, result, *args, **kwargs) -> str:
from browser_cli.markdown import render_markdown
return render_markdown(result)
class DomNS(Namespace):
"""Query and drive page elements in the active (or specified) tab."""
@sdk_command("dom.query", _selector_args, default=[])
def query(self, selector: str) -> list[dict]:
"""Return elements matching a CSS selector."""
@sdk_command("dom.click", _selector_args, return_result=False)
def click(self, selector: str) -> None:
"""Click the first element matching a CSS selector."""
@sdk_command("dom.type", lambda self, selector, text: {"selector": selector, "text": text}, return_result=False)
def type(self, selector: str, text: str) -> None:
"""Type text into the first matching element."""
@sdk_command("dom.attr", lambda self, selector, attr: {"selector": selector, "attr": attr}, default=[])
def attr(self, selector: str, attr: str) -> list[str]:
"""Return an attribute from all matching elements."""
@sdk_command("dom.text", _selector_args, default=[])
def text(self, selector: str) -> list[str]:
"""Return text from all matching elements."""
@sdk_command("dom.exists", _selector_args, default=False)
def exists(self, selector: str) -> bool:
"""Return whether a selector exists."""
@sdk_command("dom.scroll", lambda self, selector=None, *, x=None, y=None: {"selector": selector, "x": x, "y": y}, return_result=False)
def scroll(self, selector: str | None = None, *, x: int | None = None, y: int | None = None) -> None:
"""Scroll to a CSS selector or to pixel coordinates."""
@sdk_command("dom.select", _selector_value_args, return_result=False)
def select(self, selector: str, value: str) -> None:
"""Set the value of a <select> element."""
@sdk_command("dom.eval", lambda self, code, tab_id=None: {"code": code, "tabId": tab_id})
def eval(self, code: str, tab_id: int | None = None):
"""Evaluate JavaScript in the page's main world and return the result."""
@sdk_command("dom.key", lambda self, key, selector=None: {"key": key, "selector": selector}, return_result=False)
def key(self, key: str, selector: str | None = None) -> None:
"""Dispatch a keyboard event. key examples: 'Enter', 'Tab', 'Escape', 'ArrowDown'."""
@sdk_command("dom.hover", _selector_args, return_result=False)
def hover(self, selector: str) -> None:
"""Dispatch mouseover/mouseenter on an element."""
@sdk_command("dom.check", _selector_args, return_result=False)
def check(self, selector: str) -> None:
"""Check a checkbox."""
@sdk_command("dom.uncheck", _selector_args, return_result=False)
def uncheck(self, selector: str) -> None:
"""Uncheck a checkbox."""
@sdk_command("dom.clear", _selector_args, return_result=False)
def clear(self, selector: str) -> None:
"""Clear the value of an input element."""
@sdk_command("dom.focus", _selector_args, return_result=False)
def focus(self, selector: str) -> None:
"""Focus an element."""
@sdk_command("dom.submit", _selector_args, return_result=False)
def submit(self, selector: str) -> None:
"""Submit the form containing the matched element."""
def 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.command("dom.poll", {
"selector": selector,
"pattern": pattern,
"attr": attr,
"timeout": int(timeout * 1000),
"interval": int(interval * 1000),
"tabId": tab_id,
})
def 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.command("dom.wait_for", {
"selector": selector,
"timeout": int(timeout * 1000),
"visible": visible,
"hidden": hidden,
"tabId": tab_id,
})
class ExtractNS(Namespace):
"""Extract structured content from the active (or specified) tab."""
@sdk_command("extract.links", lambda self, tab_id=None: {"tabId": tab_id}, default=[])
def links(self, tab_id: int | None = None) -> list[dict]:
"""Return links from the active tab or *tab_id*."""
@sdk_command("extract.images", lambda self, tab_id=None: {"tabId": tab_id}, default=[])
def images(self, tab_id: int | None = None) -> list[dict]:
"""Return images from the active tab or *tab_id*."""
@sdk_command("extract.text", lambda self, tab_id=None: {"tabId": tab_id}, default="")
def text(self, tab_id: int | None = None) -> str:
"""Return plain text from the active tab or *tab_id*."""
@sdk_command("extract.json", lambda self, selector, tab_id=None: {"selector": selector, "tabId": tab_id})
def json(self, selector: str, tab_id: int | None = None):
"""Extract JSON-like structured data from a selector in the active tab or *tab_id*."""
@sdk_command("extract.html", lambda self, tab_id=None: {"tabId": tab_id}, default="")
def html(self, tab_id: int | None = None) -> str:
"""Return the full HTML source of the active tab or *tab_id*."""
@sdk_command("extract.markdown", lambda self, selector=None, tab_id=None: {"selector": selector, "tabId": tab_id}, mapper=_extract_markdown)
def markdown(self, selector: str | None = None, tab_id: int | None = None) -> str:
"""Extract the page's main content as clean Markdown from the active tab or *tab_id*.
The extractor may return either Markdown or raw HTML; both are
normalized to Markdown here so SDK and CLI callers get identical output.
"""
class PageNS(Namespace):
"""Inspect the active page or a specified tab."""
def info(self, tab_id: int | None = None) -> dict:
"""Return metadata for the active page, or tab status for *tab_id*."""
if tab_id is not None:
return self.command("tabs.status", {"tabId": tab_id}) or {}
return self.command("page.info", {}) or {}