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 Test That Cannot Name What It Expects
An LLM step is not a pure function, so no gate in this routine can assert what the post says. Every check I run is a property the output must obey, never a value it must equal.
AI & AutomationThe 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.
Quantitative TradingA Retry Is Not a Trading Decision
A rejected order is a decision. Retrying it without preserving the reason can turn a risk control into a duplicate trade.
Agentic AIThe Log Is Part of the Agent's Interface
An agent that can act but cannot leave a useful decision record is not autonomous. It is an opaque process with write access.
Latest Blog Posts
A Retry Is Not a Trading Decision
A rejected order is a decision. Retrying it without preserving the reason can turn a risk control into a duplicate trade.
The Log Is Part of the Agent's Interface
An agent that can act but cannot leave a useful decision record is not autonomous. It is an opaque process with write access.
The Market Is Closed Is Not a Trading Rule
A backtest can know the exchange hours and still schedule a trade into a holiday, an early close, or a stale session. Calendar state is market data.
Related Tools & Demos
The QA Field Manual to Language Models
A free 24-chapter book. Start at “what is AI, really?” and finish with a small language model you built yourself — one that reads a failing Playwright test and proposes a fix you can run. Read it in your browser, or download the PDF or the Mac app.
View Source Code →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 →
Stay in the Loop
Get weekly insights on AI-driven QA, engineering leadership, and automation strategies.
No spam, ever. Unsubscribe anytime.