Created
September 19, 2018 12:33
-
-
Save thehig/face089a7d0d469ea992fd5f4ef9776f to your computer and use it in GitHub Desktop.
js: Storyshots with multiple device/viewport puppeteer screenshots
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import path from 'path'; | |
import fs from 'fs'; | |
import initStoryshots from '@storybook/addon-storyshots'; | |
import { imageSnapshot } from './storyshots-puppeteer'; | |
import devices from 'puppeteer/DeviceDescriptors'; | |
// Store the screenshots outside the source folder to prevent jest from 'watching' them. | |
// Since they're outside the src directory we nav to them relatively | |
const ROOTDIR = path.join(__dirname, '../../../'); | |
const CONFIG_PATH = path.join(ROOTDIR, '.storybook'); | |
const OUTPUT_DIRECTORY = path.join(ROOTDIR, 'screenshots'); | |
const PATH_TO_STORYBOOK_STATIC = path.join(ROOTDIR, 'storybook-static'); | |
/** | |
* Error if there is no storybook-static folder | |
*/ | |
if (!fs.existsSync(PATH_TO_STORYBOOK_STATIC)) { | |
const errormessage = | |
'You are running image snapshots without having the static build of storybook. Please run "yarn run build-storybook" before running tests.'; | |
console.error(errormessage); | |
throw new Error(errormessage); | |
} | |
/** | |
* Error if there is no screenshots folder | |
*/ | |
if (!fs.existsSync(OUTPUT_DIRECTORY)) { | |
const errormessage = | |
'Screenshot directory does not exist. Please create the directory before running tests.'; | |
console.error(errormessage); | |
throw new Error(errormessage); | |
} | |
/** | |
* Delay the resolution of a promise | |
*/ | |
const delay = ms => (...args) => | |
new Promise(resolve => | |
setTimeout(() => { | |
resolve(...args); | |
}, ms) | |
); | |
/** | |
* Rendering a storybook in a headless environment to take screenshots takes | |
* more time than your average test. | |
*/ | |
const timeout = 360 * 1000; | |
console.log(` | |
=== | |
Setting jest/jasmine test timeout to ${timeout}ms for screenshots | |
=== | |
`); | |
if (jest && jest.setTimeout) jest.setTimeout(timeout); | |
else if (jasmine) jasmine.DEFAULT_TIMEOUT_INTERVAL = timeout; | |
initStoryshots({ | |
suite: 'Screenshots', | |
framework: 'react', | |
/** | |
* Only apply image snapshots to stories with 📷 in the name or kind | |
* | |
* NOTE: Name and Kind are mutually exclusive. If you include both, you'll get nothing | |
*/ | |
storyKindRegex: /📷/, | |
// storyNameRegex: /📷/, | |
/** | |
* Relative path from this .js file to the storybook config directory | |
*/ | |
configPath: CONFIG_PATH, | |
/** | |
* Replace normal storyshots test function with image snapshots | |
* | |
* @link https://github.com/storybooks/storybook/tree/master/addons/storyshots/storyshots-puppeteer | |
* @link https://github.com/storybooks/storybook/blob/master/addons/storyshots/storyshots-puppeteer/src/index.js | |
*/ | |
test: imageSnapshot({ | |
/** | |
* Location of the prebuilt storybook static instance | |
*/ | |
storybookUrl: `file://${PATH_TO_STORYBOOK_STATIC}`, | |
/** | |
* modify page before puppeteer.goto | |
* | |
* Note: Emulation and Viewport settings don't work here | |
*/ | |
customizePage: page => page, | |
/** | |
* puppeteer.goto options | |
* @link https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#pagegotourl-options | |
*/ | |
getGotoOptions: ({ context, url }) => ({ | |
waitUntil: 'networkidle0' | |
}), | |
/** | |
* set the page viewport size | |
* wait before taking screenshot to give uncontrollable animations time to complete (Highcharts) | |
*/ | |
beforeScreenshot: async (page, { context: { kind, story }, url }) => { | |
await page.reload(); | |
await page.waitFor(10000); | |
}, | |
/** | |
* Emulate a collection of viewport dimensions | |
*/ | |
emulateViewports: [ | |
{ width: 1920, height: 1080 }, | |
{ width: 1600, height: 900 } | |
], | |
/** | |
* Emulate a collection of devices | |
* @link https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#pageemulateoptions | |
* @link https://github.com/GoogleChrome/puppeteer/blob/master/DeviceDescriptors.js | |
*/ | |
emulateDevices: [ | |
devices['iPhone 6'], | |
devices['iPhone 6 landscape'], | |
devices['iPad'], | |
devices['iPad landscape'] | |
], | |
/** | |
* puppeteer screenshot configuration | |
* @link https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#pagescreenshotoptions | |
*/ | |
getScreenshotOptions: ({ | |
context: { kind, framework, story }, | |
url, | |
device | |
}) => ({ | |
fullPage: true | |
}), | |
/** | |
* jest-image-snapshot configuration | |
* @link https://github.com/americanexpress/jest-image-snapshot | |
*/ | |
getMatchOptions: ({ context: { kind, story }, url, device, viewport }) => ({ | |
failureThreshold: 0.01, | |
failureThresholdType: 'percent', | |
customSnapshotsDir: OUTPUT_DIRECTORY, | |
// use custom file name. Extension will be added by jest-image-snapshot | |
customSnapshotIdentifier: | |
(device ? `${device.name}-` : '') + | |
(viewport ? `${viewport.width}X${viewport.height}-` : '') + | |
`${kind}-${story}` | |
.replace(/[^a-z0-9]/gi, '_') // replace anything other than basic letters or numbers with '_' | |
.replace(/_+/g, '_') // replace any number of sequential underscores with a single underscore | |
.toLowerCase() | |
}) | |
}) | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Source: https://github.com/storybooks/storybook/tree/master/addons/storyshots/storyshots-puppeteer | |
import puppeteer from 'puppeteer'; | |
import { toMatchImageSnapshot } from 'jest-image-snapshot'; | |
import { logger } from '@storybook/node-logger'; | |
expect.extend({ toMatchImageSnapshot }); | |
// We consider taking the full page is a reasonnable default. | |
const defaultScreenshotOptions = () => ({ fullPage: true }); | |
const noop = () => {}; | |
const asyncNoop = async () => {}; | |
const defaultConfig = { | |
storybookUrl: 'http://localhost:6006', | |
chromeExecutablePath: undefined, | |
getMatchOptions: noop, | |
getScreenshotOptions: defaultScreenshotOptions, | |
beforeScreenshot: noop, | |
getGotoOptions: noop, | |
customizePage: asyncNoop, | |
emulateDevices: [], | |
emulateViewports: [] | |
}; | |
export const imageSnapshot = (customConfig = {}) => { | |
const { | |
storybookUrl, | |
chromeExecutablePath, | |
getMatchOptions, | |
getScreenshotOptions, | |
beforeScreenshot, | |
getGotoOptions, | |
customizePage, | |
emulateViewports, | |
emulateDevices | |
} = { ...defaultConfig, ...customConfig }; | |
let browser; // holds ref to browser. (ie. Chrome) | |
let page; // Hold ref to the page to screenshot. | |
const testFn = async ({ context }) => { | |
const { kind, framework, story } = context; | |
if (framework === 'rn') { | |
// Skip tests since we de not support RN image snapshots. | |
logger.error( | |
"It seems you are running imageSnapshot on RN app and it's not supported. Skipping test." | |
); | |
return; | |
} | |
const encodedKind = encodeURIComponent(kind); | |
const encodedStoryName = encodeURIComponent(story); | |
const storyUrl = `/iframe.html?selectedKind=${encodedKind}&selectedStory=${encodedStoryName}`; | |
const url = storybookUrl + storyUrl; | |
if (!browser || !page) { | |
logger.error( | |
`Error when generating image snapshot for test ${kind} - ${story} : It seems the headless browser is not running.` | |
); | |
throw new Error('no-headless-browser-running'); | |
} | |
// There should be either: | |
// 1 test (the default) if no emulation options are set | |
// sum of the emulation tests lengths | |
expect.assertions(emulateDevices.length + emulateViewports.length || 1); | |
try { | |
await customizePage(page); | |
await page.goto(url, getGotoOptions({ context, url })); | |
} catch (e) { | |
logger.error( | |
`Error when connecting to ${url}, did you start or build the storybook first? A storybook instance should be running or a static version should be built when using image snapshot feature.`, | |
e | |
); | |
throw e; | |
} | |
// If no emulation tests are specified | |
if (!emulateViewports.length && !emulateDevices.length) { | |
// Default Test | |
await beforeScreenshot(page, { context, url }); | |
const image = await page.screenshot( | |
getScreenshotOptions({ context, url }) | |
); | |
expect(image).toMatchImageSnapshot(getMatchOptions({ context, url })); | |
} | |
// Emulate Viewports | |
for (const viewport of emulateViewports) { | |
await page.setViewport(viewport); | |
await beforeScreenshot(page, { context, url, viewport }); | |
const image = await page.screenshot( | |
getScreenshotOptions({ context, url, viewport }) | |
); | |
expect(image).toMatchImageSnapshot( | |
getMatchOptions({ context, url, viewport }) | |
); | |
} | |
// Emulate Devices | |
for (const device of emulateDevices) { | |
await page.emulate(device); | |
await beforeScreenshot(page, { context, url, device }); | |
const image = await page.screenshot( | |
getScreenshotOptions({ context, url, device }) | |
); | |
expect(image).toMatchImageSnapshot( | |
getMatchOptions({ context, url, device }) | |
); | |
} | |
}; | |
testFn.afterAll = () => browser.close(); | |
testFn.beforeAll = async () => { | |
// add some options "no-sandbox" to make it work properly on some Linux systems as proposed here: https://github.com/Googlechrome/puppeteer/issues/290#issuecomment-322851507 | |
browser = await puppeteer.launch({ | |
args: ['--no-sandbox ', '--disable-setuid-sandbox'], | |
executablePath: chromeExecutablePath | |
}); | |
page = await browser.newPage(); | |
}; | |
return testFn; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment