Task that launches Storybook for isolated component testing.
amp storybook
Run amp storybook -h
or see testing.md for more launch options.
Follow these recommendations when writing stories.
Passing a value from a Control specified through args
or argTypes
without modification is implicit when spreading {...args}
.
It's preferred to name a Control property id uniformly with its corresponding component property or element attribute. This way, we write a shorter and more generic story by taking advantage of {...args}
:
✅ Do:
const MyStory = (args) => {
return <amp-my-component {...args} />;
};
MyStory.args = {
width: 500,
height: 200,
layout: 'responsive',
'data-foo': 'bar',
};
🚫 Don't:
const MyStory = ({width, height, layout, foo}) => {
return (
// bad: not spreading {...args}
// bad: different names for data-foo and foo
<amp-my-component
width={width}
height={height}
layout={layout}
data-foo={foo}
/>
);
};
MyStory.args = {
width: 500,
height: 200,
layout: 'responsive',
foo: 'bar',
};
👍 It's fine to destructure individual properties when they are needed in a different context:
const MyStory = ({includePlaceholder, ...args}) => {
return (
<amp-my-component {...args}>
{includePlaceholder && (
<div placeholder>
placeholder
</div>
)}
</amp-my-component>
);
};
MyStory.args = {
width: 500,
height: 200,
layout: 'responsive',
includePlaceholder: false,
};
Simple controls, like text
, boolean
, number
and object
derive their configuration automatically when specifying a default value only.
argTypes
is more verbose, which is why we avoid it.
However, you should feel free to use argTypes
when you need more than the default configuration, like specifying a name
label that does not match the property name, or specifying a number range.
Additionally, other controls, like select
, file
, or color
can only be configured through argTypes
, so it's fine to use in that case. See possible types in the official Storybook documentation.
✅ Do:
MyStory.args = {
two: 2,
foo: 'bar',
};
MyStory.argTypes = {
bestPants: {
name: 'best kind of pants',
control: {type: 'select'},
options: ['jeans', 'jorts', 'skirts'],
},
};
🚫 Don't:
MyStory.argTypes = {
two: {
name: 'two',
defaultValue: 2,
},
foo: {
name: 'foo',
defaultValue: 'bar',
control: {type: 'text'},
},
bestPants: {
name: 'best kind of pants',
control: {type: 'select'},
options: ['jeans', 'jorts', 'skirts'],
},
};
Listed Stories derive their names from their functions, and are split according to camelCase
(MyNamedStory
becomes My Named Story
when listed).
For this reason, the storyName
property should be avoided when it's too similar to the function name.
✅ Do:
const MyNamedStory = (args) => { /* ... */ };
🚫 Don't:
const MyNamedStory = (args) => { /* ... */ };
MyNamedStory.storyName = 'My Named Story';
👍 Feel free to use storyName
when you'd like to add more detail or formatting (like text in parentheses).
const Resizable = (args) => { /* ... */ };
Resizable.storyName = 'resizable (outside viewport)';
Stories are listed hierarchically: first by component name, then by story name. It's redundant to include the name of the component a story resides in.
✅ Do:
// amp-date-display/1.0/Basic.amp.js
const DefaultRenderer = (args) => { /* ... */ };
Listed as:
amp-date-display-1_0
├─ Default Renderer
🚫 Don't:
// amp-date-display/1.0/Basic.amp.js
const AmpDateDisplayWithDefaultRenderer = (args) => { /* ... */ };
Listed as:
amp-date-display-1_0
├─ Amp Date Display With Default Renderer
🚫 Don't:
// amp-date-display/1.0/Basic.amp.js
const DefaultRenderer = (args) => { /* ... */ };
DefaultRenderer.storyName = 'amp-date-display with default renderer';
Listed as:
amp-date-display-1_0
├─ amp-date-display with default renderer