Engineering Leadership14 min read

Self-Healing Locators: Playwright + MCP + Ollama

S

Suneet Malhotra

Dec 30, 2025

1 views
Self-Healing Locators: Playwright + MCP + Ollama - Engineering Leadership blog post
🔧Playwright🔧MCP🦙Ollama📘TypeScript🔧gpt-oss:20b

Stop Fixing Broken Locators. Meet the Self-Healing QA Agent

Traditional Playwright tests break when UI changes. You update selectors manually, debug failures, and repeat the cycle. But what if your tests could heal themselves automatically?

This is the story of building a self-healing QA agent that uses Model Context Protocol (MCP), Playwright, and local Ollama LLMs to eliminate manual locator maintenance forever.

💡

💡 Key Insight: Instead of hardcoding selectors that break when HTML changes, the agent uses natural language descriptions to find elements dynamically. When a locator fails, it automatically retries with alternative strategies.

TL;DR: Key Takeaways

  • Zero Manual Locator Fixes: Tests adapt automatically to UI changes using semantic element discovery
  • Local LLM Integration: Uses Ollama with gpt-oss:20b for private, cost-effective AI reasoning
  • Self-Healing Mechanism: Automatic retry with alternative strategies when locators fail
  • Technology Stack: Model Context Protocol (MCP) + Playwright + Ollama for resilient automation
  • Maintenance Reduction: Eliminates 2-3 hours/week spent fixing broken selectors

Why Is the Maintenance Tax Killing Test Automation?

Traditional Playwright Approach:

// ❌ Brittle - breaks when HTML changes
const toolsLink = page.locator('nav a[href="/tools"]');
await toolsLink.click();

What happens when the UI changes?

  • Selector breaks → Test fails → Manual debugging → Update selector → Repeat
  • No intelligence, no adaptation, just hardcoded paths
  • 50% of test failures are due to broken selectors
  • 2-3 hours/week spent fixing locators

How Does AI-Powered Self-Healing Work?

AI Agent Approach:

// ✅ Resilient - understands page semantics
const locator = await findElementWithAI(
  client, 
  'Tools link in the navigation menu'
);
await selfHealingTask(client, () => clickElement(client, locator), 'Click Tools link');

What happens when the UI changes?

  • Selector breaks → AI analyzes new page structure → Generates new locator → Retries automatically → Success!
  • 0% failures from broken selectors
  • 0 hours/week fixing locators
  • Tests adapt automatically to UI changes

Interactive Terminal Demo

Here's the actual execution log from the self-healing agent in action:

🚀 Starting Playwright Agent Demo
============================================================
AI-Powered QA Automation Agent
============================================================
✅ Connected to Playwright MCP server

🔧 Setting up test page...
   Setup response: 
Running 1 test using 1 worker
✅ Seed environment established
   Base URL: https://suneetmalhotra.com
   Page Title: Suneet Malhotra | AI-Driven Quality Engineering Leader

🔍 [Attempt 1] Finding element: "Tools link in the navigation menu"
   🤖 LLM Analysis: navigation link labeled AI Tools...
   ✅ Generated locator: getByRole('link', { name: 'AI Tools' })

🔧 [Self-Healing] Attempt 1/3: Click Tools link
   🖱️  Clicking element with locator...
   Response: ### Ran Playwright code
   ```js
   await page.getByRole('link', { name: 'AI Tools' }).click();

Page state

✅ Task succeeded on attempt 1

📸 Getting page snapshot after navigation...

✅ Verifying page title contains: "Tools" 🤖 LLM Verification: YES Result: ✅ PASS

============================================================ ✅ Demo completed successfully!


**Key Observations:**
- ✅ **MCP Connection**: Seamless integration with Playwright MCP server
- ✅ **AI Element Discovery**: LLM analyzed page and found "AI Tools" link semantically
- ✅ **Self-Healing**: Automatic retry mechanism (would retry up to 3 times if needed)
- ✅ **Verification**: LLM verified page title contains "Tools"

### Page Snapshot Output

The MCP server captures the full page structure in YAML format:

```yaml
- <changed> link "AI Tools" [active] [ref=e13] [cursor=pointer]:
  - /url: /tools
  - text: AI Tools
- <changed> main [ref=e17]:
  - generic [ref=e174]:
    - generic [ref=e175]:
      - heading "AI Tools" [level=1] [ref=e176]
      - paragraph [ref=e177]: Personal research and development projects...
    - generic [ref=e178]:
      - generic [ref=e179]:
        - heading "AI Test Case Generator" [level=2] [ref=e189]
        - paragraph [ref=e190]: "Personal technical demonstration..."

This snapshot provides the LLM with complete context about the page structure, enabling intelligent element discovery.

The Core Logic: Three Pillars

The self-healing agent is built on three interconnected technologies:

🤖 Ollama: The Reasoning Engine

Role: Local LLM providing intelligent decision-making

  • Model: gpt-oss:20b (20 billion parameters)
  • Function: Analyzes page snapshots and generates semantic element descriptions
  • Why Local?: Zero API costs, complete privacy, full control
  • Reasoning: Uses "high" reasoning mode for robust tool calling
// Ollama analyzes page structure
const prompt = `Find: "Tools link in navigation"
Page Snapshot: ${snapshot}`;
const llmResponse = await callOllama(prompt);
// Returns: "navigation link labeled AI Tools"

🔗 MCP: The Bridge

Role: Model Context Protocol connects browser state to LLM

  • Function: Feeds browser's accessibility tree to the LLM
  • Tools: browser_snapshot, browser_generate_locator, browser_click
  • Benefit: Real-time page state understanding, no guessing
// MCP provides live browser context
const snapshot = await client.callTool({
  name: 'browser_snapshot',
  arguments: {}
});
// Returns: Full DOM + accessibility tree

🎭 Playwright: The Execution Engine

Role: Browser automation framework

  • Function: Executes actions based on AI-generated locators
  • Integration: Works seamlessly with MCP tools
  • Output: getByRole('link', { name: 'AI Tools' })
// Playwright executes AI-generated locator
await page.getByRole('link', { name: 'AI Tools' }).click();

Self-Healing Breakdown: Hardcoded vs. Semantic

Side-by-Side Comparison

Traditional PlaywrightAI-Powered Agent (Self-Healing)
Breaks when an ID or Class changesUses browser_snapshot to understand intent
Requires manual script updatesAutomatically heals via browser_generate_locator
Rigid, script-based flowDynamic, agentic exploration

Code Comparison

Traditional Approach:

// ❌ Hardcoded selector - breaks when HTML changes
test('navigate to tools', async ({ page }) => {
  await page.goto('https://suneetmalhotra.com');
  const toolsLink = page.locator('nav a[href="/tools"]');
  await toolsLink.click(); // Fails if HTML structure changes
  await expect(page).toHaveTitle(/Tools/);
});

Self-Healing Agent Approach:

// ✅ Semantic description - adapts to changes
test('navigate to tools', async ({ client }) => {
  // AI finds element semantically
  const locator = await findElementWithAI(
    client,
    'Tools link in the navigation menu'
  );
  
  // Self-healing: automatically retries if fails
  await selfHealingTask(
    client,
    async () => {
      await clickElement(client, locator);
      await verifyPageTitle(client, 'Tools');
    },
    'Navigate to Tools page',
    3 // Max retries
  );
});

What Happens When UI Changes?

Scenario: Developer changes navigation structure

Traditional Test:

  1. ❌ Test fails: nav a[href="/tools"] no longer exists
  2. 🔍 Manual debugging: Find new selector
  3. ✏️ Update test: Change to nav a[data-testid="tools"]
  4. 🔄 Re-run test
  5. Time spent: 15-30 minutes per failure

Self-Healing Agent:

  1. ⚠️ Initial attempt fails
  2. 📸 Gets fresh page snapshot
  3. 🤖 LLM re-analyzes: "Find Tools link in new navigation structure"
  4. 🎯 Generates new locator: getByRole('link', { name: 'AI Tools' })
  5. ✅ Retries automatically - Success!
  6. Time spent: 0 minutes (automatic)

Self-Healing in Action: Video Demo

<div class="aspect-video w-full rounded-xl overflow-hidden bg-zinc-900 mb-8 border border-zinc-800"> <div class="w-full h-full flex items-center justify-center bg-gradient-to-br from-zinc-900 to-zinc-800"> <div class="text-center p-8"> <svg class="w-16 h-16 mx-auto mb-4 text-cyan-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> </svg> <p class="text-gray-400 text-lg font-medium mb-2">Self-Healing in Action</p> <p class="text-gray-500 text-sm">Video demonstration coming soon</p> </div> </div> </div>

Related Deep Dive: Watch How to Heal Failing Playwright Tests Automatically for a comprehensive walkthrough of the self-healing mechanism.

Download & Run the Demo

Get the self-healing agent running in minutes:

# 1. Clone the repository
git clone https://github.com/SuneetMalhotra/ai-driven-quality-engineering-leader.git
cd ai-driven-quality-engineering-leader/qa-automation

# 2. Install dependencies
npm install

# 3. Start Ollama (in separate terminal)
ollama serve
ollama pull gpt-oss:20b

# 4. Run the demo
npm run demo:agent

Expected Output:

🚀 Starting Playwright Agent Demo
✅ Connected to Playwright MCP server
🔧 Setting up test page...
🔍 Finding element: "Tools link in navigation menu"
🤖 LLM Analysis: navigation link labeled AI Tools
✅ Generated locator: getByRole('link', { name: 'AI Tools' })
🔧 [Self-Healing] Attempt 1/3: Click Tools link
✅ Task succeeded on attempt 1
✅ Verifying page title contains: "Tools"
✅ Demo completed successfully!

The Setup: Three Simple Steps

Step 1: Start Ollama

In a separate terminal (keep it open):

ollama run gpt-oss:20b

This starts the Ollama server and loads the gpt-oss:20b model. The model will remain in memory, ready to handle agent requests.

Why gpt-oss:20b?

  • 20 billion parameters: Powerful enough for complex test generation
  • Open source: Full transparency and control
  • Good reasoning: Handles multi-step agent workflows well

Step 2: Initialize Agents

cd qa-automation
npx playwright init-agents --loop=vscode

This command:

  • Sets up Playwright Test Agents configuration
  • Configures MCP (Model Context Protocol) server
  • Integrates with VS Code for seamless workflow
  • Creates necessary agent definition files

Step 3: Use in Cursor Composer

Now you're ready to use the agents! Open Cursor's Composer and use these prompts:

Planner Prompt

Use the MCP browser tool to explore https://suneetmalhotra.com. 
Generate a Markdown test plan in specs/portfolio.md that covers:
- Navigation to 'Projects'
- Navigation to 'About'  
- Validation of the contact form

The Planner agent will:

  1. Use MCP browser tools to explore your site
  2. Analyze the structure and user flows
  3. Generate a comprehensive test plan in specs/portfolio.md

Generator Prompt

Transform specs/portfolio.md into a Playwright test file: tests/portfolio.spec.ts.
Use the browser MCP to verify all selectors live.
Use 'User-first' locators like getByRole or getByLabel.

The Generator agent will:

  1. Read the test plan from specs/portfolio.md
  2. Use MCP browser to verify selectors are live
  3. Generate executable TypeScript tests
  4. Use accessibility-first locators (getByRole, getByLabel, getByTestId)

Step 4: Run Tests

npm run test:ai

This command uses --ui --workers=1 which is essential for local models:

  • --ui: Opens interactive dashboard with traces and screenshots
  • --workers=1: Prevents system freezing with local LLM inference

The Healer agent will automatically fix any failures during the run.

What You Get

RoleArtifactPurpose
Plannerspecs/portfolio.mdHuman-readable test blueprint covering navigation, forms, and user flows
Generatortests/portfolio.spec.tsExecutable TypeScript tests with verified selectors
HealerAutomatic FixesPatches broken selectors via live DOM analysis, handles animations and lazy loading

Planner Output

The Planner creates strategic test plans in Markdown:

# Portfolio Test Plan

## Navigation Tests
- Navigate to Projects page
- Navigate to About page
- Verify page loads correctly

## Form Tests
- Fill contact form
- Validate required fields
- Submit form

Generator Output

The Generator creates production-ready TypeScript:

import { test, expect } from '@playwright/test';

test('navigate to Projects', async ({ page }) => {
  await page.goto('https://suneetmalhotra.com');
  await page.getByRole('link', { name: 'Projects' }).click();
  await expect(page).toHaveURL(/.*projects/);
});

Healer Output

The Healer automatically fixes issues:

  • Selector failures: Updates broken CSS selectors
  • Timing issues: Adds proper wait conditions
  • Animation handling: Waits for transitions to complete
  • Lazy loading: Ensures content is loaded before interaction

Key Features

🔗 MCP Browser Integration

The Playwright MCP server provides live browser interaction:

  • Real-time exploration: Agents can browse your site as a user would
  • Selector verification: Confirm selectors work before generating tests
  • DOM analysis: Understand page structure and accessibility tree
  • No guessing: Every selector is verified live

🎯 User-First Locators

Agents prioritize accessibility-first locators:

  1. getByRole - Best for accessibility and semantic meaning
  2. getByLabel - Perfect for form inputs
  3. getByTestId - For stable test identifiers
  4. getByText - For visible text content
  5. CSS selectors - Last resort only

This approach ensures:

  • Accessibility compliance: Tests verify accessible interactions
  • Maintainability: Less brittle than CSS selectors
  • User-centric: Tests reflect how real users interact

💰 Cost Savings

Running locally means:

  • $0 API costs: No per-request charges
  • Unlimited usage: Test as much as you need
  • No rate limits: No throttling or quotas
  • Predictable costs: One-time infrastructure setup

Compare to cloud services:

  • OpenAI GPT-4: ~$0.03-0.06 per 1K tokens
  • At scale: Hundreds of dollars per month
  • With Ollama: $0/month

🔒 Privacy & Security

Local execution provides:

  • Data privacy: Test data never leaves your machine
  • No external APIs: No risk of data breaches
  • Compliance: Perfect for HIPAA, GDPR, SOC2 requirements
  • IP protection: Your application structure stays private

🛠️ Self-Healing Capabilities

The Healer agent automatically handles:

  • Portfolio animations: Waits for transitions to complete
  • Lazy loading: Ensures images and content load before interaction
  • Dynamic content: Handles JavaScript-rendered elements
  • Selector updates: Adapts when UI changes

Example Healer Fix:

// Before (fails due to animation)
await page.click('#submit-button');

// After (Healer adds wait)
await page.waitForLoadState('networkidle');
await page.getByRole('button', { name: 'Submit' }).click();

Configuration

Model Settings

Configure in .cursorrules or environment:

Model: gpt-oss:20b
Endpoint: http://localhost:11434/v1
Reasoning: high (for robust tool calling)
Temperature: 0.2
Top P: 0.9

Test Execution

Always use the test:ai script:

{
  "scripts": {
    "test:ai": "npx playwright test --ui --workers=1"
  }
}

Why --workers=1?

  • Local LLM inference is CPU/GPU intensive
  • Multiple parallel workers can freeze the system
  • Single worker ensures stable execution

Real-World Example

Let's walk through a complete workflow:

1. Planner Creates Plan

Input: "Test the portfolio navigation"

Output: specs/portfolio.md

# Portfolio Navigation Test Plan

## Test Scenarios
1. Navigate to Projects page
2. Navigate to About page
3. Verify contact form validation

2. Generator Creates Tests

Input: specs/portfolio.md

Output: tests/portfolio.spec.ts

test('portfolio navigation', async ({ page }) => {
  await page.goto('https://suneetmalhotra.com');
  
  // Navigate to Projects
  await page.getByRole('link', { name: 'Projects' }).click();
  await expect(page).toHaveURL(/.*projects/);
  
  // Navigate to About
  await page.getByRole('link', { name: 'About' }).click();
  await expect(page).toHaveURL(/.*about/);
});

3. Healer Fixes Issues

If test fails (e.g., animation timing):

Healer Analysis:

  • Detects failure in trace
  • Inspects DOM via MCP
  • Identifies animation delay
  • Applies fix

Fixed Code:

await page.goto('https://suneetmalhotra.com');
await page.waitForLoadState('networkidle'); // Added by Healer
await page.getByRole('link', { name: 'Projects' }).click();

Best Practices

  1. Always use npm run test:ai for local model execution
  2. Describe reasoning before each browser action
  3. Check accessibility tree state before interactions
  4. Wait for animations to finish
  5. Use User-first locators (getByRole, getByLabel)
  6. Verify selectors live via MCP before generating
  7. Let Healer auto-fix failures automatically

Troubleshooting

Model Not Responding

# Check Ollama is running
ollama list

# Verify model loaded
ollama show gpt-oss:20b

# Restart if needed
ollama run gpt-oss:20b

Tests Freezing

  • Always use --workers=1 (included in npm run test:ai)
  • Close other applications to free resources
  • Consider smaller model if system struggles

MCP Browser Not Working

  • Verify Playwright MCP server configured in Cursor
  • Check MCP server is enabled
  • Restart Cursor if needed

Conclusion

Running Playwright Test Agents with local Ollama provides:

  • Zero cost test automation
  • Complete privacy for sensitive applications
  • Full control over model behavior
  • Self-healing capabilities for robust tests
  • User-first locators for accessibility

The combination of local AI and Playwright Test Agents represents the future of test automation: powerful, private, and cost-effective.

Get Started Today:

# 1. Start Ollama
ollama run gpt-oss:20b

# 2. Initialize agents
npx playwright init-agents --loop=vscode

# 3. Use in Cursor Composer
# (Use Planner and Generator prompts)

# 4. Run tests
npm run test:ai

Conclusion: The Future of Test Automation

The self-healing QA agent demonstrates a fundamental shift in how we approach test automation:

  • Zero manual locator fixes: Tests adapt automatically to UI changes
  • Intelligent element discovery: Semantic understanding replaces brittle selectors
  • Automatic recovery: Self-healing mechanism handles failures gracefully
  • Cost-effective: Local LLMs eliminate API costs
  • Privacy-first: Test data never leaves your machine

The combination of MCP, Playwright, and Ollama represents the future of resilient test automation: powerful, private, and self-healing.

This setup is currently powering the test automation for https://suneetmalhotra.com, demonstrating production-ready local AI for QA.

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.