diff --git a/addons/storyshots/README.md b/addons/storyshots/README.md
index 813db6023f9f..6416a4f4bb5e 100644
--- a/addons/storyshots/README.md
+++ b/addons/storyshots/README.md
@@ -145,6 +145,10 @@ Just render the story, don't check the output at all (useful if you just want to
Like the default, but allows you to specify a set of options for the test renderer. [See for example here](https://github.com/storybooks/storybook/blob/b915b5439786e0edb17d7f5ab404bba9f7919381/examples/test-cra/src/storyshots.test.js#L14-L16).
+### `multiSnapshotWithOptions(options)`
+
+Like `snapshotWithOptions`, but generate a separate snapshot file for each stories file rather than a single monolithic file (as is the convention in Jest). This makes it dramatically easier to review changes.
+
### `shallowSnapshot`
Take a snapshot of a shallow-rendered version of the component.
diff --git a/addons/storyshots/package.json b/addons/storyshots/package.json
index b61abc267705..40b6eed59f2d 100644
--- a/addons/storyshots/package.json
+++ b/addons/storyshots/package.json
@@ -11,11 +11,14 @@
"scripts": {
"build-storybook": "build-storybook",
"prepublish": "babel ./src --out-dir ./dist",
- "storybook": "start-storybook -p 6006"
+ "storybook": "start-storybook -p 6006",
+ "example": "jest storyshot.test"
},
"dependencies": {
"babel-runtime": "^6.23.0",
+ "glob": "^7.1.2",
"global": "^4.3.2",
+ "jest-specific-snapshot": "^0.2.0",
"prop-types": "^15.5.10",
"read-pkg-up": "^2.0.0"
},
@@ -24,11 +27,14 @@
"@storybook/channels": "^3.2.0",
"@storybook/react": "^3.2.8",
"babel-cli": "^6.24.1",
+ "babel-jest": "^20.0.3",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-env": "^1.6.0",
"babel-preset-react": "^6.24.1",
"react": "^15.6.1",
- "react-dom": "^15.6.1"
+ "react-dom": "^15.6.1",
+ "jest": "^20.0.4",
+ "jest-cli": "^20.0.4"
},
"peerDependencies": {
"@storybook/addons": "^3.2.6",
diff --git a/addons/storyshots/src/index.js b/addons/storyshots/src/index.js
index 245b28a9b78f..3da1bc04e098 100644
--- a/addons/storyshots/src/index.js
+++ b/addons/storyshots/src/index.js
@@ -1,4 +1,6 @@
import path from 'path';
+import fs from 'fs';
+import glob from 'glob';
import global, { describe, it } from 'global';
import readPkgUp from 'read-pkg-up';
import addons from '@storybook/addons';
@@ -6,8 +8,15 @@ import addons from '@storybook/addons';
import runWithRequireContext from './require_context';
import createChannel from './storybook-channel-mock';
import { snapshot } from './test-bodies';
+import { getPossibleStoriesFiles } from './utils';
-export { snapshotWithOptions, snapshot, shallowSnapshot, renderOnly } from './test-bodies';
+export {
+ snapshot,
+ multiSnapshotWithOptions,
+ snapshotWithOptions,
+ shallowSnapshot,
+ renderOnly,
+} from './test-bodies';
let storybook;
let configPath;
@@ -48,6 +57,7 @@ export default function testStorySnapshots(options = {}) {
runWithRequireContext(content, contextOpts);
} else if (isRNStorybook) {
storybook = require.requireActual('@storybook/react-native');
+
configPath = path.resolve(options.configPath || 'storybook');
require.requireActual(configPath);
} else {
@@ -70,13 +80,15 @@ export default function testStorySnapshots(options = {}) {
// eslint-disable-next-line
for (const group of stories) {
- if (options.storyKindRegex && !group.kind.match(options.storyKindRegex)) {
+ const { fileName, kind } = group;
+
+ if (options.storyKindRegex && !kind.match(options.storyKindRegex)) {
// eslint-disable-next-line
continue;
}
describe(suite, () => {
- describe(group.kind, () => {
+ describe(kind, () => {
// eslint-disable-next-line
for (const story of group.stories) {
if (options.storyNameRegex && !story.name.match(options.storyNameRegex)) {
@@ -85,7 +97,7 @@ export default function testStorySnapshots(options = {}) {
}
it(story.name, () => {
- const context = { kind: group.kind, story: story.name };
+ const context = { fileName, kind, story: story.name };
options.test({ story, context });
});
}
@@ -93,3 +105,16 @@ export default function testStorySnapshots(options = {}) {
});
}
}
+
+describe('Storyshots Integrity', () => {
+ describe('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);
+ });
+});
diff --git a/addons/storyshots/src/test-bodies.js b/addons/storyshots/src/test-bodies.js
index 48362bca93c1..bf71ca3f4994 100644
--- a/addons/storyshots/src/test-bodies.js
+++ b/addons/storyshots/src/test-bodies.js
@@ -1,12 +1,40 @@
import renderer from 'react-test-renderer';
import shallow from 'react-test-renderer/shallow';
+import 'jest-specific-snapshot';
+import { getStoryshotFile } from './utils';
-export const snapshotWithOptions = options => ({ story, context }) => {
+function getRenderedTree(story, context, options) {
const storyElement = story.render(context);
- const tree = renderer.create(storyElement, options).toJSON();
+ return renderer.create(storyElement, options).toJSON();
+}
+
+function getSnapshotFileName(context) {
+ const fileName = context.fileName;
+
+ if (!fileName) {
+ return null;
+ }
+
+ return getStoryshotFile(fileName);
+}
+
+export const snapshotWithOptions = options => ({ story, context }) => {
+ const tree = getRenderedTree(story, context, options);
expect(tree).toMatchSnapshot();
};
+export const multiSnapshotWithOptions = options => ({ story, context }) => {
+ const tree = getRenderedTree(story, context, options);
+ const snapshotFileName = getSnapshotFileName(context);
+
+ if (!snapshotFileName) {
+ expect(tree).toMatchSnapshot();
+ return;
+ }
+
+ expect(tree).toMatchSpecificSnapshot(snapshotFileName);
+};
+
export const snapshot = snapshotWithOptions({});
export function shallowSnapshot({ story, context }) {
diff --git a/addons/storyshots/src/utils.js b/addons/storyshots/src/utils.js
new file mode 100644
index 000000000000..86f2d3f92941
--- /dev/null
+++ b/addons/storyshots/src/utils.js
@@ -0,0 +1,15 @@
+import path from 'path';
+
+export function getStoryshotFile(fileName) {
+ const { dir, name } = path.parse(fileName);
+ return path.format({ dir: path.join(dir, '__snapshots__'), name, ext: '.storyshot' });
+}
+
+export function getPossibleStoriesFiles(storyshotFile) {
+ const { dir, name } = path.parse(storyshotFile);
+
+ return [
+ path.format({ dir: path.dirname(dir), name, ext: '.js' }),
+ path.format({ dir: path.dirname(dir), name, ext: '.jsx' }),
+ ];
+}
diff --git a/addons/storyshots/stories/directly_required/__snapshots__/index.storyshot b/addons/storyshots/stories/directly_required/__snapshots__/index.storyshot
new file mode 100644
index 000000000000..354841be656e
--- /dev/null
+++ b/addons/storyshots/stories/directly_required/__snapshots__/index.storyshot
@@ -0,0 +1,19 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Storyshots Another Button with some emoji 1`] = `
+
+`;
+
+exports[`Storyshots Another Button with text 1`] = `
+
+`;
diff --git a/addons/storyshots/stories/required_with_context/__snapshots__/Button.stories.storyshot b/addons/storyshots/stories/required_with_context/__snapshots__/Button.stories.storyshot
new file mode 100644
index 000000000000..56b21f7fdec4
--- /dev/null
+++ b/addons/storyshots/stories/required_with_context/__snapshots__/Button.stories.storyshot
@@ -0,0 +1,19 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Storyshots Button with some emoji 1`] = `
+
+`;
+
+exports[`Storyshots Button with text 1`] = `
+
+`;
diff --git a/addons/storyshots/stories/required_with_context/__snapshots__/Welcome.stories.storyshot b/addons/storyshots/stories/required_with_context/__snapshots__/Welcome.stories.storyshot
new file mode 100644
index 000000000000..bc6abe86a6e7
--- /dev/null
+++ b/addons/storyshots/stories/required_with_context/__snapshots__/Welcome.stories.storyshot
@@ -0,0 +1,104 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Storyshots Welcome to Storybook 1`] = `
+
+
+ Welcome to storybook
+
+
+ This is a UI component dev environment for your app.
+
+
+ We've added some basic stories inside the
+
+
+ src/stories
+
+
+ directory.
+
+ A story is a single state of one or more UI components. You can have as many stories as you want.
+
+ (Basically a story is like a visual test case.)
+
+
+ See these sample
+
+
+ stories
+
+
+ for a component called
+
+
+ Button
+
+ .
+
+
+ Just like that, you can add your own components as stories.
+
+ You can also edit those components and see changes right away.
+
+ (Try editing the
+
+ Button
+
+ stories located at
+
+ src/stories/index.js
+
+ .)
+
+
+ Usually we create stories with smaller UI components in the app.
+
+ Have a look at the
+
+
+ Writing Stories
+
+
+ section in our documentation.
+
+
+
+ NOTE:
+
+
+ Have a look at the
+
+
+ .storybook/webpack.config.js
+
+
+ to add webpack loaders and plugins you are using in this project.
+