Files
browser-cli/browser_cli/sdk/dom.py
T
daniel156161 fd5447cbb9
Testing / remote-protocol-compat (0.9.3) (push) Successful in 42s
Testing / remote-protocol-compat (0.9.5) (push) Successful in 44s
Package Extension / package-extension (push) Successful in 43s
Build & Publish Package / publish (push) Successful in 43s
Testing / test (push) Successful in 45s
refactor(api): namespaced SDK + dedicated transport layer
Restructure the Python API and internals around composable namespaces and
a standalone transport/endpoint layer. Bump to 0.12.0.

Python API:
- Replace flat methods (b.tabs_list(), b.group_list()) with namespaces:
  b.nav, b.tabs, b.groups, b.windows, b.dom, b.extract, b.page, b.storage,
  b.cookies, b.session, b.perf, b.extension.
- Shrink browser_cli/__init__.py to a thin composition root; move all
  behaviour into browser_cli/sdk/ (one module per namespace + factories,
  base, routing).

Internals:
- Add browser_cli/transport.py and remote_transport.py to isolate IPC from
  command logic; client.py now delegates instead of owning transport.
- Add browser_cli/endpoints.py for endpoint resolution and
  browser_cli/errors.py for shared error types.
- Extract markdown rendering into browser_cli/markdown.py (out of extract).
- Add USER_AGENT to version_manager.

Tooling & tests:
- Add justfile with common dev tasks.
- Update CLI commands and demo to the namespaced API.
- Rework tests for the new layout; add test_transport.py and
  test_refactor_boundaries.py to lock in module boundaries.

BREAKING CHANGE: flat API methods are removed in favour of namespaces
(e.g. b.tabs_list() -> b.tabs.list(), b.group_list() -> b.groups.list()).
2026-06-11 13:58:41 +02:00

151 lines
5.4 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
class DomNS(Namespace):
"""Query and drive page elements in the active (or specified) tab."""
def query(self, selector: str) -> list[dict]:
return self._c._cmd("dom.query", {"selector": selector}) or []
def click(self, selector: str) -> None:
self._c._cmd("dom.click", {"selector": selector})
def type(self, selector: str, text: str) -> None:
self._c._cmd("dom.type", {"selector": selector, "text": text})
def attr(self, selector: str, attr: str) -> list[str]:
return self._c._cmd("dom.attr", {"selector": selector, "attr": attr}) or []
def text(self, selector: str) -> list[str]:
return self._c._cmd("dom.text", {"selector": selector}) or []
def exists(self, selector: str) -> bool:
return self._c._cmd("dom.exists", {"selector": selector}) or 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."""
self._c._cmd("dom.scroll", {"selector": selector, "x": x, "y": y})
def select(self, selector: str, value: str) -> None:
"""Set the value of a <select> element."""
self._c._cmd("dom.select", {"selector": selector, "value": value})
def eval(self, code: str, tab_id: int | None = None):
"""Evaluate JavaScript in the page's main world and return the result."""
return self._c._cmd("dom.eval", {"code": code, "tabId": tab_id})
def key(self, key: str, selector: str | None = None) -> None:
"""Dispatch a keyboard event. key examples: 'Enter', 'Tab', 'Escape', 'ArrowDown'."""
self._c._cmd("dom.key", {"key": key, "selector": selector})
def hover(self, selector: str) -> None:
"""Dispatch mouseover/mouseenter on an element."""
self._c._cmd("dom.hover", {"selector": selector})
def check(self, selector: str) -> None:
"""Check a checkbox."""
self._c._cmd("dom.check", {"selector": selector})
def uncheck(self, selector: str) -> None:
"""Uncheck a checkbox."""
self._c._cmd("dom.uncheck", {"selector": selector})
def clear(self, selector: str) -> None:
"""Clear the value of an input element."""
self._c._cmd("dom.clear", {"selector": selector})
def focus(self, selector: str) -> None:
"""Focus an element."""
self._c._cmd("dom.focus", {"selector": selector})
def submit(self, selector: str) -> None:
"""Submit the form containing the matched element."""
self._c._cmd("dom.submit", {"selector": selector})
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._c._cmd("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._c._cmd("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 tab."""
def links(self) -> list[dict]:
return self._c._cmd("extract.links", {}) or []
def images(self) -> list[dict]:
return self._c._cmd("extract.images", {}) or []
def text(self) -> str:
return self._c._cmd("extract.text", {}) or ""
def json(self, selector: str):
return self._c._cmd("extract.json", {"selector": selector})
def html(self) -> str:
"""Return the full HTML source of the active tab."""
return self._c._cmd("extract.html", {}) or ""
def markdown(self, selector: str | None = None) -> str:
"""Extract the page's main content as clean Markdown.
The extractor may return either Markdown or raw HTML; both are
normalized to Markdown here so SDK and CLI callers get identical output.
"""
from browser_cli.markdown import render_markdown
return render_markdown(self._c._cmd("extract.markdown", {"selector": selector}))
class PageNS(Namespace):
"""Inspect the active page."""
def info(self) -> dict:
"""Return title, URL, readyState, lang, and meta tags of the active tab."""
return self._c._cmd("page.info", {}) or {}