Mocking data for Storybook
Storybook is a great utility to do isolated development and testing, potentially speeding up development time greatly.
<MockResolver /> enables easy loading of fixtures or interceptors to see what different network responses might look like. It can be layered, composed, and even used for imperative fetches usually used with side-effect endpoints like create and update.
Setup
- Resource
- Component
export class Article extends Entity {
readonly id: number | undefined = undefined;
readonly content: string = '';
readonly author: number | null = null;
readonly contributors: number[] = [];
pk() {
return this.id?.toString();
}
}
export const ArticleResource = createResource({
urlPrefix: 'http://test.com',
path: '/article/:id',
schema: Article,
});
export let ArticleFixtures: Record<string, Fixture> = {};
import { ArticleResource } from 'resources/ArticleResource';
import ArticleSummary from './ArticleSummary';
export default function ArticleList({ maxResults }: { maxResults: number }) {
const articles = useSuspense(ArticleResource.getList, { maxResults });
return (
<div>
{articles.map(article => (
<ArticleSummary key={article.pk()} article={article} />
))}
</div>
);
}
Fixtures
We'll test three cases: some interesting results in the list, an empty list, and data not existing so loading fallback is shown.
// leave out in production so we don't bloat the bundle
if (process.env.NODE_ENV !== 'production') {
ArticleFixtures = {
full: [
{
endpoint: ArticleResource.getList,
args: [{ maxResults: 10 }] as const,
response: [
{
id: 5,
content: 'have a merry christmas',
author: 2,
contributors: [],
},
{
id: 532,
content: 'never again',
author: 23,
contributors: [5],
},
],
},
{
endpoint: ArticleResource.update,
response: ({ id }, body) => ({
...body,
id,
}),
},
],
empty: [
{
endpoint: ArticleResource.getList,
args: [{ maxResults: 10 }] as const,
response: [],
},
],
error: [
{
endpoint: ArticleResource.getList,
args: [{ maxResults: 10 }] as const,
response: { message: 'Bad request', status: 400, name: 'Not Found' },
error: true,
},
],
loading: [],
};
}
Decorators
You'll need to add the appropriate global decorators to establish the correct context.
This should resemble what you have added in initial setup
import { Suspense } from 'react';
import { CacheProvider, AsyncBoundary } from '@rest-hooks/react';
export const decorators = [
Story => (
<CacheProvider>
<AsyncBoundary fallback="loading">
<Story />
</AsyncBoundary>
</CacheProvider>
),
];
Story
Wrapping our component with <MockResolver /> enables us to declaratively control how Rest Hooks' fetches are resolved.
Here we select which fixtures should be used by storybook controls.
import { MockResolver } from '@rest-hooks/test';
import type { Fixture } from '@rest-hooks/test';
import { Story } from '@storybook/react/types-6-0';
import ArticleList from 'ArticleList';
import { ArticleFixtures } from 'resources/ArticleResource';
export default {
title: 'Pages/ArticleList',
component: ArticleList,
argTypes: {
result: {
description: 'Results',
defaultValue: 'full',
control: {
type: 'select',
options: Object.keys(ArticleFixtures),
},
},
},
};
const Template: Story<{ result: keyof typeof options }> = ({ result }) => (
<MockResolver fixtures={options[result]}>
<ArticleList maxResults={10} />
</MockResolver>
);
export const FullArticleList = Template.bind({});
FullArticleList.args = {
result: 'full',
};