Skip to content

Commit

Permalink
Support stories of asynchronously rendered components passing Jest do…
Browse files Browse the repository at this point in the history
…ne callback to custom test method
  • Loading branch information
hisapy committed Sep 4, 2018
1 parent a2a2a91 commit ae450a3
Show file tree
Hide file tree
Showing 11 changed files with 254 additions and 10 deletions.
115 changes: 115 additions & 0 deletions addons/storyshots/storyshots-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,121 @@ 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`.

```jsx
// file: UserForm.story.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 });
})


// file: StoryShots.test.js

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
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,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>
`;
44 changes: 44 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,44 @@
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',
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).not.toBeUndefined();
const converter = new Stories2SnapsConverter();
const snapshotFilename = converter.getSnapshotFileName(context);
const storyElement = story.render(context);

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

// This is a storyOf Async (see ./required_with_context/Async.stories)
if (context.kind === 'Async') {
// 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();
}
},
});

0 comments on commit ae450a3

Please sign in to comment.