Browser Automation with Playwriter
Important Rules
- Be autonomous — Complete tasks independently. Only ask user for help when absolutely necessary (e.g., CAPTCHA, 2FA, login credentials).
- Create your own pages — Use
context.newPage()to create pages you control, don't rely on user having tabs open. - Handle errors gracefully — If something fails, try alternative approaches before giving up.
Prerequisites & Setup
Playwriter requires two components:
- CLI tool —
playwritercommand - Chrome extension — Playwriter extension installed and enabled on the tab to control
Check environment
# Check if playwriter is installed
if ! command -v playwriter &> /dev/null; then
echo "❌ playwriter not installed"
echo " Run: npm install -g playwriter@latest"
else
echo "✅ playwriter installed"
fi
Installation
# Install CLI globally
npm install -g playwriter@latest
# Or use without installing (always use @latest for first command)
npx playwriter@latest session new
bunx playwriter@latest session new
Chrome Extension Setup
- Install from Chrome Web Store: https://chromewebstore.google.com/detail/playwriter-mcp/jfeammnjpkecdekppnclgkkffahnhfhe
- Important: Click the Playwriter extension icon on any tab you want to control
- If you get "extension is not connected" or "no browser tabs have Playwriter enabled", the agent should create a new page with
context.newPage()instead of asking user
Troubleshooting
| Problem | Solution |
|---|---|
playwriter: command not found |
Run npm install -g playwriter@latest |
| "extension is not connected" | Create page with context.newPage() or ask user to click extension icon |
| "no browser tabs have Playwriter enabled" | Create page with context.newPage() |
| Connection issues | Run playwriter session reset <id> to reset session |
| Code execution timeout | Reset session and retry, or break into smaller steps |
Quick Start
# Get a session ID first
playwriter session new
# => 1
# Execute code with your session
playwriter -s 1 -e "state.page = await context.newPage(); await state.page.goto('https://example.com')"
playwriter -s 1 -e "console.log(await accessibilitySnapshot({ page: state.page }))"
playwriter -s 1 -e "await state.page.screenshot({ path: 'shot.png', scale: 'css' })"
Full Documentation
Run playwriter skill to get complete, up-to-date documentation including:
- Session management
- Context variables (
state,page,context) - Best practices and rules
- Accessibility snapshots and screenshots
- Selector strategies
- Working with pages, navigation, popups, downloads
- Utility functions
- Network interception for API scraping
playwriter skill
Core Concepts
Session Management
Each session runs in an isolated sandbox with its own state object:
playwriter session new # Get new session ID
playwriter session list # List active sessions
playwriter session reset <id> # Reset if connection is stale
Always use -s <sessionId> to persist state across commands.
Context Variables
state— object persisted between calls within your sessionpage— default page (if user enabled extension on a tab)context— browser context, access all pages viacontext.pages()require— load Node.js modules
Execute Code
playwriter -s <sessionId> -e "<code>"
Default timeout is 10 seconds. Increase with --timeout <ms>.
Key Rules
- Use your own session — prevents interference from other agents
- Store pages in state —
state.myPage = await context.newPage() - Never close browser/context — only close pages you created
- Check state after actions — verify page state after clicking/submitting
- Clean up listeners —
page.removeAllListeners()at end of tasks
Common Patterns
Create your own page (recommended)
state.page = await context.newPage();
await state.page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
await waitForPageLoad({ page: state.page, timeout: 5000 });
Check page state after actions
console.log('url:', page.url()); console.log(await accessibilitySnapshot({ page }).then(x => x.split('\n').slice(0, 30).join('\n')));
Accessibility snapshot with aria-ref
// Get snapshot
console.log(await accessibilitySnapshot({ page }));
// Click using aria-ref (no quotes around ref value)
await page.locator('aria-ref=e13').click();
// Search for specific elements
const snapshot = await accessibilitySnapshot({ page, search: /button|submit/i });
Screenshots
// Always use scale: 'css' for proper sizing
await page.screenshot({ path: 'shot.png', scale: 'css' });
// Visual screenshot with element labels (best for understanding page layout)
await screenshotWithAccessibilityLabels({ page });
Click elements reliably
// Method 1: Using locator with force click
await page.locator('button').first().click({ force: true });
// Method 2: Using page.evaluate for stubborn elements
await page.evaluate(() => {
document.querySelector('[data-testid="like"]').click();
});
// Method 3: Filter and find specific elements
const item = page.locator('article').filter({ hasText: 'target text' }).first();
await item.locator('button').click();
Network interception for API scraping
state.responses = [];
page.on('response', async res => {
if (res.url().includes('/api/')) {
try { state.responses.push({ url: res.url(), body: await res.json() }); } catch {}
}
});
Debugging
# View relay server logs
playwriter logfile
# Reset stale session
playwriter session reset <id>
Web Interaction Tips
- Finding interactive elements: Use
data-testidattributes when available, fall back toaria-labelor text content - Toggle states: Buttons often change attributes when toggled (e.g.,
aria-labelchanges from "Like" to "Liked") - Scrolling: Use
page.mouse.wheel(0, pixels)to load dynamic content - Filtering content: Use
page.locator().filter({ hasText: 'pattern' })to find specific items - Handling timeouts: If
click()times out, tryclick({ force: true })or usepage.evaluate()to click directly in the DOM - Content discovery: Use
getCleanHTML({ locator: page, search: /pattern/i })to quickly find elements matching a pattern