Files
browser-cli/browser_cli/commands/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

192 lines
7.1 KiB
Python

import click
from browser_cli.commands import client_from_ctx, handle_errors, tab_option
from rich.console import Console
from rich.table import Table
import json
console = Console()
@click.group("dom")
def dom_group():
"""Query and interact with page DOM elements."""
@dom_group.command("query")
@click.argument("selector")
@handle_errors
def dom_query(selector):
"""Return elements matching CSS SELECTOR (like mini DevTools)."""
elements = client_from_ctx().dom.query(selector)
if not elements:
console.print("[yellow]No elements found[/yellow]")
return
table = Table(show_header=True, header_style="bold cyan")
table.add_column("Tag", width=12)
table.add_column("Text", width=40)
table.add_column("Attributes")
for el in elements:
attrs = ", ".join(f"{k}={v!r}" for k, v in (el.get("attrs") or {}).items())
table.add_row(el.get("tag", ""), (el.get("text") or "")[:60], attrs[:80])
console.print(table)
@dom_group.command("click")
@click.argument("selector")
@handle_errors
def dom_click(selector):
"""Click the first element matching CSS SELECTOR."""
client_from_ctx().dom.click(selector)
console.print(f"[green]Clicked:[/green] {selector}")
@dom_group.command("type")
@click.argument("selector")
@click.argument("text")
@handle_errors
def dom_type(selector, text):
"""Type TEXT into the element matching CSS SELECTOR."""
client_from_ctx().dom.type(selector, text)
console.print(f"[green]Typed into:[/green] {selector}")
@dom_group.command("attr")
@click.argument("selector")
@click.argument("attr_name")
@handle_errors
def dom_attr(selector, attr_name):
"""Get attribute ATTR_NAME from elements matching CSS SELECTOR."""
for v in client_from_ctx().dom.attr(selector, attr_name):
console.print(v)
@dom_group.command("text")
@click.argument("selector")
@handle_errors
def dom_text(selector):
"""Get text content of elements matching CSS SELECTOR."""
for v in client_from_ctx().dom.text(selector):
console.print(v)
@dom_group.command("exists")
@click.argument("selector")
@handle_errors
def dom_exists(selector):
"""Check if an element matching CSS SELECTOR exists on the page."""
if client_from_ctx().dom.exists(selector):
console.print(f"[green]exists[/green]: {selector}")
else:
console.print(f"[red]not found[/red]: {selector}")
raise SystemExit(1)
@dom_group.command("scroll")
@click.argument("selector", required=False)
@click.option("--x", type=int, default=None, help="Horizontal scroll position (px)")
@click.option("--y", type=int, default=None, help="Vertical scroll position (px)")
@handle_errors
def dom_scroll(selector, x, y):
"""Scroll to a CSS SELECTOR or to an X/Y coordinate."""
client_from_ctx().dom.scroll(selector, x=x, y=y)
target = selector or f"({x or 0}, {y or 0})"
console.print(f"[green]Scrolled to:[/green] {target}")
@dom_group.command("select")
@click.argument("selector")
@click.argument("value")
@handle_errors
def dom_select(selector, value):
"""Set the VALUE of a <select> dropdown matching CSS SELECTOR."""
client_from_ctx().dom.select(selector, value)
console.print(f"[green]Selected '{value}' in:[/green] {selector}")
@dom_group.command("eval")
@click.argument("code")
@tab_option
@handle_errors
def dom_eval(code, tab_id):
"""Evaluate JavaScript CODE in the page and print the result."""
result = client_from_ctx().dom.eval(code, tab_id)
if result is None:
console.print("[dim]null[/dim]")
else:
console.print(json.dumps(result, indent=2) if isinstance(result, (dict, list)) else str(result))
@dom_group.command("wait-for")
@click.argument("selector")
@click.option("--timeout", type=float, default=10.0, show_default=True, help="Max seconds to wait")
@click.option("--visible", is_flag=True, help="Wait until element is visible (non-zero size)")
@click.option("--hidden", is_flag=True, help="Wait until element is absent or hidden")
@tab_option
@handle_errors
def dom_wait_for(selector, timeout, visible, hidden, tab_id):
"""Wait until CSS SELECTOR appears (or disappears) in the DOM."""
client_from_ctx().dom.wait_for(selector, timeout=timeout, visible=visible, hidden=hidden, tab_id=tab_id)
state = "hidden" if hidden else ("visible" if visible else "present")
console.print(f"[green]Ready ({state}):[/green] {selector}")
@dom_group.command("key")
@click.argument("key")
@click.option("--selector", default=None, help="CSS selector to target (default: focused element)")
@handle_errors
def dom_key(key, selector):
"""Dispatch a keyboard KEY event (e.g. Enter, Tab, Escape, ArrowDown)."""
client_from_ctx().dom.key(key, selector)
target = selector or "active element"
console.print(f"[green]Key '{key}' sent to:[/green] {target}")
@dom_group.command("hover")
@click.argument("selector")
@handle_errors
def dom_hover(selector):
"""Dispatch mouseover/mouseenter on the element matching CSS SELECTOR."""
client_from_ctx().dom.hover(selector)
console.print(f"[green]Hovered:[/green] {selector}")
@dom_group.command("check")
@click.argument("selector")
@handle_errors
def dom_check(selector):
"""Check a checkbox matching CSS SELECTOR."""
client_from_ctx().dom.check(selector)
console.print(f"[green]Checked:[/green] {selector}")
@dom_group.command("uncheck")
@click.argument("selector")
@handle_errors
def dom_uncheck(selector):
"""Uncheck a checkbox matching CSS SELECTOR."""
client_from_ctx().dom.uncheck(selector)
console.print(f"[green]Unchecked:[/green] {selector}")
@dom_group.command("clear")
@click.argument("selector")
@handle_errors
def dom_clear(selector):
"""Clear the value of an input matching CSS SELECTOR."""
client_from_ctx().dom.clear(selector)
console.print(f"[green]Cleared:[/green] {selector}")
@dom_group.command("focus")
@click.argument("selector")
@handle_errors
def dom_focus(selector):
"""Focus the element matching CSS SELECTOR."""
client_from_ctx().dom.focus(selector)
console.print(f"[green]Focused:[/green] {selector}")
@dom_group.command("submit")
@click.argument("selector")
@handle_errors
def dom_submit(selector):
"""Submit the form that contains the element matching CSS SELECTOR."""
client_from_ctx().dom.submit(selector)
console.print(f"[green]Submitted form for:[/green] {selector}")
@dom_group.command("poll")
@click.argument("selector")
@click.argument("pattern")
@click.option("--attr", default=None, help="Attribute or property to read (default: textContent/value)")
@click.option("--timeout", type=float, default=30.0, show_default=True, help="Max seconds to wait")
@click.option("--interval", type=float, default=0.5, show_default=True, help="Poll interval in seconds")
@tab_option
@handle_errors
def dom_poll(selector, pattern, attr, timeout, interval, tab_id):
"""Poll SELECTOR until its text/value matches regex PATTERN."""
result = client_from_ctx().dom.poll(selector, pattern, attr=attr, timeout=timeout, interval=interval, tab_id=tab_id)
value = result.get("value", "") if isinstance(result, dict) else ""
console.print(f"[green]Matched:[/green] {selector!r} = {value!r}")