AI & Automation6 min read

Self-Healing QA Framework: Playwright + OpenAI

S

Suneet Malhotra

Jan 24, 2025

1 views
Self-Healing QA Framework: Playwright + OpenAI - AI & Automation blog post

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:

  1. Failure Detection: A smartClick() or smartFill() action fails to find a selector.
  2. Visual Capture: The framework automatically takes a screenshot of the current page state.
  3. AI Analysis: The screenshot and the "broken" selector are sent to OpenAI (GPT-4o).
  4. 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.
  5. 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.com without needing a local server (via playwright.config.ts).
  • Environment Aware: Uses .env for 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

Stay in the Loop

Get weekly insights on AI-driven QA, engineering leadership, and automation strategies.

No spam, ever. Unsubscribe anytime.