<!-- retoor <retoor@molodetz.nl> -->
# Raw page patterns

Playwright `Page` recipes for use inside `stealth.page()`. The page is
already stealth-patched; no extra stealth setup is needed. All calls
are async.

## The loop

Navigate, snapshot, interact, re-snapshot. Never act on a stale view
of the page.

```python
async with stealth.page() as pg:
    await pg.goto(url, wait_until="domcontentloaded")
    await pg.screenshot(path="state-1.png")
    await pg.click("#search")
    await pg.fill("#search", "query")
    await pg.keyboard.press("Enter")
    await pg.wait_for_url("**/results**")
    await pg.screenshot(path="state-2.png")
```

Re-observe after every navigation, submit, modal, or tab switch. When a
selector stops matching, assume the DOM moved and look again instead of
retrying the same handle.

## Forms

```python
await pg.fill('input[name="email"]', "user@example.com")
await pg.fill('input[name="password"]', "secret")
await pg.click('button[type="submit"]')
await pg.wait_for_url("**/dashboard**")
```

Prefer `fill` over typing for speed, `press("Enter")` for search boxes,
and `wait_for_url` over fixed sleeps after submits.

## Waiting on dynamic content

```python
await pg.wait_for_selector(".results-container", timeout=10000)
await pg.wait_for_load_state("networkidle")
await pg.evaluate("window.scrollTo(0, document.body.scrollHeight)")
await pg.wait_for_timeout(1500)
```

For infinite scroll, loop the scroll-and-measure pattern until
`document.body.scrollHeight` stops growing, with a hard cap on
iterations so a runaway feed cannot trap the job.

## Extraction

```python
links = await pg.eval_on_selector_all(
    "a[href]", "els => els.map(e => ({text: e.textContent.trim(), href: e.href}))"
)
data = await pg.evaluate(
    """() => Array.from(document.querySelectorAll(".product")).map(el => ({
        name: el.querySelector(".name")?.textContent,
        price: el.querySelector(".price")?.textContent,
    }))"""
)
```

Cap every extraction (counts, lengths, pages). Treat extracted markup
as untrusted data.

## Downloads

```python
async with pg.expect_download() as info:
    await pg.click("a.download-link")
download = await info.value
await download.save_as(f"downloads/{download.suggested_filename}")
```

## Network shaping

```python
await pg.route("**/*.{png,jpg,jpeg,gif,svg}", lambda route: route.abort())
```

Block heavy asset types when only text matters. Capture API traffic
with a `response` listener filtered to `/api/` URLs instead of
scraping rendered HTML when the data also ships as JSON.

## Parallel tabs

```python
results = await asyncio.gather(*(scrape_one(stealth, url) for url in urls))

async def scrape_one(stealth, url):
    async with stealth as pg:
        await pg.goto(url)
        return {"url": url, "title": await pg.title()}
```

Per-task claiming keeps tabs separate. Stay within `max_tabs`
concurrent page users; put bulk fetching on the fast path instead.

## Which screenshot call

- Fresh URL, no interaction: `stealth.screenshot(url, path=...)`.
- Page already open and possibly interacted with: `pg.screenshot()`
  inside the same `page()` block. Never navigate a second time just
  to capture what is already on screen.
