From 4e9b1ecaebe690bce5617afe7e5f458a7f1eda8a Mon Sep 17 00:00:00 2001 From: Kyle Gach Date: Wed, 17 Apr 2024 23:27:34 -0600 Subject: [PATCH] Address feedback - Next.js - Add portable stories section - Mocking modules - Clarify requirements of mock files - Prose and snippet tweaks - Interaction testing - Bring over `mockdate` example - Prose and snippet tweaks --- docs/get-started/nextjs.md | 8 +++++- docs/writing-stories/mocking-modules.md | 31 ++++++++++++++++------- docs/writing-tests/interaction-testing.md | 29 ++++++++++++++++----- 3 files changed, 51 insertions(+), 17 deletions(-) diff --git a/docs/get-started/nextjs.md b/docs/get-started/nextjs.md index 408e11df3a49..678be46c6b2d 100644 --- a/docs/get-started/nextjs.md +++ b/docs/get-started/nextjs.md @@ -881,6 +881,12 @@ If your server components access data via the network, we recommend using the [M In the future we will provide better mocking support in Storybook and support for [Server Actions](https://nextjs.org/docs/app/api-reference/functions/server-actions). +## Portable stories + +You can test your stories in a Jest environment by using the [portable stories](../api/portable-stories-jest.md) API. + +When using portable stories with Next.js, you need to mock the Next.js modules that your components depend on. You can use the [`@storybook/nextjs/export-mocks` module](#storybooknextjsexport-mocks) to generate the aliases needed to set up portable stories in a Jest environment. + ## Notes for Yarn v2 and v3 users If you're using [Yarn](https://yarnpkg.com/) v2 or v3, you may run into issues where Storybook can't resolve `style-loader` or `css-loader`. For example, you might get errors like: @@ -991,7 +997,7 @@ The `@storybook/nextjs` package exports a number of modules that enables you to Type: `{ getPackageAliases: ({ useESM?: boolean }) => void }` -`getPackageAliases` is a helper to generate the aliases needed to set up [portable stories in a Jest environment](../api/portable-stories-jest.md). +`getPackageAliases` is a helper to generate the aliases needed to set up [portable stories](#portable-stories). TK: Example snippet diff --git a/docs/writing-stories/mocking-modules.md b/docs/writing-stories/mocking-modules.md index 8063b9f6a718..8acfdd5b2429 100644 --- a/docs/writing-stories/mocking-modules.md +++ b/docs/writing-stories/mocking-modules.md @@ -12,7 +12,9 @@ For either approach, relative imports of the mocked module are not supported. To mock a module, create a file with the same name and in the same directory as the module you want to mock. For example, to mock a module named `session`, create a file next to it named `session.mock.js|ts`, with a few characteristics: -- It should re-export all exports from the original module - using relative imports to import the original, as using a subpath or alias import would result in it importing itself. +- It must import the original module using a relative import. + - Using a subpath or alias import would result in it importing itself. +- It should re-export all exports from the original module. - It should use the `fn` utility to mock any necessary functionality from the original module. - It should not introduce side effects that could affect other tests or components. Mock files should be isolated and only affect the module they are mocking. @@ -60,18 +62,25 @@ TK: External module example? } ``` -You can't directly mock an external module like `uuid` or `node:fs`, so instead of importing them directly in your components you can wrap them in your own modules that you import from instead, and that are mockable like any other internal module. Here's an example of wrapping `uuid` and creating a mock for the wrapper: +You can't directly mock an external module like `uuid` or `node:fs`. Instead, you must wrap the module in you own module, which you can then mock like any other internal module. In this example, we wrap `uuid`: + + ```ts // lib/uuid.ts import { v4 } from 'uuid'; + export const uuidv4 = v4; ``` +And create a mock for the wrapper: + ```ts // lib/uuid.mock.ts import { fn } from '@storybook/test'; + import * as actual from './uuid'; + export const uuidv4 = fn(actual.uuidv4); ``` @@ -97,7 +106,7 @@ The Storybook environment will match the conditions `storybook` and `test`, so y If your project is unable to use [subpath imports](#subpath-imports), you can configure your Storybook builder to alias the module to the mock file. This will instruct the builder to replace the module with the mock file when bundling your Storybook stories. -````js +```js // .storybook/main.ts viteFinal: async (config) => { @@ -113,6 +122,7 @@ viteFinal: async (config) => { } } }, +``` ```js // .storybook/main.ts @@ -128,7 +138,7 @@ webpackFinal: async (config) => { return config }, -```` +``` @@ -235,7 +245,9 @@ export const SaveFlow: Story = { ### Setting up and cleaning up -You can use `beforeEach` at the project, component or story level to perform any setup that you need, eg. setting up mock behavior. You can also return a cleanup-function from `beforeEach` which will be called after your story unmounts. This is useful for unsubscribing observers etc. +You can use the asynchronous `beforeEach` function to perform any setup that you need before the story is rendered, eg. setting up mock behavior. It can be defined at the story, component (which will run for all stories in the file), or project (defined in `.storybook/preview.js|ts`, which will run for all stories in the project) level. + +You can also return a cleanup function from `beforeEach` which will be called after your story unmounts. This is useful for tasks like unsubscribing observers, etc. @@ -243,7 +255,7 @@ It is _not_ necessary to restore `fn()` mocks with the cleanup function, as Stor -Here's an example of using the [`mockdate`](https://github.com/boblauer/MockDate) package to mock the Date and resetting it when the story unmounts. +Here's an example of using the [`mockdate`](https://github.com/boblauer/MockDate) package to mock the Date and reset it when the story unmounts. @@ -257,8 +269,11 @@ import { Page } from './Page'; const meta: Meta = { component: Page, + // 👇 Set the current date for every story in the file async beforeEach() { MockDate.set('2024-02-14'); + + // 👇 Reset the date after each test return () => { MockDate.reset(); }; @@ -268,7 +283,5 @@ export default meta; type Story = StoryObj; -export const Default: Story = { - // TK -}; +export const Default: Story = {}; ``` diff --git a/docs/writing-tests/interaction-testing.md b/docs/writing-tests/interaction-testing.md index c235e1122fe7..bf74e9869554 100644 --- a/docs/writing-tests/interaction-testing.md +++ b/docs/writing-tests/interaction-testing.md @@ -92,25 +92,38 @@ Once the story loads in the UI, it simulates the user's behavior and verifies th ### Run code before each test -It can be helpful to run code before each test to set up the initial state of the component or reset the state of modules. You can do this by adding an `async beforeEach` function to the meta in your stories file. This function will run before each test in the story file. +It can be helpful to run code before each test to set up the initial state of the component or reset the state of modules. You can do this by adding an asynchronous `beforeEach` function to the story, meta (which will run before each story in the file), or the preview file (`.storybook/preview.js|ts`, which will run before every story in the project). + +Additionally, if you return a cleanup function from the `beforeEach` function, it will run **after** each test, when the story is remounted or navigated away from. + + + +It is _not_ necessary to restore `fn()` mocks with the cleanup function, as Storybook will already do that automatically before rendering a story. See the [`parameters.test`](../api/parameters.md#test) API for more information. + + + +Here's an example of using the [`mockdate`](https://github.com/boblauer/MockDate) package to mock the Date and reset it when the story unmounts. ```js // Page.stories.tsx import { Meta, StoryObj } from '@storybook/react'; -import { fn } from '@storybook/test'; +import MockDate from 'mockdate'; import { getUserFromSession } from '#api/session.mock'; import { Page } from './Page'; const meta: Meta = { component: Page, + // 👇 Set the current date for every story in the file async beforeEach() { - // 👇 Do this for each story - // TK - // 👇 Clear the mock between stories - getUserFromSession.mockClear(); + MockDate.set('2024-02-14'); + + // 👇 Reset the date after each test + return () => { + MockDate.reset(); + }; }, }; export default meta; @@ -118,7 +131,9 @@ export default meta; type Story = StoryObj; export const Default: Story = { - // TK + async play({ canvasElement }) { + // ... This will run with the mocked date + }, }; ```