feat: Playwright browser wrapper with compact snapshot
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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()
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user