UNPKG

@applitools/eyes-storybook

Version:
371 lines (330 loc) 10.9 kB
'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;