Basic Batch Operations
Batch Screenshots
Copy
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
Copy
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
Copy
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
Copy
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
Copy
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
Copy
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
Copy
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}`);
