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 getList.push and update.
Setup
- Resource
- Component
export class Article extends Entity {
id: number | undefined = undefined;
content = '';
author: number | null = null;
contributors: number[] = [];
static key = 'Article';
}
export const ArticleResource = resource({
urlPrefix: 'http://test.com',
path: '/article/:id',
schema: Article,
searchParams: {} as { maxResults: number },
});
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 with our fixtures and interceptors: 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 { DataProvider, AsyncBoundary } from '@data-client/react';
export const decorators = [
Story => (
<DataProvider>
<AsyncBoundary>
<Story />
</AsyncBoundary>
</DataProvider>
),
];
Story
Wrapping our component with <MockResolver /> enables us to declaratively control how Reactive Data Client' fetches are resolved.
Here we select which fixtures should be used by storybook controls.
import { type StoryObj } from '@storybook/react';
import { MockResolver } from '@data-client/test';
import type { Fixture } from '@data-client/test';
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),
},
},
},
};
export const FullArticleList: StoryObj<{ result: keyof typeof options }> =
{
render: ({ result }) => (
<MockResolver fixtures={options[result]}>
<ArticleList maxResults={10} />
</MockResolver>
),
args: { result: 'full' },
};