11/**
22 * Main entry point for @vizzly-testing/static-site
33 * Functional orchestration of page discovery and screenshot capture
4+ * Uses a tab pool for efficient browser tab management
45 */
56
6- import {
7- closeBrowser ,
8- closePage ,
9- launchBrowser ,
10- preparePageForScreenshot ,
11- } from './browser.js' ;
12- import { getPageConfig , loadConfig } from './config.js' ;
13- import { discoverPages , generatePageUrl } from './crawler.js' ;
14- import { getBeforeScreenshotHook } from './hooks.js' ;
15- import { captureAndSendScreenshot } from './screenshot.js' ;
7+ import { closeBrowser , launchBrowser } from './browser.js' ;
8+ import { loadConfig } from './config.js' ;
9+ import { discoverPages } from './crawler.js' ;
10+ import { createTabPool } from './pool.js' ;
1611import { startStaticServer , stopStaticServer } from './server.js' ;
17-
18- /**
19- * Process a single page across all configured viewports
20- * @param {Object } page - Page object
21- * @param {Object } browser - Browser instance
22- * @param {string } baseUrl - Base URL for static site (HTTP server)
23- * @param {Object } config - Configuration
24- * @param {Object } context - Plugin context
25- * @returns {Promise<Object> } Result object with success count and errors
26- */
27- async function processPage ( page , browser , baseUrl , config , context ) {
28- let { logger } = context ;
29- let pageConfig = getPageConfig ( config , page ) ;
30- let pageUrl = generatePageUrl ( baseUrl , page ) ;
31- let hook = getBeforeScreenshotHook ( page , config ) ;
32- let errors = [ ] ;
33-
34- // Process each viewport for this page
35- for ( let viewport of pageConfig . viewports ) {
36- let puppeteerPage = null ;
37-
38- try {
39- puppeteerPage = await preparePageForScreenshot (
40- browser ,
41- pageUrl ,
42- viewport ,
43- hook
44- ) ;
45- await captureAndSendScreenshot (
46- puppeteerPage ,
47- page ,
48- viewport ,
49- pageConfig . screenshot
50- ) ;
51-
52- logger . info ( ` ✓ ${ page . path } @${ viewport . name } ` ) ;
53- } catch ( error ) {
54- logger . error ( ` ✗ ${ page . path } @${ viewport . name } : ${ error . message } ` ) ;
55- errors . push ( {
56- page : page . path ,
57- viewport : viewport . name ,
58- error : error . message ,
59- } ) ;
60- } finally {
61- await closePage ( puppeteerPage ) ;
62- }
63- }
64-
65- return { errors } ;
66- }
67-
68- /**
69- * Simple concurrency control - process items with limited parallelism
70- * @param {Array } items - Items to process
71- * @param {Function } fn - Async function to process each item
72- * @param {number } concurrency - Max parallel operations
73- * @returns {Promise<void> }
74- */
75- async function mapWithConcurrency ( items , fn , concurrency ) {
76- let results = [ ] ;
77- let executing = new Set ( ) ;
78-
79- for ( let item of items ) {
80- let promise = fn ( item ) . then ( result => {
81- executing . delete ( promise ) ;
82- return result ;
83- } ) ;
84-
85- results . push ( promise ) ;
86- executing . add ( promise ) ;
87-
88- if ( executing . size >= concurrency ) {
89- await Promise . race ( executing ) ;
90- }
91- }
92-
93- await Promise . all ( results ) ;
94- }
95-
96- /**
97- * Process all pages with concurrency control
98- * @param {Array<Object> } pages - Array of page objects
99- * @param {Object } browser - Browser instance
100- * @param {string } baseUrl - Base URL for static site (HTTP server)
101- * @param {Object } config - Configuration
102- * @param {Object } context - Plugin context
103- * @returns {Promise<Array> } Array of all errors encountered
104- */
105- async function processPages ( pages , browser , baseUrl , config , context ) {
106- let allErrors = [ ] ;
107-
108- await mapWithConcurrency (
109- pages ,
110- async page => {
111- let { errors } = await processPage (
112- page ,
113- browser ,
114- baseUrl ,
115- config ,
116- context
117- ) ;
118- allErrors . push ( ...errors ) ;
119- } ,
120- config . concurrency
121- ) ;
122-
123- return allErrors ;
124- }
12+ import { generateTasks , processAllTasks } from './tasks.js' ;
12513
12614/**
12715 * Check if TDD mode is available
@@ -173,6 +61,7 @@ function hasApiToken(config) {
17361
17462/**
17563 * Main run function - orchestrates the entire screenshot capture process
64+ * Uses a tab pool for efficient parallel screenshot capture
17665 * @param {string } buildPath - Path to static site build
17766 * @param {Object } options - CLI options
17867 * @param {Object } context - Plugin context (logger, config, services)
@@ -181,6 +70,7 @@ function hasApiToken(config) {
18170export async function run ( buildPath , options = { } , context = { } ) {
18271 let { logger, config : vizzlyConfig , services } = context ;
18372 let browser = null ;
73+ let pool = null ;
18474 let serverInfo = null ;
18575 let testRunner = null ;
18676 let serverManager = null ;
@@ -313,28 +203,27 @@ export async function run(buildPath, options = {}, context = {}) {
313203 return ;
314204 }
315205
316- // Launch browser
206+ // Launch browser and create tab pool
317207 browser = await launchBrowser ( config . browser ) ;
208+ pool = createTabPool ( browser , config . concurrency ) ;
318209
319- // Process all pages
320- let errors = await processPages (
321- pages ,
322- browser ,
323- serverInfo . url ,
324- config ,
325- context
210+ // Generate all tasks upfront (pages × viewports)
211+ let tasks = generateTasks ( pages , serverInfo . url , config ) ;
212+ logger . info (
213+ `📸 Processing ${ tasks . length } screenshots (${ config . concurrency } concurrent tabs)`
326214 ) ;
327215
216+ // Process all tasks through the tab pool
217+ let errors = await processAllTasks ( tasks , pool , config , logger ) ;
218+
328219 // Report summary
329220 if ( errors . length > 0 ) {
330221 logger . warn ( `\n⚠️ ${ errors . length } screenshot(s) failed:` ) ;
331222 errors . forEach ( ( { page, viewport, error } ) => {
332223 logger . error ( ` ${ page } @${ viewport } : ${ error } ` ) ;
333224 } ) ;
334225 } else {
335- logger . info (
336- `\n✅ Captured ${ pages . length * config . viewports . length } screenshots successfully`
337- ) ;
226+ logger . info ( `\n✅ Captured ${ tasks . length } screenshots successfully` ) ;
338227 }
339228
340229 // Finalize build in run mode
@@ -361,7 +250,10 @@ export async function run(buildPath, options = {}, context = {}) {
361250
362251 throw error ;
363252 } finally {
364- // Cleanup
253+ // Cleanup: drain pool first, then close browser
254+ if ( pool ) {
255+ await pool . drain ( ) ;
256+ }
365257 if ( browser ) {
366258 await closeBrowser ( browser ) ;
367259 }
0 commit comments