🤖📗 PictureBook
Automated Storybook Setup
Simplify Storybook story creation and cross-browser image comparison testing
💡 Rationale
Setting up storybook and implement cross-browser image comparison testing on multiple projects is time consuming.
Instead of providing a wrapper on top of existing projects that will fall out of date, Picturebook lets you retain control of the Storybook and Nightwatch packages.
This project aims to provide utility methods to simplify Storybook, SauceLabs and Nightwatch configuration and screenshot comparison. Specifically:
- Creation of storybook stories: They are created based on your file system structure
- Saucelabs tunnel setup: Reduce SauceConnect config to the SauceConnect binary path and username / accessKey
- Screenshot: Take screenshots of every story on different browsers using SauceLabs and Nightwatch
- Image Comparison: Compare and update screenshots to baselines collocated with your stories.
⚙️ Install
- You will need a SauceLabs account. If you don't have one you can sign up for a trial here or request a free one for open source projects here.
- If you want to run tests against Safari and IE11 from localhost, add
localtest.dev 127.0.0.1
to your/etc/hosts
(the exact steps may vary depending on your platform, see here for more details). - Add picturebook and its peer dependencies:
yarn add --dev picturebook @storybook/react nightwatch react
React is used in the examples but you can also use any flavor of storybook you want.
If you don't have Storybook set it up yet, follow these instructions first.
Some of Picturebook utility methods rely on the output of require.context
. This is a webpack construct and it's not available in Node or test environments. There are multiple ways to mock it. If you are using babel you could add the require-context-hook plugin. For instance, if you want to enable it only for tests, you can do something like:
and on your test setup:
Alternatively, there's also the require-context npm package that will also emulate webpack's require.context
.
📚 API
The API can be split into 2 sections, storybook creation for finding files in the file system and creating storybook stories around them and storybook testing, to connect to SauceLabs and run cross-browser image comparison tests.
Storybook creation
getFiles
and loadStories
have very similar APIs and output.
getFiles
allows you to retrieve a list of related files. For instance if you have:
file1.jsfile1.mdfile2.pngfile2.js
It will provide an array of 2 elements (file1 and file2) with the different related files grouped.
These values are used to identify helper files for stories where *.md
is understood to be documentation,.*(spec|test).js
is a test file and an unprefixed *.js
file is a story. The flattenFolders
parameters allow grouping nested folders as well. For instance if flattenFolders: ['a']
file1.js
and a/file1.png
will be grouped together.
loadStories
provide the same functionality than getFiles
but it also loads and creates storybook stories. This is why it requires storybook's storiesOf method as a parameter. The response is also similar but it includes a main
method with the loaded story.
You can find the exact APIs below:
type StoryPaths = | name: string parents: $ReadOnlyArray<string> title: string path: string screenshots: | extension: string: string | tests: | extension: string: string | doc: ?string url: ?string|} // Retrieve and group the different stories based on their filenames: Array<StoryPaths> // Retrieve, group, load and create storiesfunction loadStories({| baseUrl?: string // The url for the story, defaults to: // 'http://localhost:6006' flattenFolders?: Array<string> // Folders to flatten // defaults to ['__snapshots__', '__screenshots__'] stories: any // Output of require context with the stories decorators: $ReadOnlyArray<Function> storiesOf: any storyFiles: $ReadOnlyArray<string>|}): Array<StoryPaths & | any|>
Storybook testing
Once stories are loaded, runTests
will connect to SauceLabs (optionally through a SauceConnect tunnel) and return a Promise that includes the results of the test.
Since it uses nightwatch
under the hood, you must have a valid nightwatch instance running. To assist with it, nightwatchConfig
provides defaultValues
type Status = 'CREATED' | 'SUCCESS' | 'FAILED'type ImgResults = | browser: string diffPath: ?string diffThreshold: number error: ?string name: string platform: string referencePath: ?string screenshotPath: ?string status: Status |type Tunnel = | id: string binaryPath: string| // Invoke nightwatch and run image comparison in SauceLabs: Promise<{| error: Error | null // null if there were no exceptions during tests results: Array<ImgResults> // per browser / platform / story results // An overall summary status. If any tests failed, it's FAILED, if any tests // where created or updated it's CREATED, if all tests succeeded it's SUCCESS // if no tests ran, it's EMPTY status: Status | 'EMPTY' counts: | // counts for all tests CREATED: number SUCCESS: number FAILED: number | version: string // picturebook version used date: string // ISO date when was the test report created|}> // Nightwatch config generation helper: Object
🎪 Sample App
This project includes a sample app to demo the behavior. You can view it running yarn start
and opening localhost:6006
on your browser.
To run the image comparison tests, keep yarn start
running and call SAUCE_ACCESS_KEY={YOUR_ACCESS_KEY} SAUCE_USERNAME={YOUR_USERNAME} yarn test:app
.
️✏️ Storybook Usage
To take advantage of the automated storybook folder config (and testing) modify your storybook config.js
to look like this:
{ picturebook}
If you want to see a more advanced use case on how to use it with decorators and other plugins, check out the sample project.
📸 Screenshot Usage
To take screenshots, picturebook relies on nightwatch. With the exception of a few required fields, configuration is generated for you. To take advantage of it you should create a nightwatch.conf.js
file on your root (or customize it following instructions here).
const resolve = const nightwatchConfig getFiles = const requireContext = const config = moduleexports = config
To take screenshots and run image comparison, you can call:
Note that if using a tunnel, tunnel-identifier
on your nightwatch config must match the tunnel.id
parameter passed to runTests
.
🙋 QYMA (Questions you may ask)
Why do I need to add localtest.dev as a localhost alias?
This is a SauceLabs issue. To work around it, safari
and edge
browsers replace localhost
from urls to localtest.dev
as described here.
You can customize the target browsers and localhost alias with the localhostAliasBrowsers
and localhostAlias
, respectively.
How do you skip some files?
Picturebook will only take screenshots of the files you tell it exists. You can filter the output from require.context by specifying a different regExp to filter by.
SauceConnect ENOENT error
If you see the following error after a couple unsuccessful runs of SauceConnect:
events.js:167 throw er; // Unhandled 'error' event ^ Error: spawn ~/picturebook/node_modules/node-sauce-connect/lib/sc ENOENT
Sometimes it gets stuck. Try cleaning node_modules
and re-running yarn:
rm -rf node_modulesyarn
Error retrieving a new session from the selenium server
This is a Nightwatch issue, addressed in 1.0.4 versions of Nightwatch and later as per this issue.
node-dir "cannot read property 'files' of undefined" error
There's currently a bug in node-dir that fails on empty dirs.
Remove empty folders within your stories if you see the following error:
yourProject/node_modules/node-dir/lib/paths.js:92 results.files = results.files.concat; ^ TypeError: Cannot read property 'files' of undefined at subloop at /Users/obartra/repos/gymnast/node_modules/node-dir/lib/paths.js:107:13 at onDirRead at onStat
You can do so with:
find /some/path -depth -type d -exec rmdir {} \;
No Java runtime present
Selenium 3.0+ requires Java 8. Make sure it's installed if you get the following error:
Starting selenium server in parallel mode... There was an error
For additional Selenium tips, check out selenium-standalone tips.