refactor(api): namespaced SDK + dedicated transport layer
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
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
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()).
This commit is contained in:
+40
-57
@@ -1,22 +1,21 @@
|
||||
import click
|
||||
from browser_cli.commands import _handle
|
||||
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 = _handle("dom.query", {"selector": selector})
|
||||
elements = client_from_ctx().dom.query(selector)
|
||||
if not elements:
|
||||
console.print("[yellow]No elements found[/yellow]")
|
||||
return
|
||||
@@ -29,180 +28,164 @@ def dom_query(selector):
|
||||
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."""
|
||||
_handle("dom.click", {"selector": 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."""
|
||||
_handle("dom.type", {"selector": selector, "text": text})
|
||||
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."""
|
||||
values = _handle("dom.attr", {"selector": selector, "attr": attr_name})
|
||||
for v in (values or []):
|
||||
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."""
|
||||
values = _handle("dom.text", {"selector": selector})
|
||||
for v in (values or []):
|
||||
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."""
|
||||
exists = _handle("dom.exists", {"selector": selector})
|
||||
if exists:
|
||||
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."""
|
||||
_handle("dom.scroll", {"selector": selector, "x": x, "y": y})
|
||||
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."""
|
||||
_handle("dom.select", {"selector": selector, "value": value})
|
||||
client_from_ctx().dom.select(selector, value)
|
||||
console.print(f"[green]Selected '{value}' in:[/green] {selector}")
|
||||
|
||||
|
||||
@dom_group.command("eval")
|
||||
@click.argument("code")
|
||||
@click.option("--tab", "tab_id", type=int, default=None, help="Tab ID (default: active tab)")
|
||||
@tab_option
|
||||
@handle_errors
|
||||
def dom_eval(code, tab_id):
|
||||
"""Evaluate JavaScript CODE in the page and print the result."""
|
||||
result = _handle("dom.eval", {"code": code, "tabId": tab_id})
|
||||
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")
|
||||
@click.option("--tab", "tab_id", type=int, default=None, help="Tab ID (default: active tab)")
|
||||
@tab_option
|
||||
@handle_errors
|
||||
def dom_wait_for(selector, timeout, visible, hidden, tab_id):
|
||||
"""Wait until CSS SELECTOR appears (or disappears) in the DOM."""
|
||||
_handle("dom.wait_for", {
|
||||
"selector": selector,
|
||||
"timeout": int(timeout * 1000),
|
||||
"visible": visible,
|
||||
"hidden": hidden,
|
||||
"tabId": tab_id,
|
||||
})
|
||||
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)."""
|
||||
_handle("dom.key", {"key": key, "selector": selector})
|
||||
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."""
|
||||
_handle("dom.hover", {"selector": 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."""
|
||||
_handle("dom.check", {"selector": 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."""
|
||||
_handle("dom.uncheck", {"selector": 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."""
|
||||
_handle("dom.clear", {"selector": 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."""
|
||||
_handle("dom.focus", {"selector": 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."""
|
||||
_handle("dom.submit", {"selector": 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")
|
||||
@click.option("--tab", "tab_id", type=int, default=None, help="Tab ID (default: active tab)")
|
||||
@tab_option
|
||||
@handle_errors
|
||||
def dom_poll(selector, pattern, attr, timeout, interval, tab_id):
|
||||
"""Poll SELECTOR until its text/value matches regex PATTERN."""
|
||||
result = _handle("dom.poll", {
|
||||
"selector": selector,
|
||||
"pattern": pattern,
|
||||
"attr": attr,
|
||||
"timeout": int(timeout * 1000),
|
||||
"interval": int(interval * 1000),
|
||||
"tabId": tab_id,
|
||||
})
|
||||
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}")
|
||||
|
||||
Reference in New Issue
Block a user