Skip to content

Instantly share code, notes, and snippets.

@thehig
Created September 19, 2018 12:33
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save thehig/face089a7d0d469ea992fd5f4ef9776f to your computer and use it in GitHub Desktop.
Save thehig/face089a7d0d469ea992fd5f4ef9776f to your computer and use it in GitHub Desktop.
js: Storyshots with multiple device/viewport puppeteer screenshots
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()
})
})
});
// 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