This is part one of the project.
In this new era of AI, I confess that sometimes I don't feel like the owner of the projects I'm building. But I've decided to change this mindset and start using AI to help me with technical projects, and this is the first one. Since my role, in the last few months, is changing from QA to being responsible for planning and leaving the automation to AI, I asked Claude for ideas on how I could keep my technical knowledge sharp even when I don't code as much anymore. It helped me with a few project ideas, and here's where we are.
Table of Contents
What is the problem
The first project that AI suggested, and that I loved, was building a wrapper for Playwright tests. While working on projects that are in development and going through transformations, some E2E tests can fail due to locators that were updated or even changed. This project is one of the many ways to solve that problem.
The concept
Basically, the project is a wrapper: when the original selector fails, a HealingEngine kicks in and tries a sequence of alternative strategies—from the most reliable to the riskiest—before giving up:
SelfHealingPage
└── tries the original selector
└── fails? → HealingEngine
├── data-testid (fuzzy match)
├── aria-label
├── placeholder
├── alt text
├── role + name
├── visible text
└── relative position
└── found? → save to HealingLog + continue
└── not found? → fail with a detailed report
Each attempt—whether successful or not—is recorded in a HealingLog, which generates a report showing the recovery rate for the entire suite.
The order of the strategies is not arbitrary. It ranges from the most semantic and least volatile attribute (data-testid) to the most fragile (relative position on the screen):
| Priority | Strategy | Why |
|---|---|---|
| 1 |
data-testid (fuzzy) |
Semantic attribute, less volatile |
| 2 | aria-label |
Accessibility standard, stable |
| 3 | placeholder |
Stable in form fields |
| 4 | alt text |
Stable in images |
| 5 | role + name |
Semantic, behavior-based |
| 6 | Visible text | Works well with buttons and links |
| 7 | Relative position | Last resort, fragile |
About the Project
For now, this is what the architecture looks like:
self-healing-playwright/
├── src/
│ ├── core/
│ │ ├── SelfHealingPage.ts # main wrapper
│ │ ├── HealingEngine.ts # strategy orchestration
│ │ ├── HealingLog.ts # healing record & report
│ │ └── createSelfHealingPage.ts # integration factory
│ ├── strategies/
│ │ ├── DataTestIdStrategy.ts
│ │ ├── AriaLabelStrategy.ts
│ │ ├── PlaceHolderStrategy.ts
│ │ ├── AltTextStrategy.ts
│ │ ├── RoleStrategy.ts
│ │ ├── TextContentStrategy.ts
│ │ └── RelativePositionStrategy.ts
│ └── types/
│ └── index.ts
├── tests/
│ ├── example.spec.ts
│ └── healing.spec.ts # tests for the framework itself
├── reports/
│ └── healing-log.json # generated at runtime
├── playwright.config.ts
├── tsconfig.json
└── package.json
The SelfHealingPage class
This is the method that manages the healer's logic. It will try to fetch the locator used in the test:
const locator = this.page.locator(selector);
try {
await locator.waitFor({ timeout: 3000 });
return locator;
}
And if this locator doesn't exist, it will call the HealingEngine:
catch {
console.warn(`[SelfHealing] Selector failed: "${selector}". Trying alternative strategies...`);
const context: SelectorContext = {
original: selector,
...hint,
};
const healed = await this.engine.heal(this.page, context);
The HealingEngine class
This class is responsible for fetching each strategy, checking its priority, and trying to fix the tests. First, it sorts them by priority:
constructor(strategies: SelectorStrategy[]) {
// sort by priority — most reliable strategies first
this.strategies = [...strategies].sort((a, b) => a.priority - b.priority);
}
After that, when SelfHealingPage calls the engine, it starts healing the test using different types of locators:
get strategiesUsed(): string[] {
return this.strategies.map(s => s.name);
}
async heal(page: Page, context: SelectorContext): Promise<{
locator: Locator;
result: HealingResult;
} | null> {
for (const strategy of this.strategies) {
try {
const locator = await strategy.locate(page, context);
if (locator && (await locator.count()) === 1) {
return {
locator,
result: {
healed: true,
strategyUsed: strategy.name,
newSelector: context.original,
originalSelector: context.original,
timestamp: new Date().toISOString(),
},
};
}
} catch {
continue;
}
}
return null;
}
Why do we check the count? Sometimes, when locating an element, more than one match is available. If that happens, the engine tries a different approach.
The HealingLog class
This class helps identify when a heal happened. It creates a JSON file inside the reports folder, following this model:
{
"totalAttempts": 1,
"healed": 1,
"failed": 0,
"healingRate": "100.0%",
"entries": [
{
"healed": true,
"strategyUsed": "aria-label",
"newSelector": "[data-test=\"wrong-remove-btn\"]",
"originalSelector": "[data-test=\"wrong-remove-btn\"]",
"timestamp": "2026-07-23T20:32:30.359Z"
}
]
}
How to use it in tests
To make sure the healing works as expected, I asked Claude to fetch an existing project I had worked on and update it to use the healing wrapper.
In this context, the test tries to access the Saucedemo platform. I forced it to fail to find the login button, but gave it a hint to search for the locator using the label:
test.describe('Login — self-healing', () => {
test('heals a broken login button selector and still logs in', async ({ page }) => {
const shPage = createSelfHealingPage(page, log);
await page.goto(BASE_URL);
await shPage.fill('[data-test="username"]', VALID_USER);
await shPage.fill('[data-test="password"]', VALID_PASSWORD);
// this id does not exist — the engine has to recover using the "Login" hint
await shPage.click('[data-test="wrong-login-btn"]', { labelHint: 'Login' });
await expect(page.locator('[data-test="title"]')).toHaveText('Products');
const entries = log.getEntries();
expect(
entries.some(e => e.healed && e.originalSelector === '[data-test="wrong-login-btn"]')
).toBe(true);
});
});
And when running it, here are the results:
What I learned and what's next for this project
Now that the first part of the project is complete (healing when a selector breaks), I want to keep improving it.
Before going into the possible improvements, I want to share how I felt while "building" it. I confess it was a bit frustrating not to be the one writing the code, but at the same time, it gave me the space to think about how I can improve this first version and how I can expand it.
The first thing I want to improve is how the logs are generated. Right now, each run generates new reports. In a CI/CD pipeline, where tests can run daily or on every merge, it would be better to persist these logs and reuse the healed locators between runs.
On that same topic, another opportunity for improvement is how the logs are saved. Storing them in a database might be the best solution—that way, I could get metrics on whether a locator needs to be healed frequently and discuss with the team how to fix it, or even how we could implement data-testid across our UI.
There's also room to improve the tests themselves. One idea is to use fixtures to provide the SelfHealingPage automatically, so tests don't need to instantiate the wrapper manually every time—making the setup cleaner and easier to reuse across the suite.
Another idea is to capture a DOM snapshot whenever a healing happens. This would let me build a dataset over time, which I could eventually use to feed an AI model—training it to suggest or even predict the right locator before the test fails, instead of only reacting after the fact.
Finally, I'm thinking about the possibilities for distribution. Turning this into an installable package (an npm package, for example) would make it easier to plug into other projects and teams, rather than keeping it as a standalone repository.
This is part one of the project, and there's more to come; you can see the full solution here. Stay tuned for the next updates as I keep building on this!

Top comments (0)