Get started
From npm install to a test that heals its own broken selectors. You need
Playwright 1.62 or newer, and either an API key for a cloud model or Ollama running on your
machine.
Install
Add healwright to the project that holds your Playwright tests, with the SDK for your provider. OpenAI is the default.
Using another provider? Install its SDK instead: @anthropic-ai/sdk,
@google/genai, @typesafe-ai/sdk or ollama. If it's
missing, the first heal tells you the exact command to run.
Pick a provider
Healing stays off until SELF_HEAL=1, so your suite runs exactly as before until you
turn it on. healwright reads environment variables: set them in CI, or load a
.env file with import 'dotenv/config' at the top of
playwright.config.ts.
SELF_HEAL=1
AI_PROVIDER=openai
AI_API_KEY=sk-…
# AI_MODEL=… optional, overrides the default model
SELF_HEAL=1
AI_PROVIDER=anthropic
AI_API_KEY=sk-ant-…
# AI_MODEL=… optional, overrides the default model
SELF_HEAL=1
AI_PROVIDER=google
AI_API_KEY=…
# AI_MODEL=… optional, overrides the default model
SELF_HEAL=1
AI_PROVIDER=typesafe
TYPESAFE_API_KEY=…
# Jev picks one of the collected elements; healwright builds the locator.
SELF_HEAL=1
AI_PROVIDER=local
# No API key. Page content never leaves your machine.
# OLLAMA_HOST=http://127.0.0.1:11434 the default
# AI_MODEL=gemma3:4b the default; mistral also works well
Behind a gateway? AI_BASE_URL points any built-in provider at Azure,
OpenRouter, vLLM or LiteLLM. For a model healwright has no client for, pass your own with the
aiProvider option.
Add the fixture
Wrap the page fixture once. Every test that imports test from this
file gets page.heal; everything else on page works as usual.
import { test as base } from '@playwright/test';
import { createHealingFixture, HealPage } from 'healwright';
export const test = base.extend<{ page: HealPage }>(createHealingFixture());
export { expect } from '@playwright/test';
Write a test
Each page.heal action takes a selector and a description. The selector runs first;
the description is what the model looks for when the selector stops matching. Leave the
selector empty and the description is enough.
import { test, expect } from './fixtures';
test('customer can check out', async ({ page }) => {
await page.goto('https://shop.example/checkout');
// No selector: the description is enough
await page.heal.fill('', 'Email field', 'ada@example.com');
// The selector runs first; the description takes over if it breaks
await page.heal.click('#buy-now', 'Place order button');
await expect(page.getByText('Order confirmed')).toBeVisible();
});
Describe elements the way you'd point them out to a colleague: "Place order button", not "button". The model has only your words and the page to go on.
Run it
While your selectors match, nothing changes and tests run at full speed. When one breaks, the heal shows up in the output. The next run reads the fix from the cache and doesn't call the model.
$ SELF_HEAL=1 npx playwright test
✦ healwright
│
├─ ⚡︎ CLICK Place order button
│ ⬡ analyzing 14 elements... (filtered from 63)
│ ↳ received 412 chars
│
├─ ✓ Place order button
│ → getByRole("button", { name: "Place order" })
│ ↑ 1180 input · 96 output · 1276 total tokens
└──────────────────────────────
✓ checkout.spec.ts › customer can check out (2.8s)
Keep it honest
A heal that lands on the wrong element still passes. These three settings decide how far the model is trusted and keep every heal visible. You can try them on the home page.
minConfidence- Fail the action instead of guessing when the model's confidence is below this score (0 to 1).
mode: 'warn'- Report what would have healed and keep the original failure. Use it in CI to see how far your selectors have drifted.
getHealSummary()- Count every heal in the run, so global teardown can fail the build and list the selectors to fix.
export const test = base.extend<{ page: HealPage }>(
createHealingFixture({
minConfidence: 0.8,
mode: process.env.CI ? 'warn' : 'heal',
})
);
// playwright.config.ts: globalTeardown: './global-teardown.ts'
import { getHealSummary } from 'healwright';
export default function () {
const { healed, entries } = getHealSummary();
if (healed === 0) return;
console.error(`${healed} locator(s) needed healing. Update these selectors:`);
for (const e of entries.filter((e) => e.outcome === 'healed')) {
console.error(` ${e.action} "${e.contextName}"`);
}
process.exitCode = 1;
}
Running Playwright in UI or watch mode? Call resetHealSummary() from
globalSetup so each run starts at zero.
Cache and logs
healwright keeps three files in .self-heal/.
| File | What's in it | In git? |
|---|---|---|
healed_locators.json |
Every heal that worked, by action, page path and description. | Commit it to share heals with your team and CI. |
heal_events.jsonl |
One line per heal attempt, with the model's reason, confidence and token count. | Ignore |
heal_summary.jsonl |
The outcomes getHealSummary() reads at the end of a run. |
Ignore |
.self-heal/heal_events.jsonl
.self-heal/heal_summary.jsonl
.self-heal/*.lock
.self-heal/*.tmp
# Or keep heals local to each machine instead:
# .self-heal/
Reference
Methods on page.heal
Every action takes a selector or Locator (or '' for description only) and a
description.
| Method | What it does |
|---|---|
click(target, desc, { force? }) | Click. force: true clicks controls that only appear on hover. |
dblclick(target, desc) | Double-click. |
fill(target, desc, value) | Type into an input or textarea. |
selectOption(target, desc, value) | Choose an option in a dropdown. |
check(target, desc) | Check a checkbox or radio. |
uncheck(target, desc) | Uncheck a checkbox. |
hover(target, desc) | Hover over an element. |
focus(target, desc) | Focus an element. |
locator(selector, desc) | A chainable locator with the same actions: .locator('.save', 'Save button').click(). |
getLocator(selector, desc) | Returns a real Playwright Locator, for expect(), .nth(), .filter() and the rest. |
Fixture options
Pass these to createHealingFixture() or withHealing(page, options).
| Option | Default | What it does |
|---|---|---|
enabled | SELF_HEAL=1 | Turns healing on. |
mode | 'heal' | 'heal', 'warn' (report only) or 'off'. |
provider | 'openai' | openai, anthropic, google, typesafe or local. |
model | Provider default | Overrides the model. |
apiKey | AI_API_KEY | The provider's API key. |
baseURL | AI_BASE_URL | Sends requests to another endpoint, such as a gateway. |
aiProvider | None | Your own provider, used instead of the built-in clients. |
minConfidence | 0 | Rejects heals scored below this (0 to 1). 0 accepts any. |
quickTimeout | 1000 | Milliseconds your own selector gets before healing starts. |
timeout | 5000 | Milliseconds for the healed locator and the action. |
maxCandidates | 40 | How many ranked elements are sent to the model. |
maxAiTries | 4 | How many suggested strategies are checked before giving up. |
cacheFile | .self-heal/healed_locators.json | Where heals are cached. |
reportFile | .self-heal/heal_events.jsonl | Where heal attempts are logged. |
logLevel | 'info' | 'info', 'error' or 'silent'. |
Environment variables
| Variable | What it does |
|---|---|
SELF_HEAL | Set to 1 to turn healing on. |
AI_PROVIDER | openai/gpt, anthropic/claude, google/gemini, typesafe/jev, or local/ollama. |
AI_API_KEY | The provider's API key. Not needed for Ollama. |
TYPESAFE_API_KEY | TypeSafe's key, read before AI_API_KEY when the provider is TypeSafe. |
AI_MODEL | Overrides the provider's default model. |
AI_BASE_URL | Sends requests to another endpoint. |
OLLAMA_HOST | Your Ollama server. Defaults to http://127.0.0.1:11434. |
HEALWRIGHT_MODE | heal, warn or off. |
HEALWRIGHT_MIN_CONFIDENCE | Same as minConfidence. |
HEALWRIGHT_LOG | info, error or silent. |
HEALWRIGHT_SUMMARY_FILE | Moves the summary file from .self-heal/heal_summary.jsonl. |