@applitools/eyes-storybook
Version:
238 lines (215 loc) • 7.8 kB
JavaScript
;
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;