Self-Healing QA Framework: Playwright + OpenAI
Suneet Malhotra
Jan 24, 2025
Introduction: The Death of Flaky Tests
Automation engineers know the pain: a developer changes a CSS class from .btn-submit to .btn-primary, and suddenly, half your test suite is red. This "locator brittle-ness" accounts for a massive percentage of maintenance time.
To solve this, I've integrated OpenAI's Vision API into my Playwright framework. Now, when a test fails to find an element, it doesn't just crash—it heals itself.
TL;DR: Key Takeaways
- Self-Healing Framework: OpenAI Vision API + Playwright for automatic locator recovery
- 50% Maintenance Reduction: Eliminates manual selector fixes and debugging time
- Screenshot Analysis: AI analyzes page screenshots to suggest new selectors
- Automatic Recovery: Tests automatically retry with AI-generated selectors
- Open Source: Framework available on GitHub for production use
The Architecture: How it Works
I built this framework using a Page Object Model (POM) pattern, but with an intelligent BasePage layer.
The Workflow:
- Failure Detection: A
smartClick()orsmartFill()action fails to find a selector. - Visual Capture: The framework automatically takes a screenshot of the current page state.
- AI Analysis: The screenshot and the "broken" selector are sent to OpenAI (GPT-4o).
- Selector Recovery: The AI "looks" at the page, finds the element that most closely matches the intended goal, and returns a new valid CSS selector.
- Auto-Retry: Playwright retries the action with the new selector, and the test continues seamlessly.
Deep Dive: The "Healer" Utility
In my repository, the magic happens in utils/healer.ts. Here is a snippet of how I bridge Playwright with OpenAI:
// utils/healer.ts
import { Page } from '@playwright/test';
import OpenAI from 'openai';
/**
* Self-healing selector utility using OpenAI Vision API
* Analyzes screenshots to suggest new CSS selectors when tests fail
*/
export async function healSelector(
page: Page,
brokenSelector: string,
action: 'click' | 'fill' | 'select' | 'check' = 'click'
): Promise<string> {
console.log(`🔧 Attempting to heal selector: ${brokenSelector}`);
try {
// Step 1: Capture screenshot for AI analysis
const screenshot = await page.screenshot({ fullPage: false });
const screenshotBase64 = screenshot.toString('base64');
// Step 2: Get page HTML context (truncated for token efficiency)
const pageContent = await page.content();
const bodyContent = pageContent.match(/<body[^>]*>([\s\S]*)<\/body>/i)?.[1] || '';
const truncatedContent = bodyContent.substring(0, 5000);
// Step 3: Initialize OpenAI client
const apiKey = process.env.OPENAI_API_KEY;
if (!apiKey) {
console.warn('⚠️ OPENAI_API_KEY not set. Using fallback strategies.');
return await tryFallbackSelectors(page, brokenSelector, action);
}
const openai = new OpenAI({ apiKey });
// Step 4: Send screenshot and context to OpenAI Vision API
const response = await openai.chat.completions.create({
model: 'gpt-4o',
messages: [
{
role: 'system',
content: `You are a CSS selector expert. Analyze the screenshot and HTML content
to suggest a better CSS selector when the original fails. Return ONLY a valid CSS
selector, nothing else. The selector should be specific and reliable.`,
},
{
role: 'user',
content: [
{
type: 'text',
text: `The original selector "${brokenSelector}" failed for a ${action} action.
Analyze the screenshot and suggest a new CSS selector that targets the same element.
HTML context (truncated): ${truncatedContent.substring(0, 2000)}`,
},
{
type: 'image_url',
image_url: {
url: `data:image/png;base64,${screenshotBase64}`,
},
},
],
},
],
max_tokens: 100,
});
// Step 5: Extract and validate the suggested selector
const suggestedSelector = response.choices[0]?.message?.content?.trim();
if (suggestedSelector && suggestedSelector !== brokenSelector) {
// Verify the new selector exists and is visible
const isValid = await verifySelector(page, suggestedSelector);
if (isValid) {
console.log(`✅ Healed selector: ${brokenSelector} → ${suggestedSelector}`);
return suggestedSelector;
}
}
// Step 6: Fallback to alternative strategies if AI fails
return await tryFallbackSelectors(page, brokenSelector, action);
} catch (error) {
console.error(`❌ Error during healing: ${error}`);
return brokenSelector;
}
}
/**
* Fallback strategies when AI is unavailable
*/
async function tryFallbackSelectors(
page: Page,
originalSelector: string,
action: string
): Promise<string> {
// Try text-based selectors, role-based selectors, etc.
// Implementation details...
return originalSelector;
}
/**
* Verify that a selector exists and is visible
*/
async function verifySelector(page: Page, selector: string): Promise<boolean> {
try {
const count = await page.locator(selector).count();
if (count === 0) return false;
const isVisible = await page.locator(selector).first().isVisible();
return isVisible;
} catch {
return false;
}
}
Reporting with Allure: Visibility into Healing
It's not enough for a test to fix itself; we need to know when and why it happened. I integrated Allure Reporting to capture these AI-events.
- Before Healing: Allure logs the standard "Element not found" error.
- During Healing: I add an Allure Attachment showing the screenshot the AI analyzed.
- After Healing: The report shows the "Healed Selector" used to pass the test.
This gives the team a clear audit trail: "Test #4 passed, but note that the 'Contact' button selector has changed and needs a permanent update in the codebase."
Key Features of the Framework
- TypeScript & POM: Type-safe and maintainable.
- Live Site Testing: Configured to run directly against
suneetmalhotra.comwithout needing a local server (viaplaywright.config.ts). - Environment Aware: Uses
.envfor secure OpenAI API key management. - Multi-Browser: Full support for Chromium, Firefox, and WebKit.
- Automatic Allure Reports: Reports are generated and opened automatically after each test run.
The Base Page Class
All page objects extend a BasePage class that provides self-healing methods:
// pages/base.page.ts
import { Page } from '@playwright/test';
import { healSelector } from '../utils/healer';
/**
* Base Page class with self-healing capabilities
* All page objects extend this class to inherit self-healing behavior
*/
export class BasePage {
constructor(protected page: Page) {}
/**
* Smart click with self-healing fallback
* If the initial selector fails, attempts to heal and retry
*/
async smartClick(selector: string, options?: { timeout?: number }) {
const timeout = options?.timeout || 3000;
try {
// Attempt normal click first
await this.page.click(selector, { timeout });
} catch (error) {
// Selector failed - trigger healing process
console.log(`⚠️ Selector ${selector} failed. Healing...`);
const healedSelector = await healSelector(this.page, selector, 'click');
if (healedSelector !== selector) {
// Retry with healed selector
await this.page.click(healedSelector, { timeout: 5000 });
} else {
// If healing didn't find a new selector, try with force option
await this.page.click(selector, { timeout: 5000, force: true });
}
}
}
/**
* Smart fill with self-healing fallback
*/
async smartFill(selector: string, value: string) {
try {
await this.page.fill(selector, value, { timeout: 3000 });
} catch (error) {
console.log(`⚠️ Selector ${selector} failed for fill. Healing...`);
const healedSelector = await healSelector(this.page, selector, 'fill');
if (healedSelector !== selector) {
await this.page.fill(healedSelector, value, { timeout: 5000 });
} else {
await this.page.fill(selector, value, { timeout: 5000, force: true });
}
}
}
}
Future Roadmap
- Self-Repairing Code: Automatically opening a Pull Request (PR) to update the Page Object files with the AI-suggested selector.
- Local LLM Support: Using Ollama (Llama 3) to handle healing locally for increased privacy and lower costs.
- Enhanced Reporting: Integration with CI/CD pipelines to track healing patterns over time.
Conclusion
Self-healing is the future of test stability. By combining the speed of Playwright with the "vision" of GenAI, we can build suites that are truly resilient to UI changes.
Check out the full implementation on my GitHub: View the Repository
The framework is open-source and ready to use. Simply clone the repository, set your OpenAI API key, and start running self-healing tests.
Share this post
You Might Also Like
The One Step I Never Hand to a Subagent
My content routine dispatches a fleet of subagents to gather, then hands none of them the draft. A fleet parallelizes retrieval. It cannot parallelize a voice.
AI & AutomationThe Number My Model Is Not Allowed to Know
There is a rule I enforce across every agent I run, and it has nothing to do with how good the model is. The model writes the words. It never computes the numbers.
Agentic AIEverything in My Context Window Is an Instruction
This routine reads the open web, then commits to a live site with no human in the loop. Those two facts sit in the same context window, and the model has no way to tell them apart.
Quantitative TradingThe Edge I Assume Is Already Decaying
Wall Street spent this week arguing that AI is cutting the useful life of a trading edge from seven years to eighteen months. If that is even half right, it changes what a signal is worth.
Latest Blog Posts
Everything in My Context Window Is an Instruction
This routine reads the open web, then commits to a live site with no human in the loop. Those two facts sit in the same context window, and the model has no way to tell them apart.
The Edge I Assume Is Already Decaying
Wall Street spent this week arguing that AI is cutting the useful life of a trading edge from seven years to eighteen months. If that is even half right, it changes what a signal is worth.
The If Statement My Audit Never Read
On May 20 I published a rule for which steps of a routine are safe to run twice, and put my repo pull in the safest bucket. On July 9 that step failed. It never ran.
Related Tools & Demos
Multi-Model LLM Harness
One interface to call any AI model — capability routing, fallback chains, budgets, circuit breakers, and a quality feedback loop. A practical architecture pattern write-up.
Automated Trading System
Multi-engine trading platform with real-time risk management, regime-based strategy selection, and automated order execution.
View Source Code →Personal Health Analytics
Multi-modal health data platform integrating wearables, lab results, and lifestyle tracking with predictive habit modeling.
View Source Code →
Stay in the Loop
Get weekly insights on AI-driven QA, engineering leadership, and automation strategies.
No spam, ever. Unsubscribe anytime.