Skip to content
ExponaBLOG
PLAYWRIGHT & BROWSER AUTOMATION · DAY 063 / 100

Browser automation beyond testing: a step-by-step guide

Scrape, generate and automate tasks.

Expona Admin8 min read

We've spent the last 60+ days mastering Playwright for testing, but the real power of browser automation extends far beyond the test suite. Browser automation beyond testing: a step-by-step guide unlocks capabilities that can transform how your team handles content generation, administrative workflows, and repetitive tasks—all while staying within ethical and legal boundaries. In this installment, we'll walk through practical patterns that go beyond verification into automation territory.

Generate PDFs and screenshots

One of the most immediate wins is automated content generation. Whether you're building invoices, reports, or promotional materials, Playwright's PDF and screenshot APIs eliminate manual steps that waste time and introduce human error.

Consider a scenario where your SaaS platform needs to generate monthly billing statements in PDF format. Rather than relying on a dedicated PDF library or manual export process, you can render a styled HTML template in Playwright and capture it directly.

typescript

import { chromium } from 'playwright';

async function generateBillingPDF(userId: string, month: string) {
  const browser = await chromium.launch();
  const page = await browser.newPage();

  // Navigate to your internal billing template page
  await page.goto(`https://internal.example.com/invoice/${userId}/${month}`);

  // Wait for dynamic content to load
  await page.waitForLoadState('networkidle');

  // Generate PDF with custom sizing
  const pdfBuffer = await page.pdf({
    format: 'A4',
    margin: { top: '1cm', right: '1cm', bottom: '1cm', left: '1cm' },
    printBackground: true,
  });

  // Save or upload the buffer
  await saveToStorage(pdfBuffer, `invoice-${userId}-${month}.pdf`);

  await browser.close();
}

Screenshots work similarly. Imagine you're building a social media management tool that needs to preview how content will appear across different platforms. Playwright can capture viewport-specific screenshots automatically:

typescript

async function capturePreviewsAcrossDevices(url: string) {
  const browser = await chromium.launch();
  const devices = [
    { name: 'iPhone 12', viewport: { width: 390, height: 844 } },
    { name: 'iPad', viewport: { width: 1024, height: 1366 } },
    { name: 'Desktop', viewport: { width: 1920, height: 1080 } },
  ];

  const previews = [];

  for (const device of devices) {
    const page = await browser.newPage({ viewport: device.viewport });
    await page.goto(url);
    await page.waitForLoadState('networkidle');

    const screenshot = await page.screenshot();
    previews.push({
      device: device.name,
      image: screenshot,
    });

    await page.close();
  }

  await browser.close();
  return previews;
}

This pattern is particularly valuable in e-commerce and travel platforms where visual consistency across devices matters. You generate these assets on-demand or batch-process them during off-peak hours, then cache the results for quick delivery to users.

Automate internal admin tasks

Browser automation shines when applied to repetitive administrative workflows. We're talking about tasks that currently consume hours per week: bulk data entry, status updates, report compilation, or cross-system synchronization.

Let's say your team manages a CRM that doesn't have an API for bulk operations. Manually updating customer statuses or adding notes across dozens of records is tedious and error-prone. Playwright can automate this:

typescript

async function bulkUpdateCustomerStatus(
  customers: Array<{ id: string; status: string }>,
  crmBaseUrl: string
) {
  const browser = await chromium.launch();
  const page = await browser.newPage();

  // Assuming you handle authentication separately
  await page.goto(`${crmBaseUrl}/login`);
  await page.fill('input[name="email"]', process.env.CRM_EMAIL);
  await page.fill('input[name="password"]', process.env.CRM_PASSWORD);
  await page.click('button:has-text("Sign in")');
  await page.waitForLoadState('networkidle');

  for (const customer of customers) {
    try {
      // Navigate to customer detail page
      await page.goto(`${crmBaseUrl}/customers/${customer.id}`);
      await page.waitForLoadState('networkidle');

      // Update status dropdown
      await page.click('[data-testid="status-dropdown"]');
      await page.click(`text=${customer.status}`);

      // Save changes
      await page.click('button:has-text("Save")');

      // Wait for confirmation
      await page.waitForSelector('[data-testid="success-toast"]', { timeout: 5000 });

      console.log(`Updated customer ${customer.id} to ${customer.status}`);
    } catch (error) {
      console.error(`Failed to update customer ${customer.id}:`, error);
    }
  }

  await browser.close();
}

Another common scenario: consolidating reports from multiple internal dashboards. If your finance team needs to pull data from different systems and compile it into a spreadsheet weekly, Playwright can extract and aggregate that automatically.

typescript

async function aggregateWeeklyMetrics() {
  const browser = await chromium.launch();
  const page = await browser.newPage();
  const metrics = {};

  const dashboards = [
    { name: 'sales', url: 'https://dashboard1.example.com/sales' },
    { name: 'support', url: 'https://dashboard2.example.com/tickets' },
    { name: 'product', url: 'https://analytics.example.com/events' },
  ];

  for (const dashboard of dashboards) {
    await page.goto(dashboard.url);
    await page.waitForLoadState('networkidle');

    // Extract specific metrics based on dashboard structure
    const value = await page.textContent('[data-metric="weekly-total"]');
    metrics[dashboard.name] = value;
  }

  await browser.close();
  return metrics;
}

The key benefit here is consistency and audit trails. Instead of a team member manually copying numbers from different systems, the automation runs at the same time every week, in the same way, with logs you can review.

Respect site terms and rate limits

Before you automate against any website—internal or external—understand the legal and technical boundaries. Scraping public websites without permission violates many terms of service. Even automation of your own internal systems requires care around server load and rate limiting.

Always check the target site's robots.txt and terms of service. Many sites explicitly prohibit automated access. If you're building a tool that scrapes competitor pricing or market data, confirm you have explicit permission or are operating in a legally gray area you're willing to defend.

For your own systems, implement rate limiting and backoff strategies:

typescript

async function automateWithRateLimit(
  tasks: Array<() => Promise>,
  requestsPerSecond: number = 1
) {
  const delayMs = 1000 / requestsPerSecond;

  for (const task of tasks) {
    const startTime = Date.now();
    await task();
    const elapsedMs = Date.now() - startTime;

    if (elapsedMs < delayMs) {
      await new Promise(resolve => setTimeout(resolve, delayMs - elapsedMs));
    }
  }
}

// Usage
await automateWithRateLimit(
  customers.map(c => () => bulkUpdateCustomerStatus([c], baseUrl)),
  2 // 2 requests per second
);

Additionally, add retry logic with exponential backoff for transient failures:

typescript

async function retryWithBackoff(
  fn: () => Promise,
  maxAttempts: number = 3
): Promise {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (error) {
      if (attempt === maxAttempts) throw error;
      const delayMs = Math.pow(2, attempt - 1) * 1000;
      await new Promise(resolve => setTimeout(resolve, delayMs));
    }
  }
}

Run headless on servers

To make browser automation production-ready, you'll run it headless on servers—no GUI, minimal overhead. Playwright's headless mode is the default and works well in containerized environments like Docker or Lambda.

For reliability, configure resource constraints and timeouts:

typescript

import { chromium } from 'playwright';

async function createHeadlessBrowser() {
  const browser = await chromium.launch({
    headless: true,
    args: [
      '--disable-dev-shm-usage', // Avoid /dev/shm usage in Docker
      '--disable-gpu', // Reduce memory footprint
      '--no-first-run',
      '--no-default-browser-check',
    ],
  });

  return browser;
}

// In a containerized environment, pair with resource limits:
// Docker: --memory=1g --cpus=0.5
// Node memory flag: node --max-old-space-size=512 script.js

For scheduled tasks (like generating reports nightly), consider using a job queue or cron wrapper:

typescript

// Using a simple approach with process scheduling
import { CronJob } from 'cron';

const job = new CronJob('0 2 * * *', async () => {
  try {
    console.log('Starting nightly PDF generation...');
    await generateBillingPDF('all', 'current');
    console.log('Nightly generation complete');
  } catch (error) {
    console.error('Nightly job failed:', error);
    // Send alert to your monitoring system
  }
});

job.start();

For higher scale, use a proper job queue like Bull or AWS Lambda. Lambda works especially well because Playwright can be bundled as a layer, and the automation runs only when triggered, scaling automatically.

Common mistakes

  • Ignoring element stability. Clicking an element immediately after page navigation often fails because the DOM hasn't settled. Always use waitForLoadState or target-specific waits rather than arbitrary sleeps.
  • Not handling authentication securely. Hardcoding credentials in code is dangerous. Use environment variables, secrets managers, or OAuth tokens stored securely.
  • Skipping error handling and logging. Automation running unattended will fail silently without proper logging. Log every significant action and capture stack traces on error.
  • Running browser instances forever. Always close browsers and pages to prevent resource leaks. Use try/finally blocks or async context managers.
  • Assuming the UI won't change. CSS selectors break when design updates ship. Prefer data attributes or semantic HTML queries over brittle class-based selectors.

Key takeaways

  • Browser automation extends far beyond testing into PDF generation, screenshots, and workflow automation—saving your team hours of manual work.
  • Always verify legal and ethical boundaries: check terms of service, respect robots.txt, and implement rate limiting for any external site interaction.
  • Use authenticated internal systems to automate repetitive admin tasks, data consolidation, and bulk operations where no API exists.
  • Run Playwright headless in production with proper resource constraints, timeouts, error handling, and logging to ensure reliability at scale.
  • Prefer explicit waits, data attributes in selectors, and environment-based configuration over hardcoded credentials and brittle DOM queries.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles