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 (rewrites broken selectors in your test files automatically)
PH_AUTO_PATCH_SOURCE=false   # Set to true to enable

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 in test files

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

playwright-healer can automatically rewrite the broken selector strings in your test files once a heal is found. This means you don't have to manually hunt down and fix stale locators — the library fixes them for you.

How it works

  1. A broken selector is healed at runtime.
  2. The original source file and line number are captured from the call stack.
  3. At the end of the session, the broken string literal on that line is replaced with the healed selector — in-place.

Before:

await healing_page.click("#logout-btn", "Logout")

After (automatic rewrite):

await healing_page.click("#logout-button", "Logout")

Enabling auto-patch

# .env
PH_AUTO_PATCH_SOURCE=true
# pytest CLI
pytest tests/ --ph-auto-patch-source
# Programmatic
config = HealerConfig.builder().auto_patch_source(True).build()

Safety notes:

  • Only the exact broken string literal on the reported line is replaced — surrounding code is untouched.
  • All patches collected during a run are applied once at session end (no mid-run file writes).
  • Duplicate patches (same file + line + selector) are automatically deduplicated.
  • Files are overwritten in-place. Set backup=True on SourcePatcher directly if you want .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.5.tar.gz (56.5 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.5-py3-none-any.whl (52.2 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: playwright_healer-1.0.5.tar.gz
  • Upload date:
  • Size: 56.5 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.5.tar.gz
Algorithm Hash digest
SHA256 821f71d4b515a106024dd07d1832f55d50e1d53137fff7d4d3c5a7dadac8fdaa
MD5 afb5c44d194bd45142254a9efc129298
BLAKE2b-256 22c57ffe7ba04e903f1f53e3e17d839e1fe8f54ce424ad99b6b8487dd2f6d990

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for playwright_healer-1.0.5-py3-none-any.whl
Algorithm Hash digest
SHA256 edd97489433c5203ad6dc1b2d2d2cd0589750e16e6bb403f047bc8e28baebc54
MD5 dfbbbed194073485a520e70ec6f5fb23
BLAKE2b-256 fab88325a5d04aab6392636fcf05713663b2f43fc2fd0fa32e3c9760b7ce76ea

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