arda-site-scan
Version:
A standalone CLI tool for comprehensive website analysis including screenshots, SEO, and accessibility testing using Playwright
307 lines • 13.6 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ParallelExecutor = void 0;
const chalk_1 = __importDefault(require("chalk"));
class ParallelExecutor {
browser;
maxConcurrency;
constructor(browser, maxConcurrency = 5) {
this.browser = browser;
this.maxConcurrency = maxConcurrency;
}
/**
* Execute multiple tasks in parallel with concurrency control using worker pool pattern
*/
async executeTasks(tasks, options = {}) {
const startTime = Date.now();
const concurrency = options.maxConcurrency || this.maxConcurrency;
const description = options.description || 'tasks';
console.log(chalk_1.default.blue(`🚀 Starting ${tasks.length} ${description} (max ${concurrency} concurrent)`));
const successful = [];
const failed = [];
const taskQueue = [...tasks]; // Copy array to avoid mutation
const runningPromises = [];
// Worker function that processes tasks from the queue
const worker = async () => {
while (taskQueue.length > 0) {
const task = taskQueue.shift();
if (!task)
break;
try {
const result = await task.execute();
successful.push({ id: task.id, result });
if (options.onProgress) {
options.onProgress(successful.length + failed.length, tasks.length);
}
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
failed.push({ id: task.id, error: errorMessage });
console.log(chalk_1.default.yellow(` ⚠️ ${task.name} failed: ${errorMessage}`));
if (options.onProgress) {
options.onProgress(successful.length + failed.length, tasks.length);
}
}
}
};
// Start worker pool with maximum concurrency
const workerCount = Math.min(concurrency, tasks.length);
for (let i = 0; i < workerCount; i++) {
runningPromises.push(worker());
}
// Wait for all workers to complete
await Promise.all(runningPromises);
const duration = Date.now() - startTime;
console.log(chalk_1.default.green(`✅ Completed ${tasks.length} ${description} in ${duration}ms`));
console.log(chalk_1.default.green(` Success: ${successful.length}, Failed: ${failed.length}`));
return { successful, failed, duration };
}
/**
* Execute page-level tests in parallel across multiple pages using worker pool pattern
*/
async executePageTests(urls, pageTestTasks, options = {}) {
const concurrency = options.maxConcurrency || this.maxConcurrency;
const pageResults = new Map();
console.log(chalk_1.default.blue(`🌐 Testing ${urls.length} pages with ${pageTestTasks.length} test types`));
let completedPages = 0;
let completedTests = 0;
const totalTests = urls.length * pageTestTasks.length;
const urlQueue = [...urls]; // Copy array to avoid mutation
const runningPromises = [];
// Worker function that processes URLs from the queue
const worker = async () => {
while (urlQueue.length > 0) {
const url = urlQueue.shift();
if (!url)
break;
const page = await this.browser.newPage();
try {
console.log(chalk_1.default.gray(` 📄 Testing ${url}`));
await page.goto(url, { waitUntil: 'networkidle' });
const pageResult = {
url,
pageName: this.getPageName(url),
tests: [],
summary: ''
};
// Execute all tests for this page in parallel
const testPromises = pageTestTasks.map(async (task) => {
if (task.url !== url)
return null; // Skip if not for this URL
try {
const testResult = await task.execute(page);
completedTests++;
if (options.onTestProgress) {
options.onTestProgress(completedTests, totalTests);
}
return testResult;
}
catch (error) {
completedTests++;
if (options.onTestProgress) {
options.onTestProgress(completedTests, totalTests);
}
return {
testType: task.testType,
status: 'failed',
startTime: new Date(),
endTime: new Date(),
error: error instanceof Error ? error.message : String(error)
};
}
});
const testResults = (await Promise.all(testPromises)).filter(r => r !== null);
pageResult.tests = testResults;
pageResult.summary = this.generatePageSummary(pageResult);
pageResults.set(url, pageResult);
completedPages++;
if (options.onPageProgress) {
options.onPageProgress(completedPages, urls.length);
}
console.log(chalk_1.default.green(` ✅ Completed testing ${url}`));
}
catch (error) {
console.error(chalk_1.default.red(` ❌ Error testing ${url}:`), error);
pageResults.set(url, {
url,
pageName: this.getPageName(url),
tests: [{
testType: 'page-load',
status: 'failed',
startTime: new Date(),
endTime: new Date(),
error: error instanceof Error ? error.message : String(error)
}],
summary: 'Page failed to load'
});
completedPages++;
if (options.onPageProgress) {
options.onPageProgress(completedPages, urls.length);
}
}
finally {
await page.close();
}
}
};
// Start worker pool with maximum concurrency
const workerCount = Math.min(concurrency, urls.length);
for (let i = 0; i < workerCount; i++) {
runningPromises.push(worker());
}
// Wait for all workers to complete
await Promise.all(runningPromises);
return pageResults;
}
/**
* Execute screenshot tests across multiple viewports in parallel
*/
async executeScreenshotTests(page, url, viewports, sessionId, screenshotTester // Import would be circular, so using any for now
) {
const tasks = viewports.map(viewport => ({
id: `screenshot-${viewport.name}`,
name: `Screenshot (${viewport.name})`,
execute: async () => {
// Create a new page for each viewport to avoid conflicts
const viewportPage = await this.browser.newPage();
try {
await viewportPage.goto(url, { waitUntil: 'networkidle' });
return await screenshotTester.captureScreenshot(viewportPage, url, viewport, sessionId);
}
finally {
await viewportPage.close();
}
}
}));
const result = await this.executeTasks(tasks, {
maxConcurrency: viewports.length, // All viewports can run simultaneously
description: 'screenshot tests'
});
return result.successful.map(s => s.result);
}
/**
* Create page test tasks for a specific URL and test configuration
*/
createPageTestTasks(url, config, sessionId, testers) {
const tasks = [];
config.selectedTests.forEach(test => {
if (!test.enabled)
return;
switch (test.id) {
case 'screenshots':
// Create separate tasks for each viewport
config.viewports.forEach(viewport => {
tasks.push({
url,
testType: `screenshots-${viewport.name}`,
testName: `Screenshot (${viewport.name})`,
execute: async (page) => {
await page.setViewportSize({ width: viewport.width, height: viewport.height });
return await testers.screenshotTester.captureScreenshot(page, url, viewport, sessionId);
}
});
});
break;
case 'seo':
tasks.push({
url,
testType: 'seo',
testName: 'SEO Scan',
execute: async (page) => {
return await testers.seoTester.runSEOScan(page, url, sessionId);
}
});
break;
case 'accessibility':
tasks.push({
url,
testType: 'accessibility',
testName: 'Accessibility Scan',
execute: async (page) => {
return await testers.accessibilityTester.runAccessibilityScan(page, url, sessionId);
}
});
break;
case 'content-scraping':
tasks.push({
url,
testType: 'content-scraping',
testName: 'Content Scraping',
execute: async (page) => {
return await testers.contentScraper.scrapePageContent(page, url, sessionId);
}
});
break;
}
});
return tasks;
}
/**
* Utility methods
*/
getPageName(url) {
try {
const urlObj = new URL(url);
const pathSegments = urlObj.pathname.split('/').filter(segment => segment.length > 0);
if (pathSegments.length === 0) {
return 'home';
}
return pathSegments.join('-').toLowerCase().replace(/[^a-z0-9-]/g, '-');
}
catch (error) {
return 'unknown-page';
}
}
generatePageSummary(pageResult) {
const successCount = pageResult.tests.filter(t => t.status === 'success').length;
const failCount = pageResult.tests.filter(t => t.status === 'failed').length;
const totalTests = pageResult.tests.length;
return `Page: ${pageResult.url}\nTests completed: ${totalTests}\nSuccessful: ${successCount}\nFailed: ${failCount}`;
}
/**
* Parallel processing utility for large datasets using worker pool pattern
*/
async processBatches(items, processor, maxConcurrency = this.maxConcurrency, onItemComplete) {
const results = [];
const itemQueue = [...items]; // Copy array to avoid mutation
const runningPromises = [];
let completedItems = 0;
// Worker function that processes items from the queue
const worker = async () => {
while (itemQueue.length > 0) {
const item = itemQueue.shift();
if (!item)
break;
try {
const result = await processor(item);
results.push(result);
completedItems++;
if (onItemComplete) {
onItemComplete(completedItems, items.length);
}
}
catch (error) {
// Note: You might want to handle errors differently based on requirements
completedItems++;
if (onItemComplete) {
onItemComplete(completedItems, items.length);
}
throw error; // Re-throw to maintain error handling behavior
}
}
};
// Start worker pool with maximum concurrency
const workerCount = Math.min(maxConcurrency, items.length);
for (let i = 0; i < workerCount; i++) {
runningPromises.push(worker());
}
// Wait for all workers to complete
await Promise.all(runningPromises);
return results;
}
}
exports.ParallelExecutor = ParallelExecutor;
//# sourceMappingURL=parallel-executor.js.map