Merge pull request #11872 from storybookjs/chore_add_workflow_snippets

Chore add workflows snippets for 6.0 docs
This commit is contained in:
Michael Shilman 2020-08-11 12:11:24 +08:00 committed by GitHub
commit 7096da844f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 516 additions and 233 deletions

View File

@ -0,0 +1,11 @@
```js
// My-component_spec.js
describe('My Component', () => {
it('should respond to click on button with warning', () => {
cy.visit('http://localhost:6006/iframe.html?id=my-component--basic-story);
cy.get('#button').click();
cy.get('#warning').should('contain.text', 'You need to fill in the form!');
});
})
```

View File

@ -0,0 +1,25 @@
```js
// __mocks__/isomorphic-fetch.js
// Your fetch implementation to be added to ./storybook/main.js.
// In your webpackFinal configuration object.
let nextJson;
export default async function fetch() {
if (nextJson) {
return {
json: () => nextJson,
};
}
nextJson = null;
}
// the decorator to be used in ./storybook/preview to apply the mock to all stories
export function decorator(story, { parameters }) {
if (parameters && parameters.fetch) {
nextJson = parameters.fetch.json;
}
return story();
}
```

View File

@ -0,0 +1,9 @@
```js
// .storybook/main.js
module.exports = {
// your Storybook configuration
refs: {
'package-name': { disable: true }
}
```

View File

@ -0,0 +1,17 @@
```js
//.storybook/main.js
module.exports = {
// your Storybook configuration
refs: {
react: {
title: 'React',
url: 'http://localhost:7007',
},
angular: {
title: 'Angular',
url: 'http://localhost:7008',
},
},
};
```

View File

@ -0,0 +1,13 @@
```js
//.storybook/main.js
module.exports={
// your Storybook configuration
refs: {
'design-system': {
title: "Storybook Design System",
url: "https://5ccbc373887ca40020446347-yldsqjoxzb.chromatic.com"
}
}`
}
```

View File

@ -0,0 +1,12 @@
```js
// .storybook/main.js
module.exports = {
// your Storybook configuration
webpackFinal: (config) => {
config.resolve.alias['isomorphic-fetch'] = require.resolve('../__mocks__/isomorphic-fetch.js');
return config;
},
};
```

View File

@ -0,0 +1,7 @@
```js
// .storybook/preview.js
import { decorator } from '../__mocks/isomorphic-fetch';
// Add the decorator to all stories
export const decorators = [decorator];
```

View File

@ -0,0 +1,6 @@
```js
// storybook.test.js
import initStoryshots from '@storybook/addon-storyshots';
initStoryshots();
```

View File

@ -0,0 +1,31 @@
```js
// App.stories.js
import React from 'react';
import App from './App';
export default {
title: 'App',
component: App,
};
const Template = (args) => <App {...args />;
export const Success = Template.bind({});
Success.parameters = {
fetch: {
json: {
JavaScript: 3390991,
'C++': 44974,
TypeScript: 15530,
CoffeeScript: 12253,
Python: 9383,
C: 5341,
Shell: 5115,
HTML: 3420,
CSS: 3171,
Makefile: 189,
}
}
};
```

View File

@ -0,0 +1,15 @@
```js
// Button.test.js
import React from 'react';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import { renderStory } from '@storybook/react/render';
import { Primary } from './Button.stories';
it('renders the button in the primary state, () => {
render(renderStory(Primary));
expect(screen.getByRole('button')).toHaveTextContent(Primary);
});
```

View File

@ -0,0 +1,25 @@
```js
// my-component-with-query.stories.js
import MyComponentThatHasAQuery, { MyQuery} from '../component-that-has-a-query';
const Template=(args)=><MyComponentThatHasAQuery {...args}/>;
export const LoggedOut = () => Template.bind({});
LoggedOut.parameters: {
apolloClient: {
mocks: [
{
request: {
query: MyQuery,
},
result: {
data: {
viewer: null
},
},
},
],
},
};
```

View File

@ -0,0 +1,22 @@
```js
// List.stories.js
import React from 'react';
import List from './List';
import ListItem from './ListItem';
import { Unchecked } from './ListItem.stories';
const ListTemplate = ({ items, ...args }) => (
<List>
{items.map((item) => (
<ListItem {...item} />
))}
</List>
);
export const Empty = ListTemplate.bind({});
Empty.args = { items: [] };
export const OneItem = ListTemplate.bind({});
OneItem.args = { items: [Unchecked.args] };
```

View File

@ -0,0 +1,22 @@
```js
// List.stories.js
import React from 'react';
import { List, ListProps } from './List';
import { ListItem, ListItemProps} from './ListItem';
import { Unchecked } from './ListItem.stories';
const ListTemplate = ({ items, ...args }) => (
<List>
{items.map((item) => (
<ListItem {...item} />
))}
</List>
);
export const Empty = ListTemplate.bind({});
Empty.args = { items: [] };
export const OneItem = ListTemplate.bind({});
OneItem.args = { items: [Unchecked.args] };
```

View File

@ -0,0 +1,13 @@
```js
// List.stories.js
import List from './List';
// Instead of importing the ListItem, we import its stories
import { Unchecked } from './ListItem.stories';
export const OneItem = (args) => (
<List {...args}>
<Unchecked {...Unchecked.args} />
</List>
);
```

View File

@ -0,0 +1,13 @@
```js
// List.stories.js
import { List, ListProps} from './List';
// Instead of importing the ListItem, we import its stories
import { Unchecked } from './ListItem.stories';
export const OneItem = (args) => (
<List {...args}>
<Unchecked {...Unchecked.args} />
</List>
);
```

View File

@ -0,0 +1,20 @@
```js
// List.stories.js
import List from './List';
import ListItem from './ListItem';
export default {
component: List,
subcomponents: [ListItem],
title: 'List',
};
export const Empty = (args) => <List {...args} />;
export const OneItem = (args) => (
<List {...args}>
<ListItem />
</List>
);
```

View File

@ -0,0 +1,20 @@
```js
// List.stories.js
import { List, ListProps } from './List';
import { ListItem, ListItemProps } from './ListItem';
export default {
component: List,
subcomponents: [ListItem],
title: 'List',
};
export const Empty = Story<ListProps> = (args) => <List {...args} />;;
export const OneItem = (args) => (
<List {...args}>
<ListItem />
</List>
);
```

View File

@ -0,0 +1,10 @@
```js
// List.stories.js
const Template = (args) => <List {...args} />;
export const OneItem = Template.bind({});
OneItem.args = {
children: <Unchecked {...Unchecked.args} />,
};
```

View File

@ -0,0 +1,24 @@
```js
// your-page.stories.js
import React from 'react';
import DocumentScreen from './DocumentScreen';
import PageLayout from './PageLayout.stories';
import DocumentHeader from './DocumentHeader.stories';
import DocumentList from './DocumentList.stories';
export default {
component: DocumentScreen,
title: 'DocumentScreen',
};
const Template = (args) => <DocumentScreen {...args} />;
export const Simple = Template.bind({});
Simple.args = {
user: PageLayout.Simple.user,
document: DocumentHeader.Simple.document,
subdocuments: DocumentList.Simple.documents,
};
```

View File

@ -0,0 +1,24 @@
```js
// your-page.stories.js
import React from 'react';
import {DocumentScreen, DocumentScreenProps} from './DocumentScreen';
import PageLayout from './PageLayout.stories';
import DocumentHeader from './DocumentHeader.stories';
import DocumentList from './DocumentList.stories';
export default {
component: DocumentScreen,
title: 'DocumentScreen',
};
const Template: Story<DocumentScreenProps> = (args) => <DocumentScreen {...args} />;
export const Simple = Template.bind({});
Simple.args = {
user: PageLayout.Simple.user,
document: DocumentHeader.Simple.document,
subdocuments: DocumentList.Simple.documents,
};
```

View File

@ -0,0 +1,17 @@
```js
// your-page.js
import React from 'react';
import PageLayout from './PageLayout';
import DocumentHeader from './DocumentHeader';
import DocumentList from './DocumentList';
function DocumentScreen({ user, document, subdocuments }) {
return (
<PageLayout user={user}>
<DocumentHeader document={document} />
<DocumentList documents={subdocuments} />
</PageLayout>
);
}
```

View File

@ -0,0 +1,26 @@
```js
// your-page.ts
import React from 'react';
import PageLayout from './PageLayout';
import Document from './Document'
import SubDocuments from './SubDocuments'
import DocumentHeader from './DocumentHeader';
import DocumentList from './DocumentList';
export interface DocumentScreen {
user?: {};
document?: Document;
subdocuments?: SubDocuments[];
};
function DocumentScreen({ user, document, subdocuments }) {
return (
<PageLayout user={user}>
<DocumentHeader document={document} />
<DocumentList documents={subdocuments} />
</PageLayout>
);
}
```

View File

@ -32,50 +32,29 @@ The downsides:
When you are building screens in this way, it is typical that the inputs of a composite component are a combination of the inputs of the various sub-components it renders. For instance, if your screen renders a page layout (containing details of the current user), a header (describing the document you are looking at), and a list (of the subdocuments), the inputs of the screen may consist of the user, document and subdocuments.
```js
// your-page.js
<!-- prettier-ignore-start -->
import React from 'react';
import PageLayout from './PageLayout';
import DocumentHeader from './DocumentHeader';
import DocumentList from './DocumentList';
<CodeSnippets
paths={[
'react/simple-page-implementation.js.mdx',
'react/simple-page-implementation.ts.mdx'
]}
/>
function DocumentScreen({ user, document, subdocuments }) {
return (
<PageLayout user={user}>
<DocumentHeader document={document} />
<DocumentList documents={subdocuments} />
</PageLayout>
);
}
```
<!-- prettier-ignore-end -->
In such cases it is natural to use [args composition](../writing-stories/args.md#args-composition) to build the stories for the page based on the stories of the sub-components:
```js
// your-page.story.js
<!-- prettier-ignore-start -->
import React from 'react';
import DocumentScreen from './DocumentScreen';
<CodeSnippets
paths={[
'react/page-story-with-args-composition.js.mdx',
'react/page-story-with-args-composition.ts.mdx',
]}
/>
import PageLayout from './PageLayout.stories';
import DocumentHeader from './DocumentHeader.stories';
import DocumentList from './DocumentList.stories';
export default {
component: DocumentScreen,
title: 'DocumentScreen',
};
const Template = (args) => <DocumentScreen {...args} />;
export const Simple = Template.bind({});
Simple.args = {
user: PageLayout.Simple.user,
document: DocumentHeader.Simple.document,
subdocuments: DocumentList.Simple.documents,
};
```
<!-- prettier-ignore-end -->
This approach is particularly useful when the various subcomponents export a complex list of different stories, which you can pick and choose to build realistic scenarios for your screen-level stories without repeating yourself. By reusing the data and taking a Don't-Repeat-Yourself(DRY) philosophy, your story maintenance burden is minimal.
@ -89,22 +68,15 @@ If you are using a provider that supplies data via the context, you can wrap you
Additionally, there may be addons that supply such providers and nice APIs to set the data they provide. For instance [`storybook-addon-apollo-client`](https://www.npmjs.com/package/storybook-addon-apollo-client) provides this API:
```js
// my-component-with-query.story.js
<!-- prettier-ignore-start -->
import MyComponentThatHasAQuery, {
MyQuery,
} from '../component-that-has-a-query';
<CodeSnippets
paths={[
'react/component-story-with-query.js.mdx',
]}
/>
export const LoggedOut = () => <MyComponentThatHasAQuery />;
LoggedOut.parameters: {
apolloClient: {
mocks: [
{ request: { query: MyQuery }, result: { data: { viewer: null } } },
],
},
};
```
<!-- prettier-ignore-end -->
### Mocking imports
@ -114,84 +86,53 @@ We're going to use [isomorphic-fetch](https://www.npmjs.com/package/isomorphic-f
Let's start by creating our own mock, which we'll use later with a [decorator](../writing-stories/decorators#global-decorators). Create a new file called `isomorphic-fetch.js` inside a directory called `__mocks__` (we'll leave the location to you, don't forget to adjust the imports to your needs) and add the following code inside:
```js
// __mocks__/isomorphic-fetch.js
let nextJson;
export default async function fetch() {
if (nextJson) {
return {
json: () => nextJson,
};
}
nextJson = null;
}
<!-- prettier-ignore-start -->
export function decorator(story, { parameters }) {
if (parameters && parameters.fetch) {
nextJson = parameters.fetch.json;
}
return story();
}
```
<CodeSnippets
paths={[
'common/isomorphic-fetch-mock.js.mdx',
]}
/>
<!-- prettier-ignore-end -->
The above code creates a decorator which reads story-specific data off the story's [parameters](../writing-stories/parameters), allowing you to configure the mock on a per-story basis.
To use the mock in place of the real import, we use [webpack aliasing](https://webpack.js.org/configuration/resolve/#resolvealias):
```js
// .storybook/main.js
module.exports = {
// your Storybook configuration
<!-- prettier-ignore-start -->
webpackFinal: (config) => {
config.resolve.alias['isomorphic-fetch'] = require.resolve('../__mocks__/isomorphic-fetch.js');
return config;
},
};
```
<CodeSnippets
paths={[
'common/storybook-main-with-mock-decorator.js.mdx',
]}
/>
<!-- prettier-ignore-end -->
Add the decorator you've just implemented to your [storybook/preview.js](../configure/overview.md#configure-story-rendering) (if you don't have it already, you'll need to create the file):
```js
// .storybook/preview.js
import { decorator } from '../__mocks/isomorphic-fetch';
<!-- prettier-ignore-start -->
// Add the decorator to all stories
export const decorators = [decorator];
```
<CodeSnippets
paths={[
'common/storybook-preview-with-mock-decorator.js.mdx',
]}
/>
<!-- prettier-ignore-end -->
Once that configuration is complete, we can set the mock values in a specific story. Let's borrow an example from this [blog post](https://medium.com/@edogc/visual-unit-testing-with-react-storybook-and-fetch-mock-4594d3a281e6):
```js
import React from 'react';
<!-- prettier-ignore-start -->
import App from './App';
<CodeSnippets
paths={[
'react/app-story-with-mock.js.mdx',
]}
/>
export default {
title: 'App',
component: App,
};
const Template = (args) => <App {...args />;
export const Success = Template.bind({});
Success.parameters = {
fetch: {
json: {
JavaScript: 3390991,
'C++': 44974,
TypeScript: 15530,
CoffeeScript: 12253,
Python: 9383,
C: 5341,
Shell: 5115,
HTML: 3420,
CSS: 3171,
Makefile: 189,
}
}
};
```
<!-- prettier-ignore-end -->
### Specific mocks

View File

@ -8,14 +8,12 @@ Stories are convenient **starting points** and **harnesses** for interaction tes
Luckily, this is straightforward. Point your interaction testing tool at Storybooks isolated iframe [URL for a specific story](../configure/sidebar-and-urls.md#permalinking-to-stories) then execute the test script as usual. Heres an example using Cypress:
```js
// My-component_spec.js
<!-- prettier-ignore-start -->
describe('My Component', () => {
it('should respond to click on button with warning', () => {
cy.visit('http://localhost:6006/iframe.html?id=my-component--basic-story);
cy.get('#button').click();
cy.get('#warning').should('contain.text', 'You need to fill in the form!');
});
})
```
<CodeSnippets
paths={[
'common/component-cypress-test.js.mdx',
]}
/>
<!-- prettier-ignore-end -->

View File

@ -16,15 +16,15 @@ Composition happens automatically if the package [supports](#for-package-authors
If you want to configure how the composed Storybook behaves, you can disable the `ref` element in your [`.storybook/main.js`](../configure/overview.md#configure-story-rendering)
```js
// .storybook/main.js
<!-- prettier-ignore-start -->
module.exports = {
// your Storybook configuration
refs: {
'package-name': { disable: true }
}
```
<CodeSnippets
paths={[
'common/storybook-main-disable-refs.js.mdx',
]}
/>
<!-- prettier-ignore-end -->
### Changing versions

View File

@ -16,12 +16,15 @@ npm i -D @storybook/addon-storyshots
Configure Storyshots by adding the following test file to your project:
```js
// storybook.test.js
<!-- prettier-ignore-start -->
import initStoryshots from '@storybook/addon-storyshots';
initStoryshots();
```
<CodeSnippets
paths={[
'common/storybook-storyshots-config.js.mdx',
]}
/>
<!-- prettier-ignore-end -->
<div class="aside">

View File

@ -4,26 +4,16 @@ title: 'Stories for multiple components'
It's useful to write stories that [render two or more components](../writing-stories/introduction.md#stories-for-two-or-more-components) at once if those components are designed to work together. For example, `ButtonGroups`, `Lists`, and `Page` components. Here's an example with `List` and `ListItem` components:
```js
// List.story.js
<!-- prettier-ignore-start -->
import List from './List';
import ListItem from './ListItem';
<CodeSnippets
paths={[
'react/list-story-with-subcomponents.js.mdx',
'react/list-story-with-subcomponents.ts.mdx',
]}
/>
export default {
component: List,
subcomponents: [ListItem],
title: 'List',
};
export const Empty = (args) => <List {...args} />;
export const OneItem = (args) => (
<List {...args}>
<ListItem />
</List>
);
```
<!-- prettier-ignore-end -->
Note that by adding `subcomponents` to the default export, we get an extra pane on the ArgsTable, listing the props of `ListItem`:
@ -40,19 +30,16 @@ Let's talk about some techniques you can use to mitigate the above, which are es
The simplest change we can make to the above is to reuse the stories of the `ListItem` in the `List`:
```js
// List.story.js
<!-- prettier-ignore-start -->
import List from './List';
// Instead of importing the ListItem, we import its stories
import { Unchecked } from './ListItem.stories';
<CodeSnippets
paths={[
'react/list-story-unchecked.js.mdx',
'react/list-story-unchecked.ts.mdx',
]}
/>
export const OneItem = (args) => (
<List {...args}>
<Unchecked {...Unchecked.args} />
</List>
);
```
<!-- prettier-ignore-end -->
By rendering the `Unchecked` story with its args, we are able to reuse the input data from the `ListItem` stories in the `List`.
@ -62,16 +49,15 @@ However, we still arent using args to control the `ListItem` stories, which m
One way we improve that situation is by pulling the rendered subcomponent out into a `children` arg:
```js
// List.story.js
<!-- prettier-ignore-start -->
const Template = (args) => <List {...args} />;
<CodeSnippets
paths={[
'react/list-story-with-unchecked-children.js.mdx',
]}
/>
export const OneItem = Template.bind({});
OneItem.args = {
children: <Unchecked {...Unchecked.args} />,
};
```
<!-- prettier-ignore-end -->
Now that `children` is an arg, we can potentially reuse it in another story.
@ -85,28 +71,16 @@ As things stand (we hope to improve this soon) you cannot edit children in a con
Another option that is more “data”-based is to create a special “story-generating” template component:
```js
// List.story.js
<!-- prettier-ignore-start -->
import React from 'react';
import List from './List';
import ListItem from './ListItem';
import { Unchecked } from './ListItem.stories';
<CodeSnippets
paths={[
'react/list-story-template.js.mdx',
'react/list-story-template.ts.mdx'
]}
/>
const ListTemplate = ({ items, ...args }) => (
<List>
{items.map((item) => (
<ListItem {...item} />
))}
</List>
);
export const Empty = ListTemplate.bind({});
Empty.args = { items: [] };
export const OneItem = ListTemplate.bind({});
OneItem.args = { items: [Unchecked.args] };
```
<!-- prettier-ignore-end -->
This approach is a little more complex to setup, but it means you can more easily reuse the `args` to each story in a composite component. It also means that you can alter the args to the component with the Controls addon:

View File

@ -12,38 +12,28 @@ You can compose any Storybook [published online](./publish-storybook.md) or runn
In your [`storybook/main.js`](../configure/overview.md#configure-story-rendering) file add a `refs` field with information about the reference Storybook. Pass in a URL to a statically built Storybook.
```js
//.storybook/main.js
module.exports={
// your Storybook configuration
refs: {
'design-system': {
title: "Storybook Design System",
url: "https://5ccbc373887ca40020446347-yldsqjoxzb.chromatic.com"
}
}`
}
```
<!-- prettier-ignore-start -->
<CodeSnippets
paths={[
'common/storybook-main-ref-remote.js.mdx',
]}
/>
<!-- prettier-ignore-end -->
## Compose local Storybooks
You can also compose Storybook that are running locally. For instance, if you have a React Storybook and a Angular Storybook running on different ports:
```js
//.storybook/main.js
module.exports = {
// your Storybook configuration
refs: {
react: {
title: 'React',
url: 'http://localhost:7007',
},
angular: {
title: 'Angular',
url: 'http://localhost:7008',
},
},
};
```
<!-- prettier-ignore-start -->
<CodeSnippets
paths={[
'common/storybook-main-ref-local.js.mdx',
]}
/>
<!-- prettier-ignore-end -->
This composes the React and Angular Storybooks into your current Storybook. When either code base changes, hot-module-reload will work perfectly. That enables you to develop both frameworks in sync.

View File

@ -10,19 +10,14 @@ Thanks to the [CSF format](../../formats/component-story-format/), your stories
Here is an example of how you can use it in a testing library:
```js
// Button.test.js
<!-- prettier-ignore-start -->
import React from 'react';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
<CodeSnippets
paths={[
'react/button-test.js.mdx',
]}
/>
import { Primary } from './Button.stories';
it('renders the button in the primary state, () => {
render(<Primary {...Primary.args} />);
expect(screen.getByRole('button')).toHaveTextContent(Primary);
});
```
<!-- prettier-ignore-end -->
Unit tests can be brittle and expensive to maintain for _every_ component. We recommend combining unit tests with other testing methods like [visual regression testing](./visual-testing.md) for comprehensive coverage with less maintenance work.