browser-automation

star 12

Common Playwright automation patterns for web testing and scraping via MCP

code-yeongyu By code-yeongyu schedule Updated 10/20/2025

name: browser-automation description: Common Playwright automation patterns for web testing and scraping via MCP

Browser Automation with Playwright MCP

This skill provides comprehensive patterns for browser automation using the Playwright MCP server. The Playwright MCP uses the accessibility tree for element interaction, which is more reliable and semantic than screenshot-based approaches.

Overview

Playwright MCP enables:

  • Reliable web navigation and page interactions
  • Form filling and data extraction
  • Element interaction via accessibility tree
  • Cross-browser testing automation
  • Network request monitoring and mocking

The MCP server runs via npx @playwright/mcp@latest and provides tools accessible through the Claude Code interface.

Navigation Patterns

Basic Navigation

Navigate to URLs and wait for page load events:

// Navigate to a URL
await page.goto('https://example.com');

// Navigate with specific wait conditions
await page.goto('https://example.com', {
  waitUntil: 'networkidle'  // Wait until network is idle
});

// Navigate and wait for a specific element
await page.goto('https://example.com');
await page.waitForSelector('#main-content');

Handling Page Load States

// Wait for DOM to be ready
await page.waitForLoadState('domcontentloaded');

// Wait for full page load including resources
await page.waitForLoadState('load');

// Wait for network to be idle (no requests for 500ms)
await page.waitForLoadState('networkidle');

Navigation with Authentication

// Navigate with HTTP authentication
await page.goto('https://example.com', {
  waitUntil: 'networkidle',
  timeout: 30000
});

// Set authentication state
await context.addCookies([
  {
    name: 'session',
    value: 'abc123',
    domain: 'example.com',
    path: '/'
  }
]);

Handling Redirects and Popups

// Wait for navigation after click (handles redirects)
await Promise.all([
  page.waitForNavigation(),
  page.click('a[href="/login"]')
]);

// Handle popup windows
const [popup] = await Promise.all([
  page.waitForEvent('popup'),
  page.click('button[data-action="open-popup"]')
]);
await popup.waitForLoadState();

Form Filling

Text Input Fields

// Fill text input
await page.fill('input[name="username"]', 'john.doe@example.com');

// Type with realistic delays (simulates human typing)
await page.type('input[name="password"]', 'SecurePass123', {
  delay: 100  // 100ms between keystrokes
});

// Clear and fill
await page.fill('input[name="search"]', '');
await page.fill('input[name="search"]', 'new search term');

Select Dropdowns

// Select by value
await page.selectOption('select[name="country"]', 'US');

// Select by label
await page.selectOption('select[name="country"]', { label: 'United States' });

// Select multiple options
await page.selectOption('select[name="interests"]', ['coding', 'testing', 'automation']);

// Select by index
await page.selectOption('select[name="priority"]', { index: 2 });

Checkboxes and Radio Buttons

// Check a checkbox
await page.check('input[type="checkbox"][name="terms"]');

// Uncheck a checkbox
await page.uncheck('input[type="checkbox"][name="newsletter"]');

// Select radio button
await page.check('input[type="radio"][value="premium"]');

// Verify checkbox state
const isChecked = await page.isChecked('input[name="terms"]');

Complex Form Interactions

// Fill complete form
await page.fill('input[name="firstName"]', 'John');
await page.fill('input[name="lastName"]', 'Doe');
await page.fill('input[name="email"]', 'john.doe@example.com');
await page.selectOption('select[name="country"]', 'US');
await page.check('input[name="terms"]');

// Submit form
await page.click('button[type="submit"]');

// Wait for form submission to complete
await page.waitForURL('**/success');

File Uploads

// Upload single file
await page.setInputFiles('input[type="file"]', 'path/to/file.pdf');

// Upload multiple files
await page.setInputFiles('input[type="file"][multiple]', [
  'path/to/file1.jpg',
  'path/to/file2.jpg'
]);

// Clear file input
await page.setInputFiles('input[type="file"]', []);

Data Extraction

Text Content Extraction

// Extract text content
const title = await page.textContent('h1.page-title');

// Extract inner text (visible text only)
const description = await page.innerText('.product-description');

// Extract from multiple elements
const prices = await page.$$eval('.product-price', elements =>
  elements.map(el => el.textContent.trim())
);

Attribute Extraction

// Get single attribute
const imageUrl = await page.getAttribute('img.product-image', 'src');

// Get multiple attributes
const linkData = await page.$eval('a.download-link', el => ({
  href: el.getAttribute('href'),
  text: el.textContent,
  target: el.getAttribute('target')
}));

// Extract data attributes
const productId = await page.getAttribute('[data-product-id]', 'data-product-id');

Structured Data Extraction

// Extract table data
const tableData = await page.$$eval('table.data-table tbody tr', rows =>
  rows.map(row => {
    const cells = row.querySelectorAll('td');
    return {
      name: cells[0]?.textContent.trim(),
      email: cells[1]?.textContent.trim(),
      role: cells[2]?.textContent.trim()
    };
  })
);

// Extract list items
const items = await page.$$eval('ul.items li', elements =>
  elements.map(el => ({
    text: el.textContent.trim(),
    id: el.getAttribute('data-id')
  }))
);

Pagination and Infinite Scroll

// Handle pagination
const allData = [];
let hasNextPage = true;

while (hasNextPage) {
  // Extract current page data
  const pageData = await page.$$eval('.item', items =>
    items.map(item => item.textContent.trim())
  );
  allData.push(...pageData);

  // Check for next page button
  const nextButton = await page.$('button.next-page:not([disabled])');
  if (nextButton) {
    await nextButton.click();
    await page.waitForLoadState('networkidle');
  } else {
    hasNextPage = false;
  }
}

// Handle infinite scroll
await page.evaluate(async () => {
  await new Promise((resolve) => {
    let totalHeight = 0;
    const distance = 100;
    const timer = setInterval(() => {
      const scrollHeight = document.body.scrollHeight;
      window.scrollBy(0, distance);
      totalHeight += distance;

      if (totalHeight >= scrollHeight) {
        clearInterval(timer);
        resolve();
      }
    }, 100);
  });
});

Element Interaction

Clicking Elements

// Simple click
await page.click('button.submit');

// Click with options
await page.click('button.menu', {
  button: 'right',  // Right-click
  clickCount: 2     // Double-click
});

// Force click (bypass actionability checks)
await page.click('.hidden-element', { force: true });

// Click and wait for navigation
await Promise.all([
  page.waitForNavigation(),
  page.click('a.external-link')
]);

Hovering and Focus

// Hover over element
await page.hover('.menu-item');

// Focus on input
await page.focus('input[name="search"]');

// Hover and click submenu
await page.hover('.dropdown-menu');
await page.click('.dropdown-menu .submenu-item');

Keyboard Interactions

// Press single key
await page.press('input[name="search"]', 'Enter');

// Press key combination
await page.press('body', 'Control+A');

// Type special characters
await page.keyboard.press('ArrowDown');
await page.keyboard.press('Tab');

// Keyboard shortcuts
await page.keyboard.press('Control+C');  // Copy
await page.keyboard.press('Control+V');  // Paste

Drag and Drop

// Drag and drop
await page.dragAndDrop('#source-element', '#target-element');

// Manual drag and drop with more control
await page.hover('#draggable');
await page.mouse.down();
await page.hover('#drop-zone');
await page.mouse.up();

Waiting for Elements

// Wait for element to appear
await page.waitForSelector('.dynamic-content');

// Wait for element to be visible
await page.waitForSelector('.modal', { state: 'visible' });

// Wait for element to be hidden
await page.waitForSelector('.loading-spinner', { state: 'hidden' });

// Wait for element with timeout
await page.waitForSelector('.slow-element', { timeout: 10000 });

Element State Checks

// Check if element is visible
const isVisible = await page.isVisible('.element');

// Check if element is enabled
const isEnabled = await page.isEnabled('button.submit');

// Check if element is editable
const isEditable = await page.isEditable('input[name="email"]');

// Get element count
const count = await page.locator('.item').count();

Accessibility Tree Usage

The Playwright MCP leverages the accessibility tree for more reliable element interaction:

Using ARIA Roles

// Find by role
await page.click('button[role="button"]');
await page.click('[role="menuitem"]');

// Find by accessible name
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByRole('textbox', { name: 'Email' }).fill('user@example.com');

Using Labels

// Find input by label text
await page.getByLabel('Username').fill('john.doe');
await page.getByLabel('Password').fill('SecurePass123');
await page.getByLabel('Remember me').check();

Using Accessible Names

// Find by accessible name (aria-label, title, etc.)
await page.getByText('Welcome to our site').click();
await page.getByTitle('Close dialog').click();
await page.getByAltText('Company logo').click();

Best Practices

Error Handling

// Use try-catch for robust automation
try {
  await page.waitForSelector('.element', { timeout: 5000 });
  await page.click('.element');
} catch (error) {
  console.error('Element not found:', error.message);
  // Fallback action
}

Waiting Strategies

// Prefer explicit waits over fixed delays
// BAD: await page.waitForTimeout(5000);
// GOOD:
await page.waitForLoadState('networkidle');
await page.waitForSelector('.content');

Selector Best Practices

// Prefer semantic selectors
// BAD: await page.click('div > div > button:nth-child(3)');
// GOOD: await page.click('button[data-testid="submit"]');
// BETTER: await page.getByRole('button', { name: 'Submit' }).click();

Performance Optimization

// Reuse locators
const submitButton = page.locator('button[type="submit"]');
await submitButton.waitFor();
await submitButton.click();

// Batch operations
await Promise.all([
  page.fill('input[name="field1"]', 'value1'),
  page.fill('input[name="field2"]', 'value2'),
  page.fill('input[name="field3"]', 'value3')
]);

Common Patterns

Login Flow

async function login(page, username, password) {
  await page.goto('https://example.com/login');
  await page.fill('input[name="username"]', username);
  await page.fill('input[name="password"]', password);
  await page.click('button[type="submit"]');
  await page.waitForURL('**/dashboard');
}

Form Validation Testing

// Submit empty form and check validation
await page.click('button[type="submit"]');
const errorMessage = await page.textContent('.error-message');
expect(errorMessage).toContain('required');

// Fill invalid data and verify
await page.fill('input[name="email"]', 'invalid-email');
await page.click('button[type="submit"]');
const emailError = await page.textContent('.email-error');
expect(emailError).toContain('valid email');

Dynamic Content Handling

// Wait for AJAX content to load
await page.click('button.load-more');
await page.waitForResponse(response =>
  response.url().includes('/api/items') && response.status() === 200
);
await page.waitForSelector('.new-items');

This skill provides the foundation for reliable browser automation using Playwright MCP's accessibility-focused approach.

Install via CLI
npx skills add https://github.com/code-yeongyu/sisyphus-private --skill browser-automation
Repository Details
star Stars 12
call_split Forks 1
navigation Branch main
article Path SKILL.md
More from Creator
code-yeongyu
code-yeongyu Explore all skills →