Documentation Index
Fetch the complete documentation index at: https://mintlify.com/remorses/playwriter/llms.txt
Use this file to discover all available pages before exploring further.
Follow these best practices to write reliable, maintainable browser automation scripts.
Session and state management
Always use your own session
Pass -s <id> to all commands. Using the same session preserves your state between calls:
playwriter session new # outputs: 1
playwriter -s 1 -e 'state.counter = 0'
playwriter -s 1 -e 'state.counter++; console.log(state.counter)' # prints 1
Store your own page in state
Pages are shared across all sessions. Create your own page to avoid interference:
# First execute call: get or create your page
playwriter -s 1 -e 'state.page = context.pages().find(p => p.url() === "about:blank") ?? await context.newPage(); await state.page.goto("https://example.com")'
# All subsequent calls: use state.page
playwriter -s 1 -e 'await state.page.click("button")'
Handle page closures gracefully
Users may close tabs accidentally. Check before using:
playwriter -s 1 -e "$(cat <<'EOF'
if (!state.page || state.page.isClosed()) {
state.page = context.pages().find(p => p.url() === 'about:blank') ?? await context.newPage();
}
await state.page.goto('https://example.com');
EOF
)"
Interaction feedback loop
Every browser interaction should follow an observe → act → observe loop:
- Observe - Print URL and take snapshot
- Check - Verify page is ready
- Act - Perform one action
- Observe again - Verify the action’s effect
- Repeat - Continue until task is complete
# 1. Observe - always print URL first
playwriter -s 1 -e "$(cat <<'EOF'
state.page = context.pages().find(p => p.url() === 'about:blank') ?? await context.newPage();
await state.page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
console.log('URL:', state.page.url());
await snapshot({ page: state.page }).then(console.log);
EOF
)"
# 2. Act - click a button
playwriter -s 1 -e 'await state.page.click("button#submit")'
# 3. Observe again - verify what happened
playwriter -s 1 -e "$(cat <<'EOF'
console.log('URL:', state.page.url());
await snapshot({ page: state.page }).then(console.log);
EOF
)"
Quoting and escaping
Always use single quotes for -e
Single quotes prevent bash from interpreting $, backticks, and backslashes:
# GOOD
playwriter -s 1 -e 'const price = text.match(/\$[\d.]+/)'
# BAD - bash corrupts the regex
playwriter -s 1 -e "const price = text.match(/\$[\d.]+/)"
Use heredoc for complex code
When code contains mixed quotes, regex, or multiple lines:
playwriter -s 1 -e "$(cat <<'EOF'
const links = await state.page.$$eval('a', els => els.map(e => e.href));
const prices = html.match(/\$[\d.]+/g);
console.log(`Found ${links.length} links`);
EOF
)"
Choosing the right observation method
Use snapshot for most cases
Snapshots are fast, cheap, and text-based:
# Check if element appeared
playwriter -s 1 -e 'await snapshot({ page: state.page, search: /button|submit/i })'
# Verify action succeeded
playwriter -s 1 -e 'await snapshot({ page: state.page, search: /success|error/i })'
Use screenshot only for visual issues
Screenshots consume more tokens and are slower:
# GOOD: snapshot for text content
playwriter -s 1 -e 'await snapshot({ page: state.page, search: /price/i })'
# GOOD: screenshot for visual layout
playwriter -s 1 -e 'await screenshotWithAccessibilityLabels({ page: state.page })'
# BAD: screenshot just to check text
playwriter -s 1 -e 'await state.page.screenshot({ path: "check.png" })' # wasteful
Reserve page.evaluate for specific use cases
Don’t use page.evaluate() to inspect the DOM - use snapshot() instead:
# BAD: manual DOM inspection
playwriter -s 1 -e 'await state.page.evaluate(() => document.querySelector(".btn")?.className)'
# GOOD: snapshot shows all interactive elements
playwriter -s 1 -e 'await snapshot({ page: state.page, search: /btn/i })'
# GOOD: evaluate for state modification
playwriter -s 1 -e 'await state.page.evaluate(() => localStorage.clear())'
Multiple execute calls vs single call
Use multiple execute calls for complex logic - it helps understand intermediate state:
# GOOD: Multiple calls show progress
playwriter -s 1 -e 'await state.page.goto("https://example.com")'
playwriter -s 1 -e 'await snapshot({ page: state.page })'
playwriter -s 1 -e 'await state.page.click("button")'
playwriter -s 1 -e 'await snapshot({ page: state.page })'
# ACCEPTABLE: Single call for simple sequences
playwriter -s 1 -e 'await state.page.goto("https://example.com"); await state.page.click("button")'
Waiting and timeouts
Prefer proper waits over arbitrary timeouts
# GOOD: Wait for specific condition
playwriter -s 1 -e 'await state.page.waitForSelector(".content")'
playwriter -s 1 -e 'await waitForPageLoad({ page: state.page, timeout: 5000 })'
# ACCEPTABLE: Short timeout for non-deterministic events
playwriter -s 1 -e 'await state.page.waitForTimeout(1000)' # popup animation
# BAD: Long arbitrary timeout
playwriter -s 1 -e 'await state.page.waitForTimeout(5000)' # wasteful
Use waitForLoadState correctly
# GOOD
playwriter -s 1 -e 'await state.page.waitForLoadState("domcontentloaded")'
# BAD - times out if already loaded
playwriter -s 1 -e 'await state.page.waitForEvent("load")'
Selector best practices
Use snapshot locators directly
The snapshot output gives you ready-to-use locators:
# 1. Get snapshot
playwriter -s 1 -e 'await snapshot({ page: state.page })'
# Output: role=button[name="Submit"]
# 2. Use the locator directly
playwriter -s 1 -e 'await state.page.getByRole("button", { name: "Submit" }).click()'
Never invent selectors - always use what the snapshot provides.
Beware CSS text-transform
Snapshots show visual text, but DOM may differ:
# Snapshot shows: heading "NODE.JS"
# But DOM has: "Node.js"
# GOOD: Case-insensitive
playwriter -s 1 -e 'await state.page.getByRole("heading", { name: /node\.js/i }).click()'
# BAD: Case-sensitive
playwriter -s 1 -e 'await state.page.getByRole("heading", { name: "NODE.JS" }).click()'
Clean up listeners
Always remove event listeners when done:
# After using request/response listeners
playwriter -s 1 -e "$(cat <<'EOF'
state.page.removeAllListeners('request');
state.page.removeAllListeners('response');
EOF
)"
Navigation best practices
Use domcontentloaded for goto
playwriter -s 1 -e 'await state.page.goto("https://example.com", { waitUntil: "domcontentloaded" })'
playwriter -s 1 -e 'await waitForPageLoad({ page: state.page, timeout: 5000 })'
Always print URL after actions
Pages can redirect unexpectedly:
playwriter -s 1 -e 'await state.page.click("a.login"); console.log("URL:", state.page.url())'
Common patterns
Network interception
Store requests in state for analysis:
playwriter -s 1 -e "$(cat <<'EOF'
state.requests = [];
state.page.on('request', req => {
if (req.url().includes('/api/')) {
state.requests.push({ url: req.url(), method: req.method() });
}
});
EOF
)"
# Trigger actions
playwriter -s 1 -e 'await state.page.click("button")'
# Analyze captured data
playwriter -s 1 -e 'console.log(JSON.stringify(state.requests, null, 2))'
Authenticated fetches
Fetch from within page context to include session cookies:
playwriter -s 1 -e "$(cat <<'EOF'
const data = await state.page.evaluate(async (url) => {
const resp = await fetch(url);
return await resp.json();
}, 'https://example.com/api/protected');
console.log(JSON.stringify(data, null, 2));
EOF
)"
Working with iframes
playwriter -s 1 -e "$(cat <<'EOF'
const frame = await state.page.locator('iframe').contentFrame();
await snapshot({ frame });
EOF
)"
Things to avoid
Never close browser or context
# BAD - closes the user's Chrome
playwriter -s 1 -e 'await browser.close()'
playwriter -s 1 -e 'await context.close()'
Only close pages you created, and only if the user asks.
Never use bringToFront unless requested
# BAD - disruptive and unnecessary
playwriter -s 1 -e 'await state.page.bringToFront()'
You can interact with background pages.
Never use force clicks or dispatchEvent
# BAD - bypasses React/Vue handlers
playwriter -s 1 -e 'await state.page.click(".btn", { force: true })'
playwriter -s 1 -e 'await state.page.evaluate(() => document.querySelector(".btn").click())'
# GOOD - find the real interactive element
playwriter -s 1 -e 'await snapshot({ page: state.page, search: /button/i })'
playwriter -s 1 -e 'await state.page.getByRole("button", { name: "Submit" }).click()'
Never use newCDPSession directly
# BAD - doesn't work through Playwriter relay
playwriter -s 1 -e 'await state.page.context().newCDPSession()'
# GOOD - use getCDPSession utility
playwriter -s 1 -e 'state.cdp = await getCDPSession({ page: state.page })'
Debugging
When something doesn’t work:
- Print URL - Confirm you’re on the right page
- Take snapshot - See what’s actually there
- Check logs - Look for console errors
playwriter -s 1 -e 'console.log("URL:", state.page.url())'
playwriter -s 1 -e 'await snapshot({ page: state.page })'
playwriter -s 1 -e 'await getLatestLogs({ page: state.page, search: /error/i })'
For internal Playwriter errors, check the relay server logs: