11/**
22 * Main entry point for @vizzly-testing/storybook
33 * Functional orchestration of story 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- prepareStoryPage ,
11- } from './browser.js' ;
7+ import { closeBrowser , launchBrowser } from './browser.js' ;
128import { loadConfig } from './config.js' ;
13- import { discoverStories , generateStoryUrl } from './crawler.js' ;
14- import { getBeforeScreenshotHook , getStoryConfig } from './hooks.js' ;
15- import { captureAndSendScreenshot } from './screenshot.js' ;
9+ import { discoverStories } from './crawler.js' ;
10+ import { createTabPool } from './pool.js' ;
1611import { startStaticServer , stopStaticServer } from './server.js' ;
17-
18- /**
19- * Process a single story across all configured viewports
20- * @param {Object } story - Story object
21- * @param {Object } browser - Browser instance
22- * @param {string } baseUrl - Base URL for Storybook (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 processStory ( story , browser , baseUrl , config , context ) {
28- let { logger } = context ;
29- let storyConfig = getStoryConfig ( story , config ) ;
30- let storyUrl = generateStoryUrl ( baseUrl , story . id ) ;
31- let hook = getBeforeScreenshotHook ( story , config ) ;
32- let errors = [ ] ;
33-
34- // Process each viewport for this story
35- for ( let viewport of storyConfig . viewports ) {
36- let page = null ;
37-
38- try {
39- page = await prepareStoryPage ( browser , storyUrl , viewport , hook ) ;
40- await captureAndSendScreenshot (
41- page ,
42- story ,
43- viewport ,
44- storyConfig . screenshot
45- ) ;
46-
47- logger . info ( ` ✓ ${ story . title } /${ story . name } @${ viewport . name } ` ) ;
48- } catch ( error ) {
49- logger . error (
50- ` ✗ ${ story . title } /${ story . name } @${ viewport . name } : ${ error . message } `
51- ) ;
52- errors . push ( {
53- story : `${ story . title } /${ story . name } ` ,
54- viewport : viewport . name ,
55- error : error . message ,
56- } ) ;
57- } finally {
58- await closePage ( page ) ;
59- }
60- }
61-
62- return { errors } ;
63- }
64-
65- /**
66- * Simple concurrency control - process items with limited parallelism
67- * @param {Array } items - Items to process
68- * @param {Function } fn - Async function to process each item
69- * @param {number } concurrency - Max parallel operations
70- * @returns {Promise<void> }
71- */
72- async function mapWithConcurrency ( items , fn , concurrency ) {
73- let results = [ ] ;
74- let executing = [ ] ;
75-
76- for ( let item of items ) {
77- let promise = fn ( item ) . then ( result => {
78- executing . splice ( executing . indexOf ( promise ) , 1 ) ;
79- return result ;
80- } ) ;
81-
82- results . push ( promise ) ;
83- executing . push ( promise ) ;
84-
85- if ( executing . length >= concurrency ) {
86- await Promise . race ( executing ) ;
87- }
88- }
89-
90- await Promise . all ( results ) ;
91- }
92-
93- /**
94- * Process all stories with concurrency control
95- * @param {Array<Object> } stories - Array of story objects
96- * @param {Object } browser - Browser instance
97- * @param {string } baseUrl - Base URL for Storybook (HTTP server)
98- * @param {Object } config - Configuration
99- * @param {Object } context - Plugin context
100- * @returns {Promise<Array> } Array of all errors encountered
101- */
102- async function processStories ( stories , browser , baseUrl , config , context ) {
103- let allErrors = [ ] ;
104-
105- await mapWithConcurrency (
106- stories ,
107- async story => {
108- let { errors } = await processStory (
109- story ,
110- browser ,
111- baseUrl ,
112- config ,
113- context
114- ) ;
115- allErrors . push ( ...errors ) ;
116- } ,
117- config . concurrency
118- ) ;
119-
120- return allErrors ;
121- }
12+ import { generateTasks , processAllTasks } from './tasks.js' ;
12213
12314/**
12415 * Check if TDD mode is available
@@ -170,6 +61,7 @@ function hasApiToken(config) {
17061
17162/**
17263 * Main run function - orchestrates the entire screenshot capture process
64+ * Uses a tab pool for efficient parallel screenshot capture
17365 * @param {string } storybookPath - Path to static Storybook build
17466 * @param {Object } options - CLI options
17567 * @param {Object } context - Plugin context (logger, config, services)
@@ -178,6 +70,7 @@ function hasApiToken(config) {
17870export async function run ( storybookPath , options = { } , context = { } ) {
17971 let { logger, config : vizzlyConfig , services } = context ;
18072 let browser = null ;
73+ let pool = null ;
18174 let serverInfo = null ;
18275 let testRunner = null ;
18376 let serverManager = null ;
@@ -287,6 +180,8 @@ export async function run(storybookPath, options = {}, context = {}) {
287180 } catch ( error ) {
288181 // Log the error and continue without cloud mode
289182 logger . error ( `Failed to initialize cloud mode: ${ error . message } ` ) ;
183+ logger . warn ( '⚠️ Falling back to local-only mode' ) ;
184+ logger . info ( ' Screenshots will not be uploaded to cloud' ) ;
290185 testRunner = null ;
291186 }
292187 }
@@ -310,26 +205,25 @@ export async function run(storybookPath, options = {}, context = {}) {
310205 return ;
311206 }
312207
313- // Launch browser
208+ // Launch browser and create tab pool
314209 browser = await launchBrowser ( config . browser ) ;
210+ pool = createTabPool ( browser , config . concurrency ) ;
315211
316- // Process all stories
317- let errors = await processStories (
318- stories ,
319- browser ,
320- serverInfo . url ,
321- config ,
322- context
212+ // Generate all tasks upfront (stories × viewports)
213+ let tasks = generateTasks ( stories , serverInfo . url , config ) ;
214+ logger . info (
215+ `📸 Processing ${ tasks . length } screenshots (${ config . concurrency } concurrent tabs)`
323216 ) ;
324217
218+ // Process all tasks through the tab pool
219+ let errors = await processAllTasks ( tasks , pool , config , logger ) ;
220+
325221 // Report summary
326222 if ( errors . length > 0 ) {
327223 logger . warn ( `\n⚠️ ${ errors . length } screenshot(s) failed:` ) ;
328224 errors . forEach ( ( { story, viewport, error } ) => {
329225 logger . error ( ` ${ story } @${ viewport } : ${ error } ` ) ;
330226 } ) ;
331- } else {
332- logger . info ( `\n✅ Captured ${ stories . length } screenshots successfully` ) ;
333227 }
334228
335229 // Finalize build in run mode
@@ -356,7 +250,10 @@ export async function run(storybookPath, options = {}, context = {}) {
356250
357251 throw error ;
358252 } finally {
359- // Cleanup
253+ // Cleanup: drain pool first, then close browser
254+ if ( pool ) {
255+ await pool . drain ( ) ;
256+ }
360257 if ( browser ) {
361258 await closeBrowser ( browser ) ;
362259 }
0 commit comments