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 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()

# More queries
all_cards = await hp.find_all(".card", "Product cards")    # returns list of HealingLocators
await hp.uncheck("#newsletter", "Newsletter checkbox")

# ARIA / semantic locators (no healing needed — already stable)
role_loc  = hp.get_by_role("button", name="Login", description="Login button")
text_loc  = hp.get_by_text("Sign in")
label_loc = hp.get_by_label("Password")
ph_loc    = hp.get_by_placeholder("Enter email")
tid_loc   = hp.get_by_test_id("submit-btn")

# Page utilities
await hp.go_forward()
await hp.screenshot(path="shot.png")
title = await hp.title()
val   = await hp.evaluate("document.title")
await hp.set_viewport_size(1920, 1080)
await hp.close()

# Internal access
raw  = hp.raw_page        # underlying Playwright Page
report = hp.session_report  # SessionReport object for custom processing

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

HealingLocator

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

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

# All standard Playwright Locator methods work
await locator.click()
await locator.fill("value")
await locator.check()
await locator.uncheck()
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")
await locator.wait_for()
await locator.nth(0)

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

# Source auto-patcher
PH_AUTO_PATCH_SOURCE=false   # File-wide string search (Stage 1)
PH_AI_PATCH_SOURCE=false     # AI-powered rewrite (Stage 2, implies Stage 1)
PH_PATCH_SOURCE_BACKUP=false # Create .bak copies before overwriting

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)
    .auto_patch_source(False)   # Set True to rewrite broken selectors in source files
    .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
pytest tests/ --ph-auto-patch-source             # rewrite broken selectors (file-wide search)
pytest tests/ --ph-ai-patch-source               # AI-powered rewrite (handles complex cases)

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
├── source_patcher.py    # Auto-patches broken selectors back into test source files
├── 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

Auto-Patch Source

playwright-healer can automatically rewrite the broken selector strings in your test files once a heal is found — even when the call spans multiple lines.

Two-stage patching

Stage 1 — File-wide string search (--ph-auto-patch-source)

Searches the entire file for the broken selector as a quoted string literal and replaces every occurrence. No line-number tracking — handles multi-line calls perfectly:

# BEFORE (broken multi-line call)
await healing_page.fill(
    "#userid",          # ← selector on a different line to the call
    "Username field",
    value="tomsmith",
)

# AFTER (automatically rewritten)
await healing_page.fill(
    "#username",
    "Username field",
    value="tomsmith",
)

Stage 2 — AI-powered rewrite (--ph-ai-patch-source)

Sends the full file content plus the healing context to the configured AI provider. The AI rewrites broken selectors even in complex situations:

  • Selectors stored in variables
  • Role-based selectors that need a full method-call change (get_by_role())
  • f-string or dynamically constructed selectors
  • Multiple selectors on the same line

--ph-ai-patch-source implies --ph-auto-patch-source — Stage 1 always runs first.

Enabling auto-patch

# Stage 1 only (fast, free, handles 95% of cases)
pytest tests/ --ph-auto-patch-source

# Stage 1 + 2 (AI handles complex/remaining cases)
pytest tests/ --ph-ai-patch-source
# .env equivalents
PH_AUTO_PATCH_SOURCE=true   # Stage 1
PH_AI_PATCH_SOURCE=true     # Stage 1 + 2
# Programmatic
config = HealerConfig.builder().auto_patch_source(True).build()   # Stage 1
config = HealerConfig.builder().ai_patch_source(True).build()     # Stage 1 + 2

Safety notes:

  • Both stages read and write each file exactly once per session.
  • Duplicate patches (same file + selector) are deduplicated automatically.
  • Files are overwritten in-place. Set PH_PATCH_SOURCE_BACKUP=true for .bak backups.

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)
Source not patched Ensure PH_AUTO_PATCH_SOURCE=true and the test file is writable

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.6.tar.gz (59.0 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.6-py3-none-any.whl (54.7 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: playwright_healer-1.0.6.tar.gz
  • Upload date:
  • Size: 59.0 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.6.tar.gz
Algorithm Hash digest
SHA256 63fb7f706d4fc59f7053c39cb37c415202d2e3b9585251c7b15ee2131da7f9b6
MD5 5fefd2e01db083aa609d05f389d7f454
BLAKE2b-256 341f6f27c21bdf706e92439800abb6bbca03329b2d649079aa7609289aab8645

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for playwright_healer-1.0.6-py3-none-any.whl
Algorithm Hash digest
SHA256 e1d4f9477ed7053454f3802ca3ac0be2563bb03f089c88a09af4c1c6710b81ae
MD5 3898ed62a2a548a7ec7954a8e0542f04
BLAKE2b-256 bf12871f8a004bffb616739a446a9f72cf3b11e07a4faafcd4a1572f9b3563a6

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