@applitools/eyes-storybook
Version:
371 lines (330 loc) • 10.9 kB
JavaScript
'use strict';
const puppeteer = require('puppeteer');
const getStories = require('../dist/getStories');
const {presult} = require('@applitools/functional-commons');
const {executeWithRetry} = require('./utils/executeWithRetry');
const chalk = require('chalk');
const makeInitPage = require('./initPage');
const makeRenderStory = require('./renderStory');
const makeRenderStories = require('./renderStories');
const makeGetStoryData = require('./getStoryData');
const ora = require('ora');
const filterStories = require('./filterStories');
const addVariationStories = require('./addVariationStories');
const browserLog = require('./browserLog');
const getIframeUrl = require('./getIframeUrl');
const createPagePool = require('./pagePool');
const getClientAPI = require('../dist/getClientAPI');
const {mergeConfigs} = require('./utils/merge-configs');
const {Driver} = require('@applitools/driver');
const spec = require('@applitools/spec-driver-puppeteer');
const {refineErrorMessage} = require('./errMessages');
const executeRenders = require('./executeRenders');
const {extractEnvironment} = require('./extractEnvironment');
const {makeCore} = require('@applitools/core');
const makeGetStoriesWithConfig = require('./getStoriesWithConfig');
const {makeNetworkUtils} = require('./utils/pageNetworkUtils');
async function eyesStorybook({
config,
logger,
performance,
timeItAsync,
outputStream = process.stderr,
}) {
let renderIE = false;
let transitioning = false;
logger.log('eyesStorybook started');
const CONCURRENT_TABS = isNaN(Number(process.env.APPLITOOLS_CONCURRENT_TABS))
? 3
: Number(process.env.APPLITOOLS_CONCURRENT_TABS);
logger.log(`Running with ${CONCURRENT_TABS} concurrent tabs`);
const {storybookUrl, readStoriesTimeout, reloadPagePerStory, navigationWaitUntil} = config;
let iframeUrl;
try {
iframeUrl = getIframeUrl(storybookUrl);
logger.log('iframeUrl:', iframeUrl);
} catch (ex) {
logger.log(ex);
throw new Error(`Storybook URL is not valid: ${storybookUrl}`);
}
const agentId = `eyes-storybook/${require('../package.json').version}`;
process.env.PUPPETEER_DISABLE_HEADLESS_WARNING = true;
const browser = await puppeteer.launch(config.puppeteerOptions);
logger.log('browser launched');
const page = await browser.newPage();
const {startInterception} = makeNetworkUtils({
page,
logger,
});
// we send http headers here and in init page
if (config.puppeteerExtraHTTPHeaders) {
await page.setExtraHTTPHeaders(config.puppeteerExtraHTTPHeaders);
}
await startInterception({
timeout: config.browserRequestsTimeout,
blockPatterns: config.networkBlockPatterns,
browserHeadersOverride: config.browserHeadersOverride,
cache: config.browserCacheRequests,
});
const environment = extractEnvironment();
const core = await makeCore({spec, agentId, environment, logger});
const manager = await core.makeManager({
type: 'ufg',
settings: {concurrency: config.testConcurrency},
});
const account = await core
.getAccountInfo({
settings: {
eyesServerUrl: config.eyesServerUrl,
apiKey: config.apiKey,
agentId,
proxy: config.proxy,
useDnsCache: config.useDnsCache,
},
})
.catch(async error => {
if (error && error.message && error.message.includes('Unauthorized(401)')) {
const failMsg = 'Incorrect API Key';
logger.log(failMsg);
await browser.close();
throw new Error(failMsg);
} else {
throw error;
}
});
const getStoriesWithConfig = makeGetStoriesWithConfig({config});
const initPage = makeInitPage({
iframeUrl,
config,
browser,
logger,
getTransitiongIntoIE,
getRenderIE,
});
const pagePool = createPagePool({initPage, logger});
const doTakeDomSnapshots = async ({page, ...settings}) => {
const driver = await new Driver({spec, driver: page, logger});
return await core.takeSnapshots({
logger,
driver,
settings: mergeConfigs({config, settings}),
account,
});
};
logger.log('got script for processPage');
browserLog({
page,
onLog: text => {
logger.log(`master tab: ${text}`);
},
});
try {
const stories = await getStoriesWithSpinner();
const filteredStories = filterStories({stories, config});
const storiesIncludingVariations = addVariationStories({
stories: filteredStories,
config,
});
logger.log(
`there are ${storiesIncludingVariations.length} stories after filtering and adding variations `,
);
const storiesByBrowserWithConfig = getStoriesWithConfig({
stories: storiesIncludingVariations,
logger,
});
logger.log(
`starting to run ${storiesByBrowserWithConfig.stories.length} normal stories ("non fake IE") and ${storiesByBrowserWithConfig.storiesWithIE.length} "fake IE stories"`,
);
const getStoryData = makeGetStoryData({
logger,
takeDomSnapshots: doTakeDomSnapshots,
reloadPagePerStory,
navigationWaitUntil,
});
const renderStory = makeRenderStory({
logger: logger.extend({label: 'renderStory'}),
openEyes: manager.openEyes,
performance,
timeItAsync,
storyDataGap: config.storyDataGap,
concurrency: config.testConcurrency,
appName: config.appName,
serverSettings: account.eyesServer,
});
const renderStories = makeRenderStories({
getStoryData,
renderStory,
sanityCheckForPage,
storybookUrl,
logger,
stream: outputStream,
pagePool,
});
logger.log('finished creating functions');
await initializeTabs();
const [error, results] = await presult(
executeRenders({
renderStories,
setRenderIE,
setTransitioningIntoIE,
storiesByBrowserWithConfig,
pagePool,
logger,
timeItAsync,
}),
);
const [errorInGetResults, testResultsSummary] = await presult(
manager.getResults({throwErr: false}),
);
if (errorInGetResults) {
logger.log('failed to get results', errorInGetResults);
}
if (error) {
const msg = refineErrorMessage({prefix: 'Error in executeRenders:', error});
logger.log(error);
throw new Error(msg);
} else {
return {summary: testResultsSummary, results};
}
} finally {
logger.log('total time: ', performance['renderStories']);
logger.log('perf results', performance);
pagePool.isClosed = true;
await browser.close();
}
async function getStoriesWithSpinner() {
let hasConsoleErr;
page.on('console', msg => {
hasConsoleErr =
msg.args()[0] &&
msg.args()[0]._remoteObject &&
msg.args()[0]._remoteObject.subtype === 'error';
});
logger.log('Getting stories from storybook');
const spinner = ora({text: 'Reading stories', stream: outputStream});
spinner.start();
logger.log('navigating to storybook url:', storybookUrl);
const [navigateErr] = await presult(
page.goto(storybookUrl, {timeout: readStoriesTimeout, waitUntil: navigationWaitUntil}),
);
if (navigateErr) {
logger.log('Error when loading storybook', navigateErr);
const failMsg = refineErrorMessage({
prefix: 'Error when loading storybook.',
error: navigateErr,
});
spinner.fail(failMsg);
throw new Error();
}
const [getStoriesErr, stories] = await presult(readStoriesWithRetry());
if (getStoriesErr) {
logger.log('Error when reading stories:', getStoriesErr);
const failMsg = refineErrorMessage({
prefix: 'Error when reading stories:',
error: getStoriesErr,
});
spinner.fail(failMsg);
throw new Error();
}
if (!stories.length && hasConsoleErr) {
return [
new Error(
'Could not load stories, make sure your storybook renders correctly. Perhaps no stories were rendered?',
),
];
}
const badParamsError = stories
.map(s => s.error)
.filter(Boolean)
.join('\n');
if (badParamsError) {
console.log(chalk.red(`\n${badParamsError}`));
}
spinner.succeed();
logger.log(`got ${stories.length} stories:`, JSON.stringify(stories));
return stories;
}
function getRenderIE() {
return renderIE;
}
function setRenderIE(value) {
renderIE = value;
}
function setTransitioningIntoIE(value) {
transitioning = value;
}
function getTransitiongIntoIE() {
return transitioning;
}
async function readStoriesWithRetry() {
return executeWithRetry(
_readStoriesWithRetry,
{
timeout: readStoriesTimeout,
delayBetweenRetries: 1000,
initialErrMessage: `Could not get stories since readStoriesTimeout is too short (${readStoriesTimeout}ms)`,
},
readStoriesTimeout,
);
async function _readStoriesWithRetry() {
try {
const stories = await page.evaluate(getStories);
if (stories.length > 0) {
return stories;
} else {
throw new Error('Got 0 stories');
}
} catch (err) {
logger.log('Error in _readStoriesWithRetry:', err, ', retrying...');
throw err;
}
}
}
async function sanityCheckForPage({page, pageId}) {
try {
await executeWithRetry(_sanityCheckForPage, {
timeout: readStoriesTimeout,
delayBetweenRetries: 1000,
initialErrMessage: `Could not get client API in sanity check since readStoriesTimeout is too short (${readStoriesTimeout}ms)`,
});
} catch (err) {
logger.log(`[page ${pageId}] Sanity check failed. Getting page HTML for inspection`);
try {
const html = await page.content();
logger.log(`[page ${pageId}] Page HTML: ${html}`);
} catch (htmlErr) {
logger.log(`[page ${pageId}] Error getting page HTML:`, htmlErr);
}
throw err;
}
async function _sanityCheckForPage() {
try {
await page.evaluate(getClientAPI);
} catch (err) {
logger.log(`[page ${pageId}] Error in getClientAPI during sanity check. Retrying...`, err);
throw err;
}
}
}
async function initializeTabs() {
logger.log('initializing tabs');
try {
await Promise.all(
Array.from({length: CONCURRENT_TABS}, async () => {
const {page, pageId} = await pagePool.createPage();
await sanityCheckForPage({page, pageId});
pagePool.addToPool(pageId);
}),
);
} catch (initializeTabsError) {
const msg = refineErrorMessage({
prefix: 'Error initializing browser tabs with storybook:',
error: initializeTabsError,
});
logger.log(msg);
ora({text: msg, stream: outputStream}).fail();
throw new Error();
}
}
}
module.exports = eyesStorybook;