← Back to Docs

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");
}

Multi-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.