feat: Playwright browser wrapper with compact snapshot

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-01 00:11:47 +08:00
parent cf022c5096
commit 9161f623a7
2 changed files with 90 additions and 0 deletions
+67
View File
@@ -0,0 +1,67 @@
from __future__ import annotations
from typing import Any
class Browser:
"""Headed (or headless) Playwright Chromium, optionally routed via proxy."""
def __init__(self, proxy_port: int | None, headless: bool = False) -> None:
self._proxy_port = proxy_port
self._headless = headless
self._pw: Any = None
self._browser: Any = None
self._page: Any = None
def start(self) -> None:
from playwright.sync_api import sync_playwright
self._pw = sync_playwright().start()
launch_kwargs: dict[str, Any] = {
"headless": self._headless,
"args": ["--ignore-certificate-errors"],
}
if self._proxy_port is not None:
launch_kwargs["proxy"] = {"server": f"http://127.0.0.1:{self._proxy_port}"}
self._browser = self._pw.chromium.launch(**launch_kwargs)
context = self._browser.new_context(ignore_https_errors=True)
self._page = context.new_page()
def navigate(self, url: str) -> dict[str, Any]:
self._page.goto(url, wait_until="networkidle")
return self.snapshot()
def click(self, selector: str) -> dict[str, Any]:
self._page.click(selector, timeout=5000)
self._page.wait_for_load_state("networkidle")
return self.snapshot()
def type_text(self, selector: str, text: str) -> dict[str, Any]:
self._page.fill(selector, text)
return self.snapshot()
def snapshot(self) -> dict[str, Any]:
"""Compact view for the agent: url, title, and visible interactive elements."""
elements = self._page.eval_on_selector_all(
"a, button, input, [role=button], [role=link]",
"""els => els.slice(0, 40).map(e => ({
tag: e.tagName.toLowerCase(),
text: (e.innerText || e.value || e.getAttribute('aria-label') || '').slice(0, 60),
id: e.id || null,
}))""",
)
return {
"url": self._page.url,
"title": self._page.title(),
"elements": elements,
}
def pause_for_human(self) -> None:
"""Surface the headed browser for manual control (Playwright inspector)."""
self._page.pause()
def stop(self) -> None:
if self._browser is not None:
self._browser.close()
if self._pw is not None:
self._pw.stop()
+23
View File
@@ -0,0 +1,23 @@
import pytest
playwright = pytest.importorskip("playwright.sync_api")
from auto_reverse.browser import Browser # noqa: E402
@pytest.fixture
def browser():
try:
b = Browser(proxy_port=None, headless=True)
b.start()
except Exception as exc: # browser binary missing, etc.
pytest.skip(f"browser unavailable: {exc}")
yield b
b.stop()
def test_navigate_and_snapshot(browser, fixture_site):
browser.navigate(fixture_site + "/")
snap = browser.snapshot()
assert snap["url"].endswith("/")
assert "title" in snap