diff --git a/CHANGELOG.md b/CHANGELOG.md index 8979b190287d..7a3d4a5410ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 9.1.1 + +- CLI: Fix throwing in readonly environments - [#31785](https://github.com/storybookjs/storybook/pull/31785), thanks @JReinhold! +- Onboarding: Tweak referral wording in survey - [#32185](https://github.com/storybookjs/storybook/pull/32185), thanks @shilman! +- Telemetry: Send index stats on dev exit - [#32168](https://github.com/storybookjs/storybook/pull/32168), thanks @shilman! + ## 9.1.0 Storybook 9.1 is packed with new features and improvements to enhance accessibility, streamline testing, and make your development workflow even smoother! diff --git a/CHANGELOG.prerelease.md b/CHANGELOG.prerelease.md index 4130c77de4da..3e7ed98e7a12 100644 --- a/CHANGELOG.prerelease.md +++ b/CHANGELOG.prerelease.md @@ -1,3 +1,7 @@ +## 9.2.0-alpha.2 + +- Onboarding: Tweak referral wording in survey - [#32185](https://github.com/storybookjs/storybook/pull/32185), thanks @shilman! + ## 9.2.0-alpha.1 - Addon Docs: Add `__STORYBOOK_UNSAFE_TOCBOT__` global - [#32176](https://github.com/storybookjs/storybook/pull/32176), thanks @yannbf! diff --git a/code/addons/onboarding/src/features/IntentSurvey/IntentSurvey.tsx b/code/addons/onboarding/src/features/IntentSurvey/IntentSurvey.tsx index b9b703831ca1..282fbec199ee 100644 --- a/code/addons/onboarding/src/features/IntentSurvey/IntentSurvey.tsx +++ b/code/addons/onboarding/src/features/IntentSurvey/IntentSurvey.tsx @@ -111,7 +111,7 @@ export const IntentSurvey = ({ }, }, referrer: { - label: 'How did you learn about Storybook?', + label: 'How did you discover Storybook?', type: 'select', required: true, options: shuffleObject({ diff --git a/code/lib/cli-storybook/src/automigrate/helpers/logMigrationSummary.test.ts b/code/lib/cli-storybook/src/automigrate/helpers/logMigrationSummary.test.ts index 32b26bec8b33..1dcdce0c2526 100644 --- a/code/lib/cli-storybook/src/automigrate/helpers/logMigrationSummary.test.ts +++ b/code/lib/cli-storybook/src/automigrate/helpers/logMigrationSummary.test.ts @@ -111,7 +111,7 @@ describe('logMigrationSummary', () => { The automigrations try to migrate common patterns in your project, but might not contain everything needed to migrate to the latest version of Storybook. - Please check the changelog and migration guide for manual migrations and more information: https://storybook.js.org/docs/migration-guide + Please check the changelog and migration guide for manual migrations and more information: https://storybook.js.org/docs/releases/migration-guide And reach out on Discord if you need help: https://discord.gg/storybook" `); }); @@ -132,7 +132,7 @@ describe('logMigrationSummary', () => { The automigrations try to migrate common patterns in your project, but might not contain everything needed to migrate to the latest version of Storybook. - Please check the changelog and migration guide for manual migrations and more information: https://storybook.js.org/docs/migration-guide + Please check the changelog and migration guide for manual migrations and more information: https://storybook.js.org/docs/releases/migration-guide And reach out on Discord if you need help: https://discord.gg/storybook" `); }); @@ -153,7 +153,7 @@ describe('logMigrationSummary', () => { The automigrations try to migrate common patterns in your project, but might not contain everything needed to migrate to the latest version of Storybook. - Please check the changelog and migration guide for manual migrations and more information: https://storybook.js.org/docs/migration-guide + Please check the changelog and migration guide for manual migrations and more information: https://storybook.js.org/docs/releases/migration-guide And reach out on Discord if you need help: https://discord.gg/storybook" `); }); diff --git a/code/lib/cli-storybook/src/automigrate/helpers/logMigrationSummary.ts b/code/lib/cli-storybook/src/automigrate/helpers/logMigrationSummary.ts index 60e74d3c2b47..6e676dcd53e4 100644 --- a/code/lib/cli-storybook/src/automigrate/helpers/logMigrationSummary.ts +++ b/code/lib/cli-storybook/src/automigrate/helpers/logMigrationSummary.ts @@ -63,7 +63,7 @@ export function logMigrationSummary({ The automigrations try to migrate common patterns in your project, but might not contain everything needed to migrate to the latest version of Storybook. Please check the changelog and migration guide for manual migrations and more information: ${picocolors.yellow( - 'https://storybook.js.org/docs/migration-guide' + 'https://storybook.js.org/docs/releases/migration-guide' )} And reach out on Discord if you need help: ${picocolors.yellow('https://discord.gg/storybook')} `); diff --git a/code/lib/cli-storybook/src/upgrade.ts b/code/lib/cli-storybook/src/upgrade.ts index 33ed32c2f8dc..ce2dd03fbcd7 100644 --- a/code/lib/cli-storybook/src/upgrade.ts +++ b/code/lib/cli-storybook/src/upgrade.ts @@ -265,7 +265,7 @@ function logUpgradeResults( } logger.log( - `For a full list of changes, please check our migration guide: ${CLI_COLORS.cta('https://storybook.js.org/docs/migration-guide')}` + `For a full list of changes, please check our migration guide: ${CLI_COLORS.cta('https://storybook.js.org/docs/releases/migration-guide')}` ); } diff --git a/code/package.json b/code/package.json index d109e886e7bc..fa90579129b1 100644 --- a/code/package.json +++ b/code/package.json @@ -285,5 +285,6 @@ "Dependency Upgrades" ] ] - } + }, + "deferredNextVersion": "9.2.0-alpha.2" } diff --git a/docs/_snippets/after-each-in-meta.md b/docs/_snippets/after-each-in-meta.md new file mode 100644 index 000000000000..e0868d17a206 --- /dev/null +++ b/docs/_snippets/after-each-in-meta.md @@ -0,0 +1,185 @@ +```ts filename="Page.stories.ts" renderer="angular" language="ts" +import type { Meta, StoryObj } from '@storybook/angular'; + +import { Page } from './Page'; + +const meta: Meta = { + component: Page, + // 👇 Runs after each story in this file + async afterEach(context) { + console.log(`✅ Tested ${context.name} story`); + }, +}; +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + async play({ canvas }) { + // ... + }, +}; +``` + +```svelte filename="Page.stories.svelte" renderer="svelte" language="js" tabTitle="Svelte CSF" + + + { + // ... + }} +/> +``` + +```js filename="Page.stories.js" renderer="svelte" language="js" tabTitle="CSF" +import Page from './Page.svelte'; + +export default { + component: Page, + // 👇 Runs after each story in this file + async afterEach(context) { + console.log(`✅ Tested ${context.name} story`); + }, +}; + +export const Default = { + async play({ canvas }) { + // ... + }, +}; +``` + +```js filename="Page.stories.js" renderer="common" language="js" +import { Page } from './Page'; + +export default { + component: Page, + // 👇 Runs after each story in this file + async afterEach(context) { + console.log(`✅ Tested ${context.name} story`); + }, +}; + +export const Default = { + async play({ canvas }) { + // ... + }, +}; +``` + +```svelte filename="Page.stories.svelte" renderer="svelte" language="ts" tabTitle="Svelte CSF" + + + { + // ... + }} +/> +``` + +```ts filename="Page.stories.ts" renderer="svelte" language="ts" tabTitle="CSF" +// Replace your-framework with svelte-vite or sveltekit +import type { Meta, StoryObj } from '@storybook/your-framework'; + +import Page from './Page.svelte'; + +const meta = { + component: Page, + // 👇 Runs after each story in this file + async afterEach(context) { + console.log(`✅ Tested ${context.name} story`); + }, +} satisfies Meta; +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + async play({ canvas }) { + // ... + }, +}; +``` + +```ts filename="Page.stories.ts" renderer="common" language="ts" +// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, vue3-vite, etc. +import type { Meta, StoryObj } from '@storybook/your-framework'; + +import { Page } from './Page'; + +const meta = { + component: Page, + // 👇 Runs after each story in this file + async afterEach(context) { + console.log(`✅ Tested ${context.name} story`); + }, +} satisfies Meta; +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + async play({ canvas }) { + // ... + }, +}; +``` + +```js filename="Page.stories.js" renderer="web-components" language="js" +export default { + component: 'my-page', + // 👇 Runs after each story in this file + async afterEach(context) { + console.log(`✅ Tested ${context.name} story`); + }, +}; + +export const Default = { + async play({ canvas }) { + // ... + }, +}; +``` + +```ts filename="Page.stories.ts" renderer="web-components" language="ts" +import type { Meta, StoryObj } from '@storybook/web-components-vite'; + +const meta: Meta = { + component: 'my-page', + // 👇 Runs after each story in this file + async afterEach(context) { + console.log(`✅ Tested ${context.name} story`); + }, +}; +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + async play({ canvas }) { + // ... + }, +}; +``` diff --git a/docs/_snippets/automock-register-full.md b/docs/_snippets/automock-register-full.md new file mode 100644 index 000000000000..5bcaaad2b28d --- /dev/null +++ b/docs/_snippets/automock-register-full.md @@ -0,0 +1,21 @@ +```ts filename=".storybook/preview.ts" renderer="common" language="ts" +import { sb } from 'storybook/test'; + +// 👇 Automatically replaces all exports from the `lib/session` local module with mock functions +sb.mock(import('../lib/session.ts')); +// 👇 Automatically replaces all exports from the `uuid` package in `node_modules` with mock functions +sb.mock(import('uuid')); + +// ...rest of the file +``` + +```js filename=".storybook/preview.js" renderer="common" language="js" +import { sb } from 'storybook/test'; + +// 👇 Automatically replaces all exports from the `lib/session` local module with mock functions +sb.mock('../lib/session.js'); +// 👇 Automatically replaces all exports from the `uuid` package in `node_modules` with mock functions +sb.mock('uuid'); + +// ...rest of the file +``` diff --git a/docs/_snippets/automock-register-mock-file.md b/docs/_snippets/automock-register-mock-file.md new file mode 100644 index 000000000000..a4febca91487 --- /dev/null +++ b/docs/_snippets/automock-register-mock-file.md @@ -0,0 +1,21 @@ +```ts filename=".storybook/preview.ts" renderer="common" language="ts" +import { sb } from 'storybook/test'; + +// 👇 Replaces imports of this module with imports to `../lib/__mocks__/session.ts` +sb.mock(import('../lib/session.ts')); +// 👇 Replaces imports of this module with imports to `../__mocks__/uuid.ts` +sb.mock(import('uuid')); + +// ...rest of the file +``` + +```js filename=".storybook/preview.js" renderer="common" language="js" +import { sb } from 'storybook/test'; + +// 👇 Replaces imports of this module with imports to `../lib/__mocks__/session.ts` +sb.mock('../lib/session.js'); +// 👇 Replaces imports of this module with imports to `../__mocks__/uuid.ts` +sb.mock('uuid'); + +// ...rest of the file +``` diff --git a/docs/_snippets/automock-register-spy.md b/docs/_snippets/automock-register-spy.md new file mode 100644 index 000000000000..15486afa63b0 --- /dev/null +++ b/docs/_snippets/automock-register-spy.md @@ -0,0 +1,21 @@ +```ts filename=".storybook/preview.ts" renderer="common" language="ts" +import { sb } from 'storybook/test'; + +// 👇 Automatically spies on all exports from the `lib/session` local module +sb.mock(import('../lib/session.ts'), { spy: true }); +// 👇 Automatically spies on all exports from the `uuid` package in `node_modules` +sb.mock(import('uuid'), { spy: true }); + +// ...rest of the file +``` + +```js filename=".storybook/preview.js" renderer="common" language="js" +import { sb } from 'storybook/test'; + +// 👇 Automatically spies on all exports from the `lib/session` local module +sb.mock('../lib/session.js', { spy: true }); +// 👇 Automatically spies on all exports from the `uuid` package in `node_modules` +sb.mock('uuid', { spy: true }); + +// ...rest of the file +``` diff --git a/docs/_snippets/automocked-modules-in-story.md b/docs/_snippets/automocked-modules-in-story.md new file mode 100644 index 000000000000..9aa23a41f93f --- /dev/null +++ b/docs/_snippets/automocked-modules-in-story.md @@ -0,0 +1,282 @@ +```ts filename="AuthButton.stories.ts" renderer="angular" language="ts" +import type { Meta, StoryObj } from '@storybook/angular'; +import { expect, mocked } from 'storybook/test'; + +import { AuthButton } from './AuthButton.component'; + +import { v4 as uuidv4 } from 'uuid'; +import { getUserFromSession } from '../lib/session'; + +const meta: Meta = { + component: AuthButton, + // 👇 This will run before each story is rendered + beforeEach: async () => { + // 👇 Force known, consistent behavior for mocked modules + mocked(uuidv4).mockReturnValue('1234-5678-90ab-cdef'); + mocked(getUserFromSession).mockReturnValue({ name: 'John Doe' }); + }, +}; +export default meta; + +type Story = StoryObj; + +export const LogIn: Story = { + play: async ({ canvas, userEvent }) => { + const button = canvas.getByRole('button', { name: 'Sign in' }); + userEvent.click(button); + + // Assert that the getUserFromSession function was called + expect(getUserFromSession).toHaveBeenCalled(); + }, +}; +``` + +```ts filename="AuthButton.stories.ts" renderer="common" language="ts" +// Replace your-framework with the name of your framework (e.g. react-vite, vue3-vite, etc.) +import type { Meta, StoryObj } from '@storybook/your-framework'; +import { expect, mocked } from 'storybook/test'; + +import { AuthButton } from './AuthButton'; + +import { v4 as uuidv4 } from 'uuid'; +import { getUserFromSession } from '../lib/session'; + +const meta = { + component: AuthButton, + // 👇 This will run before each story is rendered + beforeEach: async () => { + // 👇 Force known, consistent behavior for mocked modules + mocked(uuidv4).mockReturnValue('1234-5678-90ab-cdef'); + mocked(getUserFromSession).mockReturnValue({ name: 'John Doe' }); + }, +} satisfies Meta; +export default meta; + +type Story = StoryObj; + +export const LogIn: Story = { + play: async ({ canvas, userEvent }) => { + const button = canvas.getByRole('button', { name: 'Sign in' }); + userEvent.click(button); + + // Assert that the getUserFromSession function was called + expect(getUserFromSession).toHaveBeenCalled(); + }, +}; +``` + +```js filename="AuthButton.stories.js" renderer="common" language="js" +import { expect } from 'storybook/test'; + +import { AuthButton } from './AuthButton'; + +import { v4 as uuidv4 } from 'uuid'; +import { getUserFromSession } from '../lib/session'; + +export default { + component: AuthButton, + // 👇 This will run before each story is rendered + beforeEach: async () => { + // 👇 Force known, consistent behavior for mocked modules + uuidv4.mockReturnValue('1234-5678-90ab-cdef'); + getUserFromSession.mockReturnValue({ name: 'John Doe' }); + }, +}; + +export const LogIn = { + play: async ({ canvas, userEvent }) => { + const button = canvas.getByRole('button', { name: 'Sign in' }); + userEvent.click(button); + + // Assert that the getUserFromSession function was called + expect(getUserFromSession).toHaveBeenCalled(); + }, +}; +``` + +```svelte filename="AuthButton.stories.svelte" renderer="svelte" language="ts" tabTitle="Svelte CSF" + + + { + const button = canvas.getByRole('button', { name: 'Sign in' }); + userEvent.click(button); + + // Assert that the getUserFromSession function was called + expect(getUserFromSession).toHaveBeenCalled(); + }} +/> +``` + +```ts filename="AuthButton.stories.ts" renderer="svelte" language="ts" tabTitle="CSF" +// Replace your-framework with the framework you are using, e.g. sveltekit or svelte-vite +import type { Meta, StoryObj } from '@storybook/your-framework'; +import { expect, mocked } from 'storybook/test'; + +import { AuthButton } from './AuthButton.svelte'; + +import { v4 as uuidv4 } from 'uuid'; +import { getUserFromSession } from '../lib/session'; + +const meta = { + component: AuthButton, + // 👇 This will run before each story is rendered + beforeEach: async () => { + // 👇 Force known, consistent behavior for mocked modules + mocked(uuidv4).mockReturnValue('1234-5678-90ab-cdef'); + mocked(getUserFromSession).mockReturnValue({ name: 'John Doe' }); + }, +} satisfies Meta; +export default meta; + +type Story = StoryObj; + +export const LogIn: Story = { + play: async ({ canvas, userEvent }) => { + const button = canvas.getByRole('button', { name: 'Sign in' }); + userEvent.click(button); + + // Assert that the getUserFromSession function was called + expect(getUserFromSession).toHaveBeenCalled(); + }, +}; +``` + +```svelte filename="AuthButton.stories.svelte" renderer="svelte" language="js" tabTitle="Svelte CSF" + + + { + const button = canvas.getByRole('button', { name: 'Sign in' }); + userEvent.click(button); + + // Assert that the getUserFromSession function was called + expect(getUserFromSession).toHaveBeenCalled(); + }} +/> +``` + +```js filename="AuthButton.stories.js" renderer="svelte" language="js" tabTitle="CSF" +import { expect } from 'storybook/test'; + +import { AuthButton } from './AuthButton.svelte'; + +import { v4 as uuidv4 } from 'uuid'; +import { getUserFromSession } from '../lib/session'; + +export default { + component: AuthButton, + // 👇 This will run before each story is rendered + beforeEach: async () => { + // 👇 Force known, consistent behavior for mocked modules + uuidv4.mockReturnValue('1234-5678-90ab-cdef'); + getUserFromSession.mockReturnValue({ name: 'John Doe' }); + }, +}; + +export const LogIn = { + play: async ({ canvas, userEvent }) => { + const button = canvas.getByRole('button', { name: 'Sign in' }); + userEvent.click(button); + + // Assert that the getUserFromSession function was called + expect(getUserFromSession).toHaveBeenCalled(); + }, +}; +``` + +```ts filename="AuthButton.stories.ts" renderer="web-components" language="ts" +import type { Meta, StoryObj } from '@storybook/web-components-vite'; +import { expect, mocked } from 'storybook/test'; + +import { v4 as uuidv4 } from 'uuid'; +import { getUserFromSession } from '../lib/session'; + +const meta: Meta = { + component: 'demo-auth-button', + // 👇 This will run before each story is rendered + beforeEach: async () => { + // 👇 Force known, consistent behavior for mocked modules + mocked(uuidv4).mockReturnValue('1234-5678-90ab-cdef'); + mocked(getUserFromSession).mockReturnValue({ name: 'John Doe' }); + }, +}; +export default meta; + +type Story = StoryObj; + +export const LogIn: Story = { + play: async ({ canvas, userEvent }) => { + const button = canvas.getByRole('button', { name: 'Sign in' }); + userEvent.click(button); + + // Assert that the getUserFromSession function was called + expect(getUserFromSession).toHaveBeenCalled(); + }, +}; +``` + +```js filename="AuthButton.stories.js" renderer="web-components" language="js" +import { expect } from 'storybook/test'; + +import { v4 as uuidv4 } from 'uuid'; +import { getUserFromSession } from '../lib/session'; + +export default { + component: 'demo-auth-button', + // 👇 This will run before each story is rendered + beforeEach: async () => { + // 👇 Force known, consistent behavior for mocked modules + uuidv4.mockReturnValue('1234-5678-90ab-cdef'); + getUserFromSession.mockReturnValue({ name: 'John Doe' }); + }, +}; + +export const LogIn = { + play: async ({ canvas, userEvent }) => { + const button = canvas.getByRole('button', { name: 'Sign in' }); + userEvent.click(button); + + // Assert that the getUserFromSession function was called + expect(getUserFromSession).toHaveBeenCalled(); + }, +}; +``` diff --git a/docs/releases/migration-guide.mdx b/docs/releases/migration-guide.mdx index b1deb6803f58..a730194cf058 100644 --- a/docs/releases/migration-guide.mdx +++ b/docs/releases/migration-guide.mdx @@ -56,7 +56,7 @@ You may wish to read the [full migration notes][full-migration-notes] before mig ## Automatic upgrade -To upgrade your Storybook, run the [upgrade](../configure/upgrading.mdx) command in the root of your repository: +To upgrade your Storybook, run the [upgrade](./upgrading.mdx) command in the root of your repository: {/* prettier-ignore-start */} diff --git a/docs/versions/next.json b/docs/versions/next.json index 47e250f0d12b..d708cc612ef3 100644 --- a/docs/versions/next.json +++ b/docs/versions/next.json @@ -1 +1 @@ -{"version":"9.2.0-alpha.1","info":{"plain":"- Addon Docs: Add `__STORYBOOK_UNSAFE_TOCBOT__` global - [#32176](https://github.com/storybookjs/storybook/pull/32176), thanks @yannbf!\n- CLI: Fix throwing in readonly environments - [#31785](https://github.com/storybookjs/storybook/pull/31785), thanks @JReinhold!\n- Telemetry: Send index stats on dev exit - [#32168](https://github.com/storybookjs/storybook/pull/32168), thanks @shilman!"}} +{"version":"9.2.0-alpha.2","info":{"plain":"- Onboarding: Tweak referral wording in survey - [#32185](https://github.com/storybookjs/storybook/pull/32185), thanks @shilman!"}} diff --git a/docs/writing-stories/mocking-data-and-modules/mocking-modules.mdx b/docs/writing-stories/mocking-data-and-modules/mocking-modules.mdx index c80e13b104f1..bb9d592ad24f 100644 --- a/docs/writing-stories/mocking-data-and-modules/mocking-modules.mdx +++ b/docs/writing-stories/mocking-data-and-modules/mocking-modules.mdx @@ -26,6 +26,8 @@ export function AuthButton() { } ``` +The above example is written with React, but the same principles apply to other renderers like Vue, Svelte, or Web Components. The important part is the usage of the two module dependencies. + When writing stories or tests for this component, you may want to mock the `getUserFromSession` function to control the user data returned, or mock the `uuidv4` function to return a predictable ID. This allows you to test the component's behavior without relying on the actual implementations of these modules. For maximum flexibility, Storybook provides three ways to mock modules for your stories. Let's walk through each of them, starting with the most straightforward approach. @@ -57,17 +59,9 @@ For most cases, you should register a mocked module as spy-only, by setting the For example, if you want to spy on the `getUserFromSession` function and the `uuidv4` function from the `uuid` package, you can call the `sb.mock` utility function in your `.storybook/preview.js|ts` file: -{/* TODO: Snippetize */} -```ts title=".storybook/preview.ts" -import { sb } from 'storybook/test'; - -// 👇 Automatically spies on all exports from the `lib/session` local module -sb.mock('../lib/session.ts', { spy: true }); -// 👇 Automatically spies on all exports from the `uuid` package in `node_modules` -sb.mock('uuid', { spy: true }); + -// ...rest of the file -``` +If you need to mock an external module that has a deeper import path (e.g. `lodash-es/add`), register the mock with that path. You can then [control the behavior of these modules](#using-automocked-modules-in-stories) and make assertions about them in your stories, such as checking if a function was called or what arguments it was called with. @@ -75,17 +69,7 @@ You can then [control the behavior of these modules](#using-automocked-modules-i For cases where you need to prevent the original module's functionality from executing, set the `spy` option to `false` (or omit it, because that is the default value). This will automatically replace all exports from the module with [Vitest mock functions](https://vitest.dev/api/mock.html), allowing you to control their behavior and make assertions while being certain that the original functionality never runs. -{/* TODO: Snippetize */} -```ts title=".storybook/preview.ts" -import { sb } from 'storybook/test'; - -// 👇 Automatically replaces all exports from the `lib/session` local module with mock functions -sb.mock('../lib/session.ts'); -// 👇 Automatically replaces all exports from the `uuid` package in `node_modules` with mock functions -sb.mock('uuid'); - -// ...rest of the file -``` + @@ -107,7 +91,7 @@ export function getUserFromSession() { } ``` -For packages in your `node_modules`, create a `__mocks__` directory in the root of your project and create the mock file there. For example, to mock the `uuid` package, create a file named `uuid.js|ts` in the `__mocks__` directory: +For packages in your `node_modules`, create a `__mocks__` directory in the root of your project and create the mock file there. For example, to mock the `uuid` package, create a file named `uuid.js` in the `__mocks__` directory: ```js title="__mocks__/uuid.js" export function v4() { @@ -115,7 +99,17 @@ export function v4() { } ``` -The root of your project is typically the directory where `.git` is located. You can set the project root manually by providing the `STORYBOOK_PROJECT_ROOT` environment variable when running or building Storybook. +If you need to mock an external module that has a deeper import path (e.g. `lodash-es/add`), create a corresponding mock file (e.g. `__mocks__/lodash-es/add.js`) in the root of your project. + +The root of your project is determined differently depending on your builder: + +**Vite projects** + +The root `__mocks__` directory should be placed in the [`root` directory](https://vite.dev/config/shared-options.html#root), as defined in your project's Vite configuration (typically `process.cwd()`) If that is unavailable, it defaults to the directory containing your `.storybook` directory. + +**Webpack projects** + +The root `__mocks__` directory should be placed in the [`context` directory](https://webpack.js.org/configuration/entry-context/#context), as defined in your project's Webpack configuration (typically `process.cwd()`). If that is unavailable, it defaults to the root of your repository. @@ -127,17 +121,7 @@ They must export the same named exports as the original module. If you want to m You can then use the `sb.mock` utility to register these mock files in your `preview.js|ts` file: -{/* TODO: Snippetize */} -```ts title=".storybook/preview.ts" -import { sb } from 'storybook/test'; - -// 👇 Replaces imports of this module with imports to `../lib/__mocks__/session.ts` -sb.mock('../lib/session.ts'); -// 👇 Replaces imports of this module with imports to `../__mocks__/uuid.ts` -sb.mock('uuid'); - -// ...rest of the file -``` + Note that the API for registering automatically mocked modules and mock files is the same. The only difference is that `sb.mock` will first look for a mock file in the appropriate directory before automatically mocking the module. @@ -145,38 +129,7 @@ Note that the API for registering automatically mocked modules and mock files is All registered automocked modules are used the same way within your stories. You can control the behavior, such as defining what it returns, and make assertions about the modules. -{/* TODO: Snippetize */} -```ts title="AuthButton.stories.ts" -import { Meta, StoryObj } from '@storybook/react-vite'; -import { expect, mocked } from 'storybook/test'; -import { AuthButton } from './AuthButton'; - -import { v4 as uuidv4 } from 'uuid'; -import { getUserFromSession } from '../lib/session'; - -const meta = { - component: AuthButton, - // 👇 This will run before each story is rendered - beforeEach: async () => { - // 👇 Force known, consistent behavior for mocked modules - mocked(uuidv4).mockReturnValue('1234-5678-90ab-cdef'); - mocked(getUserFromSession).mockReturnValue({ name: 'John Doe' }); - }, -} satisfies Meta; -export default meta; - -type Story = StoryObj; - -export const LogIn: Story = { - play: async ({ canvas, userEvent }) => { - const button = canvas.getByRole('button', { name: 'Sign in' }); - userEvent.click(button); - - // Assert that the getUserFromSession function was called - expect(getUserFromSession).toHaveBeenCalled(); - }, -}; -``` + Mocked functions created with the `sb.mock` utility are full [Vitest mock functions](https://vitest.dev/api/mock.html), which means you can use all the methods available on them. Some of the most useful methods include: @@ -217,14 +170,6 @@ While this feature uses Vitest's mocking engine, the implementation within Story - Runtime Mocking: While the module swap is static, you can still control the behavior of the mocked functions at runtime within a play function or `beforeEach` hook (e.g., `mocked(myFunction).mockReturnValue('new value')`). - No Factory Functions: The `sb.mock()` API does not accept a factory function as its second argument (e.g., `sb.mock('path', () => ({...}))`). This is because all mocking decisions are resolved at build time, whereas factories are executed at runtime. -### Troubleshooting - -#### Receiving an `exports is not defined` error - -Webpack projects may encounter an `exports is not defined` error when using automocking. This is usually caused by attempting to mock a module with CommonJS (CJS) entry points. Automocking with Webpack only works with modules that have ESModules (ESM) entry points exclusively, so you must use a [mock file](#mock-files) to mock CJS modules. - ---- - ## Alternative methods If [automocking](#automocking) is not suitable for your project, there are two alternative methods to mock modules in Storybook: [subpath imports](#subpath-imports) and [builder aliases](#builder-aliases). These methods require a bit more setup but provide similar functionality to automocking, allowing you to control the behavior of modules in your stories. @@ -378,3 +323,11 @@ Here's an example of using the [`mockdate`](https://github.com/boblauer/MockDate {/* prettier-ignore-end */} + +----- + +## Troubleshooting + +### Receiving an `exports is not defined` error + +Webpack projects may encounter an `exports is not defined` error when using [automocking](#automocking). This is usually caused by attempting to mock a module with CommonJS (CJS) entry points. Automocking with Webpack only works with modules that have ESModules (ESM) entry points exclusively, so you must use a [mock file](#mock-files) to mock CJS modules. diff --git a/docs/writing-tests/interaction-testing.mdx b/docs/writing-tests/interaction-testing.mdx index 2ae5b4ef4c81..a8974ccad840 100644 --- a/docs/writing-tests/interaction-testing.mdx +++ b/docs/writing-tests/interaction-testing.mdx @@ -239,6 +239,24 @@ It is _not_ necessary to restore `fn()` mocks, as Storybook will already do that +### Make assertions after interactions + +Sometimes, you may need to make assertions or run code after the component has been rendered and interacted with. + +#### `afterEach` + +`afterEach` runs after the story is rendered and the play function has completed. It can be used at the project level in the preview file (`.storybook/preview.js|ts`), at the component level in the component meta, or at the story level in the story definition. This is useful for making assertions after the component has been rendered and interacted with, such as running checks on the final rendered output or logging information. + +Like the `play` function, `afterEach` receives the `context` object, which contains the `args`, `canvas`, and other properties related to the story. You can use this to make assertions or run code after the story has been rendered. + + + + + +You should not use `afterEach` to reset state in your tests. Because it runs after the story, resetting state here could prevent you from seeing the correct end state of your story. Instead, use the [`beforeEach`'s returned cleanup function](#beforeeach) to reset state, which will run only when navigating between stories to preserve the end state. + + + ### Group interactions with the step function For complex flows, it can be worthwhile to group sets of related interactions together using the step function. This allows you to provide a custom label that describes a set of interactions: