Skip to main content

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

ArticleResource.ts
export class Article extends Entity {
id: number | undefined = undefined;
content = '';
author: number | null = null;
contributors: number[] = [];

pk() {
return this.id?.toString();
}
static key = 'Article';
}
export const ArticleResource = createResource({
urlPrefix: 'http://test.com',
path: '/article/:id',
schema: Article,
searchParams: {} as { maxResults: number },
});

export let ArticleFixtures: Record<string, Fixture> = {};

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.

ArticleResource.ts
// 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

.storybook/preview.tsx
import { Suspense } from 'react';
import { CacheProvider, AsyncBoundary } from '@data-client/react';

export const decorators = [
Story => (
<CacheProvider>
<AsyncBoundary>
<Story />
</AsyncBoundary>
</CacheProvider>
),
];

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.

ArticleList.stories.tsx
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' },
};