@storyshots/next
Реализует клиент и сервер preview для nextjs приложений.
Для интеграции необходимо:
Externals
Описать базовые контракты внешних зависимостей:
export type Externals = {
// В тестах подменяется функция getPosts
getPosts(): Promise<Post[]>;
};
createStoryFactories
Связать базовые фабрики с типом Externals и указать реализации по умолчанию:
import { createStoryFactories } from '@storyshots/next';
// Описывает подменяемые в тестах зависимости
import { Externals } from '@/storyshots/arrangers';
// Инициализация фабрик историй и утилит
export const { it, describe, each, createOnStorySwitch } = createStoryFactories<Externals>({
// Определение поведения "по умолчанию" для внешних зависимостей
createExternals: () => ({ getPosts: async () => [] }),
// Маркировка методов для записи в журнал вызовов
createJournalExternals: (externals) => externals,
});
Подробнее фабрики по умолчанию описываются в данном разделе.
createOnStorySwitch
Используя специальную функцию переключатель, описать механизм подмены зависимостей:
import { stories } from '@/storyshots/stories';
// createOnStorySwitch связывает фабрики с описанными историями
const onStorySwitch = createOnStorySwitch(stories);
// Далее, можно описать две инструкции инициализации зависимостей
const getPosts = onStorySwitch({
// В тестовом окружении будет использоваться специальное поведение
onStory: (externals) => externals.getPosts,
// В реальном окружении выполняется настоящий запрос
otherwise: () => () => fetch('...'),
});
createOnStorySwitch рекомендуется использовать только для подмены зависимостей в основных точках входа, чтобы избежать
утечки тестовых зависимостей.
createStoryRootComponent
Связать Root компонент с описанными историями:
'use client';
import { createStoryRootComponent } from '@storyshots/next/client';
import { stories } from '@/storyshots/stories';
export const Root = createStoryRootComponent(stories);
Далее, компонент необходимо подключить как корневой:
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
Следующим шагом необходимо подключить 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>
{/* Должен выполняться до любого другого CSR кода на проекте */}
<ModeInjector />
</head>
<body>
<Root>{children}</Root>
</body>
</html>
);
}
createNextPreview
Далее подключить сам сервер превью:
import { ManagerConfig } from '@storyshots/core/manager';
import { createNextPreview } from '@storyshots/next/preview';
export default {
preview: createNextPreview(),
/* ... */
} satisfies ManagerConfig;
extendNextConfig
Расширить конфигурацию nextjs:
import type { NextConfig } from 'next';
import { extendNextConfig } from '@storyshots/next/preview';
import path from 'node:path';
const nextConfig: NextConfig = extendNextConfig({
// Путь до файла где экспортируются истории
storiesRoot: path.join(process.cwd(), 'storyshots', 'stories'),
config: {
// Оригинальная конфигурация nextjs
},
});
export default nextConfig;
extendNextConfig исключает тестовый код из production артефакта.
После этого можно описывать истории как обычно:
[
it('shows empty posts', {}),
it('shows few posts', {
arrange: (externals) => ({ ...externals, getPosts: async () => createFewPostsStub() })
}),
// ... //
];
createSharedState
Позволяет эмулировать stateful поведения в историях:
it('allows to create post', {
arrange: (externals) => {
// Состояние будет идентичным не зависимо от runtime функций
const posts = createSharedState<Post[]>('posts', []);
return {
...externals,
createPost: (body) => posts.update((all) => [...all, { ...body, id: all.length }]),
getPosts: () => posts.get()
};
}
});
createSharedState создаёт общее состояние для браузерного и серверного окружения, но изолирует данные в рамках взятой
истории.
Расширения
@storyshots/next дополнительно расширяет фабрику it следующими элементами:
at
Принимает стартовый url истории:
[
it('renders home page', {
// Откроет приложение по корневому адресу (поведение по умолчанию)
at: '/',
}),
it('renders products', {
// Сразу откроет список продуктов
at: '/products',
})
];
arrange
Подготавливает внешние зависимости для истории.
Эта функция используется для подготовки окружения перед запуском истории.
it('...', {
arrange: (externals) => ({
...externals,
// Для текущей истории установить определённое поведение метода.
getUser: async () => ({ name: 'John Doe', age: 25 }),
}),
});
Принимает конфигурацию истории как второй аргумент.
Может также использоваться для разметки методов для логирования с помощью [Journal]/API/test-components/story-config#journal:
it('...', {
arrange: (externals, { journal }) => ({
...externals,
getUser: journal.asRecordable('getUser', externals.getUser),
}),
});
journal.record и journal.asRecordable работают только с асинхронными функциями.
Также может использоваться для хранения временного состояния в контексте истории:
it('...', {
arrange: (externals) => {
// count сохранится в контексте работающей истории.
const count = createSharedState('count', 0);
return {
increment: () => count.update(value => value + 1),
get: () => count.get(),
};
},
});