UNPKG

@applitools/eyes-storybook

Version:
238 lines (215 loc) 7.8 kB
'use strict'; const getStoryUrl = require('./getStoryUrl'); const getStoryBaselineName = require('./getStoryBaselineName'); const ora = require('ora'); const {EventEmitter} = require('node:events'); const {presult} = require('@applitools/functional-commons'); const {AbortedByUserError} = require('./errMessages'); function makeRenderStories({ getStoryData, pagePool, renderStory, storybookUrl, logger, stream, sanityCheckForPage, maxPageTTL = 60000, eventEmitter = new EventEmitter(), signal = new AbortController().signal, }) { const sparePages = { queue: [], isPreparing: false, add(pageId) { this.queue.push(pageId); }, async getOrCreatAdHoc() { const pageId = this.queue.shift(); if (pageId) { this.replenish(); return pageId; } logger.log(`[sparePages] queue empty, creating ad-hoc page`); try { const pageObj = await pagePool.createPage(); await sanityCheckForPage({page: pageObj.page, pageId: pageObj.pageId}); return pageObj.pageId; } catch (e) { logger.log(`[sparePages] failed to create ad-hoc page: ${e}`); return null; } }, async replenish() { if (this.queue.length > 0 || this.isPreparing) { return; } this.isPreparing = true; logger.log('[sparePages] preparing...'); try { const [errorInCreate, pageObj] = await presult(pagePool.createPage()); if (errorInCreate) { logger.log( `[sparePages] error preparing new page. This is probably a fatal problem. ${errorInCreate}`, ); return; } const {pageId, page} = pageObj; logger.log(`[sparePages] new page is ready: ${pageId}`); try { await sanityCheckForPage({page, pageId}); logger.log(`[sparePages] adding page to queue: ${pageId}`); this.add(pageId); } catch (errorInSanity) { logger.log( `[sparePages] new page ${pageId} is corrupted. preparing new page.`, errorInSanity, ); // If the page is corrupted, we close it and try again below page.close().catch(() => {}); } } finally { this.isPreparing = false; } // If we are still empty (sanity check failed), try again. // Note: this runs only when createPage succeeded but sanityCheck failed, // because a failed createPage returns early above. if (this.queue.length === 0) { this.replenish(); } }, }; return async function renderStories(stories, isIE) { let doneStories = 0; const totalStories = stories.length; const allTestResults = []; let allStoriesPromise = Promise.resolve(); let currIndex = 0; const spinner = ora({ text: updateSpinnerText(0, totalStories), stream, }); eventEmitter.emit('progress', {doneStories, totalStories}); spinner.start(); sparePages.replenish(); await processStoryLoop(); await allStoriesPromise; updateSpinnerEnd(); return allTestResults; async function processStoryLoop() { if (currIndex === totalStories) return; if (signal.aborted) { const story = stories[currIndex++]; const title = getStoryBaselineName(story); logger.log('aborting story before processing', title); onDoneStory( new AbortedByUserError(`${title} aborted before processing ${signal.reason}`), story, ); return processStoryLoop(); } const {page, pageId, markPageAsFree, removePage, getCreatedAt} = await pagePool.getFreePage(); const livedTime = Date.now() - getCreatedAt(); logger.log(`[prepareNewPage] got free page: ${pageId}, lived time: ${livedTime}`); if (livedTime > maxPageTTL) { const replacementPageId = await sparePages.getOrCreatAdHoc(); if (replacementPageId) { logger.log(`[prepareNewPage] replacing page ${pageId} with page ${replacementPageId}`); removePage(); page.close(); pagePool.addToPool(replacementPageId); return processStoryLoop(); } else { logger.log(`[prepareNewPage] failed to replace expired page ${pageId}. Reusing it.`); } } logger.log(`[page ${pageId}] waiting for queued renders`); // await waitForQueuedRenders(storyDataGap); logger.log(`[page ${pageId}] done waiting for queued renders`); const storyPromise = processStory(); allStoriesPromise = allStoriesPromise.then(() => storyPromise); return processStoryLoop(); async function processStory() { const story = stories[currIndex++]; const storyUrl = getStoryUrl(story, storybookUrl); const title = getStoryBaselineName(story); try { let [error, storyData] = await presult( getStoryData({ story, storyUrl, page, pageId, }), ); if ( error && /(Runtime.callFunctionOn timed out|Protocol error|Execution context was destroyed|timeout reached when trying to take DOM for story|page evaluate timed out)/.test( error.message, ) ) { logger.log( `Puppeteer error from [page ${pageId}] while getting story data. Replacing page. ${error.message}`, ); removePage(); page .close() .catch(e => logger.log(`stale [page ${pageId}] already closed: ${e.message}`)); const newPageObj = await pagePool.createPage(); logger.log(`new page ${newPageObj.pageId} created ad hoc. trying it out`); const [newError, newStoryData] = await presult( getStoryData({ story, storyUrl, page: newPageObj.page, pageId: newPageObj.pageId, }), ); error = newError; storyData = newStoryData; pagePool.addToPool(newPageObj.pageId); } else { markPageAsFree(); } if (error) { const errMsg = `[page ${pageId}] Failed to get story data for "${title}". ${error}`; logger.log(errMsg); } if (signal.aborted) { logger.log('aborting story before open', title); return onDoneStory(new Error(`${title} aborted before open ${signal.reason}`), story); } const testResults = await renderStory({ snapshots: storyData, url: storyUrl, story, }); return onDoneStory(testResults, story); } catch (ex) { logger.log(`[page ${pageId}] error while processing story "${title}". ${ex}`); return onDoneStory(ex, story); } } } function didTestPass({resultsOrErr}) { return ( Array.isArray(resultsOrErr) && resultsOrErr.every(r => !r.isDifferent && !r.isAborted && !r.isNew) ); } function updateSpinnerEnd() { allTestResults.every(didTestPass) ? spinner.succeed() : spinner.fail(); } function updateSpinnerText(number, length) { return `Done ${number} stories out of ${length} ${isIE ? '(IE)' : ''}`; } function onDoneStory(resultsOrErr, story) { spinner.text = updateSpinnerText(++doneStories, totalStories, story.config); const title = getStoryBaselineName(story); const result = {title, resultsOrErr, story}; allTestResults.push(result); eventEmitter.emit('progress', {doneStories, totalStories}); return result; } }; } module.exports = makeRenderStories;