Skip to content

Commit

Permalink
Add support for async StoryShots using asyncJest option
Browse files Browse the repository at this point in the history
  • Loading branch information
hisapy committed Sep 18, 2018
1 parent 9b4f50c commit bb76bfd
Show file tree
Hide file tree
Showing 13 changed files with 273 additions and 11 deletions.
120 changes: 120 additions & 0 deletions addons/storyshots/storyshots-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,122 @@ initStoryshots({
})
```


### StoryShots for async rendered components

You can make use of [Jest done callback](https://jestjs.io/docs/en/asynchronous) to test components that render asynchronously. This callback is passed as param to test method passed to `initStoryshots(...)` when the `asyncJest` option is given as true.

#### Example

The following example shows how we can use the **done callback** to take StoryShots of a [Relay](http://facebook.github.io/relay/) component. Each kind of story is written into its own snapshot file with the use of `getSnapshotFileName`.

Add _stories of UserForm_ in the file: UserForm.story.jsx

```jsx
/* global module */
import React from "react";
import { QueryRenderer } from "react-relay";
import { storiesOf } from "@storybook/react";

// Use the same queries used in YOUR app routes
import { newUserFormQuery, editUserFormQuery } from "app/routes";
import UserFormContainer from "app/users/UserForm";

// YOUR function to generate a Relay Environment mock.
// See https://github.com/1stdibs/relay-mock-network-layer for more info
import getEnvironment from "test/support/relay-environment-mock";

// User test data YOU generated for your tests
import { user } from "test/support/data/index";

// Use this function to return a new Environment for each story
const Environment = () =>
getEnvironment({
mocks: {
Node: () => ({ __typename: "User" }),
User: () => user
}
});

/**
NOTICE that the QueryRenderer render its children via its render props.
If we don't take the StoryShot async then we will only see the QueryRenderer in the StoryShot.
The following QueryRenderer returns null in the first render (it can be a loading indicator instead in real file) and then when it gets the data to respond to query, it renders again with props containing the data for the Component
*/
const renderStory = (query, environment, variables = {}) => (
<QueryRenderer
environment={environment}
query={query}
variables={variables}
render={({ props, error }) => {
if (error) {
console.error(error);
} else if (props) {
return <UserFormContainer {...props} />;
}
return null;
}}
/>
);

storiesOf("users/UserForm", module)
.add("New User", () => {
const environment = new Environment();
return renderStory(newUserFormQuery, environment);
})
.add("Editing User", () => {
const environment = new Environment();
return renderStory(editUserFormQuery, environment, { id: user.id });
})
```

Then, init Storyshots for async component in the file: StoryShots.test.js

```jsx
import initStoryshots, { Stories2SnapsConverter } from "@storybook/addon-storyshots";
import { mount } from "enzyme";
import toJson from "enzyme-to-json";

// Runner
initStoryshots({
asyncJest: true, // this is the option that activates the async behaviour
test: ({
story,
context,
done // --> callback passed to test method when asyncJest option is true
}) => {
const converter = new Stories2SnapsConverter();
const snapshotFilename = converter.getSnapshotFileName(context);
const storyElement = story.render(context);

// mount the story
const tree = mount(storyElement);

// wait until the mount is updated, in our app mostly by Relay
// but maybe something else updating the state of the component
// somewhere
const waitTime = 1;
setTimeout(() => {
if (snapshotFilename) {
expect(toJson(tree.update())).toMatchSpecificSnapshot(snapshotFilename);
}

done();
}, waitTime)
},
// other options here
});

```
NOTICE that When using the `asyncJest: true` option, you also must specify a `test` method that calls the `done()` callback.

This is a really powerful technique to write stories of Relay components because it integrates data fetching with component rendering. So instead of passing data props manually, we can let Relay do the job for us as it does in our application.

Whenever you change you're data requirements by adding (and rendering) or (accidentally) deleting fields in your graphql query fragments, you'll get a different snapshot and thus an error in the StoryShot test.

## Options

### `config`
Expand Down Expand Up @@ -429,3 +545,7 @@ initStoryshots({
}
});
```

### `asyncJest`

Enables Jest `done()` callback in the StoryShots tests for async testing. See [StoryShots for async rendered components](#storyshots-for-async-rendered-components) for more info.
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ function getIntegrityOptions({ integrityOptions }) {
function ensureOptionsDefaults(options) {
const {
suite = 'Storyshots',
asyncJest,
storyNameRegex,
storyKindRegex,
renderer,
Expand All @@ -34,6 +35,7 @@ function ensureOptionsDefaults(options) {
const integrityOptions = getIntegrityOptions(options);

return {
asyncJest,
suite,
storyNameRegex,
storyKindRegex,
Expand Down
2 changes: 2 additions & 0 deletions addons/storyshots/storyshots-core/src/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ function testStorySnapshots(options = {}) {
}

const {
asyncJest,
suite,
storyNameRegex,
storyKindRegex,
Expand All @@ -50,6 +51,7 @@ function testStorySnapshots(options = {}) {

snapshotsTests({
groups: storiesGroups,
asyncJest,
suite,
framework,
storyKindRegex,
Expand Down
38 changes: 28 additions & 10 deletions addons/storyshots/storyshots-core/src/api/snapshotsTestsTemplate.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,35 @@
import { describe, it } from 'global';

function snapshotTest({ story, kind, fileName, framework, testMethod, testMethodParams }) {
function snapshotTest({
asyncJest,
story,
kind,
fileName,
framework,
testMethod,
testMethodParams,
}) {
const { name } = story;
const context = { fileName, kind, story: name, framework };

it(name, () => {
const context = { fileName, kind, story: name, framework };

return testMethod({
story,
context,
...testMethodParams,
});
});
if (asyncJest === true) {
it(name, done =>
testMethod({
done,
story,
context,
...testMethodParams,
})
);
} else {
it(name, () =>
testMethod({
story,
context,
...testMethodParams,
})
);
}
}

function snapshotTestSuite({ kind, stories, suite, storyNameRegex, ...restParams }) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ exports[`Storyshots Another Button with text 1`] = `
</ForwardRef>
`;

exports[`Storyshots Async with 5ms timeout simulating async operation 1`] = `
<AsyncTestComponent>
<h1 />
</AsyncTestComponent>
`;

exports[`Storyshots Button with some emoji 1`] = `
<ForwardRef
onClick={[Function]}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ exports[`Storyshots Another Button with text 1`] = `
</Styled(button)>
`;

exports[`Storyshots Async with 5ms timeout simulating async operation 1`] = `
<h1>
</h1>
`;

exports[`Storyshots Button with some emoji 1`] = `
<Styled(button)
onClick={[Function]}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ exports[`Storyshots Another Button with text 1`] = `
</Styled(button)>
`;

exports[`Storyshots Async with 5ms timeout simulating async operation 1`] = `
<h1>
</h1>
`;

exports[`Storyshots Button with some emoji 1`] = `
<Styled(button)
onClick={[Function]}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from 'react';
import { storiesOf } from '@storybook/react';

export const EXPECTED_VALUE = 'THIS IS SO DONE';
export const TIMEOUT = 5;

class AsyncTestComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
value: '',
};
}

componentDidMount() {
setTimeout(() => {
this.setState({
value: EXPECTED_VALUE,
});
}, TIMEOUT);
}

render() {
const { value } = this.state;
return <h1>{value}</h1>;
}
}

storiesOf('Async', module).add(`with ${TIMEOUT}ms timeout simulating async operation`, () => (
<AsyncTestComponent />
));
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Storyshots Async with 5ms timeout simulating async operation 1`] = `
<AsyncTestComponent>
<h1>
THIS IS SO DONE
</h1>
</AsyncTestComponent>
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Storyshots Async with 5ms timeout simulating async operation 1`] = `
<h1>

</h1>
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Storyshots Async with 5ms timeout simulating async operation 1`] = `
<h1>

</h1>
`;
47 changes: 47 additions & 0 deletions addons/storyshots/storyshots-core/stories/storyshot.async.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import path from 'path';
import { mount } from 'enzyme';
import toJson from 'enzyme-to-json';
import initStoryshots, { Stories2SnapsConverter } from '../src';
import { TIMEOUT, EXPECTED_VALUE } from './required_with_context/Async.stories';

initStoryshots({
asyncJest: true,
framework: 'react',
integrityOptions: false,
configPath: path.join(__dirname, '..', '.storybook'),

// When async is true we need to provide a test method that
// calls done() when at the end of the test method
test: ({ story, context, done }) => {
expect(done).toBeDefined();

// This is a storyOf Async (see ./required_with_context/Async.stories)
if (context.kind === 'Async') {
const converter = new Stories2SnapsConverter({ snapshotExtension: '.async.storyshot' });
const snapshotFilename = converter.getSnapshotFileName(context);
const storyElement = story.render(context);

// Mount the component
let wrapper = mount(storyElement);

// The Async component should not contain the expected value
expect(wrapper.find('AsyncTestComponent').contains(EXPECTED_VALUE)).toBe(false);

// wait until the "Async" component is updated
setTimeout(() => {
// Update the wrapper with the changes in the underlying component
wrapper = wrapper.update();

// Assert the expected value and the corresponding snapshot
expect(wrapper.find('AsyncTestComponent').contains(EXPECTED_VALUE)).toBe(true);
expect(toJson(wrapper)).toMatchSpecificSnapshot(snapshotFilename);

// finally mark test as done
done();
}, TIMEOUT);
} else {
// If not async just mark the test as done
done();
}
},
});
3 changes: 2 additions & 1 deletion addons/storyshots/storyshots-core/stories/storyshot.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import initStoryshots, { multiSnapshotWithOptions } from '../src';
// with react-test-renderer
initStoryshots({
framework: 'react',
integrityOptions: { cwd: __dirname },
// Ignore integrityOptions for async.storyshot because only run when asyncJest is true
integrityOptions: { cwd: __dirname, ignore: ['**/**.async.storyshot'] },
configPath: path.join(__dirname, '..', '.storybook'),
test: multiSnapshotWithOptions(),
});

0 comments on commit bb76bfd

Please sign in to comment.