playwright-self-healing
v1.0.0
Published
Self-healing locator framework for Playwright. Automatically recovers from broken selectors using intelligent DOM comparison — while ensuring real bugs are never masked.
Maintainers
Readme
🩹 playwright-self-healing
Self-healing locator framework for Playwright. Automatically recovers from broken selectors using intelligent DOM comparison — while ensuring real bugs are never masked.
When your UI changes and selectors break, this framework finds the correct element by comparing six dimensions of similarity against stored snapshots. It heals what it can, flags what it's unsure about, and fails on what it should.
Why?
UI test suites are fragile. A developer renames a CSS class, changes an ID, or restructures a component — and suddenly 30 tests fail even though the application works perfectly. You spend hours updating selectors instead of finding real bugs.
Self-healing fixes this by making your locators resilient to cosmetic UI changes, while keeping your assertions strict so real regressions are always caught.
Design Philosophy
| Principle | How it works |
|---|---|
| Heal locators, not assertions | Only element-finding is healed. expect() failures always surface as real bugs. |
| Confidence-based decisions | Every heal has a score. High confidence: auto-heal. Medium: flag for review. Low: fail normally. |
| Full transparency | Every healing event is logged with a detailed breakdown of what changed and why. |
| No vendor lock-in | Your tests are standard Playwright. Remove the wrapper and they still work. |
Quick Start
1. Install
npm install playwright-self-healing2. Initialize
npx self-healing-initThis creates the snapshot database and adds .healing-data/ to your .gitignore.
3. Add the Reporter
In your playwright.config.ts:
import { defineConfig } from '@playwright/test';
export default defineConfig({
reporter: [
['html'],
['playwright-self-healing/reporter'],
],
// ... rest of your config
});4. Write Tests
Replace page with healablePage for locator operations:
import { test, expect } from 'playwright-self-healing/fixtures';
test('user can log in', async ({ healablePage }) => {
await healablePage.goto('/login');
// Self-healing locators — resilient to selector changes
await healablePage.locator('#email').fill('[email protected]');
await healablePage.locator('#password').fill('secret');
await healablePage.locator('#submit-btn').click();
// Assertions use expect() and are NEVER healed
const greeting = healablePage.locator('.welcome-msg');
await expect(await greeting.resolved()).toHaveText('Welcome!');
await expect(healablePage.page).toHaveURL('/dashboard');
});5. Run
npx playwright testOn the first run, the framework snapshots every element. On subsequent runs, if a selector breaks, healing kicks in automatically.
How It Works
locator('#submit-btn')
|
v
+----------------+
| Try original |--- Found ---> Snapshot for future healing
| selector | Done
+-------+--------+
|
Not found
|
v
+----------------+
| Load stored |--- No snapshot ---> Fail normally
| snapshot | (must pass once)
+-------+--------+
|
v
+----------------+
| Scan current |
| page DOM for |
| candidates |
+-------+--------+
|
v
+----------------+
| Score each | >= 75% -> Auto-heal + log
| candidate by |---
| similarity | 50-74% -> Flag for review
| |---
+----------------+ < 50% -> Fail normallySimilarity Scoring
Each candidate element is scored across six dimensions:
| Dimension | Default Weight | What it compares | |---|---|---| | Text Content | 30% | Visible text (Levenshtein distance) | | Attributes | 25% | id, data-testid, name, type, etc. (weighted Jaccard) | | DOM Structure | 15% | Parent chain, sibling context, child index | | Tag Name | 10% | Element type, with partial credit for similar tags | | Visual Position | 10% | Bounding box proximity and size | | ARIA | 10% | Role and label attributes |
All weights are configurable per your application's characteristics.
Configuration
Pass configuration through playwright.config.ts:
export default defineConfig({
use: {
healingConfig: {
confidenceThreshold: 0.80,
maxHealsPerTest: 2,
verbose: true,
},
},
});Options
| Option | Type | Default | Description |
|---|---|---|---|
| enabled | boolean | true | Enable/disable healing globally |
| confidenceThreshold | number | 0.75 | Minimum score to auto-heal |
| reviewFloorThreshold | number | 0.50 | Below this, healing is rejected entirely |
| maxHealsPerTest | number | 3 | Max heals per individual test |
| maxHealsPerRun | number | 15 | Max heals across the entire run |
| dbPath | string | .healing-data/snapshots.db | SQLite database location |
| verbose | boolean | false | Print detailed healing info to console |
| domContextDepth | number | 3 | Levels of parent/sibling context to capture |
| captureScreenshots | boolean | false | Store element screenshots in snapshots |
| noHealPatterns | string[] | ['assert','expect',...] | Selector patterns that are never healed |
| similarityWeights | object | (see above) | Weight for each scoring dimension |
API Reference
Fixtures
import { test, expect } from 'playwright-self-healing/fixtures';healablePage
A wrapper around Playwright's Page with self-healing capabilities:
// CSS selectors
healablePage.locator('#my-id')
healablePage.locator('.my-class')
healablePage.locator('button[type="submit"]')
// Playwright shorthand methods
healablePage.getByRole('button', { name: 'Submit' })
healablePage.getByTestId('login-btn')
healablePage.getByText('Click me')
healablePage.getByLabel('Email')
healablePage.getByPlaceholder('Enter email')healableLocator.resolved()
Returns the underlying Playwright Locator after healing resolution. Use for assertions:
const btn = healablePage.locator('#btn');
await expect(await btn.resolved()).toHaveText('Submit');
await expect(await btn.resolved()).toBeVisible();healablePage.page
Access the raw Playwright Page for non-healing operations:
await expect(healablePage.page).toHaveURL('/dashboard');
await expect(healablePage.page).toHaveTitle('My App');Standalone Usage (without fixtures)
For custom test setups or integration with other frameworks:
import { createHealablePage } from 'playwright-self-healing/fixtures';
const healable = createHealablePage(page, {
testFile: __filename,
testName: 'my test',
config: { verbose: true, confidenceThreshold: 0.80 },
});Direct API Access
For advanced use cases:
import {
HealingEngine,
SnapshotStore,
createConfig,
} from 'playwright-self-healing';
const config = createConfig({ verbose: true });
const store = new SnapshotStore(config.dbPath);
const engine = new HealingEngine(config, store);
const result = await engine.attemptHeal(page, '#old-selector', 'test.ts', 'my test');
console.log(result.healed, result.confidence, result.explanation);Safety Guardrails
Assertions Are Never Healed
The framework draws a hard line: element-finding can be healed, assertions cannot. If expect(element).toHaveText('Submit') fails because the text is now 'Delete', that's a real bug.
Heal Limits
If too many locators break in a single test (maxHealsPerTest: 3) or across a run (maxHealsPerRun: 15), the framework stops healing and fails normally. This prevents cascading fixes that might mask a larger problem.
No-Heal Patterns
Selectors containing words like "assert", "expect", or "verify" are never healed.
The Review Tier
When confidence is between 50% and 75%, the heal is flagged as "needs review" rather than auto-applied. The test continues, but your team is alerted to verify the healing was appropriate.
Reporting
Console Output
✅ [HEALED] Login > should log in
"#login-btn" → "#submit-button" (87.3% confidence)
⚠️ [NEEDS REVIEW] Navigation > sidebar links
".nav-settings" → "a:nth-of-type(3)" (62.1% confidence)HTML Dashboard
After a run, find the report at .healing-data/reports/healing-report.html with summary cards, event details, and score breakdowns.
JSON Report
Machine-readable report at .healing-data/reports/healing-report.json for CI integration.
CI Integration
GitHub Actions
- name: Run tests
run: npx playwright test
- name: Upload healing report
if: always()
uses: actions/upload-artifact@v4
with:
name: healing-report
path: .healing-data/reports/Failing CI on Review-Needed Heals
REVIEWS=$(jq '.summary.reviewNeeded' .healing-data/reports/healing-report.json 2>/dev/null || echo "0")
if [ "$REVIEWS" -gt "0" ]; then
echo "Healing events need manual review"
exit 1
fiExtending
Custom Similarity Weights
healingConfig: {
similarityWeights: {
textContent: 0.40, // Increase for content-heavy apps
attributes: 0.20,
structure: 0.20,
tagName: 0.05,
visualPosition: 0.10,
ariaRole: 0.05,
},
}LLM Integration
The HealingResult object contains full context (snapshots, candidates, scores, DOM diffs) that can be passed to an LLM for advanced triage decisions.
Comparison with Other Tools
| Feature | playwright-self-healing | Healenium | Testim | mabl | |---|---|---|---|---| | Open source | MIT | Apache 2.0 | Proprietary | Proprietary | | Playwright support | Native | Selenium only | Yes | Yes | | Transparent scoring | Full breakdown | Partial | Black box | Black box | | Configurable thresholds | All tunable | Limited | No | No | | Review tier | 3-tier system | Binary | No | No | | Infrastructure | None (SQLite) | PostgreSQL + backend | Cloud | Cloud | | Vendor lock-in | None | Low | High | High |
FAQ
Q: What happens on the first run? The framework snapshots every element but can't heal anything yet. Tests run normally.
Q: Can this mask real bugs? It's designed not to. Assertions are never healed. Confidence thresholds, the review tier, and heal limits add multiple layers of protection.
Q: Does it slow down my tests? Negligibly. Snapshot capture adds ~5-10ms per element. Healing only triggers on failure.
Q: What about dynamic content? Multi-dimensional scoring handles this. Even if an ID changes, text, structure, and position still identify the element.
Q: Can I migrate incrementally?
Yes. Change the import in individual test files. Tests using the regular page fixture continue working unchanged.
Contributing
See CONTRIBUTING.md for development setup and guidelines.
