# playwright-healer

[![PyPI version](https://img.shields.io/pypi/v/playwright-healer.svg)](https://pypi.org/project/playwright-healer/)
[![Python](https://img.shields.io/badge/python-3.9%2B-blue.svg)](https://www.python.org/downloads/)
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)
[![Tests](https://img.shields.io/badge/tests-passing-brightgreen.svg)]()

**Production-grade self-healing locator engine for Playwright Python.**

Automatically heals broken element selectors caused by UI changes — with a
multi-tier pipeline that's **faster, cheaper, and more reliable** than any
other self-healing library.

---

## Why playwright-healer vs the competition?

| Feature | playwright-healer | autoheal-locator |
|---|---|---|
| **Heuristic pre-AI healing** | ✅ Free, instant, catches ~70% of renames | ❌ Calls AI immediately |
| **DOM fuzzy match** | ✅ Free, no API calls | ❌ Not present |
| **Provider fallback chain** | ✅ All configured providers used as fallbacks | ❌ Single provider — one outage = failure |
| **ARIA-first healed selectors** | ✅ Returns `get_by_role`, `get_by_text` — stable | ❌ Returns brittle CSS |
| **HealingPage wrapper** | ✅ Zero boilerplate, wrap the page object | ❌ Requires separate adapter class |
| **Playwright-native API** | ✅ Returns `Locator` objects | ❌ Returns raw elements |
| **Shadow DOM support** | ✅ Built-in | ❌ Not supported |
| **Adaptive cache TTL** | ✅ Stable selectors cached longer | ❌ Fixed TTL only |
| **Confidence scoring** | ✅ Ranked candidates per healing attempt | ❌ First match wins |
| **pytest plugin** | ✅ Zero conftest — `healing_page` fixture auto-registers | ❌ Manual conftest required |
| **Async context manager** | ✅ `async with HealingPage(page) as hp:` | ❌ Not supported |
| **Pure Playwright focus** | ✅ Playwright-only, first-class support | ❌ Shared codebase with Selenium |
| **Structured AI prompts** | ✅ JSON schema enforced, robust parsing | ❌ Regex-based extraction |

---

## How It Works

```
Test calls hp.click("#selector", "description")
           │
           ▼
┌─────────────────────┐
│  0. Try Original    │──── Found ──────────────────────► Return Locator
└─────────────────────┘
           │ NOT FOUND (quick_timeout_ms = 500ms)
           ▼
┌─────────────────────┐
│  1. Check Cache     │──── HIT ──► validate ───────────► Return Locator
└─────────────────────┘              │ broken (mark miss)
           │ miss                    └── continue →
           ▼
┌─────────────────────┐
│  2. Heuristic       │──── Found ────────────────────► Cache + Return
│  • ID suffix swaps  │   FREE  ~0ms     70% hit rate
│  • Class Jaccard    │
│  • Fuzzy attributes │
│  • Text match       │
│  • ARIA role match  │
└─────────────────────┘
           │ no candidates matched
           ▼
┌─────────────────────┐
│  3. DOM Fuzzy Match │──── Found ────────────────────► Cache + Return
│  • rapidfuzz score  │   FREE  ~50ms
│  • All attributes   │
│  • Fingerprinting   │
└─────────────────────┘
           │ none pass
           ▼
┌─────────────────────┐
│  4. AI — DOM        │──── Found ────────────────────► Cache + Return
│  • Structured prompt│   PAID  ~1-3s   Provider chain
│  • Ranked candidates│           ↑ auto-fallback
│  • Confidence scores│
└─────────────────────┘
           │ (FULL strategy only)
           ▼
┌─────────────────────┐
│  5. AI — Visual     │──── Found ────────────────────► Cache + Return
│  • Screenshot + AI  │   PAID  ~2-5s
│  • Vision model     │
└─────────────────────┘
           │ all failed
           ▼
    ElementNotFoundError
```

---

## Installation

```bash
# Core (Playwright required to be installed separately)
pip install playwright-healer

# With Redis cache support
pip install playwright-healer[redis]

# Install playwright browsers
playwright install chromium
```

---

## Zero-Config Quick Start

### Step 1 — Set one API key

```bash
# .env  (Groq is free — get key at console.groq.com)
GROQ_API_KEY=gsk_your_key_here
```

### Step 2 — Use HealingPage (zero boilerplate)

```python
from playwright.async_api import async_playwright
from playwright_healer import HealingPage, auto_config

async def test_something():
    async with async_playwright() as p:
        browser = await p.chromium.launch()
        page = await browser.new_page()

        async with HealingPage(page, auto_config()) as hp:
            await hp.goto("https://example.com/login")

            # Broken selectors — healed automatically
            await hp.fill("#user-name-BROKEN", "Username field", value="user")
            await hp.fill("#password-BROKEN",  "Password field", value="pass")
            await hp.click("#login-button-BROKEN", "Login button")

            assert "dashboard" in hp.url
```

### Step 3 — With pytest (no conftest.py required)

```python
# test_login.py — no conftest, no imports from playwright_healer needed

async def test_login(healing_page):           # ← fixture auto-provided
    await healing_page.goto("https://www.saucedemo.com")
    await healing_page.fill("#user-name-BROKEN", "Username field", value="standard_user")
    await healing_page.fill("#password-BROKEN",  "Password field", value="secret_sauce")
    await healing_page.click("#login-button-BROKEN", "Login button")
    assert "inventory" in healing_page.url    # ← passes even with broken selectors
```

Run with:

```bash
pytest tests/ -v
# Or with custom strategy:
pytest tests/ --ph-strategy DOM_ONLY -v
# Or disable AI (heuristic only):
pytest tests/ --ph-no-heal -v
```

---

## API Reference

### `HealingPage`

The primary interface — wraps a Playwright `Page`.

```python
from playwright_healer import HealingPage, auto_config

hp = HealingPage(page, auto_config(), test_name="my_test")

# Navigation
await hp.goto("https://example.com")
await hp.reload()
await hp.go_back()
await hp.wait_for_load_state("networkidle")
await hp.wait_for_url("**/dashboard")

# Element interactions (selector, description, ...kwargs)
await hp.click("#submit-btn", "Submit button")
await hp.fill("#email", "Email field", value="user@example.com")
await hp.type_text("#search", "Search input", text="playwright")
await hp.check("#agree-checkbox", "Terms checkbox")
await hp.select("#country", "Country dropdown", value="US")
await hp.hover("#menu", "Navigation menu")

# Queries
text    = await hp.get_text(".page-title", "Page title")
attr    = await hp.get_attribute("#link", "href", "Navigation link")
visible = await hp.is_visible("#banner", "Banner")
present = await hp.is_present("#promo", "Promo section")
count   = await hp.count(".card", "Product cards")

# Return HealingLocator (lazy resolve on first action)
locator = hp.locator("#submit", "Submit button")
await locator.click()
await locator.fill("value")

# ARIA convenience
role_loc = hp.get_by_role("button", name="Login", description="Login button")
await role_loc.click()

# Lifecycle
await hp.shutdown()      # generates reports, prints summary
```

### `HealingLocator`

A transparent wrapper that heals and then forwards all Playwright Locator methods.

```python
locator = hp.locator("#submit", "Submit button")

await locator.click()
await locator.fill("value")
await locator.check()
await locator.is_visible()
await locator.inner_text()
await locator.get_attribute("href")
await locator.count()
await locator.all()
await locator.screenshot()
await locator.evaluate("el => el.value")
```

---

## Configuration

### Environment Variables (Recommended)

```bash
# AI Providers — configure one or more (all become fallbacks)
GROQ_API_KEY=gsk_your_key                 # Free — recommended
GEMINI_API_KEY=AIza_your_key              # Free tier
OPENAI_API_KEY=sk-proj-your_key           # Paid
ANTHROPIC_API_KEY=sk-ant-your_key         # Paid
DEEPSEEK_API_KEY=your_key                 # Cheap
PH_API_URL=http://localhost:11434/v1/...  # Local (Ollama/LM Studio)

# Healing strategy
PH_STRATEGY=SMART          # SMART | HEURISTIC_ONLY | DOM_ONLY | VISUAL_ONLY | FULL | PARALLEL

# Timeouts
PH_QUICK_TIMEOUT_MS=500    # How long to try original selector before healing
PH_ELEMENT_TIMEOUT_MS=10000

# Cache
PH_CACHE_BACKEND=FILE      # FILE | MEMORY | REDIS
PH_CACHE_TTL_HOURS=48
PH_CACHE_FILE=.healer_cache.json

# Features
PH_PREFER_ARIA=true        # Heal to get_by_role/text over CSS (recommended)
PH_SHADOW_DOM=true         # Penetrate shadow roots
PH_ADAPTIVE_TTL=true       # Longer TTL for stable selectors

# Reporting
PH_REPORT_DIR=playwright-healer-reports
PH_CONSOLE_LOG=true
```

### Programmatic Configuration

```python
from playwright_healer import HealerConfig, AIProviderConfig, CacheConfig
from playwright_healer.config import AIProvider, CacheBackend, HealingStrategy

config = (
    HealerConfig.builder()
    .add_provider(
        AIProviderConfig.builder()
        .provider(AIProvider.GROQ)
        .api_key("gsk_...")
        .model("llama-3.3-70b-versatile")
        .build()
    )
    .add_provider(                              # ← fallback
        AIProviderConfig.builder()
        .provider(AIProvider.OPENAI)
        .api_key("sk-proj-...")
        .build()
    )
    .strategy(HealingStrategy.SMART)
    .quick_timeout_ms(500)
    .prefer_aria(True)
    .cache(
        CacheConfig.builder()
        .backend(CacheBackend.FILE)
        .ttl_hours(48.0)
        .build()
    )
    .report_dir("reports")
    .adaptive_ttl(True)
    .build()
)
```

---

## Healing Strategies

| Strategy | Cost | Speed | When to use |
|---|---|---|---|
| `SMART` | Low | Fast | Default — heuristic → DOM fuzzy → AI DOM |
| `HEURISTIC_ONLY` | Zero | Instant | Local dev; when AI not configured |
| `DOM_ONLY` | Low | Fast | CI/CD with cost constraints |
| `VISUAL_ONLY` | Medium | Medium | Highly visual UIs |
| `FULL` | High | Slower | Maximum healing power |
| `PARALLEL` | High | Fast healing | Speed-critical scenarios |

---

## AI Providers

All discovered keys become a **fallback chain** — if Groq rate-limits or goes
down, `playwright-healer` automatically retries with the next provider.
This is **unique** to playwright-healer.

| Provider | Vision | Cost | Notes |
|---|---|---|---|
| Groq | ❌ | Free | Fast, recommended for start |
| Google Gemini | ✅ | Free/Low | Free tier available |
| OpenAI | ✅ | Medium | gpt-4o-mini cheapest; gpt-4o for vision |
| Anthropic | ✅ | Medium | Haiku for text; Sonnet for vision |
| DeepSeek | ❌ | Very Low | Good for DOM-only |
| Local (Ollama) | Depends | Free | Private, no data sent externally |

---

## Cache Configuration

```python
# File (default) — survives restarts
CacheConfig.builder().backend(CacheBackend.FILE).ttl_hours(48).build()

# Memory — fastest, ephemeral
CacheConfig.builder().backend(CacheBackend.MEMORY).max_size(1000).build()

# Redis — shared across parallel CI workers
CacheConfig.builder().redis(host="localhost", port=6379).build()
```

**Adaptive TTL** — a key feature that competitors lack:

- Selectors healed consistently (high hit ratio) → TTL extended up to 30 days
- Selectors that keep breaking (high miss ratio) → TTL shortened to 1 hour
- Prevents stale healings accumulating in CI

---

## Reports

After `shutdown()` or context manager exit, playwright-healer generates:

- **Rich terminal summary** with coloured output
- **HTML report** — dark-themed, self-contained
- **JSON report** — machine-readable, CI artifact friendly

Console output example:
```
  ✓ ORIGINAL  #user-name
  ✓ HEALED  HEURISTIC  #user-name-BROKEN → #user-name  12ms  conf=95%
  ✓ HEALED  CACHE      #password-BROKEN  → #password    2ms  conf=90%
  ✓ HEALED  AI_DOM     #btn-BROKEN       → button::Login  1240ms [820t]  conf=88%
  ✗ FAILED  #nonexistent

  ┌─────────────────────────────────────────────┐
  │     playwright-healer Session Summary        │
  ├──────────────────────┬──────────────────────┤
  │ Total lookups        │ 4                    │
  │ Healed (non-trivial) │ 3                    │
  │ From cache           │ 1                    │
  │ Failed               │ 1                    │
  │ AI tokens used       │ 820                  │
  │ Avg latency          │ 313 ms               │
  └──────────────────────┴──────────────────────┘
```

---

## pytest CLI Options

```bash
pytest tests/                                  # default SMART strategy
pytest tests/ --ph-strategy HEURISTIC_ONLY    # free, instant
pytest tests/ --ph-strategy DOM_ONLY          # AI with DOM context only
pytest tests/ --ph-strategy FULL              # maximum healing
pytest tests/ --ph-no-heal                    # disable AI (heuristic only)
pytest tests/ --ph-report-dir ./reports       # custom report dir
pytest tests/ --ph-cache-file ./my.cache.json # custom cache file
```

---

## Project Structure

```
playwright_healer/
├── __init__.py          # Public API: HealingPage, HealingLocator, auto_config
├── config.py            # HealerConfig, AIProviderConfig, CacheConfig, enums
├── auto_config.py       # Zero-config auto-detection from env vars
├── healer.py            # HealingPage — primary user-facing class
├── locator.py           # HealingLocator — transparent Locator wrapper
├── pipeline.py          # HealingPipeline — orchestrates all stages
├── heuristic.py         # Stage 2: free heuristic mutations (ID/class/text/ARIA)
├── ai_providers.py      # Stage 4/5: OpenAI-compat, Gemini, Anthropic + ProviderChain
├── cache.py             # Memory, File, Redis cache backends + adaptive TTL
├── reporting.py         # Rich console, HTML, JSON reporters + data models
├── utils.py             # Selector detection, DOM stripping, fingerprinting
└── pytest_plugin.py     # pytest plugin — healing_page + healing_config fixtures
```

---

## Best Practices

**Use descriptive element descriptions** — the description is sent to the AI:

```python
# Vague — AI has little context
await hp.click("#btn-1", "button")

# Specific — AI understands exactly what to find
await hp.click("#btn-1", "Primary submit button on the checkout confirmation page")
```

**Configure multiple providers** as fallbacks:

```bash
GROQ_API_KEY=gsk_...      # primary (free)
OPENAI_API_KEY=sk-...     # fallback (if Groq rate-limits)
```

**Use `SMART` strategy in CI** — cost-effective, heuristic fires first:

```bash
PH_STRATEGY=SMART
```

**Scope `healing_config` to `session`** (default) — one cache per test run.

**Use `HEURISTIC_ONLY` for local dev** when you don't need AI:

```bash
pytest tests/ --ph-no-heal
```

---

## Troubleshooting

| Problem | Fix |
|---|---|
| `No AI provider configured` | Set at least one: `GROQ_API_KEY`, `GEMINI_API_KEY`, `OPENAI_API_KEY`, etc. |
| `All AI providers exhausted` | Check API keys are valid; add a fallback provider |
| Rate limit errors | Add a second provider as fallback; switch to `HEURISTIC_ONLY` temporarily |
| Healed selector also broken | Cache adapts via adaptive TTL; call `cache.invalidate(key)` manually |
| Reports in wrong folder | Use `PH_REPORT_DIR` env var or `report_dir` in config |
| Shadow DOM elements not found | Ensure `PH_SHADOW_DOM=true` (default) |

---

## License

MIT — see [LICENSE](LICENSE).
