Production-grade self-healing locator engine for Playwright Python — heuristic pre-AI, DOM fuzzy match, ARIA-first healing, provider fallback chains, adaptive cache, shadow DOM & iframe support
Project description
playwright-healer
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 | Competition |
|---|---|---|
| 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
# 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
# .env (Groq is free — get key at console.groq.com)
GROQ_API_KEY=gsk_your_key_here
Step 2 — Use HealingPage (zero boilerplate)
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)
# 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:
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.
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.
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)
# 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
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
# 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
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:
# 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:
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:
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:
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.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file playwright_healer-1.0.2.tar.gz.
File metadata
- Download URL: playwright_healer-1.0.2.tar.gz
- Upload date:
- Size: 43.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
596e7bad02df42c070aa652e113781ec400d482f19bf5041c1c33a35392ba827
|
|
| MD5 |
a47daf9aab252e7bb7833290912c8cc7
|
|
| BLAKE2b-256 |
faed3a1745a1f705850f64d786d8939ac4d51198909f7d6febe7f53d79ab8cd2
|
File details
Details for the file playwright_healer-1.0.2-py3-none-any.whl.
File metadata
- Download URL: playwright_healer-1.0.2-py3-none-any.whl
- Upload date:
- Size: 42.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
af8f8a11a68596918e260bff170eb9f82692a444d1ad7ac41672ea63ef6933a4
|
|
| MD5 |
6e6eefb07f48e8b1fc856c4a099ed18d
|
|
| BLAKE2b-256 |
684c52fa1e5258af2fcaf303cf0cb2b0e97f2bd1fc124ec93750bf7addc92a82
|