@storyshots/next
Implements the client preview and app server for Next.js applications.
To integrate, follow these steps:
Externals
Define the basic contracts for external dependencies:
export type Externals = {
// In tests, the getPosts function is mocked
getPosts(): Promise<Post[]>;
};
createStoryFactories
Link base factories with the Externals type and specify default implementations:
import { createStoryFactories } from '@storyshots/next';
// Describes testable dependencies
import { Externals } from '@/storyshots/arrangers';
// Initialize story factories and utilities
export const { it, describe, each, createOnStorySwitch } = createStoryFactories<Externals>({
// Define default behavior for external dependencies
createExternals: () => ({ getPosts: async () => [] }),
// Mark methods for call logging
createJournalExternals: (externals) => externals,
});
Default factories are described in this section.
createOnStorySwitch
Using a special switch function, define the dependency substitution mechanism:
import { stories } from '@/storyshots/stories';
// createOnStorySwitch ties factories to defined stories
const onStorySwitch = createOnStorySwitch(stories);
// Then, define two initialization instructions for dependencies
const getPosts = onStorySwitch({
// In test environment, use special behavior
onStory: (externals) => externals.getPosts,
// In real environment, execute actual request
otherwise: () => () => fetch('...'),
});
createOnStorySwitch should be used only for substituting dependencies at main entry points to avoid
leaking test dependencies.
createStoryRootComponent
Link the Root component with the defined stories:
'use client';
import { createStoryRootComponent } from '@storyshots/next/client';
import { stories } from '@/storyshots/stories';
export const Root = createStoryRootComponent(stories);
Next, connect the component as the root:
import React from 'react';
import { Root } from '@/storyshots/Root';
export default function RootLayout({ children }: LayoutProps<'/'>) {
return (
<html lang="en">
<body>
<Root>{children}</Root>
</body>
</html>
);
}
ModeInjector
Next, connect the ModeInjector:
import React from 'react';
import { Root } from '@/storyshots/Root';
import { ModeInjector } from '@storyshots/next/client';
export default function RootLayout({ children }: LayoutProps<'/'>) {
return (
<html lang="en">
<head>
{/* Must run before any other CSR code in the project */}
<ModeInjector />
</head>
<body>
<Root>{children}</Root>
</body>
</html>
);
}
createNextPreview
Next, connect the app server:
import { ManagerConfig } from '@storyshots/core/manager';
import { createNextPreview } from '@storyshots/next/preview';
export default {
preview: createNextPreview(),
/* ... */
} satisfies ManagerConfig;
extendNextConfig
Extend the Next.js configuration:
import type { NextConfig } from 'next';
import { extendNextConfig } from '@storyshots/next/preview';
import path from 'node:path';
const nextConfig: NextConfig = extendNextConfig({
// Path to the file exporting stories
storiesRoot: path.join(process.cwd(), 'storyshots', 'stories'),
config: {
// Original Next.js configuration
},
});
export default nextConfig;
extendNextConfig excludes test code from production artifacts.
After this, you can define stories as usual:
[
it('shows empty posts', {}),
it('shows few posts', {
arrange: (externals) => ({ ...externals, getPosts: async () => createFewPostsStub() })
}),
// ... //
];
createSharedState
Allows emulating stateful behavior in stories:
it('allows to create post', {
arrange: (externals) => {
// State will be identical regardless of runtime functions
const posts = createSharedState<Post[]>('posts', []);
return {
...externals,
createPost: (body) => posts.update((all) => [...all, { ...body, id: all.length }]),
getPosts: () => posts.get()
};
}
});
createSharedState creates shared state for both browser and server environments, but isolates data within the context of a given story.
Extensions
@storyshots/next extends the it factory with the following features:
at
Takes the starting url for the story:
[
it('renders home page', {
// Opens the app at the root URL (default behavior)
at: '/',
}),
it('renders products', {
// Immediately opens the products list
at: '/products',
})
];
arrange
Prepares external dependencies for the story.
This function is used to set up the environment before running the story.
it('...', {
arrange: (externals) => ({
...externals,
// Set specific behavior for the method in this story
getUser: async () => ({ name: 'John Doe', age: 25 }),
}),
});
Accepts story configuration as a second argument.
Can also be used to mark methods for logging via Journal:
it('...', {
arrange: (externals, { journal }) => ({
...externals,
getUser: journal.asRecordable('getUser', externals.getUser),
}),
});
journal.record and journal.asRecordable work only with async functions.
Also used for storing temporary state within the story context:
it('...', {
arrange: (externals) => {
// count will be preserved within the running story context
const count = createSharedState('count', 0);
return {
increment: () => count.update(value => value + 1),
get: () => count.get(),
};
},
});