Skip to main content

Basic Batch Operations

Batch Screenshots

import { BrowserTest } from 'browsertest-sdk';

const bt = new BrowserTest({
  apiKey: process.env.BROWSERTEST_API_KEY
});

// Basic batch screenshot
async function batchScreenshots() {
  const urls = [
    'https://example.com',
    'https://google.com',
    'https://github.com',
    'https://stackoverflow.com'
  ];

  const results = await bt.batch.screenshots(urls.map(url => ({ url })));

  console.log(`Processed ${results.successful}/${results.total} screenshots`);

  // Handle results
  results.results.forEach(result => {
    if (result.success) {
      console.log(`✅ ${result.url}: ${result.data?.meta.size} bytes`);
    } else {
      console.log(`❌ ${result.url}: ${result.error}`);
    }
  });

  return results;
}

Batch Tests

async function batchTests() {
  const testCases = [
    {
      instructions: 'Check if homepage loads correctly',
      url: 'https://example.com'
    },
    {
      instructions: 'Verify login form validation',
      url: 'https://example.com/login'
    },
    {
      instructions: 'Test search functionality',
      url: 'https://example.com/search'
    },
    {
      instructions: 'Check contact form submission',
      url: 'https://example.com/contact'
    }
  ];

  const results = await bt.batch.tests(testCases);

  console.log(`Test Results: ${results.successful}/${results.total} passed`);

  results.results.forEach((result, index) => {
    const testCase = testCases[index];
    if (result.success) {
      console.log(`✅ ${testCase.instructions.substring(0, 30)}...`);
    } else {
      console.log(`❌ ${testCase.instructions.substring(0, 30)}...: ${result.error}`);
    }
  });

  return results;
}

Advanced Batch Processing

Concurrent Processing with Progress Tracking

class BatchProcessor {
  constructor(bt, options = {}) {
    this.bt = bt;
    this.concurrency = options.concurrency || 3;
    this.onProgress = options.onProgress || (() => {});
    this.onComplete = options.onComplete || (() => {});
  }

  async processBatch(items, processor, options = {}) {
    const results = [];
    const inProgress = new Set();
    const completed = new Set();
    let nextIndex = 0;

    const processNext = async () => {
      if (nextIndex >= items.length) return;

      const index = nextIndex++;
      const item = items[index];

      inProgress.add(index);

      try {
        const result = await processor(item, index);
        results[index] = { success: true, data: result, item };
        completed.add(index);
        this.onProgress({
          completed: completed.size,
          total: items.length,
          current: index,
          result
        });
      } catch (error) {
        results[index] = { success: false, error: error.message, item };
        completed.add(index);
        this.onProgress({
          completed: completed.size,
          total: items.length,
          current: index,
          error: error.message
        });
      } finally {
        inProgress.delete(index);
      }
    };

    // Start initial batch
    const initialPromises = [];
    for (let i = 0; i < Math.min(this.concurrency, items.length); i++) {
      initialPromises.push(processNext());
    }

    // Process remaining items as previous ones complete
    while (completed.size < items.length) {
      await Promise.race(initialPromises);
      // Fill available slots
      while (inProgress.size < this.concurrency && nextIndex < items.length) {
        processNext();
      }
    }

    await Promise.all(initialPromises);

    const finalResults = {
      successful: results.filter(r => r.success).length,
      total: items.length,
      results,
      successRate: (results.filter(r => r.success).length / items.length) * 100
    };

    this.onComplete(finalResults);
    return finalResults;
  }
}

// Usage
const processor = new BatchProcessor(bt, {
  concurrency: 2,
  onProgress: (progress) => {
    console.log(`Progress: ${progress.completed}/${progress.total}`);
  },
  onComplete: (results) => {
    console.log(`Batch complete: ${results.successRate.toFixed(1)}% success rate`);
  }
});

const screenshotResults = await processor.processBatch(
  ['https://site1.com', 'https://site2.com', 'https://site3.com'],
  async (url) => {
    return bt.screenshot.take({ url, fullPage: true });
  }
);

Memory-Efficient Large Batch Processing

class MemoryEfficientBatchProcessor {
  constructor(bt) {
    this.bt = bt;
    this.chunkSize = 10; // Process 10 at a time
    this.delayBetweenChunks = 1000; // 1 second delay
  }

  async processLargeBatch(items, processor, options = {}) {
    const {
      chunkSize = this.chunkSize,
      delayBetweenChunks = this.delayBetweenChunks,
      onChunkComplete,
      onProgress
    } = options;

    const allResults = [];
    const startTime = Date.now();

    for (let i = 0; i < items.length; i += chunkSize) {
      const chunk = items.slice(i, i + chunkSize);
      const chunkStartTime = Date.now();

      console.log(`Processing chunk ${Math.floor(i / chunkSize) + 1}/${Math.ceil(items.length / chunkSize)} (${chunk.length} items)`);

      try {
        // Process chunk items concurrently
        const chunkPromises = chunk.map((item, index) => {
          const globalIndex = i + index;
          return processor(item, globalIndex).catch(error => ({
            success: false,
            error: error.message,
            item,
            index: globalIndex
          }));
        });

        const chunkResults = await Promise.allSettled(chunkPromises);

        const processedResults = chunkResults.map((result, index) => {
          const globalIndex = i + index;
          if (result.status === 'fulfilled') {
            return result.value;
          } else {
            return {
              success: false,
              error: result.reason.message,
              item: chunk[index],
              index: globalIndex
            };
          }
        });

        allResults.push(...processedResults);

        const chunkDuration = Date.now() - chunkStartTime;
        console.log(`Chunk completed in ${chunkDuration}ms`);

        if (onChunkComplete) {
          onChunkComplete({
            chunkIndex: Math.floor(i / chunkSize) + 1,
            results: processedResults,
            duration: chunkDuration
          });
        }

        if (onProgress) {
          onProgress({
            completed: allResults.length,
            total: items.length,
            progress: (allResults.length / items.length) * 100,
            currentChunk: Math.floor(i / chunkSize) + 1,
            totalChunks: Math.ceil(items.length / chunkSize)
          });
        }

        // Force garbage collection if available
        if (global.gc) {
          global.gc();
        }

      } catch (error) {
        console.error(`Chunk processing failed:`, error);
        // Add failed results for this chunk
        const failedResults = chunk.map((item, index) => ({
          success: false,
          error: error.message,
          item,
          index: i + index
        }));
        allResults.push(...failedResults);
      }

      // Delay between chunks to prevent overwhelming the API
      if (i + chunkSize < items.length) {
        await new Promise(resolve => setTimeout(resolve, delayBetweenChunks));
      }
    }

    const totalDuration = Date.now() - startTime;
    const successful = allResults.filter(r => r.success).length;

    return {
      successful,
      total: items.length,
      results: allResults,
      duration: totalDuration,
      throughput: items.length / (totalDuration / 1000), // items per second
      successRate: (successful / items.length) * 100
    };
  }
}

// Usage for large-scale screenshot processing
const largeProcessor = new MemoryEfficientBatchProcessor(bt);

const largeUrls = Array.from({ length: 100 }, (_, i) => `https://example-${i + 1}.com`);

const results = await largeProcessor.processLargeBatch(
  largeUrls,
  async (url, index) => {
    console.log(`Processing ${index + 1}/100: ${url}`);
    const result = await bt.screenshot.take({ url, fullPage: true });
    return {
      success: true,
      data: result,
      url,
      index
    };
  },
  {
    chunkSize: 5, // Process 5 at a time
    delayBetweenChunks: 2000, // 2 second delay between chunks
    onChunkComplete: (chunk) => {
      console.log(`Chunk ${chunk.chunkIndex} completed in ${chunk.duration}ms`);
    },
    onProgress: (progress) => {
      console.log(`Overall progress: ${progress.completed}/${progress.total} (${progress.progress.toFixed(1)}%)`);
    }
  }
);

console.log(`Large batch complete: ${results.successRate.toFixed(1)}% success rate, ${results.throughput.toFixed(2)} items/sec`);

Real-World Batch Scenarios

Website Monitoring Dashboard

class WebsiteMonitor {
  constructor(bt) {
    this.bt = bt;
    this.sites = new Map();
    this.monitoringInterval = null;
  }

  addSite(name, config) {
    this.sites.set(name, {
      ...config,
      lastScreenshot: null,
      lastCheck: null,
      status: 'unknown',
      consecutiveFailures: 0
    });
  }

  async performMonitoringCheck() {
    const sites = Array.from(this.sites.entries());
    const now = new Date();

    console.log(`Starting monitoring check for ${sites.length} sites at ${now.toISOString()}`);

    const results = await bt.batch.screenshots(
      sites.map(([name, site]) => ({
        url: site.url,
        fullPage: true,
        timeout: site.timeout || 30000
      }))
    );

    // Update site status
    results.results.forEach((result, index) => {
      const [siteName] = sites[index];
      const site = this.sites.get(siteName);

      if (result.success) {
        site.lastScreenshot = result.data;
        site.lastCheck = now;
        site.status = 'healthy';
        site.consecutiveFailures = 0;

        // Check for visual changes if we have a baseline
        if (site.baselineScreenshot) {
          const hasChanged = this.compareScreenshots(site.baselineScreenshot, result.data.screenshot);
          if (hasChanged) {
            this.alertVisualChange(siteName, site);
          }
        }
      } else {
        site.consecutiveFailures++;
        site.status = site.consecutiveFailures >= 3 ? 'down' : 'degraded';
        site.lastCheck = now;

        if (site.consecutiveFailures >= 3) {
          this.alertSiteDown(siteName, site, result.error);
        }
      }
    });

    return {
      timestamp: now,
      totalSites: sites.length,
      healthySites: results.successful,
      issues: results.total - results.successful,
      results: results.results
    };
  }

  compareScreenshots(baseline, current) {
    // Simple comparison - in reality you'd use image diffing
    return baseline.length !== current.length;
  }

  alertSiteDown(siteName, site, error) {
    console.error(`🚨 SITE DOWN: ${siteName} (${site.url})`);
    console.error(`Error: ${error}`);
    console.error(`Consecutive failures: ${site.consecutiveFailures}`);

    // Send alert (email, Slack, etc.)
    // sendAlert('site_down', { siteName, url: site.url, error });
  }

  alertVisualChange(siteName, site) {
    console.warn(`⚠️ VISUAL CHANGE: ${siteName} (${site.url})`);

    // Send alert for visual changes
    // sendAlert('visual_change', { siteName, url: site.url });
  }

  startMonitoring(intervalMinutes = 5) {
    const intervalMs = intervalMinutes * 60 * 1000;

    this.monitoringInterval = setInterval(async () => {
      try {
        await this.performMonitoringCheck();
      } catch (error) {
        console.error('Monitoring check failed:', error);
      }
    }, intervalMs);

    console.log(`Website monitoring started - checking every ${intervalMinutes} minutes`);

    // Perform initial check
    return this.performMonitoringCheck();
  }

  stopMonitoring() {
    if (this.monitoringInterval) {
      clearInterval(this.monitoringInterval);
      this.monitoringInterval = null;
      console.log('Website monitoring stopped');
    }
  }

  getStatus() {
    const sites = Array.from(this.sites.entries()).map(([name, site]) => ({
      name,
      url: site.url,
      status: site.status,
      lastCheck: site.lastCheck,
      consecutiveFailures: site.consecutiveFailures
    }));

    const healthy = sites.filter(s => s.status === 'healthy').length;
    const degraded = sites.filter(s => s.status === 'degraded').length;
    const down = sites.filter(s => s.status === 'down').length;

    return {
      totalSites: sites.length,
      healthy,
      degraded,
      down,
      uptime: healthy / sites.length * 100,
      sites
    };
  }
}

// Usage
const monitor = new WebsiteMonitor(bt);

// Add sites to monitor
monitor.addSite('main-website', {
  url: 'https://myapp.com',
  timeout: 30000
});

monitor.addSite('api-docs', {
  url: 'https://docs.myapp.com',
  timeout: 20000
});

monitor.addSite('blog', {
  url: 'https://blog.myapp.com',
  timeout: 25000
});

// Start monitoring
await monitor.startMonitoring(10); // Check every 10 minutes

// Get status
setInterval(() => {
  const status = monitor.getStatus();
  console.log(`Status: ${status.healthy}/${status.totalSites} healthy (${status.uptime.toFixed(1)}% uptime)`);
}, 60000); // Log every minute

Visual Regression Testing Suite

class VisualRegressionTester {
  constructor(bt) {
    this.bt = bt;
    this.baselines = new Map();
    this.threshold = 0.01; // 1% difference threshold
  }

  async createBaselines(pages) {
    console.log('Creating visual baselines...');

    const results = await bt.batch.screenshots(
      pages.map(page => ({
        url: page.url,
        fullPage: true,
        width: page.viewport?.width || 1280,
        height: page.viewport?.height || 720
      }))
    );

    results.results.forEach((result, index) => {
      if (result.success) {
        const page = pages[index];
        this.baselines.set(page.name, {
          screenshot: result.data.screenshot,
          url: page.url,
          viewport: page.viewport,
          created: new Date().toISOString()
        });
        console.log(`✅ Baseline created for ${page.name}`);
      } else {
        console.error(`❌ Failed to create baseline for ${pages[index].name}: ${result.error}`);
      }
    });
  }

  async runRegressionTests(pages) {
    console.log('Running visual regression tests...');

    const results = await bt.batch.screenshots(
      pages.map(page => ({
        url: page.url,
        fullPage: true,
        width: page.viewport?.width || 1280,
        height: page.viewport?.height || 720
      }))
    );

    const testResults = [];
    let passed = 0;
    let failed = 0;

    results.results.forEach((result, index) => {
      const page = pages[index];
      const baseline = this.baselines.get(page.name);

      if (!result.success) {
        testResults.push({
          page: page.name,
          status: 'error',
          error: result.error
        });
        failed++;
        return;
      }

      if (!baseline) {
        testResults.push({
          page: page.name,
          status: 'no_baseline',
          message: 'No baseline found for comparison'
        });
        failed++;
        return;
      }

      const difference = this.calculateDifference(baseline.screenshot, result.data.screenshot);

      if (difference > this.threshold) {
        testResults.push({
          page: page.name,
          status: 'failed',
          difference: difference * 100,
          current: result.data.screenshot,
          baseline: baseline.screenshot
        });
        failed++;
      } else {
        testResults.push({
          page: page.name,
          status: 'passed',
          difference: difference * 100
        });
        passed++;
      }
    });

    return {
      passed,
      failed,
      total: pages.length,
      results: testResults,
      successRate: (passed / pages.length) * 100
    };
  }

  calculateDifference(baseline, current) {
    // Simple difference calculation - in production use proper image diffing
    if (baseline.length !== current.length) {
      return 1; // Completely different
    }

    let differences = 0;
    const baselineBuffer = Buffer.from(baseline, 'base64');
    const currentBuffer = Buffer.from(current, 'base64');

    const length = Math.min(baselineBuffer.length, currentBuffer.length);

    for (let i = 0; i < length; i++) {
      if (baselineBuffer[i] !== currentBuffer[i]) {
        differences++;
      }
    }

    return differences / length;
  }

  async generateReport(results, outputDir = './reports') {
    const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
    const reportDir = `${outputDir}/visual-regression-${timestamp}`;

    // Ensure output directory exists
    await fs.promises.mkdir(reportDir, { recursive: true });

    // Generate HTML report
    const html = this.generateHTMLReport(results);
    await fs.promises.writeFile(`${reportDir}/report.html`, html);

    // Save diff images for failed tests
    for (const result of results.results) {
      if (result.status === 'failed') {
        await fs.promises.writeFile(
          `${reportDir}/${result.page}-current.png`,
          Buffer.from(result.current, 'base64')
        );
        await fs.promises.writeFile(
          `${reportDir}/${result.page}-baseline.png`,
          Buffer.from(result.baseline, 'base64')
        );
      }
    }

    // Generate JSON summary
    const summary = {
      timestamp,
      summary: {
        passed: results.passed,
        failed: results.failed,
        total: results.total,
        successRate: results.successRate
      },
      results: results.results
    };

    await fs.promises.writeFile(
      `${reportDir}/summary.json`,
      JSON.stringify(summary, null, 2)
    );

    return reportDir;
  }

  generateHTMLReport(results) {
    const failedTests = results.results.filter(r => r.status === 'failed');

    return `
<!DOCTYPE html>
<html>
<head>
  <title>Visual Regression Test Report</title>
  <style>
    body { font-family: Arial, sans-serif; margin: 20px; }
    .summary { background: #f5f5f5; padding: 20px; margin-bottom: 20px; }
    .test { margin-bottom: 10px; padding: 10px; border: 1px solid #ddd; }
    .passed { border-color: #4CAF50; background: #E8F5E8; }
    .failed { border-color: #F44336; background: #FFEBEE; }
    .error { border-color: #FF9800; background: #FFF3E0; }
  </style>
</head>
<body>
  <h1>Visual Regression Test Report</h1>

  <div class="summary">
    <h2>Summary</h2>
    <p>Total Tests: ${results.total}</p>
    <p>Passed: ${results.passed}</p>
    <p>Failed: ${results.failed}</p>
    <p>Success Rate: ${results.successRate.toFixed(1)}%</p>
  </div>

  <h2>Test Results</h2>
  ${results.results.map(result => `
    <div class="test ${result.status}">
      <h3>${result.page}</h3>
      <p>Status: ${result.status}</p>
      ${result.difference ? `<p>Difference: ${result.difference.toFixed(2)}%</p>` : ''}
      ${result.error ? `<p>Error: ${result.error}</p>` : ''}
      ${result.message ? `<p>${result.message}</p>` : ''}
    </div>
  `).join('')}

  ${failedTests.length > 0 ? `
    <h2>Failed Tests Details</h2>
    ${failedTests.map(result => `
      <div class="test failed">
        <h3>${result.page}</h3>
        <p>Difference: ${result.difference.toFixed(2)}%</p>
        <p>Current vs Baseline images saved in report directory</p>
      </div>
    `).join('')}
  ` : ''}
</body>
</html>`;
  }
}

// Usage
const tester = new VisualRegressionTester(bt);

// Define pages to test
const pages = [
  { name: 'homepage', url: 'https://myapp.com' },
  { name: 'login', url: 'https://myapp.com/login' },
  { name: 'dashboard', url: 'https://myapp.com/dashboard' }
];

// Create baselines (run once)
await tester.createBaselines(pages);

// Run regression tests
const results = await tester.runRegressionTests(pages);

console.log(`Regression tests: ${results.passed}/${results.total} passed`);

// Generate report
if (results.failed > 0) {
  const reportDir = await tester.generateReport(results);
  console.log(`Report generated: ${reportDir}/report.html`);
}

Content Audit and SEO Monitoring

class ContentAuditor {
  constructor(bt) {
    this.bt = bt;
    this.auditRules = {
      title: {
        check: (content) => content.title && content.title.length > 0,
        message: 'Page must have a title'
      },
      titleLength: {
        check: (content) => content.title && content.title.length <= 60,
        message: 'Title should be 60 characters or less'
      },
      description: {
        check: (content) => {
          const metaDesc = content.head?.querySelector('meta[name="description"]');
          return metaDesc && metaDesc.getAttribute('content').length > 0;
        },
        message: 'Page should have meta description'
      },
      images: {
        check: (content) => {
          const images = content.body?.querySelectorAll('img');
          return images && Array.from(images).every(img =>
            img.hasAttribute('alt') && img.getAttribute('alt').trim().length > 0
          );
        },
        message: 'All images must have alt text'
      },
      headings: {
        check: (content) => {
          const h1s = content.body?.querySelectorAll('h1');
          return h1s && h1s.length === 1;
        },
        message: 'Page should have exactly one H1 heading'
      }
    };
  }

  async auditPages(urls) {
    console.log(`Starting content audit for ${urls.length} pages...`);

    // First, take screenshots to get page content
    const screenshots = await bt.batch.screenshots(
      urls.map(url => ({ url, fullPage: true }))
    );

    const auditResults = [];

    for (const screenshot of screenshots.results) {
      if (!screenshot.success) {
        auditResults.push({
          url: screenshot.url,
          success: false,
          error: screenshot.error,
          issues: []
        });
        continue;
      }

      // Note: In a real implementation, you'd need to fetch HTML content
      // and parse it. For this example, we'll simulate the audit.
      const issues = await this.runAuditChecks(screenshot.url);

      auditResults.push({
        url: screenshot.url,
        success: true,
        issues,
        score: this.calculateScore(issues)
      });
    }

    return {
      totalPages: urls.length,
      auditedPages: auditResults.filter(r => r.success).length,
      results: auditResults,
      summary: this.generateSummary(auditResults)
    };
  }

  async runAuditChecks(url) {
    // Simulate audit checks - in reality you'd fetch and parse HTML
    const issues = [];

    // Simulate some random issues for demonstration
    const possibleIssues = [
      { rule: 'title', message: 'Page must have a title' },
      { rule: 'titleLength', message: 'Title should be 60 characters or less' },
      { rule: 'images', message: 'All images must have alt text' },
      { rule: 'headings', message: 'Page should have exactly one H1 heading' }
    ];

    // Randomly add some issues for demonstration
    if (Math.random() > 0.7) {
      issues.push(...possibleIssues.slice(0, Math.floor(Math.random() * 3) + 1));
    }

    return issues;
  }

  calculateScore(issues) {
    const maxScore = 100;
    const penaltyPerIssue = 10;
    const score = Math.max(0, maxScore - (issues.length * penaltyPerIssue));
    return score;
  }

  generateSummary(results) {
    const successfulAudits = results.filter(r => r.success);
    const totalIssues = successfulAudits.reduce((sum, r) => sum + r.issues.length, 0);
    const averageScore = successfulAudits.length > 0 ?
      successfulAudits.reduce((sum, r) => sum + r.score, 0) / successfulAudits.length : 0;

    return {
      totalPages: results.length,
      successfulAudits: successfulAudits.length,
      totalIssues,
      averageScore: Math.round(averageScore),
      topIssues: this.getTopIssues(successfulAudits)
    };
  }

  getTopIssues(results) {
    const issueCounts = {};

    results.forEach(result => {
      result.issues.forEach(issue => {
        issueCounts[issue.rule] = (issueCounts[issue.rule] || 0) + 1;
      });
    });

    return Object.entries(issueCounts)
      .sort(([,a], [,b]) => b - a)
      .slice(0, 5)
      .map(([rule, count]) => ({ rule, count }));
  }

  async generateAuditReport(results, outputDir = './reports') {
    const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
    const reportDir = `${outputDir}/content-audit-${timestamp}`;

    await fs.promises.mkdir(reportDir, { recursive: true });

    // Generate CSV report
    const csv = this.generateCSVReport(results);
    await fs.promises.writeFile(`${reportDir}/audit-results.csv`, csv);

    // Generate HTML report
    const html = this.generateHTMLAuditReport(results);
    await fs.promises.writeFile(`${reportDir}/audit-report.html`, html);

    // Generate JSON summary
    const summary = {
      timestamp,
      ...results.summary,
      results: results.results
    };
    await fs.promises.writeFile(
      `${reportDir}/audit-summary.json`,
      JSON.stringify(summary, null, 2)
    );

    return reportDir;
  }

  generateCSVReport(results) {
    const headers = ['URL', 'Success', 'Score', 'Issues Count', 'Issues'];
    const rows = results.results.map(result => [
      result.url,
      result.success,
      result.score || '',
      result.issues?.length || 0,
      result.issues?.map(i => i.message).join('; ') || ''
    ]);

    return [headers, ...rows]
      .map(row => row.map(cell => `"${cell}"`).join(','))
      .join('\n');
  }

  generateHTMLAuditReport(results) {
    return `
<!DOCTYPE html>
<html>
<head>
  <title>Content Audit Report</title>
  <style>
    body { font-family: Arial, sans-serif; margin: 20px; }
    .summary { background: #f5f5f5; padding: 20px; margin-bottom: 20px; }
    .page { margin-bottom: 15px; padding: 15px; border: 1px solid #ddd; }
    .passed { border-color: #4CAF50; background: #E8F5E8; }
    .failed { border-color: #F44336; background: #FFEBEE; }
    .issues { color: #F44336; }
    .score { font-weight: bold; }
  </style>
</head>
<body>
  <h1>Content Audit Report</h1>

  <div class="summary">
    <h2>Summary</h2>
    <p>Total Pages: ${results.totalPages}</p>
    <p>Successful Audits: ${results.auditedPages}</p>
    <p>Total Issues Found: ${results.summary.totalIssues}</p>
    <p>Average Score: ${results.summary.averageScore}/100</p>
  </div>

  <h2>Top Issues</h2>
  <ul>
    ${results.summary.topIssues.map(issue => `<li>${issue.rule}: ${issue.count} occurrences</li>`).join('')}
  </ul>

  <h2>Page Results</h2>
  ${results.results.map(result => `
    <div class="page ${result.success ? 'passed' : 'failed'}">
      <h3>${result.url}</h3>
      ${result.success ? `
        <p class="score">Score: ${result.score}/100</p>
        <p>Issues Found: ${result.issues.length}</p>
        ${result.issues.length > 0 ? `
          <ul class="issues">
            ${result.issues.map(issue => `<li>${issue.message}</li>`).join('')}
          </ul>
        ` : '<p>No issues found</p>'}
      ` : `
        <p class="issues">Audit Failed: ${result.error}</p>
      `}
    </div>
  `).join('')}
</body>
</html>`;
  }
}

// Usage
const auditor = new ContentAuditor(bt);

const urls = [
  'https://myapp.com',
  'https://myapp.com/about',
  'https://myapp.com/services',
  'https://myapp.com/contact'
];

const auditResults = await auditor.auditPages(urls);

console.log(`Audit complete: ${auditResults.summary.averageScore}/100 average score`);

// Generate reports
const reportDir = await auditor.generateAuditReport(auditResults);
console.log(`Reports generated in: ${reportDir}`);
These batch processing examples demonstrate how to efficiently handle large-scale operations, implement monitoring and alerting, perform visual regression testing, and conduct content audits using the BrowserTest SDK’s batch processing capabilities.