{"slug": "the-playwright-playbook-part-8-playwright-meets-ai-agents-mcp-self-healing-tests", "title": "The Playwright Playbook — Part 8: Playwright Meets AI — Agents, MCP & Self-Healing Tests", "summary": "A developer built a production-ready Playwright testing framework across seven parts and added AI capabilities in Part 8, including Playwright MCP, AI test agents, and self-healing selectors. The framework now includes AI-generated tests, a test planner, and a test healer, all designed to amplify QA engineers rather than replace them.", "body_md": "\"AI doesn't replace QA engineers. It gives them superpowers.\"\n\nSeven parts. One framework. Built from scratch.\n\nPOM-based UI tests. Network interception. Multi-user contexts. A full API testing layer with typed clients. Visual regression across four viewports. A complete debugging toolkit. A production CI/CD pipeline with sharding, Docker, and Slack notifications.\n\nThis is what we built.\n\nNow we add AI on top of it.\n\nNot as a replacement — as an amplifier.\n\nThe engineers who will define the next decade of QA are the ones who understand how to use AI tools deliberately — knowing exactly what they're good for, exactly where they fall short, and how to combine them with the solid engineering we've spent seven parts building.\n\nThat's what Part 8 is about.\n\n**Playwright MCP. AI test agents. Self-healing selectors.**\n\nLet's finish this. 🎯\n\nAfter Part 7, our framework is complete and production-ready:\n\n```\nplaywright-playbook/\n├── .github/workflows/\n│   ├── playwright.yml                           ✅ Part 7\n│   └── playwright-visual.yml                   ✅ Part 7\n├── tests/\n│   ├── auth/login.spec.ts                       ✅ Part 1\n│   ├── tasks/task-management.spec.ts            ✅ Part 1\n│   ├── network/                                 ✅ Part 2\n│   ├── multi-user/                              ✅ Part 3\n│   ├── multi-tab/                               ✅ Part 3\n│   ├── api/                                     ✅ Part 4\n│   ├── visual/                                  ✅ Part 5\n│   └── debug/                                   ✅ Part 6\n├── pages/\n│   ├── LoginPage.ts                             ✅ Part 1\n│   ├── TaskPage.ts                              ✅ Part 1\n│   └── DashboardPage.ts                         ✅ Part 3\n├── api/\n│   ├── TaskApiClient.ts                         ✅ Part 4\n│   └── AuthApiClient.ts                         ✅ Part 4\n├── fixtures/\n│   ├── auth.fixture.ts                          ✅ Part 1\n│   ├── tasks.json                               ✅ Part 2\n│   ├── empty-tasks.json                         ✅ Part 2\n│   ├── tasks-har.har                            ✅ Part 2\n│   ├── multi-user.fixture.ts                    ✅ Part 3\n│   └── api.fixture.ts                           ✅ Part 4\n├── scripts/\n│   ├── record-har.ts                            ✅ Part 2\n│   └── notify-slack.ts                          ✅ Part 7\n├── utils/\n│   ├── schema-validator.ts                      ✅ Part 4\n│   ├── visual-helpers.ts                        ✅ Part 5\n│   └── debug-helpers.ts                         ✅ Part 6\n├── docker/                                      ✅ Part 7\n├── snapshots/                                   ✅ Part 5\n├── .vscode/                                     ✅ Part 6\n├── global-setup.ts                              ✅ Part 1\n├── playwright.config.ts                         ✅ Parts 1–7\n├── .gitignore                                   ✅ Part 7\n└── .env\n```\n\nBy the end of Part 8, we add:\n\n```\nplaywright-playbook/\n├── tests/\n│   └── ai/                                      ← NEW\n│       └── ai-generated.spec.ts\n├── ai/                                          ← NEW\n│   ├── TestPlanner.ts\n│   └── TestHealer.ts\n├── scripts/\n│   ├── generate-test-plan.ts                    ← NEW\n│   └── heal-selectors.ts                        ← NEW\n└── .mcp.json                                    ← NEW\n```\n\nEvery file gets fully built below. 👇\n\nBefore we write code, be clear on what we're actually dealing with. There are three separate AI capabilities in the Playwright ecosystem, and they do very different things:\n\n```\nTool                    What it does                          Status\n──────────────────────  ───────────────────────────────────   ──────────────\nPlaywright MCP          Exposes Playwright as a tool for      Available now\n                        AI assistants (Claude, Copilot)       (v1.47+)\n                        to control a real browser\n\nAI Test Agents          Playwright's built-in agents that     Experimental\n(Planner/Generator)     explore your app and generate         (v1.47+ with flag)\n                        test skeletons automatically\n\nSelf-Healing            Automatically patches broken          Custom (we build\n                        locators when selectors change        this in Part 8)\n```\n\nWe'll build proper, practical implementations of all three — and be honest about where each one earns its place. 🎯\n\nMCP stands for **Model Context Protocol** — an open standard for connecting AI assistants to external tools. Playwright's MCP server lets an AI assistant (Claude, GitHub Copilot, Cursor) directly control a real Chromium browser.\n\nThis means you can say to Claude or Copilot: *\"Navigate to the task creation flow and write me a Playwright test for it.\"* And it will literally open a browser, click around, inspect the DOM, and generate real test code.\n\nInstall the MCP server:\n\n```\nnpm install -D @playwright/mcp\n```\n\nConfigure it for your project:\n\n```\n// .mcp.json — MCP configuration for your project\n{\n  \"mcpServers\": {\n    \"playwright\": {\n      \"command\": \"npx\",\n      \"args\": [\n        \"@playwright/mcp\",\n        \"--browser\", \"chromium\",\n        \"--base-url\", \"http://localhost:3000\",\n        \"--output-dir\", \"tests/ai\"\n      ],\n      \"env\": {\n        \"PLAYWRIGHT_STORAGE_STATE\": \".auth/admin.json\"\n      }\n    }\n  }\n}\n```\n\nFor VS Code with GitHub Copilot, add to `.vscode/settings.json`\n\n:\n\n```\n{\n  \"mcp\": {\n    \"servers\": {\n      \"playwright\": {\n        \"command\": \"npx\",\n        \"args\": [\"@playwright/mcp\", \"--browser\", \"chromium\"],\n        \"env\": {\n          \"BASE_URL\": \"http://localhost:3000\"\n        }\n      }\n    }\n  }\n}\n```\n\nWith MCP configured, your AI assistant can:\n\n```\nYou say:\n  \"Write a test for the task deletion flow including the confirmation dialog\"\n\nAI does:\n  1. Opens Chromium via MCP\n  2. Navigates to http://localhost:3000/tasks\n  3. Inspects the DOM — finds task items, hover targets, delete buttons\n  4. Identifies the confirmation dialog structure\n  5. Generates a TypeScript test using getByRole/getByTestId locators\n  6. Writes it to tests/ai/ directory\n\nYou get:\n  A real, runnable test file — not a hallucinated one\n```\n\nThe key difference from AI generating test code from a description: MCP sees the actual DOM. It generates locators that actually exist, not ones it imagines. 🔥\n\nOnce MCP is configured in Claude Desktop or Claude.ai:\n\n```\nPrompt: \"I have a Playwright project at http://localhost:3000.\nNavigate to the task management page, explore the UI,\nand write a complete TypeScript Playwright test for creating,\nediting, and deleting a task. Use our TaskPage POM from pages/TaskPage.ts.\"\n\nClaude will:\n  → Open browser via MCP\n  → Navigate and inspect the real UI\n  → Generate tests that match actual element structure\n  → Reference your existing POM correctly\n```\n\nThe Test Planner uses AI to analyse your existing test suite and your app's DOM structure, then suggests missing test scenarios.\n\n```\n// ai/TestPlanner.ts\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { chromium } from '@playwright/test';\n\nexport interface TestScenario {\n  feature: string;\n  scenario: string;\n  priority: 'high' | 'medium' | 'low';\n  covered: boolean;\n  suggestedTestFile: string;\n}\n\nexport interface TestPlan {\n  url: string;\n  generatedAt: string;\n  totalScenarios: number;\n  coveredCount: number;\n  uncoveredCount: number;\n  scenarios: TestScenario[];\n}\n\nexport class TestPlanner {\n  private readonly baseUrl: string;\n  private readonly testsDir: string;\n\n  constructor(baseUrl: string, testsDir: string = './tests') {\n    this.baseUrl = baseUrl;\n    this.testsDir = testsDir;\n  }\n\n  /**\n   * Crawl the app and extract all interactive elements.\n   * Returns a structured map of page → interactive elements.\n   */\n  async crawlApp(\n    storageState?: string\n  ): Promise<Map<string, string[]>> {\n    const browser = await chromium.launch();\n    const context = storageState\n      ? await browser.newContext({ storageState })\n      : await browser.newContext();\n\n    const page = await context.newPage();\n    const pageMap = new Map<string, string[]>();\n\n    const pagesToCrawl = [\n      '/login',\n      '/dashboard',\n      '/tasks',\n      '/admin',\n    ];\n\n    for (const route of pagesToCrawl) {\n      try {\n        await page.goto(`${this.baseUrl}${route}`, {\n          waitUntil: 'networkidle',\n          timeout: 10000,\n        });\n\n        // Extract all interactive elements and their accessible names\n        const elements = await page.evaluate(() => {\n          const interactable: string[] = [];\n\n          // Buttons\n          document.querySelectorAll('button').forEach(btn => {\n            const name = btn.getAttribute('aria-label') ||\n                         btn.textContent?.trim() ||\n                         btn.getAttribute('data-testid') || '';\n            if (name) interactable.push(`button: ${name}`);\n          });\n\n          // Links\n          document.querySelectorAll('a[href]').forEach(link => {\n            const name = link.textContent?.trim() ||\n                         link.getAttribute('aria-label') || '';\n            if (name) interactable.push(`link: ${name}`);\n          });\n\n          // Forms\n          document.querySelectorAll('form').forEach((form, i) => {\n            const inputs = form.querySelectorAll('input, textarea, select');\n            interactable.push(`form[${i}]: ${inputs.length} fields`);\n          });\n\n          // Modals/Dialogs\n          document.querySelectorAll('[role=\"dialog\"]').forEach(dialog => {\n            const label = dialog.getAttribute('aria-label') ||\n                          dialog.getAttribute('aria-labelledby') || 'dialog';\n            interactable.push(`dialog: ${label}`);\n          });\n\n          return interactable;\n        });\n\n        pageMap.set(route, elements);\n      } catch {\n        pageMap.set(route, ['[page not accessible]']);\n      }\n    }\n\n    await browser.close();\n    return pageMap;\n  }\n\n  /**\n   * Scan existing test files and extract test names.\n   * Used to check which scenarios are already covered.\n   */\n  getExistingTestNames(): string[] {\n    const testNames: string[] = [];\n    const testGlob = this.testsDir;\n\n    const scanDir = (dir: string) => {\n      if (!fs.existsSync(dir)) return;\n      const entries = fs.readdirSync(dir, { withFileTypes: true });\n      for (const entry of entries) {\n        const fullPath = path.join(dir, entry.name);\n        if (entry.isDirectory()) {\n          scanDir(fullPath);\n        } else if (entry.name.endsWith('.spec.ts')) {\n          const content = fs.readFileSync(fullPath, 'utf-8');\n          const matches = content.matchAll(/test\\(['\"`](.+?)['\"`]/g);\n          for (const match of matches) {\n            testNames.push(match[1]);\n          }\n        }\n      }\n    };\n\n    scanDir(testGlob);\n    return testNames;\n  }\n\n  /**\n   * Generate a test plan — what's covered, what's missing, priorities.\n   */\n  async generatePlan(): Promise<TestPlan> {\n    const pageMap = await this.crawlApp('.auth/admin.json');\n    const existingTests = this.getExistingTestNames();\n\n    const scenarios: TestScenario[] = [\n      // Auth scenarios\n      { feature: 'Authentication', scenario: 'successful login redirects to dashboard', priority: 'high', suggestedTestFile: 'tests/auth/login.spec.ts', covered: false },\n      { feature: 'Authentication', scenario: 'invalid credentials show error message', priority: 'high', suggestedTestFile: 'tests/auth/login.spec.ts', covered: false },\n      { feature: 'Authentication', scenario: 'empty fields show validation errors', priority: 'high', suggestedTestFile: 'tests/auth/login.spec.ts', covered: false },\n      { feature: 'Authentication', scenario: 'logout clears session', priority: 'high', suggestedTestFile: 'tests/auth/login.spec.ts', covered: false },\n\n      // Task CRUD\n      { feature: 'Task Management', scenario: 'user can create a new task', priority: 'high', suggestedTestFile: 'tests/tasks/task-management.spec.ts', covered: false },\n      { feature: 'Task Management', scenario: 'user can edit an existing task', priority: 'high', suggestedTestFile: 'tests/tasks/task-management.spec.ts', covered: false },\n      { feature: 'Task Management', scenario: 'user can delete a task', priority: 'high', suggestedTestFile: 'tests/tasks/task-management.spec.ts', covered: false },\n      { feature: 'Task Management', scenario: 'task list shows correct count after creation', priority: 'medium', suggestedTestFile: 'tests/tasks/task-management.spec.ts', covered: false },\n      { feature: 'Task Management', scenario: 'task creation with empty title is blocked', priority: 'medium', suggestedTestFile: 'tests/tasks/task-management.spec.ts', covered: false },\n      { feature: 'Task Management', scenario: 'task status can be changed to completed', priority: 'medium', suggestedTestFile: 'tests/tasks/task-management.spec.ts', covered: false },\n\n      // Permissions\n      { feature: 'Role Permissions', scenario: 'admin sees admin panel — regular user does not', priority: 'high', suggestedTestFile: 'tests/multi-user/role-permissions.spec.ts', covered: false },\n      { feature: 'Role Permissions', scenario: 'admin can delete any task — user cannot', priority: 'high', suggestedTestFile: 'tests/multi-user/role-permissions.spec.ts', covered: false },\n      { feature: 'Role Permissions', scenario: 'user accessing /admin is redirected', priority: 'high', suggestedTestFile: 'tests/multi-user/role-permissions.spec.ts', covered: false },\n\n      // Real-time\n      { feature: 'Real-Time', scenario: 'task assigned by admin appears instantly for user', priority: 'medium', suggestedTestFile: 'tests/multi-user/realtime-collaboration.spec.ts', covered: false },\n      { feature: 'Real-Time', scenario: 'notification appears when task assigned', priority: 'medium', suggestedTestFile: 'tests/multi-user/realtime-collaboration.spec.ts', covered: false },\n\n      // API\n      { feature: 'API', scenario: 'GET /api/tasks returns 200 with valid schema', priority: 'high', suggestedTestFile: 'tests/api/tasks-api.spec.ts', covered: false },\n      { feature: 'API', scenario: 'POST /api/tasks without auth returns 401', priority: 'high', suggestedTestFile: 'tests/api/tasks-api.spec.ts', covered: false },\n      { feature: 'API', scenario: 'DELETE /api/tasks/:id returns 404 for non-existent', priority: 'medium', suggestedTestFile: 'tests/api/tasks-api.spec.ts', covered: false },\n\n      // Error handling\n      { feature: 'Error Handling', scenario: 'shows error banner when API returns 500', priority: 'high', suggestedTestFile: 'tests/network/error-simulation.spec.ts', covered: false },\n      { feature: 'Error Handling', scenario: 'shows retry button when network fails', priority: 'medium', suggestedTestFile: 'tests/network/error-simulation.spec.ts', covered: false },\n      { feature: 'Error Handling', scenario: 'shows loading skeleton when API is slow', priority: 'low', suggestedTestFile: 'tests/network/error-simulation.spec.ts', covered: false },\n    ];\n\n    // Match existing tests against scenarios\n    for (const scenario of scenarios) {\n      scenario.covered = existingTests.some(name =>\n        name.toLowerCase().includes(scenario.scenario.toLowerCase().slice(0, 30))\n      );\n    }\n\n    const coveredCount = scenarios.filter(s => s.covered).length;\n\n    return {\n      url: this.baseUrl,\n      generatedAt: new Date().toISOString(),\n      totalScenarios: scenarios.length,\n      coveredCount,\n      uncoveredCount: scenarios.length - coveredCount,\n      scenarios,\n    };\n  }\n\n  /**\n   * Write the test plan to a Markdown file.\n   */\n  async writePlanMarkdown(outputPath: string): Promise<void> {\n    const plan = await this.generatePlan();\n\n    const coveragePercent = Math.round(\n      (plan.coveredCount / plan.totalScenarios) * 100\n    );\n\n    const lines: string[] = [\n      `# Test Plan — ${plan.url}`,\n      ``,\n      `Generated: ${new Date(plan.generatedAt).toLocaleString()}`,\n      ``,\n      `## Coverage Summary`,\n      ``,\n      `| Total Scenarios | Covered | Uncovered | Coverage |`,\n      `|-----------------|---------|-----------|----------|`,\n      `| ${plan.totalScenarios} | ✅ ${plan.coveredCount} | ❌ ${plan.uncoveredCount} | ${coveragePercent}% |`,\n      ``,\n      `## Scenarios by Feature`,\n      ``,\n    ];\n\n    const features = [...new Set(plan.scenarios.map(s => s.feature))];\n    for (const feature of features) {\n      lines.push(`### ${feature}`);\n      lines.push('');\n      const featureScenarios = plan.scenarios.filter(s => s.feature === feature);\n      for (const s of featureScenarios) {\n        const status = s.covered ? '✅' : '❌';\n        const priority = s.priority === 'high' ? '🔴' : s.priority === 'medium' ? '🟡' : '🟢';\n        lines.push(`- ${status} ${priority} ${s.scenario}`);\n        if (!s.covered) {\n          lines.push(`  - Add to: \\`${s.suggestedTestFile}\\``);\n        }\n      }\n      lines.push('');\n    }\n\n    fs.writeFileSync(outputPath, lines.join('\\n'), 'utf-8');\n    console.log(`✅ Test plan written to ${outputPath}`);\n    console.log(`   Coverage: ${coveragePercent}% (${plan.coveredCount}/${plan.totalScenarios})`);\n  }\n}\n```\n\nThe most practical AI feature in a real test suite. When a locator breaks because a developer changed a `data-testid`\n\nor renamed a button — instead of the test just failing, the healer automatically finds the best alternative locator and patches the source file.\n\n```\n// ai/TestHealer.ts\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { Page, Locator } from '@playwright/test';\n\nexport interface HealResult {\n  originalSelector: string;\n  healedSelector: string;\n  confidence: 'high' | 'medium' | 'low';\n  strategy: string;\n}\n\nexport interface HealReport {\n  file: string;\n  healed: HealResult[];\n  failed: string[];\n}\n\nexport class TestHealer {\n  /**\n   * Attempt to find an element using fallback strategies when the primary locator fails.\n   *\n   * Healing strategy priority:\n   *   1. getByTestId (data-testid — most stable)\n   *   2. getByRole + name (semantic — stable)\n   *   3. getByLabel (form elements)\n   *   4. getByText (exact visible text)\n   *   5. CSS selector (last resort)\n   */\n  static async healLocator(\n    page: Page,\n    originalSelector: string,\n    context?: {\n      expectedText?: string;\n      expectedRole?: string;\n      nearText?: string;\n    }\n  ): Promise<HealResult | null> {\n    // Strategy 1: Try getByTestId variations\n    if (context?.expectedText) {\n      const testIdVariants = [\n        context.expectedText.toLowerCase().replace(/\\s+/g, '-'),\n        context.expectedText.toLowerCase().replace(/\\s+/g, '_'),\n        context.expectedText.toLowerCase().replace(/\\s+/g, ''),\n      ];\n\n      for (const testId of testIdVariants) {\n        const locator = page.getByTestId(testId);\n        if (await locator.count() > 0) {\n          return {\n            originalSelector,\n            healedSelector: `page.getByTestId('${testId}')`,\n            confidence: 'high',\n            strategy: 'data-testid variant',\n          };\n        }\n      }\n    }\n\n    // Strategy 2: getByRole + name\n    if (context?.expectedRole && context?.expectedText) {\n      const roleLocator = page.getByRole(\n        context.expectedRole as 'button' | 'link' | 'heading' | 'listitem',\n        { name: context.expectedText }\n      );\n      if (await roleLocator.count() === 1) {\n        return {\n          originalSelector,\n          healedSelector: `page.getByRole('${context.expectedRole}', { name: '${context.expectedText}' })`,\n          confidence: 'high',\n          strategy: 'role + name',\n        };\n      }\n    }\n\n    // Strategy 3: getByText (exact)\n    if (context?.expectedText) {\n      const textLocator = page.getByText(context.expectedText, { exact: true });\n      if (await textLocator.count() === 1) {\n        return {\n          originalSelector,\n          healedSelector: `page.getByText('${context.expectedText}', { exact: true })`,\n          confidence: 'medium',\n          strategy: 'exact text match',\n        };\n      }\n\n      // Partial text match\n      const partialLocator = page.getByText(context.expectedText);\n      if (await partialLocator.count() === 1) {\n        return {\n          originalSelector,\n          healedSelector: `page.getByText('${context.expectedText}')`,\n          confidence: 'low',\n          strategy: 'partial text match',\n        };\n      }\n    }\n\n    // Strategy 4: Try to find near a known element\n    if (context?.nearText) {\n      const nearLocator = page.locator(`:near(:text(\"${context.nearText}\"))`);\n      if (await nearLocator.count() > 0) {\n        return {\n          originalSelector,\n          healedSelector: `page.locator(':near(:text(\"${context.nearText}\"))').first()`,\n          confidence: 'low',\n          strategy: 'proximity to known text',\n        };\n      }\n    }\n\n    return null;\n  }\n\n  /**\n   * Scan test files for broken selectors after a test run.\n   * Returns a report of what was healed and what couldn't be fixed.\n   */\n  static scanForBrokenSelectors(testResultsPath: string): string[] {\n    const brokenSelectors: string[] = [];\n\n    if (!fs.existsSync(testResultsPath)) {\n      return brokenSelectors;\n    }\n\n    const results = JSON.parse(fs.readFileSync(testResultsPath, 'utf-8'));\n\n    // Extract locator errors from test results\n    for (const suite of results.suites ?? []) {\n      for (const spec of suite.specs ?? []) {\n        if (!spec.ok) {\n          for (const test of spec.tests ?? []) {\n            for (const result of test.results ?? []) {\n              const error = result.error?.message ?? '';\n              // Match common locator failure messages\n              const locatorMatch = error.match(\n                /waiting for (locator|getByTestId|getByRole|getByText)\\(['\"`](.+?)['\"`]\\)/\n              );\n              if (locatorMatch) {\n                brokenSelectors.push(locatorMatch[0]);\n              }\n            }\n          }\n        }\n      }\n    }\n\n    return [...new Set(brokenSelectors)]; // deduplicate\n  }\n\n  /**\n   * Patch a test file — replace an old selector with a healed one.\n   */\n  static patchTestFile(\n    filePath: string,\n    original: string,\n    healed: string\n  ): boolean {\n    if (!fs.existsSync(filePath)) return false;\n\n    const content = fs.readFileSync(filePath, 'utf-8');\n    if (!content.includes(original)) return false;\n\n    const patched = content.replace(new RegExp(escapeRegex(original), 'g'), healed);\n    fs.writeFileSync(filePath, patched, 'utf-8');\n\n    console.log(`  ✅ Patched: ${path.basename(filePath)}`);\n    console.log(`     Before: ${original}`);\n    console.log(`     After:  ${healed}`);\n\n    return true;\n  }\n}\n\nfunction escapeRegex(str: string): string {\n  return str.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\njs\n// scripts/generate-test-plan.ts\nimport { TestPlanner } from '../ai/TestPlanner';\nimport * as path from 'path';\nimport * as dotenv from 'dotenv';\n\ndotenv.config();\n\nasync function main() {\n  const baseUrl = process.env.BASE_URL || 'http://localhost:3000';\n  const outputPath = path.join(process.cwd(), 'TEST_PLAN.md');\n\n  console.log(`🤖 Generating test plan for ${baseUrl}...`);\n  console.log('   Crawling app and scanning existing tests...\\n');\n\n  const planner = new TestPlanner(baseUrl, './tests');\n  await planner.writePlanMarkdown(outputPath);\n\n  console.log('\\n📋 Test plan saved to TEST_PLAN.md');\n  console.log('   Review uncovered scenarios and add missing tests.\\n');\n}\n\nmain().catch(err => {\n  console.error('Test plan generation failed:', err);\n  process.exit(1);\n});\n```\n\nRun it anytime to get an up-to-date picture of coverage:\n\n```\nnpx ts-node scripts/generate-test-plan.ts\n```\n\nOutput:\n\n```\n# Test Plan — http://localhost:3000\n\nGenerated: 18/06/2026, 09:15:00\n\n## Coverage Summary\n\n| Total Scenarios | Covered | Uncovered | Coverage |\n|-----------------|---------|-----------|----------|\n| 21 | ✅ 17 | ❌ 4 | 81% |\n\n## Scenarios by Feature\n\n### Authentication\n\n- ✅ 🔴 successful login redirects to dashboard\n- ✅ 🔴 invalid credentials show error message\n- ✅ 🔴 empty fields show validation errors\n- ❌ 🔴 logout clears session\n  - Add to: `tests/auth/login.spec.ts`\n\n...\njs\n// scripts/heal-selectors.ts\nimport { chromium } from '@playwright/test';\nimport { TestHealer } from '../ai/TestHealer';\nimport * as path from 'path';\nimport * as dotenv from 'dotenv';\n\ndotenv.config();\n\n/**\n * Run this after a test failure to attempt automatic selector healing.\n *\n * Usage:\n *   npx ts-node scripts/heal-selectors.ts\n *\n * What it does:\n *   1. Reads test-results/results.json for broken selectors\n *   2. Launches a browser and tries alternative locator strategies\n *   3. Patches test files with healed selectors\n *   4. Prints a healing report\n */\nasync function main() {\n  const resultsPath = path.join(process.cwd(), 'test-results', 'results.json');\n  const baseUrl = process.env.BASE_URL || 'http://localhost:3000';\n\n  console.log('🔧 Playwright Self-Healing Selector Tool\\n');\n\n  // Step 1 — Find broken selectors from test results\n  const broken = TestHealer.scanForBrokenSelectors(resultsPath);\n\n  if (broken.length === 0) {\n    console.log('✅ No broken selectors found in test results.');\n    return;\n  }\n\n  console.log(`Found ${broken.length} broken selector(s):\\n`);\n  broken.forEach(s => console.log(`  ❌ ${s}`));\n  console.log('');\n\n  // Step 2 — Launch browser and try to heal each one\n  const browser = await chromium.launch({ headless: true });\n  const context = await browser.newContext({\n    storageState: '.auth/admin.json',\n  });\n  const page = await context.newPage();\n\n  const report: { healed: number; failed: number } = { healed: 0, failed: 0 };\n\n  for (const selector of broken) {\n    console.log(`\\n🔍 Attempting to heal: ${selector}`);\n\n    await page.goto(`${baseUrl}/tasks`, { waitUntil: 'networkidle' });\n\n    // Extract context clues from the selector string\n    const textMatch = selector.match(/['\"`]([^'\"`]+)['\"`]/);\n    const expectedText = textMatch?.[1];\n\n    const result = await TestHealer.healLocator(page, selector, {\n      expectedText,\n      expectedRole: selector.includes('button') ? 'button' : undefined,\n    });\n\n    if (result) {\n      console.log(`  ✅ Healed with strategy: ${result.strategy}`);\n      console.log(`     Confidence: ${result.confidence}`);\n      console.log(`     New selector: ${result.healedSelector}`);\n      report.healed++;\n    } else {\n      console.log(`  ❌ Could not automatically heal this selector.`);\n      console.log(`     Manual fix required.`);\n      report.failed++;\n    }\n  }\n\n  await browser.close();\n\n  // Step 3 — Summary\n  console.log('\\n─────────────────────────────────────');\n  console.log('🔧 Healing Summary');\n  console.log(`   ✅ Healed:  ${report.healed}`);\n  console.log(`   ❌ Failed:  ${report.failed}`);\n  console.log('─────────────────────────────────────');\n\n  if (report.healed > 0) {\n    console.log('\\n⚠️  Review all healed selectors before committing.');\n    console.log('   Run npx playwright test to verify the fixes.');\n  }\n}\n\nmain().catch(err => {\n  console.error('Healer failed:', err);\n  process.exit(1);\n});\n```\n\nHere's what AI-generated tests actually look like when you use MCP properly — and where the gaps are.\n\n```\n// tests/ai/ai-generated.spec.ts\n// These tests were generated with Playwright MCP + Claude\n// Reviewed and validated by a human before being committed\n// Note: AI generated the structure — human verified selectors and assertions\n\nimport { test, expect } from '@playwright/test';\nimport { TaskPage } from '../../pages/TaskPage';\nimport { DashboardPage } from '../../pages/DashboardPage';\n\n// ✅ Good AI generation — simple, well-structured, uses existing POM\ntest('task title is displayed in the task list after creation', async ({ page }) => {\n  const taskPage = new TaskPage(page);\n  await taskPage.goto();\n\n  await taskPage.createTask('AI generated test task');\n\n  await expect(taskPage.getTaskLocator('AI generated test task')).toBeVisible();\n  await expect(page.getByRole('listitem').filter({ hasText: 'AI generated test task' }))\n    .toBeVisible();\n\n  await taskPage.deleteTask('AI generated test task');\n});\n\n// ✅ Good — AI correctly identified the dashboard navigation flow\ntest('clicking task from dashboard navigates to task detail', async ({ page }) => {\n  const dashboard = new DashboardPage(page);\n  await dashboard.goto();\n\n  // AI correctly observed this interaction via MCP\n  const firstTask = page.getByRole('listitem').first();\n  const taskTitle = await firstTask.getByTestId('task-title').textContent();\n\n  await firstTask.getByRole('link', { name: 'View details' }).click();\n\n  await expect(page).toHaveURL(/\\/tasks\\/\\d+/);\n  await expect(page.getByRole('heading', { level: 1 })).toHaveText(taskTitle ?? '');\n});\n\n// ✅ Good — AI found the filter interaction correctly\ntest('filtering tasks by status shows only matching tasks', async ({ page }) => {\n  const taskPage = new TaskPage(page);\n  await taskPage.goto();\n\n  // AI observed the filter dropdown via MCP browser exploration\n  await page.getByRole('combobox', { name: 'Filter by status' }).selectOption('completed');\n\n  const taskItems = page.getByRole('listitem');\n  const count = await taskItems.count();\n\n  for (let i = 0; i < count; i++) {\n    await expect(taskItems.nth(i).getByTestId('status-badge')).toHaveText('Completed');\n  }\n});\n\n// ⚠️ Needs human review — AI generated this but assertion is weak\n// TODO: Strengthen the assertion to check specific error text\ntest('task creation form shows validation when title is empty', async ({ page }) => {\n  const taskPage = new TaskPage(page);\n  await taskPage.goto();\n\n  await taskPage.newTaskButton.click();\n  // AI correctly identified save button but missed the specific error selector\n  await taskPage.saveTaskButton.click();\n\n  // ⚠️ AI used a generic assertion here — human should make this more specific\n  await expect(page.getByRole('alert')).toBeVisible();\n  // TODO: Replace with: await expect(page.getByText('Title is required')).toBeVisible();\n});\n\n// ❌ AI got this wrong — generated a selector that doesn't exist\n// Left here deliberately to show where AI fails\n// test('admin can bulk delete tasks', async ({ page }) => {\n//   AI generated: await page.getByTestId('bulk-select-all').click();\n//   Problem: this element doesn't exist in our app\n//   AI hallucinated a feature that isn't there\n//   This is why you always review AI-generated tests before running\n// });\n```\n\nThe commented-out test at the bottom is important. It shows the failure mode honestly — AI sometimes generates tests for features it *thinks* exist but don't. The lesson: **AI generates test scaffolding. Humans validate it.**\n\nHere's the honest breakdown of where AI helps and where it doesn't:\n\n```\n✅ AI is excellent for:\n  ├── Generating test skeletons for new features fast\n  ├── Exploring an unfamiliar codebase via MCP\n  ├── Suggesting test scenarios you hadn't thought of\n  ├── Writing boilerplate (beforeEach, fixtures, imports)\n  ├── Generating test data variations\n  └── Coverage gap analysis (TestPlanner.ts)\n\n⚠️ AI needs human oversight for:\n  ├── Verifying generated selectors actually exist in the DOM\n  ├── Checking assertions are strong enough (not just toBeVisible)\n  ├── Ensuring tests are truly independent (AI loves shared state)\n  ├── Business logic assertions (AI doesn't know your rules)\n  └── Any security or permissions testing\n\n❌ AI should not be trusted for:\n  ├── Testing non-deterministic behaviour without human review\n  ├── Deciding what constitutes a \"pass\" for complex flows\n  ├── Replacing the understanding of what actually matters to test\n  └── Automatically committing healed selectors without review\n```\n\nThe engineers who get the most value from AI tools are the ones who know these boundaries. They use AI to go faster on the tasks it does well. They apply their own judgment on the tasks it doesn't. 🎯\n\n`package.json`\n\nScripts\nAdd these to your `package.json`\n\nto make the AI tools easy to run:\n\n```\n{\n  \"scripts\": {\n    \"test\": \"playwright test\",\n    \"test:headed\": \"playwright test --headed\",\n    \"test:debug\": \"playwright test --debug\",\n    \"test:ci\": \"playwright test --project=admin --project=user --project=multi-context --project=api\",\n    \"test:visual\": \"playwright test --project=visual --project=visual-responsive\",\n    \"test:api\": \"playwright test --project=api\",\n    \"test:update-snapshots\": \"playwright test --project=visual --update-snapshots\",\n    \"report\": \"playwright show-report\",\n    \"record-har\": \"ts-node scripts/record-har.ts\",\n    \"heal\": \"ts-node scripts/heal-selectors.ts\",\n    \"plan\": \"ts-node scripts/generate-test-plan.ts\",\n    \"notify\": \"ts-node scripts/notify-slack.ts\"\n  }\n}\n```\n\nEvery file across all 8 parts — fully built, fully documented:\n\n```\nplaywright-playbook/\n├── .github/\n│   └── workflows/\n│       ├── playwright.yml                       ✅ Part 7\n│       └── playwright-visual.yml               ✅ Part 7\n├── tests/\n│   ├── auth/\n│   │   └── login.spec.ts                        ✅ Part 1\n│   ├── tasks/\n│   │   └── task-management.spec.ts              ✅ Part 1\n│   ├── network/                                 ✅ Part 2\n│   │   ├── api-mocking.spec.ts\n│   │   ├── error-simulation.spec.ts\n│   │   └── network-assertions.spec.ts\n│   ├── multi-user/                              ✅ Part 3\n│   │   ├── role-permissions.spec.ts\n│   │   └── realtime-collaboration.spec.ts\n│   ├── multi-tab/                               ✅ Part 3\n│   │   └── multi-tab-flows.spec.ts\n│   ├── api/                                     ✅ Part 4\n│   │   ├── tasks-api.spec.ts\n│   │   ├── auth-api.spec.ts\n│   │   ├── graphql-api.spec.ts\n│   │   └── api-ui-chain.spec.ts\n│   ├── visual/                                  ✅ Part 5\n│   │   ├── dashboard-visual.spec.ts\n│   │   ├── task-visual.spec.ts\n│   │   └── responsive-visual.spec.ts\n│   ├── debug/                                   ✅ Part 6\n│   │   └── trace-examples.spec.ts\n│   └── ai/                                      ✅ Part 8\n│       └── ai-generated.spec.ts\n├── pages/\n│   ├── LoginPage.ts                             ✅ Part 1\n│   ├── TaskPage.ts                              ✅ Part 1\n│   └── DashboardPage.ts                         ✅ Part 3\n├── api/\n│   ├── TaskApiClient.ts                         ✅ Part 4\n│   └── AuthApiClient.ts                         ✅ Part 4\n├── ai/                                          ✅ Part 8\n│   ├── TestPlanner.ts\n│   └── TestHealer.ts\n├── fixtures/\n│   ├── auth.fixture.ts                          ✅ Part 1\n│   ├── tasks.json                               ✅ Part 2\n│   ├── empty-tasks.json                         ✅ Part 2\n│   ├── tasks-har.har                            ✅ Part 2\n│   ├── multi-user.fixture.ts                    ✅ Part 3\n│   └── api.fixture.ts                           ✅ Part 4\n├── scripts/\n│   ├── record-har.ts                            ✅ Part 2\n│   ├── notify-slack.ts                          ✅ Part 7\n│   ├── generate-test-plan.ts                    ✅ Part 8\n│   └── heal-selectors.ts                        ✅ Part 8\n├── utils/\n│   ├── schema-validator.ts                      ✅ Part 4\n│   ├── visual-helpers.ts                        ✅ Part 5\n│   └── debug-helpers.ts                         ✅ Part 6\n├── docker/                                      ✅ Part 7\n│   ├── Dockerfile\n│   └── docker-compose.yml\n├── snapshots/                                   ✅ Part 5\n├── .vscode/                                     ✅ Part 6\n│   ├── extensions.json\n│   └── launch.json\n├── .auth/                                       ← git-ignored\n│   ├── admin.json\n│   └── user.json\n├── .mcp.json                                    ✅ Part 8\n├── global-setup.ts                              ✅ Part 1\n├── playwright.config.ts                         ✅ Parts 1–7 (final)\n├── .gitignore                                   ✅ Part 7\n├── .env                                         ← git-ignored\n├── .env.example                                 ← committed\n├── TEST_PLAN.md                                 ← generated by script\n└── package.json\nPart 1 — Stop Writing Tests Like a Beginner              ✅ Done\nPart 2 — Network Interception: The Complete Guide        ✅ Done\nPart 3 — Multi-User, Multi-Tab & Context Testing         ✅ Done\nPart 4 — API Testing (The Underrated Superpower)         ✅ Done\nPart 5 — Visual Regression Testing                       ✅ Done\nPart 6 — Debugging Like a Pro: Trace Viewer & Inspector  ✅ Done\nPart 7 — The CI/CD Setup Nobody Shows You                ✅ Done\nPart 8 — Playwright Meets AI: Agents, MCP & Self-Healing ✅ Done\n```\n\nEight parts. One complete framework.\n\nLook at what you now have:\n\n**The foundation (Parts 1–2)**\n\nSemantic locators. `storageState`\n\n. Page Object Model. Full network interception layer. HAR recording. API mocking.\n\n**The depth (Parts 3–4)**\n\nMulti-user simultaneous testing. Real-time collaboration validation. Raw API testing with typed clients. Schema validation. GraphQL testing. API-UI chaining.\n\n**The quality layer (Parts 5–6)**\n\nVisual regression across full pages, components, and four viewports. Trace Viewer integration. Debug helpers that catch console errors and audit network calls automatically. VS Code launch configs.\n\n**The pipeline (Part 7)**\n\nSharded GitHub Actions workflow — 4x faster. Cross-browser matrix. Docker for consistent VRT. Slack notifications. Published HTML reports as artifacts.\n\n**The AI layer (Part 8)**\n\nPlaywright MCP — giving AI assistants a real browser. Test coverage analysis. Self-healing selector engine. AI-generated test scaffolding with honest evaluation of where it works and where it doesn't.\n\nThis isn't a toy. This is a production-grade Playwright framework — the kind teams spend months building organically.\n\nYou built it in 8 parts. 🏆\n\nThis series covered the framework. But a framework is only as good as the team using it.\n\nIf this series helped you — share it with your team. Point them to Part 1. Let them follow along from the beginning.\n\nAnd if you want to go deeper on any specific part — drop a comment. The next series is already forming based on what you ask.\n\n**Thank you for following The Playwright Playbook from start to finish.**\n\nThis is the series I wish had existed when I started testing AI systems. I hope it gave you what you needed to build something real.\n\nDrop a comment below 👇\n\nIt's been a great ride. Let's keep building. 🙌\n\n*Faizal Shaikh | Senior Automation Engineer | Playwright & AI Testing*\n\n*Connect with me on LinkedIn*", "url": "https://wpnews.pro/news/the-playwright-playbook-part-8-playwright-meets-ai-agents-mcp-self-healing-tests", "canonical_source": "https://dev.to/sshhfaiz/the-playwright-playbook-part-8-playwright-meets-ai-agents-mcp-self-healing-tests-136p", "published_at": "2026-06-21 12:57:08+00:00", "updated_at": "2026-06-21 13:06:40.633507+00:00", "lang": "en", "topics": ["artificial-intelligence", "developer-tools", "ai-agents", "ai-tools", "large-language-models"], "entities": ["Playwright", "Claude", "Copilot", "Docker", "Slack", "GitHub Actions", "MCP", "AI Test Agents"], "alternates": {"html": "https://wpnews.pro/news/the-playwright-playbook-part-8-playwright-meets-ai-agents-mcp-self-healing-tests", "markdown": "https://wpnews.pro/news/the-playwright-playbook-part-8-playwright-meets-ai-agents-mcp-self-healing-tests.md", "text": "https://wpnews.pro/news/the-playwright-playbook-part-8-playwright-meets-ai-agents-mcp-self-healing-tests.txt", "jsonld": "https://wpnews.pro/news/the-playwright-playbook-part-8-playwright-meets-ai-agents-mcp-self-healing-tests.jsonld"}}