Skip to content
This repository was archived by the owner on Jan 31, 2024. It is now read-only.

Commit e263e32

Browse files
authored
Merge pull request #37 from storybookjs/feat/support-csf3-format
feat: support CSF3 format
2 parents cb0c7b2 + ed1f139 commit e263e32

File tree

11 files changed

+1434
-636
lines changed

11 files changed

+1434
-636
lines changed

README.md

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,68 @@ test('onclick handler is called', async () => {
126126
});
127127
```
128128

129+
### Reusing story properties
130+
131+
The components returend by `composeStories` or `composeStory` not only can be rendered as React components, but also come with the combined properties from story, meta and global configuration. This means that if you want to access `args` or `parameters`, for instance, you can do so:
132+
133+
```tsx
134+
import { render, screen } from '@testing-library/react';
135+
import { composeStory } from '@storybook/testing-react';
136+
import * as stories from './Button.stories';
137+
138+
const { Primary } = composeStories(stories);
139+
140+
test('reuses args from composed story', () => {
141+
render(<Primary />);
142+
143+
const buttonElement = screen.getByRole('button');
144+
// Testing against values coming from the story itself! No need for duplication
145+
expect(buttonElement.textContent).toEqual(Primary.args.children);
146+
});
147+
```
148+
149+
> **If you're using Typescript**: Given that some of the returned properties are not required, typescript might perceive them as nullable properties and present an error. If you are sure that they exist (e.g. certain arg that is set in the story), you can use the [non-null assertion operator](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html#non-null-assertion-operator) to tell typescript that it's all good:
150+
151+
```tsx
152+
// ERROR: Object is possibly 'undefined'
153+
Primary.args.children;
154+
155+
// SUCCESS: 🎉
156+
Primary.args!.children;
157+
```
158+
159+
### CSF3
160+
161+
Storybook released a [new version of CSF](https://storybook.js.org/blog/component-story-format-3-0/), where the story can also be an object. This is supported in @storybook/testing-react. CSF3 also brings a new function called `play`, where you can write automated interactions to the story.
162+
163+
In @storybook/testing-react, the `play` does not run automatically for you, but rather comes in the returned component, and you can execute it as you please.
164+
165+
Consider the following example:
166+
167+
```tsx
168+
export const InputFieldFilled: Story<InputFieldProps> = {
169+
play: async () => {
170+
await userEvent.type(screen.getByRole('textbox'), 'Hello world!');
171+
},
172+
};
173+
```
174+
175+
You can use the play function like this:
176+
177+
```tsx
178+
const { InputFieldFilled } = composeStories(stories);
179+
180+
test('renders with play function', async () => {
181+
render(<InputFieldFilled />);
182+
183+
// play an interaction that fills the input
184+
await InputFieldFilled.play!();
185+
186+
const input = screen.getByRole('textbox') as HTMLInputElement;
187+
expect(input.value).toEqual('Hello world!');
188+
});
189+
```
190+
129191
## Typescript
130192

131193
`@storybook/testing-react` is typescript ready and provides autocompletion to easily detect all stories of your component:
@@ -151,7 +213,6 @@ Type inference is only possible in projects that have either `strict` or `strict
151213
}
152214
```
153215

154-
155216
### Disclaimer
156217

157218
For the types to be automatically picked up, your stories must be typed. See an example:

example/package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,10 @@
3535
"typescript": "^3.9.7"
3636
},
3737
"devDependencies": {
38-
"@storybook/addon-essentials": "^6.4.0-alpha.12",
39-
"@storybook/react": "^6.4.0-alpha.12",
40-
"@storybook/components": "^6.4.0-alpha.12",
41-
"@storybook/theming": "^6.4.0-alpha.12",
38+
"@storybook/addon-essentials": "^6.4.0-alpha.17",
39+
"@storybook/react": "^6.4.0-alpha.17",
40+
"@storybook/components": "^6.4.0-alpha.17",
41+
"@storybook/theming": "^6.4.0-alpha.17",
4242
"@storybook/preset-create-react-app": "^3.1.5",
4343
"@testing-library/react": "^11.2.5",
4444
"@testing-library/user-event": "^13.1.9"

example/src/components/Button.stories.tsx

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import React from 'react';
22
import { Story, Meta } from '@storybook/react';
3+
import { screen } from '@testing-library/react';
4+
import userEvent from '@testing-library/user-event';
35

46
import { Button, ButtonProps } from './Button';
57

@@ -51,9 +53,32 @@ export const StoryWithParamsAndDecorator: Story<ButtonProps> = (args) => {
5153
return <Button {...args} />;
5254
};
5355
StoryWithParamsAndDecorator.args = {
54-
children: 'foo'
55-
}
56+
children: 'foo',
57+
};
5658
StoryWithParamsAndDecorator.parameters = {
57-
layout: 'centered'
58-
}
59-
StoryWithParamsAndDecorator.decorators = [(StoryFn) => <StoryFn />]
59+
layout: 'centered',
60+
};
61+
StoryWithParamsAndDecorator.decorators = [(StoryFn) => <StoryFn />];
62+
63+
export const CSF3Button: Story<ButtonProps> = {
64+
args: { children: 'foo' },
65+
};
66+
67+
export const CSF3ButtonWithRender: Story<ButtonProps> = {
68+
...CSF3Button,
69+
render: (args: ButtonProps) => (
70+
<div>
71+
<p data-testid="custom-render">I am a custom render function</p>
72+
<Button {...args} />
73+
</div>
74+
),
75+
};
76+
77+
export const InputFieldFilled: Story = {
78+
render: () => {
79+
return <input />
80+
},
81+
play: async () => {
82+
await userEvent.type(screen.getByRole('textbox'), 'Hello world!');
83+
}
84+
};

example/src/components/Button.test.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,40 @@ describe('GlobalConfig', () => {
6060
expect(buttonElement).not.toBeNull();
6161
});
6262
});
63+
64+
describe('CSF3', () => {
65+
test('renders with inferred globalRender', () => {
66+
const Primary = composeStory(
67+
stories.CSF3Button,
68+
stories.default
69+
);
70+
71+
render(<Primary>Hello world</Primary>);
72+
const buttonElement = screen.getByText(/Hello world/i);
73+
expect(buttonElement).not.toBeNull();
74+
});
75+
76+
test('renders with custom render function', () => {
77+
const Primary = composeStory(
78+
stories.CSF3ButtonWithRender,
79+
stories.default
80+
);
81+
82+
render(<Primary />);
83+
expect(screen.getByTestId("custom-render")).not.toBeNull();
84+
});
85+
86+
test('renders with play function', async () => {
87+
const InputFieldFilled = composeStory(
88+
stories.InputFieldFilled,
89+
stories.default
90+
);
91+
92+
render(<InputFieldFilled />);
93+
94+
await InputFieldFilled.play!();
95+
96+
const input = screen.getByRole('textbox') as HTMLInputElement;
97+
expect(input.value).toEqual('Hello world!');
98+
});
99+
});

example/src/internals.test.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ test('returns composed parameters from story', () => {
1313
expect(StoryWithParamsAndDecorator.args).toEqual(stories.StoryWithParamsAndDecorator.args);
1414
expect(StoryWithParamsAndDecorator.parameters).toEqual({
1515
...stories.StoryWithParamsAndDecorator.parameters,
16-
...globalConfig.parameters
16+
...globalConfig.parameters,
17+
component: stories.default.component
1718
});
1819
expect(StoryWithParamsAndDecorator.decorators).toEqual([
1920
...stories.StoryWithParamsAndDecorator.decorators!,

0 commit comments

Comments
 (0)