Skip to main content

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

PyPI version Python License: MIT Tests

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

# 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

playwright_healer-1.0.0.tar.gz (42.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

playwright_healer-1.0.0-py3-none-any.whl (41.4 kB view details)

Uploaded Python 3

File details

Details for the file playwright_healer-1.0.0.tar.gz.

File metadata

  • Download URL: playwright_healer-1.0.0.tar.gz
  • Upload date:
  • Size: 42.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.2

File hashes

Hashes for playwright_healer-1.0.0.tar.gz
Algorithm Hash digest
SHA256 300577d65c0de2943c05e248272a17ff2baefc3c46e7fc54c55c44e3c494ddca
MD5 b9258e4637b83dd1efb55b6faa07c666
BLAKE2b-256 358f8aeae2c70c3936848d984cb96a36a8a4be21420f39583919cccbf0217d92

See more details on using hashes here.

File details

Details for the file playwright_healer-1.0.0-py3-none-any.whl.

File metadata

File hashes

Hashes for playwright_healer-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 2471ea06902b1271721883163b6520ccaee56dfe6cbadf78141acd98464c135d
MD5 3cf0c868e62ab7ce0d2e07a71e4f7ceb
BLAKE2b-256 ac62d623f020d154f8deeee2e69c2e96e0afbb318bd73ce0973e936183a9fd16

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page