Skip to content

Commit

Permalink
Merge pull request #2413 from thomasbertet/storyshots-add-image-snaps…
Browse files Browse the repository at this point in the history
…hots

Add ability to use image snapshots to addon-storyshots
  • Loading branch information
Hypnosphi authored Jan 11, 2018
2 parents 65a121f + 797f2b7 commit 5dc6e8f
Show file tree
Hide file tree
Showing 94 changed files with 323 additions and 93 deletions.
31 changes: 19 additions & 12 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@ jobs:
name: "Link packages"
command: |
yarn install
- run:
name: Workaround for https://github.com/GoogleChrome/puppeteer/issues/290
command: sh ./scripts/workaround-puppeteer-issue-290.sh
- run:
name: "Build official-storybook"
command: |
cd examples/official-storybook
yarn build-storybook
- run:
name: "Build react kitchen-sink"
command: |
Expand All @@ -73,34 +81,33 @@ jobs:
command: |
cd examples/angular-cli
yarn build-storybook
- run:
name: "Run react kitchen-sink"
name: "Run react kitchen-sink (smoke test)"
command: |
cd examples/cra-kitchen-sink
yarn storybook
background: true
yarn storybook --smoke-test
- run:
name: "Run vue kitchen-sink"
name: "Run vue kitchen-sink (smoke test)"
command: |
cd examples/vue-kitchen-sink
yarn storybook
background: true
yarn storybook --smoke-test
- run:
name: "Run angular-cli"
name: "Run angular-cli (smoke test)"
command: |
cd examples/angular-cli
yarn storybook
background: true
yarn storybook --smoke-test
- run:
name: Workaround for https://github.com/GoogleChrome/puppeteer/issues/290
command: sh ./scripts/workaround-puppeteer-issue-290.sh
name: "Run image snapshots"
command: yarn test --image
- run:
name: Integration Test - Kichen sinks
command: yarn test --integration
- store_artifacts:
path: integration/__image_snapshots__
destination: integration_image_snapshots
- store_artifacts:
path: examples/official-storybook/image-snapshots/__image_snapshots__
destination: official_storybook_image_snapshots
react-native:
<<: *defaults
steps:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ package-lock.json
storybook-static
integration/__image_snapshots__/__diff_output__
.jest-test-results.json
/examples/cra-kitchen-sink/src/__image_snapshots__/__diff_output__/
8 changes: 8 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,14 @@ In order for the snapshot-integration tests to be executed properly, examples be

Puppeteer is used to launch and grab screenshots of example pages, while jest is used to assert matching images.

##### CRA-kitchen-sink - Image snapshots using Storyshots

`yarn test --image`

This option executes tests from `<rootdir>/examples/cra-kitchen-sink`
In order for the image snapshots to be correctly generated, you must have static build of the storybook up-to-date.

Puppeteer is used to launch and grab screenshots of example pages, while jest is used to assert matching images. (just like integration tests)

#### 2b. Run e2e tests for CLI

Expand Down
99 changes: 98 additions & 1 deletion addons/storyshots/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Usually, you might already have completed this step. If not, here are some resou

> Note: If you use React 16, you'll need to follow [these additional instructions](https://github.com/facebook/react/issues/9102#issuecomment-283873039).
## Configure Storyshots
## Configure Storyshots for HTML snapshots

Create a new test file with the name `Storyshots.test.js`. (Or whatever the name you prefer, as long as it matches Jest's config [`testMatch`](http://facebook.github.io/jest/docs/en/configuration.html#testmatch-array-string)).
Then add following content to it:
Expand All @@ -53,6 +53,103 @@ Now run your Jest test command. (Usually, `npm test`.) Then you can see all of y

![Screenshot](docs/storyshots.png)


## Configure Storyshots for image snapshots

/*\ **React-native** is **not supported** by this test function.

Internally, it uses [jest-image-snapshot](https://github.com/americanexpress/jest-image-snapshot).

When willing to generate and compare image snapshots for your stories, you have to two options:
- Have a storybook running (ie. accessible via http(s):// , for instance using `yarn run storybook`)
- Have a static build of the storybook (for instance, using `yarn run build-storybook`)

Then you will need to reference the storybook URL (`file://...` if local, `http(s)://...` if served)

### Using default values for _imageSnapshots_

Then you can either create a new Storyshots instance or edit the one you previously used:
```js
import initStoryshots, { imageSnapshot } from '@storybook/addon-storyshots';

initStoryshots({suite: 'Image storyshots', test: imageSnapshot});
```
This will assume you have a storybook running on at _http://localhost:6006_.
Internally here are the steps:
- Launches a Chrome headless using [puppeteer](https://github.com/GoogleChrome/puppeteer)
- Browses each stories (calling _http://localhost:6006/iframe.html?..._ URL),
- Take screenshots & save all images under _\_image_snapshots\__ folder.

### Specifying the storybook URL

If you want to set specific storybook URL, you can specify via the `storybookUrl` parameter, see below:
```js
import initStoryshots, { imageSnapshot } from '@storybook/addon-storyshots';

initStoryshots({suite: 'Image storyshots', test: imageSnapshot({storybookUrl: 'http://my-specific-domain.com:9010'})});
```
The above config will use _https://my-specific-domain.com:9010_ for screenshots.


You may also use a local static build of storybook if you do not want to run the webpack dev-server:
```js
import initStoryshots, { imageSnapshot } from '@storybook/addon-storyshots';

initStoryshots({suite: 'Image storyshots', test: imageSnapshot({storybookUrl: 'file:///path/to/my/storybook-static'})});
```

### Specifying options to _jest-image-snapshots_

If you wish to customize [jest-image-snapshot](https://github.com/americanexpress/jest-image-snapshot), then you can provide a `getMatchOptions` parameter that should return the options config object.
```js
import initStoryshots, { imageSnapshot } from '@storybook/addon-storyshots';
const getMatchOptions = ({context : {kind, story}, url}) => {
return {
failureThreshold: 0.2,
failureThresholdType: 'percent',
}
}
initStoryshots({suite: 'Image storyshots', test: imageSnapshot({storybookUrl: 'http://localhost:6006', getMatchOptions})});
```
`getMatchOptions` receives an object: `{ context: {kind, story}, url}`. _kind_ is the kind of the story and the _story_ its name. _url_ is the URL the browser will use to screenshot.


### Integrate image storyshots with regular app
You may want to use another Jest project to run your image snapshots as they require more resources: Chrome and Storybook built/served.
You can find a working example of this in the [official-storybook](https://github.com/storybooks/storybook/tree/master/examples/official-storybook) example.

### Integrate image storyshots with [Create React App](https://github.com/facebookincubator/create-react-app)
You have two options here, you can either:

- Simply add the storyshots configuration inside any of your `test.js` file. You must ensure you have either a running storybook or a static build available.

- Create a custom test file using Jest outside of the CRA scope:

A more robust approach would be to separate existing test files ran by create-react-app (anything `(test|spec).js` suffixed files) from the test files to run storyshots with image snapshots.
This use case can be achieved by using a custom name for the test file, ie something like `image-storyshots.runner.js`. This file will contains the `initStoryshots` call with image snapshots configuration.
Then you will create a separate script entry in your package.json, for instance
```json
{
"scripts": {
"image-snapshots" : "jest image-storyshots.runner.js --config path/to/custom/jest.config.json"
}
}
```
Note that you will certainly need a custom config file for Jest as you run it outside of the CRA scope and thus you do not have the built-in config.

Once that's setup, you can run `yarn run image-snapshots` (or `npm run image-snapshots`).

### Reminder
An image snapshot is simply a screenshot taken by a web browser (in our case, Chrome).

The browser opens a page (either using the static build of storybook or a running instance of Storybook)

If you run your test without either the static build or a running instance, this wont work.

To make sure your screenshots are taken from latest changes of your Storybook, you must keep your static build or running Storybook up-to-date.
This can be achieved by adding a step before running the test ie: `yarn run build-storybook && yarn run image-snapshots`.
If you run the image snapshots against a running Storybook in dev mode, you don't have to care about being up-to-date because the dev-server is watching changes and rebuilds automatically.

## Options

### `configPath`
Expand Down
2 changes: 2 additions & 0 deletions addons/storyshots/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
"glob": "^7.1.2",
"global": "^4.3.2",
"jest-specific-snapshot": "^0.3.0",
"jest-image-snapshot": "^2.2.0",
"prop-types": "^15.6.0",
"puppeteer": "^0.13.0",
"read-pkg-up": "^3.0.0"
},
"devDependencies": {
Expand Down
23 changes: 19 additions & 4 deletions addons/storyshots/src/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import path from 'path';
import fs from 'fs';
import glob from 'glob';
import global, { describe, it } from 'global';
import global, { describe, it, beforeEach, afterEach } from 'global';
import readPkgUp from 'read-pkg-up';
import addons from '@storybook/addons';

Expand All @@ -18,6 +18,8 @@ export {
renderOnly,
} from './test-bodies';

export { imageSnapshot } from './test-body-image-snapshot';

export { getSnapshotFileName };

let storybook;
Expand Down Expand Up @@ -100,6 +102,20 @@ export default function testStorySnapshots(options = {}) {
}

describe(suite, () => {
beforeEach(() => {
if (typeof options.test.beforeEach === 'function') {
return options.test.beforeEach();
}
return Promise.resolve();
});

afterEach(() => {
if (typeof options.test.afterEach === 'function') {
return options.test.afterEach();
}
return Promise.resolve();
});

describe(kind, () => {
// eslint-disable-next-line
for (const story of group.stories) {
Expand All @@ -109,7 +125,7 @@ export default function testStorySnapshots(options = {}) {
}

it(story.name, () => {
const context = { fileName, kind, story: story.name };
const context = { fileName, kind, story: story.name, isRNStorybook };
return options.test({
story,
context,
Expand All @@ -122,14 +138,13 @@ export default function testStorySnapshots(options = {}) {
}

describe('Storyshots Integrity', () => {
describe('Abandoned Storyshots', () => {
test('Abandoned Storyshots', () => {
const storyshots = glob.sync('**/*.storyshot');

const abandonedStoryshots = storyshots.filter(fileName => {
const possibleStoriesFiles = getPossibleStoriesFiles(fileName);
return !possibleStoriesFiles.some(fs.existsSync);
});

expect(abandonedStoryshots).toHaveLength(0);
});
});
67 changes: 67 additions & 0 deletions addons/storyshots/src/test-body-image-snapshot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import puppeteer from 'puppeteer';
import { toMatchImageSnapshot } from 'jest-image-snapshot';

expect.extend({ toMatchImageSnapshot });

export const imageSnapshot = ({
storybookUrl = 'http://localhost:6006',
getMatchOptions = () => {},
}) => {
let browser; // holds ref to browser. (ie. Chrome)
let page; // Hold ref to the page to screenshot.

const testFn = ({ context }) => {
if (context.isRNStorybook) {
// Skip tests since we de not support RN image snapshots.
console.error(
"It seems you are running imageSnapshot on RN app and it's not supported. Skipping test."
);
return Promise.resolve();
}

const encodedKind = encodeURIComponent(context.kind);
const encodedStoryName = encodeURIComponent(context.story);
const storyUrl = `/iframe.html?selectedKind=${encodedKind}&selectedStory=${encodedStoryName}`;
const url = storybookUrl + storyUrl;
if (!browser || !page) {
console.error(
`Error when generating image snapshot for test ${context.kind} - ${
context.story
} : It seems the headless browser is not running.`
);
return Promise.reject(new Error('no-headless-browser-running'));
}

expect.assertions(1);
return page
.goto(url)
.catch(e => {
console.error(
`ERROR WHILE 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;
})
.then(() =>
page.screenshot().then(image => {
expect(image).toMatchImageSnapshot(getMatchOptions({ context, url }));
})
);
};

testFn.beforeEach = () =>
puppeteer
// 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
.launch({ args: ['--no-sandbox ', '--disable-setuid-sandbox'] })
.then(b => {
browser = b;
})
.then(() => browser.newPage())
.then(p => {
page = p;
});

testFn.afterEach = () => browser.close();

return testFn;
};
1 change: 0 additions & 1 deletion examples/cra-kitchen-sink/README.md

This file was deleted.

1 change: 0 additions & 1 deletion examples/cra-kitchen-sink/__mocks__/fileMock.js

This file was deleted.

1 change: 0 additions & 1 deletion examples/cra-kitchen-sink/__mocks__/styleMock.js

This file was deleted.

28 changes: 0 additions & 28 deletions examples/cra-kitchen-sink/config-addon-jest.json

This file was deleted.

1 change: 0 additions & 1 deletion examples/cra-kitchen-sink/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
"build": "react-scripts build",
"build-storybook": "build-storybook -s public",
"eject": "react-scripts eject",
"generate-addon-jest-testresults": "jest src/stories/addon-jest.test.js --config=config-addon-jest.json --env=jsdom --json --outputFile=src/stories/addon-jest.testresults.json",
"start": "react-scripts start",
"storybook": "start-storybook -p 9010 -s public",
"test": "react-scripts test --env=jsdom"
Expand Down
4 changes: 0 additions & 4 deletions examples/cra-kitchen-sink/src/enzyme.js

This file was deleted.

Loading

0 comments on commit 5dc6e8f

Please sign in to comment.