Skip to content

Commit

Permalink
Address feedback
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
kylegach committed Apr 18, 2024
1 parent 4faf1ae commit 4e9b1ec
Show file tree
Hide file tree
Showing 3 changed files with 51 additions and 17 deletions.
8 changes: 7 additions & 1 deletion docs/get-started/nextjs.md
Expand Up @@ -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:
Expand Down Expand Up @@ -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

Expand Down
31 changes: 22 additions & 9 deletions docs/writing-stories/mocking-modules.md
Expand Up @@ -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.

Expand Down Expand Up @@ -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`:

<!-- TODO: Snippetize -->

```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);
```

Expand All @@ -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) => {
Expand All @@ -113,6 +122,7 @@ viteFinal: async (config) => {
}
}
},
```

```js
// .storybook/main.ts
Expand All @@ -128,7 +138,7 @@ webpackFinal: async (config) => {

return config
},
````
```

<!-- OR? -->
<!-- prettier-ignore-start -->
Expand Down Expand Up @@ -235,15 +245,17 @@ 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.

<Callout variant="info">

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.

</Callout>

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.

<!-- TODO: Snippetize -->

Expand All @@ -257,8 +269,11 @@ import { Page } from './Page';

const meta: Meta<typeof Page> = {
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();
};
Expand All @@ -268,7 +283,5 @@ export default meta;

type Story = StoryObj<typeof Page>;

export const Default: Story = {
// TK
};
export const Default: Story = {};
```
29 changes: 22 additions & 7 deletions docs/writing-tests/interaction-testing.md
Expand Up @@ -92,33 +92,48 @@ 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.

<Callout variant="info">

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.

</Callout>

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.

<!-- TODO: Snippetize -->

```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<typeof Page> = {
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;

type Story = StoryObj<typeof Page>;

export const Default: Story = {
// TK
async play({ canvasElement }) {
// ... This will run with the mocked date
},
};
```

Expand Down

0 comments on commit 4e9b1ec

Please sign in to comment.