| 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:
await page.goto('https://example.com');
await page.goto('https://example.com', {
waitUntil: 'networkidle'
});
await page.goto('https://example.com');
await page.waitForSelector('#main-content');
Handling Page Load States
await page.waitForLoadState('domcontentloaded');
await page.waitForLoadState('load');
await page.waitForLoadState('networkidle');
Navigation with Authentication
await page.goto('https://example.com', {
waitUntil: 'networkidle',
timeout: 30000
});
await context.addCookies([
{
name: 'session',
value: 'abc123',
domain: 'example.com',
path: '/'
}
]);
Handling Redirects and Popups
await Promise.all([
page.waitForNavigation(),
page.click('a[href="/login"]')
]);
const [popup] = await Promise.all([
page.waitForEvent('popup'),
page.click('button[data-action="open-popup"]')
]);
await popup.waitForLoadState();
Form Filling
Text Input Fields
await page.fill('input[name="username"]', 'john.doe@example.com');
await page.type('input[name="password"]', 'SecurePass123', {
delay: 100
});
await page.fill('input[name="search"]', '');
await page.fill('input[name="search"]', 'new search term');
Select Dropdowns
await page.selectOption('select[name="country"]', 'US');
await page.selectOption('select[name="country"]', { label: 'United States' });
await page.selectOption('select[name="interests"]', ['coding', 'testing', 'automation']);
await page.selectOption('select[name="priority"]', { index: 2 });
Checkboxes and Radio Buttons
await page.check('input[type="checkbox"][name="terms"]');
await page.uncheck('input[type="checkbox"][name="newsletter"]');
await page.check('input[type="radio"][value="premium"]');
const isChecked = await page.isChecked('input[name="terms"]');
Complex Form Interactions
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"]');
await page.click('button[type="submit"]');
await page.waitForURL('**/success');
File Uploads
await page.setInputFiles('input[type="file"]', 'path/to/file.pdf');
await page.setInputFiles('input[type="file"][multiple]', [
'path/to/file1.jpg',
'path/to/file2.jpg'
]);
await page.setInputFiles('input[type="file"]', []);
Data Extraction
Text Content Extraction
const title = await page.textContent('h1.page-title');
const description = await page.innerText('.product-description');
const prices = await page.$$eval('.product-price', elements =>
elements.map(el => el.textContent.trim())
);
Attribute Extraction
const imageUrl = await page.getAttribute('img.product-image', 'src');
const linkData = await page.$eval('a.download-link', el => ({
href: el.getAttribute('href'),
text: el.textContent,
target: el.getAttribute('target')
}));
const productId = await page.getAttribute('[data-product-id]', 'data-product-id');
Structured Data Extraction
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()
};
})
);
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
const allData = [];
let hasNextPage = true;
while (hasNextPage) {
const pageData = await page.$$eval('.item', items =>
items.map(item => item.textContent.trim())
);
allData.push(...pageData);
const nextButton = await page.$('button.next-page:not([disabled])');
if (nextButton) {
await nextButton.click();
await page.waitForLoadState('networkidle');
} else {
hasNextPage = false;
}
}
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
await page.click('button.submit');
await page.click('button.menu', {
button: 'right',
clickCount: 2
});
await page.click('.hidden-element', { force: true });
await Promise.all([
page.waitForNavigation(),
page.click('a.external-link')
]);
Hovering and Focus
await page.hover('.menu-item');
await page.focus('input[name="search"]');
await page.hover('.dropdown-menu');
await page.click('.dropdown-menu .submenu-item');
Keyboard Interactions
await page.press('input[name="search"]', 'Enter');
await page.press('body', 'Control+A');
await page.keyboard.press('ArrowDown');
await page.keyboard.press('Tab');
await page.keyboard.press('Control+C');
await page.keyboard.press('Control+V');
Drag and Drop
await page.dragAndDrop('#source-element', '#target-element');
await page.hover('#draggable');
await page.mouse.down();
await page.hover('#drop-zone');
await page.mouse.up();
Waiting for Elements
await page.waitForSelector('.dynamic-content');
await page.waitForSelector('.modal', { state: 'visible' });
await page.waitForSelector('.loading-spinner', { state: 'hidden' });
await page.waitForSelector('.slow-element', { timeout: 10000 });
Element State Checks
const isVisible = await page.isVisible('.element');
const isEnabled = await page.isEnabled('button.submit');
const isEditable = await page.isEditable('input[name="email"]');
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
await page.click('button[role="button"]');
await page.click('[role="menuitem"]');
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByRole('textbox', { name: 'Email' }).fill('user@example.com');
Using Labels
await page.getByLabel('Username').fill('john.doe');
await page.getByLabel('Password').fill('SecurePass123');
await page.getByLabel('Remember me').check();
Using Accessible Names
await page.getByText('Welcome to our site').click();
await page.getByTitle('Close dialog').click();
await page.getByAltText('Company logo').click();
Best Practices
Error Handling
try {
await page.waitForSelector('.element', { timeout: 5000 });
await page.click('.element');
} catch (error) {
console.error('Element not found:', error.message);
}
Waiting Strategies
await page.waitForLoadState('networkidle');
await page.waitForSelector('.content');
Selector Best Practices
Performance Optimization
const submitButton = page.locator('button[type="submit"]');
await submitButton.waitFor();
await submitButton.click();
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
await page.click('button[type="submit"]');
const errorMessage = await page.textContent('.error-message');
expect(errorMessage).toContain('required');
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
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.