Browser Automation Recipes
Copy-paste scripts for common tasks. Use with the Playground or POST /v1/run.
Tip: For AI agents, consider /v1/run (JSON steps) instead of /v1/run (Playwright code). See Structured Steps API for simpler, safer agent integration.
Scroll Infinite Loading Page
Scrolls down to load more content, useful for social feeds and product listings.
// Scroll to load infinite content
await page.goto("https://example.com/feed");
// Scroll down 5 times, waiting for content to load
for (let i = 0; i < 5; i++) {
await page.evaluate(() => {
window.scrollTo(0, document.body.scrollHeight);
});
// Wait for new content to load
await page.waitForTimeout(1500);
console.log(`Scroll ${i + 1} complete`);
}
// Scroll back to top for full screenshot
await page.evaluate(() => window.scrollTo(0, 0));
await page.waitForTimeout(500);
await saveScreenshot("infinite-scroll-loaded", { fullPage: true });Wait for SPA to Hydrate
Waits for React/Vue/Angular apps to fully load before screenshotting.
// Wait for SPA framework to hydrate
await page.goto("https://spa-app.com", {
waitUntil: "domcontentloaded"
});
// Wait for network to settle (no pending requests)
await page.waitForLoadState("networkidle");
// Optionally wait for specific content
await page.waitForSelector("[data-loaded='true']", {
timeout: 10000
}).catch(() => {
console.log("Data-loaded attribute not found, continuing...");
});
// Extra safety wait for JS execution
await page.waitForTimeout(1000);
await saveScreenshot("spa-loaded");Login and Screenshot Dashboard
Logs into a site and captures the authenticated page.
// Login flow
await page.goto("https://app.example.com/login");
// Fill credentials
await page.fill("input[name='email']", "user@example.com");
await page.fill("input[name='password']", "your-password");
// Click submit and wait for navigation
await Promise.all([
page.waitForNavigation({ waitUntil: "networkidle" }),
page.click("button[type='submit']")
]);
// Verify we're logged in
const url = page.url();
if (url.includes("dashboard")) {
console.log("Login successful!");
await saveScreenshot("dashboard");
} else {
console.error("Login may have failed, current URL:", url);
await saveScreenshot("login-result");
}Extract All Links from Page
Captures screenshot and logs all links found on the page.
// Extract all links from a page
await page.goto("https://example.com");
const links = await page.evaluate(() => {
const anchors = document.querySelectorAll("a[href]");
return Array.from(anchors).map(a => ({
text: a.textContent?.trim().substring(0, 50),
href: a.href
}));
});
console.log("Found", links.length, "links:");
links.forEach((link, i) => {
console.log(`${i + 1}. ${link.text || "(no text)"} -> ${link.href}`);
});
await saveScreenshot("page-with-links");
// Links will be in console.json artifactMulti-Viewport Screenshots
Takes screenshots at desktop, tablet, and mobile sizes.
// Responsive screenshots at multiple viewports
const viewports = [
{ name: "desktop", width: 1920, height: 1080 },
{ name: "tablet", width: 768, height: 1024 },
{ name: "mobile", width: 375, height: 667 }
];
await page.goto("https://example.com");
for (const vp of viewports) {
await page.setViewportSize({ width: vp.width, height: vp.height });
await page.waitForTimeout(500); // Let layout settle
await saveScreenshot(vp.name);
console.log(`Captured ${vp.name} (${vp.width}x${vp.height})`);
}Wait for Dynamic Content
Waits for specific elements to appear before capturing.
// Wait for dynamic content to load
await page.goto("https://example.com/dashboard");
// Wait for loading spinner to disappear
await page.waitForSelector(".loading-spinner", { state: "hidden", timeout: 30000 })
.catch(() => console.log("No spinner found"));
// Wait for actual content to appear
await page.waitForSelector(".dashboard-content", { timeout: 30000 });
// Wait for specific data
await page.waitForSelector(".user-stats:not(:empty)", { timeout: 10000 })
.catch(() => console.log("Stats not loaded"));
await saveScreenshot("dashboard-loaded");Screenshot Specific Element
Captures just a specific element, not the whole page.
// Screenshot a specific element directly
await page.goto("https://example.com");
// Wait for the element you want to capture
const element = await page.waitForSelector("h1");
// Take a screenshot of just that element
// The screenshot is automatically saved to artifacts
await element.screenshot({ path: "heading.png" });
console.log("Captured element screenshot!");
// You can also use page.screenshot with clip option:
const box = await element.boundingBox();
if (box) {
await page.screenshot({
path: "element-clipped.png",
clip: {
x: box.x,
y: box.y,
width: box.width,
height: box.height
}
});
console.log("Captured clipped screenshot at:", JSON.stringify(box));
}Handle Popup/Modal
Closes popups and modals that appear on page load.
// Handle popups and modals
await page.goto("https://example.com");
// Common popup/modal close selectors
const closeSelectors = [
"[aria-label='Close']",
".modal-close",
".popup-close",
"button.close",
".dismiss",
"text=No thanks",
"text=Maybe later",
"text=Close"
];
// Wait a moment for popup to appear
await page.waitForTimeout(2000);
// Try to close any popup
for (const selector of closeSelectors) {
try {
const element = await page.$(selector);
if (element && await element.isVisible()) {
await element.click();
console.log("Closed popup with:", selector);
await page.waitForTimeout(500);
break;
}
} catch (e) {
// Continue to next selector
}
}
await saveScreenshot("after-popup-closed");Block Ads and Trackers
Blocks common ad and tracking scripts for cleaner screenshots.
// Block ads and trackers for cleaner screenshots
await page.route("**/*", (route) => {
const url = route.request().url();
const blocklist = [
"googleadservices.com",
"doubleclick.net",
"googlesyndication.com",
"facebook.net",
"analytics.google.com",
"hotjar.com",
"intercom.io"
];
if (blocklist.some(domain => url.includes(domain))) {
console.log("Blocked:", url.substring(0, 60));
route.abort();
} else {
route.continue();
}
});
await page.goto("https://example.com");
await saveScreenshot("ad-free");Capture Full Page with Lazy Images
Forces all lazy-loaded images to load before capturing a full-page screenshot.
// Scroll to load lazy images
await page.goto("https://example.com");
// Get total page height
const scrollHeight = await page.evaluate(() => document.body.scrollHeight);
const viewportHeight = 800;
// Scroll through the entire page to trigger lazy loading
console.log("Scrolling to load lazy images...");
for (let y = 0; y < scrollHeight; y += viewportHeight) {
await page.evaluate((scrollY) => window.scrollTo(0, scrollY), y);
await page.waitForLoadState("networkidle").catch(() => {});
}
// Wait for all images to fully load
await page.evaluate(async () => {
const images = document.querySelectorAll("img");
await Promise.all(Array.from(images).map(img => {
if (img.complete) return Promise.resolve();
return new Promise(resolve => {
img.onload = resolve;
img.onerror = resolve;
setTimeout(resolve, 5000);
});
}));
});
// Scroll back to top
await page.evaluate(() => window.scrollTo(0, 0));
const imageCount = await page.evaluate(() =>
document.querySelectorAll("img").length
);
console.log("Total images found:", imageCount);
await saveScreenshot("full-page-lazy-loaded", { fullPage: true });Dark Mode vs Light Mode
Takes screenshots in both light and dark mode to compare how a site renders.
// Compare light and dark mode
await page.goto("https://github.com");
// Capture light mode (default)
await page.emulateMedia({ colorScheme: "light" });
await page.waitForTimeout(500);
await saveScreenshot("light-mode");
console.log("Captured light mode");
// Capture dark mode
await page.emulateMedia({ colorScheme: "dark" });
await page.waitForTimeout(500);
await saveScreenshot("dark-mode");
console.log("Captured dark mode");Extract and Screenshot Data Tables
Extracts structured data from HTML tables and captures a screenshot.
// Extract table data
await page.goto("https://en.wikipedia.org/wiki/List_of_countries_by_population_(United_Nations)");
// Wait for table to load
await page.waitForSelector("table.wikitable");
// Extract table data
const tableData = await page.evaluate(() => {
const table = document.querySelector("table.wikitable");
if (!table) return null;
const headers = Array.from(table.querySelectorAll("th"))
.slice(0, 5)
.map(th => th.textContent?.trim().replace(/\n/g, " ") || "");
const rows = Array.from(table.querySelectorAll("tbody tr"))
.slice(0, 10)
.map(row => {
const cells = Array.from(row.querySelectorAll("td, th"));
return cells.slice(0, 5).map(cell =>
cell.textContent?.trim().substring(0, 50) || ""
);
});
return { headers, rowCount: rows.length };
});
if (tableData) {
console.log("Headers:", JSON.stringify(tableData.headers));
console.log("Rows extracted:", tableData.rowCount);
}
await saveScreenshot("table-data");Mock API Responses
Mocks API responses to capture specific UI states like empty or error states.
// Mock API responses for testing UI states
await page.route("**/api/user", route => {
route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
id: 1,
name: "Test User",
email: "test@example.com"
})
});
});
console.log("Mocking /api/user endpoint");
await page.goto("https://example.com");
await page.waitForLoadState("networkidle");
await saveScreenshot("mocked-success");
// Now test error state
await page.route("**/api/user", route => {
route.fulfill({
status: 500,
contentType: "application/json",
body: JSON.stringify({ error: "Server error" })
});
});
await page.reload();
await page.waitForLoadState("networkidle");
await saveScreenshot("error-state");
console.log("Captured error state");Capture Performance Metrics
Captures Core Web Vitals and performance metrics alongside a screenshot.
// Capture performance metrics
await page.goto("https://example.com", { waitUntil: "networkidle" });
const metrics = await page.evaluate(() => {
const nav = performance.getEntriesByType("navigation")[0];
const paint = performance.getEntriesByType("paint");
const resources = performance.getEntriesByType("resource");
return {
ttfb: Math.round(nav.responseStart - nav.requestStart),
domLoaded: Math.round(nav.domContentLoadedEventEnd - nav.navigationStart),
loadComplete: Math.round(nav.loadEventEnd - nav.navigationStart),
fcp: Math.round(paint.find(p => p.name === "first-contentful-paint")?.startTime || 0),
resourceCount: resources.length,
transferKB: Math.round(resources.reduce((sum, r) => sum + (r.transferSize || 0), 0) / 1024)
};
});
console.log("=== Performance Metrics ===");
console.log("Time to First Byte:", metrics.ttfb, "ms");
console.log("First Contentful Paint:", metrics.fcp, "ms");
console.log("DOM Content Loaded:", metrics.domLoaded, "ms");
console.log("Full Load:", metrics.loadComplete, "ms");
console.log("Resources:", metrics.resourceCount);
console.log("Transfer Size:", metrics.transferKB, "KB");
await saveScreenshot("performance-capture");Capture Form Validation States
Captures different form validation states - empty, partial, and complete.
// Capture form states - adapt selectors to your form
await page.goto("https://www.w3schools.com/html/html_forms.asp");
// Wait for form to load
await page.waitForSelector("form");
await saveScreenshot("form-01-initial");
console.log("Captured: Initial form state");
// Fill first input field
const inputs = await page.$$("input[type='text']");
if (inputs.length > 0) {
await inputs[0].fill("John Doe");
await saveScreenshot("form-02-partial");
console.log("Captured: Partial form");
}
// Fill more fields if available
if (inputs.length > 1) {
await inputs[1].fill("jane@example.com");
await saveScreenshot("form-03-complete");
console.log("Captured: Complete form");
}
// Note: Adapt these selectors to your specific form:
// await page.fill("input[name='email']", "user@example.com");
// await page.selectOption("select[name='country']", "US");
// await page.check("input[type='checkbox']");
// await page.click("button[type='submit']");Extract Open Graph Meta Tags
Extracts Open Graph meta tags to preview how a page will appear when shared.
// Extract Open Graph data for social preview
await page.goto("https://github.com/microsoft/vscode");
const metaData = await page.evaluate(() => {
const getMeta = (selector) => {
const el = document.querySelector(selector);
return el?.getAttribute("content") || null;
};
return {
ogTitle: getMeta("meta[property='og:title']"),
ogDescription: getMeta("meta[property='og:description']"),
ogImage: getMeta("meta[property='og:image']"),
ogUrl: getMeta("meta[property='og:url']"),
twitterCard: getMeta("meta[name='twitter:card']"),
title: document.title,
description: getMeta("meta[name='description']")
};
});
console.log("=== Open Graph Data ===");
console.log("Title:", metaData.ogTitle || metaData.title);
console.log("Description:", (metaData.ogDescription || "N/A").substring(0, 100));
console.log("Image:", metaData.ogImage || "N/A");
console.log("Twitter Card:", metaData.twitterCard || "N/A");
await saveScreenshot("page-preview");Quick Accessibility Audit
Performs basic accessibility checks and highlights potential issues.
// Quick accessibility audit
await page.goto("https://example.com");
const a11y = await page.evaluate(() => {
const issues = [];
// Images without alt text
const noAlt = document.querySelectorAll("img:not([alt])");
if (noAlt.length > 0) {
issues.push(`${noAlt.length} images missing alt text`);
}
// Missing lang attribute
if (!document.documentElement.hasAttribute("lang")) {
issues.push("Missing lang attribute on <html>");
}
// Heading hierarchy
const h1s = document.querySelectorAll("h1").length;
if (h1s === 0) issues.push("No H1 heading found");
if (h1s > 1) issues.push(`Multiple H1 headings (${h1s} found)`);
return {
issues,
stats: {
totalImages: document.querySelectorAll("img").length,
withAlt: document.querySelectorAll("img[alt]").length,
headings: document.querySelectorAll("h1,h2,h3,h4,h5,h6").length,
lang: document.documentElement.getAttribute("lang"),
title: document.title
}
};
});
console.log("=== Accessibility Audit ===");
console.log("Page Title:", a11y.stats.title);
console.log("Language:", a11y.stats.lang || "NOT SET");
console.log("Images:", a11y.stats.withAlt, "/", a11y.stats.totalImages, "have alt");
console.log("Headings:", a11y.stats.headings);
if (a11y.issues.length > 0) {
console.log("\n=== Issues ===");
a11y.issues.forEach(i => console.log("- " + i));
} else {
console.log("\nNo major issues found!");
}
await saveScreenshot("a11y-audit");Multi-Page Crawl with Screenshots
Crawls multiple pages from a starting URL and captures screenshots of each.
// Multi-page crawl
await page.goto("https://example.com");
const startUrl = "https://example.com";
const visited = new Set();
const toVisit = [startUrl];
const maxPages = 3;
while (toVisit.length > 0 && visited.size < maxPages) {
const url = toVisit.shift();
if (visited.has(url)) continue;
visited.add(url);
console.log(`Visiting (${visited.size}/${maxPages}): ${url}`);
try {
await page.goto(url, { waitUntil: "networkidle", timeout: 30000 });
const safeName = url.replace(/https?:\/\//, "")
.replace(/[^a-zA-Z0-9]/g, "-")
.substring(0, 50);
await saveScreenshot(`page-${visited.size}-${safeName}`);
// Find internal links
const links = await page.evaluate((baseUrl) => {
const base = new URL(baseUrl);
return Array.from(document.querySelectorAll("a[href]"))
.map(a => a.href)
.filter(href => {
try {
const u = new URL(href);
return u.hostname === base.hostname && !href.includes("#");
} catch { return false; }
})
.slice(0, 5);
}, startUrl);
links.forEach(link => {
if (!visited.has(link)) toVisit.push(link);
});
} catch (e) {
console.error("Error:", e.message.substring(0, 80));
}
}
console.log("\nCrawl complete. Pages visited:", visited.size);Generate PDF from Page
Captures a page as a PDF document with customizable options.
// Generate PDF from webpage
await page.goto("https://example.com");
// Wait for content to load
await page.waitForLoadState("networkidle");
// Generate PDF with default settings (Letter size)
await page.pdf({ path: "page.pdf" });
console.log("Generated default PDF");
// Generate PDF with custom options
await page.pdf({
path: "custom.pdf",
format: "A4",
landscape: true,
margin: {
top: "1in",
bottom: "1in",
left: "0.5in",
right: "0.5in"
},
printBackground: true
});
console.log("Generated custom A4 landscape PDF");
// Generate PDF of specific page range
await page.pdf({
path: "header-only.pdf",
scale: 0.8,
pageRanges: "1"
});
console.log("Generated scaled PDF");Try These Recipes
Test scripts in the Playground, then integrate via API.