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