From 858f1b6fe93dfa337bcf70e447c0a64818a1cf3d Mon Sep 17 00:00:00 2001 From: Kyle Gach Date: Wed, 10 Jul 2024 13:00:11 -0600 Subject: [PATCH] Merge pull request #28444 from storybookjs/kasper/mount-docs Docs: Add docs for mounting inside the play function --- code/core/src/preview-errors.ts | 28 ++--- docs/_snippets/before-all-in-preview.md | 24 ++++ docs/_snippets/before-each-in-preview.md | 24 ++++ docs/_snippets/mount-advanced.md | 135 +++++++++++++++++++++ docs/_snippets/mount-basic.md | 44 +++++++ docs/api/doc-blocks/doc-block-story.mdx | 4 + docs/writing-tests/interaction-testing.mdx | 127 ++++++++++++++++--- 7 files changed, 352 insertions(+), 34 deletions(-) create mode 100644 docs/_snippets/before-all-in-preview.md create mode 100644 docs/_snippets/before-each-in-preview.md create mode 100644 docs/_snippets/mount-advanced.md create mode 100644 docs/_snippets/mount-basic.md diff --git a/code/core/src/preview-errors.ts b/code/core/src/preview-errors.ts index eddc355ce3b6..31341bb6132c 100644 --- a/code/core/src/preview-errors.ts +++ b/code/core/src/preview-errors.ts @@ -210,27 +210,23 @@ export class StoryStoreAccessedBeforeInitializationError extends StorybookError export class MountMustBeDestructuredError extends StorybookError { constructor(public data: { playFunction: string }) { - const transpiled = - /function\s*\*|regeneratorRuntime|asyncToGenerator|_ref|param|_0|__async/.test( - data.playFunction - ); - super({ category: Category.PREVIEW_API, code: 12, message: dedent` - To use mount in the play function, you must use object destructuring, e.g. play: ({ mount }) => {}. - - ${ - !transpiled - ? '' - : dedent` - It seems that your builder is configured to transpile destructuring. - To use the mount prop of the story context, you must configure your builder to transpile to no earlier than ES2017. - ` - } - More info: https://storybook.js.org/docs/writing-tests/interaction-testing#run-code-before-each-test + To use mount in the play function, you must satisfy the following two requirements: + + 1. You *must* destructure the mount property from the \`context\` (the argument passed to your play function). + This makes sure that Storybook does not start rendering the story before the play function begins. + + 2. Your Storybook framework or builder must be configured to transpile to ES2017 or newer. + This is because destructuring statements and async/await usages are otherwise transpiled away, + which prevents Storybook from recognizing your usage of \`mount\`. + + Note that Angular is not supported. As async/await is transpiled to support the zone.js polyfill. + + More info: https://storybook.js.org/docs/writing-tests/interaction-testing#run-code-before-the-component-gets-rendered Received the following play function: ${data.playFunction}`, diff --git a/docs/_snippets/before-all-in-preview.md b/docs/_snippets/before-all-in-preview.md new file mode 100644 index 000000000000..6cd7e246a19b --- /dev/null +++ b/docs/_snippets/before-all-in-preview.md @@ -0,0 +1,24 @@ +```js filename=".storybook/preview.js" renderer="common" language="js" +import { init } from '../project-bootstrap'; + +export default { + async beforeAll() { + await init(); + }, +}; +``` + +```ts filename=".storybook/preview.ts" renderer="common" language="ts" +// Replace your-renderer with the renderer you are using (e.g., react, vue3, angular, etc.) +import { Preview } from '@storybook/your-renderer'; + +import { init } from '../project-bootstrap'; + +const preview: Preview = { + async beforeAll() { + await init(); + }, +}; + +export default preview; +``` diff --git a/docs/_snippets/before-each-in-preview.md b/docs/_snippets/before-each-in-preview.md new file mode 100644 index 000000000000..47abe76c5057 --- /dev/null +++ b/docs/_snippets/before-each-in-preview.md @@ -0,0 +1,24 @@ +```js filename=".storybook/preview.js" renderer="common" language="js" +import MockDate from 'mockdate'; + +export default { + async beforeEach() { + MockDate.reset() + } +}; +``` + +```ts filename=".storybook/preview.ts" renderer="common" language="ts" +// Replace your-renderer with the renderer you are using (e.g., react, vue3, angular, etc.) +import { Preview } from '@storybook/your-renderer'; +import MockDate from 'mockdate'; + +const preview: Preview = { + async beforeEach() { + MockDate.reset() + } +}; + +export default preview; +``` + diff --git a/docs/_snippets/mount-advanced.md b/docs/_snippets/mount-advanced.md new file mode 100644 index 000000000000..7078b662914e --- /dev/null +++ b/docs/_snippets/mount-advanced.md @@ -0,0 +1,135 @@ +```tsx filename="Page.stories.tsx" renderer="react" language="ts" +export const Default: Story = { + play: async ({ mount, args }) => { + const note = await db.note.create({ + data: { title: 'Mount inside of play' }, + }); + + const canvas = await mount( + // ๐Ÿ‘‡ Pass data that is created inside of the play function to the component + // For example, a just-generated UUID + + ); + + await userEvent.click(await canvas.findByRole('menuitem', { name: /login to add/i })); + }, + argTypes: { + // ๐Ÿ‘‡ Make the params prop un-controllable, as the value is always overriden in the play function. + params: { control: { disable: true } }, + } +}; +``` + +```jsx filename="Page.stories.jsx" renderer="react" language="js" +export const Default = { + play: async ({ mount, args }) => { + const note = await db.note.create({ + data: { title: 'Mount inside of play' }, + }); + + const canvas = await mount( + // ๐Ÿ‘‡ Pass data that is created inside of the play function to the component + // For example, a just-generated UUID + + ); + + await userEvent.click(await canvas.findByRole('menuitem', { name: /login to add/i })); + }, + argTypes: { + // ๐Ÿ‘‡ Make the params prop un-controllable, as the value is always overriden in the play function. + params: { control: { disable: true } }, + } +}; +``` + +```ts filename="Page.stories.ts" renderer="svelte" language="ts" +export const Default: Story = { + play: async ({ mount, args }) => { + const note = await db.note.create({ + data: { title: 'Mount inside of play' }, + }); + + const canvas = await mount( + Page, + // ๐Ÿ‘‡ Pass data that is created inside of the play function to the component + // For example, a just-generated UUID + { props: { ...args, params: { id: String(note.id) } } } + ); + + await userEvent.click(await canvas.findByRole('menuitem', { name: /login to add/i })); + }, + argTypes: { + // ๐Ÿ‘‡ Make the params prop un-controllable, as the value is always overriden in the play function. + params: { control: { disable: true } }, + } +}; +``` + +```js filename="Page.stories.js" renderer="svelte" language="js" +export const Default = { + play: async ({ mount, args }) => { + const note = await db.note.create({ + data: { title: 'Mount inside of play' }, + }); + + const canvas = await mount( + Page, + // ๐Ÿ‘‡ Pass data that is created inside of the play function to the component + // For example, a just-generated UUID + { props: { ...args, params: { id: String(note.id) } } } + ); + + await userEvent.click(await canvas.findByRole('menuitem', { name: /login to add/i })); + }, + argTypes: { + // ๐Ÿ‘‡ Make the params prop un-controllable, as the value is always overriden in the play function. + params: { control: { disable: true } }, + } +}; +``` + +```ts filename="Page.stories.ts" renderer="vue3" language="ts" +export const Default: Story = { + play: async ({ mount, args }) => { + const note = await db.note.create({ + data: { title: 'Mount inside of play' }, + }); + + const canvas = await mount( + Page, + // ๐Ÿ‘‡ Pass data that is created inside of the play function to the component + // For example, a just-generated UUID + { props: { ...args, params: { id: String(note.id) } } } + ); + + await userEvent.click(await canvas.findByRole('menuitem', { name: /login to add/i })); + }, + argTypes: { + // ๐Ÿ‘‡ Make the params prop un-controllable, as the value is always overriden in the play function. + params: { control: { disable: true } }, + } +}; +``` + +```js filename="Page.stories.js" renderer="vue3" language="js" +export const Default = { + play: async ({ mount, args }) => { + const note = await db.note.create({ + data: { title: 'Mount inside of play' }, + }); + + const canvas = await mount( + Page, + // ๐Ÿ‘‡ Pass data that is created inside of the play function to the component + // For example, a just-generated UUID + { props: { ...args, params: { id: String(note.id) } } } + ); + + await userEvent.click(await canvas.findByRole('menuitem', { name: /login to add/i })); + }, + argTypes: { + // ๐Ÿ‘‡ Make the params prop un-controllable, as the value is always overriden in the play function. + params: { control: { disable: true } }, + } +}; +``` diff --git a/docs/_snippets/mount-basic.md b/docs/_snippets/mount-basic.md new file mode 100644 index 000000000000..135a9159452a --- /dev/null +++ b/docs/_snippets/mount-basic.md @@ -0,0 +1,44 @@ +```js filename="Page.stories.js" renderer="common" language="js" +import MockDate from 'mockdate'; + +// ...rest of story file + +export const ChristmasUI = { + async play({ mount }) { + MockDate.set('2024-12-25'); + // ๐Ÿ‘‡ Render the component with the mocked date + await mount(); + // ...rest of test + }, +}; +``` + +```ts filename="Page.stories.ts" renderer="common" language="ts-4-9" +import MockDate from 'mockdate'; + +// ...rest of story file + +export const ChristmasUI: Story = { + async play({ mount }) { + MockDate.set('2024-12-25'); + // ๐Ÿ‘‡ Render the component with the mocked date + await mount(); + // ...rest of test + }, +}; +``` + +```ts filename="Page.stories.ts" renderer="common" language="ts" +import MockDate from 'mockdate'; + +// ...rest of story file + +export const ChristmasUI: Story = { + async play({ mount }) { + MockDate.set('2024-12-25'); + // ๐Ÿ‘‡ Render the component with the mocked date + await mount(); + // ...rest of test + }, +}; +``` diff --git a/docs/api/doc-blocks/doc-block-story.mdx b/docs/api/doc-blocks/doc-block-story.mdx index a4b217345eb2..19509b523a3c 100644 --- a/docs/api/doc-blocks/doc-block-story.mdx +++ b/docs/api/doc-blocks/doc-block-story.mdx @@ -78,6 +78,10 @@ Because all stories render simultaneously in docs entries, play functions can pe However, if you know your play function is โ€œsafeโ€ to run in docs, you can use this prop to run it automatically. + + If a story uses [`mount` in its play function](../../writing-tests/interaction-testing.mdx#run-code-before-the-component-gets-rendered), it will not render in docs unless `autoplay` is set to `true`. + + ### `height` Type: `string` diff --git a/docs/writing-tests/interaction-testing.mdx b/docs/writing-tests/interaction-testing.mdx index fff35b192010..a2ee5908316e 100644 --- a/docs/writing-tests/interaction-testing.mdx +++ b/docs/writing-tests/interaction-testing.mdx @@ -56,17 +56,85 @@ Once the story loads in the UI, it simulates the user's behavior and verifies th