Skip to content

Commit

Permalink
Merge pull request #1584 from storybooks/887-generate-snapshot-per-st…
Browse files Browse the repository at this point in the history
…ory-file

887 Generate snapshot per story file
  • Loading branch information
ndelangen authored Aug 27, 2017
2 parents b77376c + 6496454 commit c0e468a
Show file tree
Hide file tree
Showing 24 changed files with 420 additions and 58 deletions.
4 changes: 4 additions & 0 deletions addons/storyshots/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
10 changes: 8 additions & 2 deletions addons/storyshots/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand All @@ -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",
Expand Down
33 changes: 29 additions & 4 deletions addons/storyshots/src/index.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
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';

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;
Expand Down Expand Up @@ -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 {
Expand All @@ -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)) {
Expand All @@ -85,11 +97,24 @@ 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 });
});
}
});
});
}
}

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);
});
});
32 changes: 30 additions & 2 deletions addons/storyshots/src/test-bodies.js
Original file line number Diff line number Diff line change
@@ -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 }) {
Expand Down
15 changes: 15 additions & 0 deletions addons/storyshots/src/utils.js
Original file line number Diff line number Diff line change
@@ -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' }),
];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Storyshots Another Button with some emoji 1`] = `
<button
className="css-1yjiefr"
onClick={[Function]}
>
😀 😎 👍 💯
</button>
`;

exports[`Storyshots Another Button with text 1`] = `
<button
className="css-1yjiefr"
onClick={[Function]}
>
Hello Button
</button>
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Storyshots Button with some emoji 1`] = `
<button
className="css-1yjiefr"
onClick={[Function]}
>
😀 😎 👍 💯
</button>
`;

exports[`Storyshots Button with text 1`] = `
<button
className="css-1yjiefr"
onClick={[Function]}
>
Hello Button
</button>
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Storyshots Welcome to Storybook 1`] = `
<article
className="css-1fqbdip"
>
<h1
className="css-nil"
>
Welcome to storybook
</h1>
<p>
This is a UI component dev environment for your app.
</p>
<p>
We've added some basic stories inside the

<code
className="css-mteq83"
>
src/stories
</code>

directory.
<br />
A story is a single state of one or more UI components. You can have as many stories as you want.
<br />
(Basically a story is like a visual test case.)
</p>
<p>
See these sample

<a
className="css-ca0824"
onClick={[Function]}
role="button"
tabIndex="0"
>
stories
</a>

for a component called

<code
className="css-mteq83"
>
Button
</code>
.
</p>
<p>
Just like that, you can add your own components as stories.
<br />
You can also edit those components and see changes right away.
<br />
(Try editing the
<code
className="css-mteq83"
>
Button
</code>
stories located at
<code
className="css-mteq83"
>
src/stories/index.js
</code>
.)
</p>
<p>
Usually we create stories with smaller UI components in the app.
<br />
Have a look at the

<a
className="css-ca0824"
href="https://storybook.js.org/basics/writing-stories"
rel="noopener noreferrer"
target="_blank"
>
Writing Stories
</a>

section in our documentation.
</p>
<p
className="css-bwdon3"
>
<b>
NOTE:
</b>
<br />
Have a look at the

<code
className="css-mteq83"
>
.storybook/webpack.config.js
</code>

to add webpack loaders and plugins you are using in this project.
</p>
</article>
`;
8 changes: 8 additions & 0 deletions addons/storyshots/stories/storyshot.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import path from 'path';
import initStoryshots, { multiSnapshotWithOptions } from '../src';

initStoryshots({
framework: 'react',
configPath: path.join(__dirname, '..', '.storybook'),
test: multiSnapshotWithOptions({}),
});
10 changes: 8 additions & 2 deletions app/react-native/src/preview/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ export default class Preview {
if (module && module.hot) {
// TODO remove the kind on dispose
}
return new StoryKindApi(this._stories, this._addons, this._decorators, kind);

const fileName = module ? module.filename : null;

return new StoryKindApi(this._stories, this._addons, this._decorators, kind, fileName);
}

setAddon(addon) {
Expand All @@ -44,11 +47,14 @@ export default class Preview {

getStorybook() {
return this._stories.getStoryKinds().map(kind => {
const fileName = this._stories.getStoryFileName(kind);

const stories = this._stories.getStories(kind).map(name => {
const render = this._stories.getStory(kind, name);
return { name, render };
});
return { kind, stories };

return { kind, fileName, stories };
});
}

Expand Down
5 changes: 3 additions & 2 deletions app/react-native/src/preview/story_kind.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
/* eslint no-underscore-dangle: 0 */

export default class StoryKindApi {
constructor(stories, addons, decorators, kind) {
constructor(stories, addons, decorators, kind, fileName) {
this.kind = kind;
this._stories = stories;
this._decorators = decorators.slice();
this._fileName = fileName;
Object.assign(this, addons);
}

Expand All @@ -15,7 +16,7 @@ export default class StoryKindApi {

add(story, fn) {
const decorated = this._decorate(fn);
this._stories.addStory(this.kind, story, decorated);
this._stories.addStory(this.kind, story, decorated, this._fileName);
return this;
}

Expand Down
Loading

0 comments on commit c0e468a

Please sign in to comment.