diff --git a/docs/api/portable-stories-jest.md b/docs/api/portable-stories-jest.md new file mode 100644 index 00000000000..4c4141a5392 --- /dev/null +++ b/docs/api/portable-stories-jest.md @@ -0,0 +1,315 @@ +--- +title: 'Portable stories in Jest' +--- + +export const SUPPORTED_RENDERERS = ['react', 'vue']; + + + + + +Portable stories in Jest are currently only supported in [React](?renderer=react) and [Vue](?renderer=vue) projects. + + + + + + + + + +Portable stories are Storybook [stories](../writing-stories/index.md) which can be used in external environments, such as [Jest](https://jestjs.io). + +Normally, Storybok composes a story and its [annotations](#annotations) automatically, as part of the [story pipeline](#story-pipeline). When using stories in Jest tests, you must handle the story pipeline yourself, which is what the [`composeStories`](#composestories) and [`composeStory`](#composestory) functions enable. + + + + + +**Using `Next.js`?** You need to do two things differently when using portable stories in Jest with Next.js projects: + +- Configure the [`next/jest.js` transformer](https://nextjs.org/docs/pages/building-your-application/testing/jest#manual-setup), which will handle all of the necessary Next.js configuration for you. +- Import [`composeStories`](#composestories) or [`composeStory`](#composestory) from the `@storybook/nextjs` package (e.g. `import { composeStories } from '@storybook/nextjs'`). + + + + + +## composeStories + +`composeStories` will process the component's stories you specify, compose each of them with the necessary [annotations](#annotations), and return an object containing the composed stories. + +By default, the composed story will render the component with the [args](../writing-stories/args.md) that are defined in the story. You can also pass any props to the component in your test and those props will override the values passed in the story's args. + + + + + + + +### Type + + +```ts +( + csfExports: CSF file exports, + projectAnnotations?: ProjectAnnotations +) => Record +``` + + +### Parameters + +#### `csfExports` + +(**Required**) + +Type: CSF file exports + +Specifies which component's stories you want to compose. Pass the **full set of exports** from the CSF file (not the default export!). E.g. `import * as stories from './Button.stories'` + +#### `projectAnnotations` + +Type: `ProjectAnnotation | ProjectAnnotation[]` + +Specifies the project annotations to be applied to the composed stories. + +This parameter is provided for convenience. You should likely use [`setProjectAnnotations`](#setprojectannotations) instead. Details about the `ProjectAnnotation` type can be found in that function's [`projectAnnotations`](#projectannotations-2) parameter. + +This parameter can be used to [override](#overriding-globals) the project annotations applied via `setProjectAnnotations`. + +### Return + +Type: `Record` + +An object where the keys are the names of the stories and the values are the composed stories. + +Additionally, the composed story will have the following properties: + +| Property | Type | Description | +| ---------- | -------------------------------------------------------- | --------------------------------------------------------------- | +| storyName | `string` | The story's name | +| args | `Record` | The story's [args](../writing-stories/args.md) | +| argTypes | `ArgType` | The story's [argTypes](./arg-types.md) | +| id | `string` | The story's id | +| parameters | `Record` | The story's [parameters](./parameters.md) | +| load | `() => Promise` | Executes all the [loaders](#2-load-optional) for a given story | +| play | `(context?: StoryContext) => Promise \| undefined` | Executes the [play function](#4-play-optional) of a given story | + +## composeStory + +You can use `composeStory` if you wish to compose a single story for a component. + + + + + + + +### Type + + +```ts +( + story: Story export, + componentAnnotations: Meta, + projectAnnotations?: ProjectAnnotations, + exportsName?: string +) => ComposedStoryFn +``` + + +### Parameters + +#### `story` + +(**Required**) + +Type: `Story export` + +Specifies which story you want to compose. + +#### `componentAnnotations` + +(**Required**) + +Type: `Meta` + +The default export from the stories file containing the [`story`](#story). + +#### `projectAnnotations` + +Type: `ProjectAnnotation | ProjectAnnotation[]` + +Specifies the project annotations to be applied to the composed story. + +This parameter is provided for convenience. You should likely use [`setProjectAnnotations`](#setprojectannotations) instead. Details about the `ProjectAnnotation` type can be found in that function's [`projectAnnotations`](#projectannotations-2) parameter. + +This parameter can be used to [override](#overriding-globals) the project annotations applied via `setProjectAnnotations`. + +#### `exportsName` + +Type: `string` + +You probably don't need this. Because `composeStory` accepts a single story, it does not have access to the name of that story's export in the file (like `composeStories` does). If you must ensure unique story names in your tests and you cannot use `composeStories`, you can pass the name of the story's export here. + +### Return + +Type: `ComposedStoryFn` + +A single [composed story](#return). + +## setProjectAnnotations + +This API should be called once, before the tests run, typically in a [setup file](https://jestjs.io/docs/configuration#setupfiles-array). This will make sure that whenever `composeStories` or `composeStory` are called, the project annotations are taken into account as well. + + + + + +**Using `Next.js`?** When you import [`composeStories`](#composestories) or [`composeStory`](#composestory) from the `@storybook/nextjs` package (e.g. `import { composeStories } from '@storybook/nextjs'`), you probably do not need to call `setProjectAnnotations` yourself. The Next.js framework will handle this for you. + +If you are using an addon that is required for your stories to render, you will still need to include that addon's `preview` export in the project annotations set. See the example and callout below. + + + +

+ +
+ +```ts +// setup-portable-stories.ts +// Replace with your renderer, e.g. nextjs, react, vue3 +import { setProjectAnnotations } from '@storybook/'; +import * as addonAnnotations from 'my-addon/preview'; +import * as previewAnnotations from './.storybook/preview'; + +setProjectAnnotations([previewAnnotations, addonAnnotations]); +``` + + + +Sometimes a story can require an addon's [decorator](../writing-stories/decorators.md) or [loader](../writing-stories/loaders.md) to render properly. For example, an addon can apply a decorator that wraps your story in the necessary router context. In this case, you must include that addon's `preview` export in the project annotations set. See `addonAnnotations` in the example above. + +Note: If the addon doesn't automatically apply the decorator or loader itself, but instead exports them for you to apply manually in `.storybook/preview.js|ts` (e.g. using `withThemeFromJSXProvider` from [@storybook/addon-themes](https://github.com/storybookjs/storybook/blob/next/code/addons/themes/docs/api.md#withthemefromjsxprovider)), then you do not need to do anything else. They are already included in the `previewAnnotations` in the example above. + + + +### Type + +```ts +(projectAnnotations: ProjectAnnotation | ProjectAnnotation[]) => void +``` + +### Parameters + +#### `projectAnnotations` + +(**Required**) + +Type: `ProjectAnnotation | ProjectAnnotation[]` + +A set of project [annotations](#annotations) (those defined in `.storybook/preview.js|ts`) or an array of sets of project annotations, which will be applied to all composed stories. + +## Annotations + +Annotations are the metadata applied to a story, like [args](../writing-stories/args.md), [decorators](../writing-stories/decorators.md), [loaders](../writing-stories/loaders.md), and [play functions](../writing-stories/play-function.md). They can be defined for a specific story, all stories for a component, or all stories in the project. + +## Story pipeline + +To preview your stories, Storybook runs a story pipeline, which includes applying project annotations, loading data, rendering the story, and playing interactions. This is a simplified version of the pipeline: + +![A flow diagram of the story pipeline. First, set project annotations. Storybook automatically collects decorators etc. which are exported by addons and the .storybook/preview file. .storybook/preview.js produces project annotations; some-addon/preview produces addon annotations. Second, prepare. Storybook gathers all the metadata required for a story to be composed. Select.stories.js produces component annotations from the default export and story annotations from the named export. Third, load. Storybook executes all loaders (async). Fourth, render. Storybook renders the story as a component. Illustration of the rendered Select component. Fifth, play. Storybook runs the play function (interacting with component). Illustration of the renderer Select component, now open.](story-pipeline.png) + +When you want to reuse a story in a different environment, however, it's crucial to understand that all these steps make a story. The portable stories API provides you with the mechanism to recreate that story pipeline in your external environment: + +### 1. Apply project-level annotations + +[Annotations](#annotations) come from the story itself, that story's component, and the project. The project-level annotatations are those defined in your `.storybook/preview.js` file and by addons you're using. In portable stories, these annotations are not applied automatically—you must apply them yourself. + +👉 For this, you use the [`setProjectAnnotations`](#setprojectannotations) API. + +### 2. Prepare + +The story is prepared by running [`composeStories`](#composestories) or [`composeStory`](#composestory). You do not need to do anything for this step. + +### 3. Load + +**(optional)** + +Stories can prepare data they need (e.g. setting up some mocks or fetching data) before rendering by defining [loaders](../writing-stories/loaders.md). In portable stories, the loaders are not applied automatically—you have to apply them yourself. + +👉 For this, you use the [`composeStories`](#composestories) or [`composeStory`](#composestory) API. The composed story will return a `load` method to be called **before** it is rendered. + + + + + + + +### 4. Render + +At this point, the story has been prepared and can be rendered. You pass it into the + +The story has been prepared and can be rendered. To render, you pass it into the rendering mechanism of your choice (e.g. Testing Library render function, Vue test utils mount function, etc). + +👉 For this, you use the [`composeStories`](#composestories) or [`composeStory`](#composestory) API. The composed Story is a renderable component that can be passed to your rendering mechanism. + +### 5. Play + +**(optional)** + +Finally, stories can define a [play function](../essentials/interactions.md#play-function-for-interactions) to interact with the story and assert on details after it has rendered. In portable stories, the play function does not run automatically—you have to call it yourself. + +👉 For this, you use the [`composeStories`](#composestories) or [`composeStory`](#composestory) API. The composed Story will return a `play` method to be called **after** it has rendered. + + + + + + + + + +If your play function contains assertions (e.g. `expect` calls), your test will fail when those assertions fail. + + + +## Overriding globals + +If your stories behave differently based on [globals](../essentials/toolbars-and-globals.md#globals) (e.g. rendering text in English or Spanish), you can define those global values in portable stories by overriding project annotations when composing a story: + + + + + + + + + +
diff --git a/docs/api/portable-stories-playwright.md b/docs/api/portable-stories-playwright.md new file mode 100644 index 00000000000..1089a1159ae --- /dev/null +++ b/docs/api/portable-stories-playwright.md @@ -0,0 +1,238 @@ +--- +title: 'Portable stories in Playwright' +--- + +export const SUPPORTED_RENDERERS = ['react', 'vue']; + +(⚠️ **Experimental**) + + + + + +Portable stories are currently only supported in [React](?renderer=react) and [Vue](?renderer=vue) projects. + + + + + + + + + +Portable stories are Storybook [stories](../writing-stories/index.md) which can be used in external environments, such as [Playwright Component Tests (CT)](https://playwright.dev/docs/test-components). + +Normally, Storybok composes a story and its [annotations](#annotations) automatically, as part of the [story pipeline](#story-pipeline). When using stories in Playwright CT, you can use the [`createTest`](#createtest) function, which extends Playwright's test functionality to add a custom `mount` mechanism, to take care of the story pipeline for you. + + + + + +**Using `Next.js`?** Next.js requires specific configuration that is only available in [Jest](./portable-stories-jest.md). The portable stories API is not supported in Next.js with Playwright CT. + + + + + + + + + +If your stories use template-based Vue components, you may need to alias the `vue` module to resolve correctly in the Playwright CT environment. You can do this via the [`ctViteConfig` property](https://playwright.dev/docs/test-components#i-have-a-project-that-already-uses-vite-can-i-reuse-the-config): + +
+Example Playwright configuration + +```ts +// playwright-config.ts +import { defineConfig } from '@playwright/experimental-ct-vue'; + +export default defineConfig({ + ctViteConfig: { + resolve: { + alias: { + vue: 'vue/dist/vue.esm-bundler.js', + }, + }, + }, +}); +``` + +
+ +
+ +
+ +## createTest + +(⚠️ **Experimental**) + +Instead of using Playwright's own `test` function, you can use Storybook's special `createTest` function to [extend Playwright's base fixture](https://playwright.dev/docs/test-fixtures#creating-a-fixture) and override the `mount` function to load, render, and play the story. This function is experimental and is subject to changes. + + + + + + + + + +Please note the [limitations of importing stories in Playwright CT](#importing-stories-in-playwright-ct). + + + +### Type + +```ts +createTest( + baseTest: PlaywrightFixture +) => PlaywrightFixture +``` + +### Parameters + +#### `baseTest` + +(**Required**) + +Type: `PlaywrightFixture` + +The base test function to use, e.g. `test` from Playwright. + +### Return + +Type: `PlaywrightFixture` + +A Storybook-specific test function with the custom `mount` mechanism. + +## setProjectAnnotations + +This API should be called once, before the tests run, in [`playwright/index.ts`](https://playwright.dev/docs/test-components#step-1-install-playwright-test-for-components-for-your-respective-framework). This will make sure that when `mount` is called, the project annotations are taken into account as well. + +```ts +// playwright/index.ts +// Replace with your renderer, e.g. react, vue3 +import { setProjectAnnotations } from '@storybook/'; +import * as addonAnnotations from 'my-addon/preview'; +import * as previewAnnotations from './.storybook/preview'; + +setProjectAnnotations([previewAnnotations, addonAnnotations]); +``` + + + +Sometimes a story can require an addon's [decorator](../writing-stories/decorators.md) or [loader](../writing-stories/loaders.md) to render properly. For example, an addon can apply a decorator that wraps your story in the necessary router context. In this case, you must include that addon's `preview` export in the project annotations set. See `addonAnnotations` in the example above. + +Note: If the addon doesn't automatically apply the decorator or loader itself, but instead exports them for you to apply manually in `.storybook/preview.js|ts` (e.g. using `withThemeFromJSXProvider` from [@storybook/addon-themes](https://github.com/storybookjs/storybook/blob/next/code/addons/themes/docs/api.md#withthemefromjsxprovider)), then you do not need to do anything else. They are already included in the `previewAnnotations` in the example above. + + + +### Type + +```ts +(projectAnnotations: ProjectAnnotation | ProjectAnnotation[]) => void +``` + +### Parameters + +#### `projectAnnotations` + +(**Required**) + +Type: `ProjectAnnotation | ProjectAnnotation[]` + +A set of project [annotations](#annotations) (those defined in `.storybook/preview.js|ts`) or an array of sets of project annotations, which will be applied to all composed stories. + +## Annotations + +Annotations are the metadata applied to a story, like [args](../writing-stories/args.md), [decorators](../writing-stories/decorators.md), [loaders](../writing-stories/loaders.md), and [play functions](../writing-stories/play-function.md). They can be defined for a specific story, all stories for a component, or all stories in the project. + +## Importing stories in Playwright CT + +The code which you write in your Playwright test file is transformed and orchestrated by Playwright, where part of the code executes in Node, while other parts execute in the browser. + +Because of this, you have to compose the stories _in a separate file than your own test file_: + +```ts +// Button.portable.ts +// Replace with your renderer, e.g. react, vue3 +import { composeStories } from '@storybook/'; + +import * as stories from './Button.stories'; + +// This function will be executed in the browser +// and compose all stories, exporting them in a single object +export default composeStories(stories); +``` + +You can then import the composed stories in your Playwright test file, as in the [example above](#createtest). + + + +[Read more about Playwright's component testing](https://playwright.dev/docs/test-components#test-stories). + + + +## Story pipeline + +To preview your stories, Storybook runs a story pipeline, which includes applying project annotations, loading data, rendering the story, and playing interactions. This is a simplified version of the pipeline: + +![A flow diagram of the story pipeline. First, set project annotations. Storybook automatically collects decorators etc. which are exported by addons and the .storybook/preview file. .storybook/preview.js produces project annotations; some-addon/preview produces addon annotations. The rest of the steps are labeled as a group, Playwright test. Second, prepare. Storybook gathers all the metadata required for a story to be composed. Select.stories.js produces component annotations from the default export and story annotations from the named export. Third, load. Storybook executes all loaders (async). Fourth, render. Storybook renders the story as a component. Illustration of the rendered Select component. Fifth, play. Storybook runs the play function (interacting with component). Illustration of the renderer Select component, now open.](story-pipeline-playwright-ct.png) + +When you want to reuse a story in a different environment, however, it's crucial to understand that all these steps make a story. The portable stories API provides you with the mechanism to recreate that story pipeline in your external environment: + +### 1. Apply project-level annotations + +[Annotations](#annotations) come from the story itself, that story's component, and the project. The project-level annotatations are those defined in your `.storybook/preview.js` file and by addons you're using. In portable stories, these annotations are not applied automatically—you must apply them yourself. + +👉 For this, you use the [`setProjectAnnotations`](#setprojectannotations) API. + +### 2. Prepare, load, render, and play + +The story pipeline includes preparing the story, [loading data](../writing-stories/loaders.md), rendering the story, and [playing interactions](../essentials/interactions.md#play-function-for-interactions). In portable stories within Playwright CT, the `mount` function takes care of these steps for you. + +👉 For this, you use the [`createTest`](#createtest) API. + + + +If your play function contains assertions (e.g. `expect` calls), your test will fail when those assertions fail. + + + +## Overriding globals + +If your stories behave differently based on [globals](../essentials/toolbars-and-globals.md#globals) (e.g. rendering text in English or Spanish), you can define those global values in portable stories by overriding project annotations when composing a story: + + + +```tsx +// Button.portable.ts +import { test } from 'vitest'; +import { render } from '@testing-library/react'; +import { composeStory } from '@storybook/react'; + +import meta, { Primary } from './Button.stories'; + +export const PrimaryEnglish = composeStory( + Primary, + meta, + { globals: { locale: 'en' } } // 👈 Project annotations to override the locale +); + +export const PrimarySpanish = + composeStory(Primary, meta, { globals: { locale: 'es' } }); +``` + +You can then use those composed stories in your Playwright test file using the [`createTest`](#createtest) function. + + + + + +
diff --git a/docs/api/portable-stories-vitest.md b/docs/api/portable-stories-vitest.md new file mode 100644 index 00000000000..0c08c46665c --- /dev/null +++ b/docs/api/portable-stories-vitest.md @@ -0,0 +1,298 @@ +--- +title: 'Portable stories in Vitest' +--- + +export const SUPPORTED_RENDERERS = ['react', 'vue']; + + + + + +Portable stories in Vitest are currently only supported in [React](?renderer=react) and [Vue](?renderer=vue) projects. + + + + + + + + + +Portable stories are Storybook [stories](../writing-stories/index.md) which can be used in external environments, such as [Vitest](https://vitest.dev). + +Normally, Storybok composes a story and its [annotations](#annotations) automatically, as part of the [story pipeline](#story-pipeline). When using stories in Vitest tests, you must handle the story pipeline yourself, which is what the [`composeStories`](#composestories) and [`composeStory`](#composestory) functions enable. + + + + + +**Using `Next.js`?** Next.js requires specific configuration that is only available in [Jest](./portable-stories-jest.md). The portable stories API is not supported in Next.js with Vitest. + + + + + +## composeStories + +`composeStories` will process the component's stories you specify, compose each of them with the necessary [annotations](#annotations), and return an object containing the composed stories. + +By default, the composed story will render the component with the [args](../writing-stories/args.md) that are defined in the story. You can also pass any props to the component in your test and those props will override the values passed in the story's args. + + + + + + + +### Type + + +```ts +( + csfExports: CSF file exports, + projectAnnotations?: ProjectAnnotations +) => Record +``` + + +### Parameters + +#### `csfExports` + +(**Required**) + +Type: CSF file exports + +Specifies which component's stories you want to compose. Pass the **full set of exports** from the CSF file (not the default export!). E.g. `import * as stories from './Button.stories'` + +#### `projectAnnotations` + +Type: `ProjectAnnotation | ProjectAnnotation[]` + +Specifies the project annotations to be applied to the composed stories. + +This parameter is provided for convenience. You should likely use [`setProjectAnnotations`](#setprojectannotations) instead. Details about the `ProjectAnnotation` type can be found in that function's [`projectAnnotations`](#projectannotations-2) parameter. + +This parameter can be used to [override](#overriding-globals) the project annotations applied via `setProjectAnnotations`. + +### Return + +Type: `Record` + +An object where the keys are the names of the stories and the values are the composed stories. + +Additionally, the composed story will have the following properties: + +| Property | Type | Description | +| ---------- | ----------------------------------------- | --------------------------------------------------------------- | +| storyName | `string` | The story's name | +| args | `Record` | The story's [args](../writing-stories/args.md) | +| argTypes | `ArgType` | The story's [argTypes](./arg-types.md) | +| id | `string` | The story's id | +| parameters | `Record` | The story's [parameters](./parameters.md) | +| load | `() => Promise` | Executes all the [loaders](#2-load-optional) for a given story | +| play | `(context) => Promise \| undefined` | Executes the [play function](#4-play-optional) of a given story | + +## composeStory + +You can use `composeStory` if you wish to compose a single story for a component. + + + + + + + +### Type + + +```ts +( + story: Story export, + componentAnnotations: Meta, + projectAnnotations?: ProjectAnnotations, + exportsName?: string +) => ComposedStoryFn +``` + + +### Parameters + +#### `story` + +(**Required**) + +Type: `Story export` + +Specifies which story you want to compose. + +#### `componentAnnotations` + +(**Required**) + +Type: `Meta` + +The default export from the stories file containing the [`story`](#story). + +#### `projectAnnotations` + +Type: `ProjectAnnotation | ProjectAnnotation[]` + +Specifies the project annotations to be applied to the composed story. + +This parameter is provided for convenience. You should likely use [`setProjectAnnotations`](#setprojectannotations) instead. Details about the `ProjectAnnotation` type can be found in that function's [`projectAnnotations`](#projectannotations-2) parameter. + +This parameter can be used to [override](#overriding-globals) the project annotations applied via `setProjectAnnotations`. + +#### `exportsName` + +Type: `string` + +You probably don't need this. Because `composeStory` accepts a single story, it does not have access to the name of that story's export in the file (like `composeStories` does). If you must ensure unique story names in your tests and you cannot use `composeStories`, you can pass the name of the story's export here. + +### Return + +Type: `ComposedStoryFn` + +A single [composed story](#return). + +## setProjectAnnotations + +This API should be called once, before the tests run, typically in a [setup file](https://vitest.dev/config/#setupfiles). This will make sure that whenever `composeStories` or `composeStory` are called, the project annotations are taken into account as well. + +```ts +// setup-portable-stories.ts +// Replace with your renderer, e.g. react, vue3 +import { setProjectAnnotations } from '@storybook/'; +import * as addonAnnotations from 'my-addon/preview'; +import * as previewAnnotations from './.storybook/preview'; + +setProjectAnnotations([previewAnnotations, addonAnnotations]); +``` + + + +Sometimes a story can require an addon's [decorator](../writing-stories/decorators.md) or [loader](../writing-stories/loaders.md) to render properly. For example, an addon can apply a decorator that wraps your story in the necessary router context. In this case, you must include that addon's `preview` export in the project annotations set. See `addonAnnotations` in the example above. + +Note: If the addon doesn't automatically apply the decorator or loader itself, but instead exports them for you to apply manually in `.storybook/preview.js|ts` (e.g. using `withThemeFromJSXProvider` from [@storybook/addon-themes](https://github.com/storybookjs/storybook/blob/next/code/addons/themes/docs/api.md#withthemefromjsxprovider)), then you do not need to do anything else. They are already included in the `previewAnnotations` in the example above. + + + +### Type + +```ts +(projectAnnotations: ProjectAnnotation | ProjectAnnotation[]) => void +``` + +### Parameters + +#### `projectAnnotations` + +(**Required**) + +Type: `ProjectAnnotation | ProjectAnnotation[]` + +A set of project [annotations](#annotations) (those defined in `.storybook/preview.js|ts`) or an array of sets of project annotations, which will be applied to all composed stories. + +## Annotations + +Annotations are the metadata applied to a story, like [args](../writing-stories/args.md), [decorators](../writing-stories/decorators.md), [loaders](../writing-stories/loaders.md), and [play functions](../writing-stories/play-function.md). They can be defined for a specific story, all stories for a component, or all stories in the project. + +## Story pipeline + +To preview your stories, Storybook runs a story pipeline, which includes applying project annotations, loading data, rendering the story, and playing interactions. This is a simplified version of the pipeline: + +![A flow diagram of the story pipeline. First, set project annotations. Storybook automatically collects decorators etc. which are exported by addons and the .storybook/preview file. .storybook/preview.js produces project annotations; some-addon/preview produces addon annotations. Second, prepare. Storybook gathers all the metadata required for a story to be composed. Select.stories.js produces component annotations from the default export and story annotations from the named export. Third, load. Storybook executes all loaders (async). Fourth, render. Storybook renders the story as a component. Illustration of the rendered Select component. Fifth, play. Storybook runs the play function (interacting with component). Illustration of the renderer Select component, now open.](story-pipeline.png) + +When you want to reuse a story in a different environment, however, it's crucial to understand that all these steps make a story. The portable stories API provides you with the mechanism to recreate that story pipeline in your external environment: + +### 1. Apply project-level annotations + +[Annotations](#annotations) come from the story itself, that story's component, and the project. The project-level annotatations are those defined in your `.storybook/preview.js` file and by addons you're using. In portable stories, these annotations are not applied automatically—you must apply them yourself. + +👉 For this, you use the [`setProjectAnnotations`](#setprojectannotations) API. + +### 2. Prepare + +The story is prepared by running [`composeStories`](#composestories) or [`composeStory`](#composestory). You do not need to do anything for this step. + +### 3. Load + +**(optional)** + +Stories can prepare data they need (e.g. setting up some mocks or fetching data) before rendering by defining [loaders](../writing-stories/loaders.md). In portable stories, the loaders are not applied automatically—you have to apply them yourself. + +👉 For this, you use the [`composeStories`](#composestories) or [`composeStory`](#composestory) API. The composed story will return a `load` method to be called **before** it is rendered. + + + + + + + +### 4. Render + +At this point, the story has been prepared and can be rendered. You pass it into the + +The story has been prepared and can be rendered. To render, you pass it into the rendering mechanism of your choice (e.g. Testing Library render function, Vue test utils mount function, etc). + +👉 For this, you use the [`composeStories`](#composestories) or [`composeStory`](#composestory) API. The composed Story is a renderable component that can be passed to your rendering mechanism. + +### 5. Play + +**(optional)** + +Finally, stories can define a [play function](../essentials/interactions.md#play-function-for-interactions) to interact with the story and assert on details after it has rendered. In portable stories, the play function does not run automatically—you have to call it yourself. + +👉 For this, you use the [`composeStories`](#composestories) or [`composeStory`](#composestory) API. The composed Story will return a `play` method to be called **after** it has rendered. + + + + + + + + + +If your play function contains assertions (e.g. `expect` calls), your test will fail when those assertions fail. + + + +## Overriding globals + +If your stories behave differently based on [globals](../essentials/toolbars-and-globals.md#globals) (e.g. rendering text in English or Spanish), you can define those global values in portable stories by overriding project annotations when composing a story: + + + + + + + + + + diff --git a/docs/api/story-pipeline-playwright-ct.png b/docs/api/story-pipeline-playwright-ct.png new file mode 100644 index 00000000000..3f34ef61eb2 Binary files /dev/null and b/docs/api/story-pipeline-playwright-ct.png differ diff --git a/docs/api/story-pipeline.png b/docs/api/story-pipeline.png new file mode 100644 index 00000000000..4bfbd1a44e8 Binary files /dev/null and b/docs/api/story-pipeline.png differ diff --git a/docs/snippets/react/portable-stories-jest-compose-stories.ts.mdx b/docs/snippets/react/portable-stories-jest-compose-stories.ts.mdx new file mode 100644 index 00000000000..31e77a5bf97 --- /dev/null +++ b/docs/snippets/react/portable-stories-jest-compose-stories.ts.mdx @@ -0,0 +1,27 @@ +```tsx +// Button.test.tsx +import { test, expect } from '@jest/globals'; +import { render, screen } from '@testing-library/react'; +// 👉 Using Next.js? Import from @storybook/nextjs instead +import { composeStories } from '@storybook/react'; + +// Import all stories and the component annotations from the stories file +import * as stories from './Button.stories'; + +// Every component that is returned maps 1:1 with the stories, +// but they already contain all annotations from story, meta, and project levels +const { Primary, Secondary } = composeStories(stories); + +test('renders primary button with default args', () => { + render(); + const buttonElement = screen.getByText('Text coming from args in stories file!'); + expect(buttonElement).not.toBeNull(); +}); + +test('renders primary button with overriden props', () => { + // You can override props and they will get merged with values from the story's args + render(Hello world); + const buttonElement = screen.getByText(/Hello world/i); + expect(buttonElement).not.toBeNull(); +}); +``` diff --git a/docs/snippets/react/portable-stories-jest-compose-story.ts.mdx b/docs/snippets/react/portable-stories-jest-compose-story.ts.mdx new file mode 100644 index 00000000000..9fca7e760b8 --- /dev/null +++ b/docs/snippets/react/portable-stories-jest-compose-story.ts.mdx @@ -0,0 +1,20 @@ +```tsx +// Button.test.tsx +import { jest, test, expect } from '@jest/globals'; +import { render, screen } from '@testing-library/react'; +// 👉 Using Next.js? Import from @storybook/nextjs instead +import { composeStory } from '@storybook/react'; + +import meta, { Primary } from './Button.stories'; + +test('onclick handler is called', () => { + // Returns a story which already contains all annotations from story, meta and global levels + const PrimaryStory = composeStory(Primary, meta); + + const onClickSpy = jest.fn(); + render(); + const buttonElement = screen.getByRole('button'); + buttonElement.click(); + expect(onClickSpy).toHaveBeenCalled(); +}); +``` diff --git a/docs/snippets/react/portable-stories-jest-override-globals.ts.mdx b/docs/snippets/react/portable-stories-jest-override-globals.ts.mdx new file mode 100644 index 00000000000..3f9cfb1287f --- /dev/null +++ b/docs/snippets/react/portable-stories-jest-override-globals.ts.mdx @@ -0,0 +1,25 @@ +```tsx +// Button.test.tsx +import { test } from '@jest/globals'; +import { render } from '@testing-library/react'; +// 👉 Using Next.js? Import from @storybook/nextjs instead +import { composeStory } from '@storybook/react'; + +import meta, { Primary } from './Button.stories'; + +test('renders in English', async () => { + const PrimaryStory = composeStory( + Primary, + meta, + { globals: { locale: 'en' } }, // 👈 Project annotations to override the locale + ); + + render(); +}); + +test('renders in Spanish', async () => { + const PrimaryStory = composeStory(Primary, meta, { globals: { locale: 'es' } }); + + render(); +}); +``` diff --git a/docs/snippets/react/portable-stories-jest-with-loaders.ts.mdx b/docs/snippets/react/portable-stories-jest-with-loaders.ts.mdx new file mode 100644 index 00000000000..5c902b2ece1 --- /dev/null +++ b/docs/snippets/react/portable-stories-jest-with-loaders.ts.mdx @@ -0,0 +1,19 @@ +```tsx +// Button.test.tsx +import { test } from '@jest/globals'; +import { render } from '@testing-library/react'; +// 👉 Using Next.js? Import from @storybook/nextjs instead +import { composeStory } from '@storybook/react'; + +import meta, { Primary } from './Button.stories'; + +test('applies the loaders and renders', async () => { + const PrimaryStory = composeStory(Primary, meta); + + // First, load the data for the story + await PrimaryStory.load(); + + // Then, render the story + render(); +}); +``` diff --git a/docs/snippets/react/portable-stories-jest-with-play-function.ts.mdx b/docs/snippets/react/portable-stories-jest-with-play-function.ts.mdx new file mode 100644 index 00000000000..de0ee6bedd6 --- /dev/null +++ b/docs/snippets/react/portable-stories-jest-with-play-function.ts.mdx @@ -0,0 +1,19 @@ +```tsx +// Button.test.tsx +import { test } from '@jest/globals'; +import { render } from '@testing-library/react'; +// 👉 Using Next.js? Import from @storybook/nextjs instead +import { composeStory } from '@storybook/react'; + +import meta, { Primary } from './Button.stories'; + +test('renders and executes the play function', async () => { + const PrimaryStory = composeStory(Primary, meta); + + // First, render the story + render(); + + // Then, execute the play function + await PrimaryStory.play(); +}); +``` diff --git a/docs/snippets/react/portable-stories-playwright-ct.ts.mdx b/docs/snippets/react/portable-stories-playwright-ct.ts.mdx new file mode 100644 index 00000000000..3b6b9a213eb --- /dev/null +++ b/docs/snippets/react/portable-stories-playwright-ct.ts.mdx @@ -0,0 +1,15 @@ +```tsx +// Button.playwright.test.tsx +import { createTest } from '@storybook/react/experimental-playwright'; +import { test as base } from '@playwright/experimental-ct-react'; + +import stories from './Button.portable'; + +const test = createTest(base); + +test('renders primary button', async ({ mount }) => { + // The mount function will execute all the necessary steps in the story, + // such as loaders, render, and play function + await mount(); +}); +``` diff --git a/docs/snippets/react/portable-stories-vitest-compose-stories.ts.mdx b/docs/snippets/react/portable-stories-vitest-compose-stories.ts.mdx new file mode 100644 index 00000000000..2e4c6b22d81 --- /dev/null +++ b/docs/snippets/react/portable-stories-vitest-compose-stories.ts.mdx @@ -0,0 +1,26 @@ +```tsx +// Button.test.tsx +import { test, expect } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { composeStories } from '@storybook/react'; + +// Import all stories and the component annotations from the stories file +import * as stories from './Button.stories'; + +// Every component that is returned maps 1:1 with the stories, +// but they already contain all annotations from story, meta, and project levels +const { Primary, Secondary } = composeStories(stories); + +test('renders primary button with default args', () => { + render(); + const buttonElement = screen.getByText('Text coming from args in stories file!'); + expect(buttonElement).not.toBeNull(); +}); + +test('renders primary button with overriden props', () => { + // You can override props and they will get merged with values from the story's args + render(Hello world); + const buttonElement = screen.getByText(/Hello world/i); + expect(buttonElement).not.toBeNull(); +}); +``` diff --git a/docs/snippets/react/portable-stories-vitest-compose-story.ts.mdx b/docs/snippets/react/portable-stories-vitest-compose-story.ts.mdx new file mode 100644 index 00000000000..04733bab89e --- /dev/null +++ b/docs/snippets/react/portable-stories-vitest-compose-story.ts.mdx @@ -0,0 +1,19 @@ +```tsx +// Button.test.tsx +import { vi, test, expect } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { composeStory } from '@storybook/react'; + +import meta, { Primary } from './Button.stories'; + +test('onclick handler is called', () => { + // Returns a story which already contains all annotations from story, meta and global levels + const PrimaryStory = composeStory(Primary, meta); + + const onClickSpy = vi.fn(); + render(); + const buttonElement = screen.getByRole('button'); + buttonElement.click(); + expect(onClickSpy).toHaveBeenCalled(); +}); +``` diff --git a/docs/snippets/react/portable-stories-vitest-override-globals.ts.mdx b/docs/snippets/react/portable-stories-vitest-override-globals.ts.mdx new file mode 100644 index 00000000000..b5f3c27f41a --- /dev/null +++ b/docs/snippets/react/portable-stories-vitest-override-globals.ts.mdx @@ -0,0 +1,24 @@ +```tsx +// Button.test.tsx +import { test } from 'vitest'; +import { render } from '@testing-library/react'; +import { composeStory } from '@storybook/react'; + +import meta, { Primary } from './Button.stories'; + +test('renders in English', async () => { + const PrimaryStory = composeStory( + Primary, + meta, + { globals: { locale: 'en' } }, // 👈 Project annotations to override the locale + ); + + render(); +}); + +test('renders in Spanish', async () => { + const PrimaryStory = composeStory(Primary, meta, { globals: { locale: 'es' } }); + + render(); +}); +``` diff --git a/docs/snippets/react/portable-stories-vitest-with-loaders.ts.mdx b/docs/snippets/react/portable-stories-vitest-with-loaders.ts.mdx new file mode 100644 index 00000000000..97d8f25cf19 --- /dev/null +++ b/docs/snippets/react/portable-stories-vitest-with-loaders.ts.mdx @@ -0,0 +1,18 @@ +```tsx +// Button.test.tsx +import { test } from 'vitest'; +import { render } from '@testing-library/react'; +import { composeStory } from '@storybook/react'; + +import meta, { Primary } from './Button.stories'; + +test('applies the loaders and renders', async () => { + const PrimaryStory = composeStory(Primary, meta); + + // First, load the data for the story + await PrimaryStory.load(); + + // Then, render the story + render(); +}); +``` diff --git a/docs/snippets/react/portable-stories-vitest-with-play-function.ts.mdx b/docs/snippets/react/portable-stories-vitest-with-play-function.ts.mdx new file mode 100644 index 00000000000..708d5c03805 --- /dev/null +++ b/docs/snippets/react/portable-stories-vitest-with-play-function.ts.mdx @@ -0,0 +1,18 @@ +```tsx +// Button.test.tsx +import { test } from 'vitest'; +import { render } from '@testing-library/react'; +import { composeStory } from '@storybook/react'; + +import meta, { Primary } from './Button.stories'; + +test('renders and executes the play function', async () => { + const PrimaryStory = composeStory(Primary, meta); + + // First, render the story + render(); + + // Then, execute the play function + await PrimaryStory.play(); +}); +``` diff --git a/docs/snippets/vue/portable-stories-jest-compose-stories.ts.mdx b/docs/snippets/vue/portable-stories-jest-compose-stories.ts.mdx new file mode 100644 index 00000000000..62e40bc5cc3 --- /dev/null +++ b/docs/snippets/vue/portable-stories-jest-compose-stories.ts.mdx @@ -0,0 +1,26 @@ +```ts +// Button.test.ts +import { test, expect } from '@jest/globals'; +import { render, screen } from '@testing-library/vue'; +import { composeStories } from '@storybook/vue3'; + +// Import all stories and the component annotations from the stories file +import * as stories from './Button.stories'; + +// Every component that is returned maps 1:1 with the stories, +// but they already contain all annotations from story, meta, and project levels +const { Primary, Secondary } = composeStories(stories); + +test('renders primary button with default args', () => { + render(Primary); + const buttonElement = screen.getByText('Text coming from args in stories file!'); + expect(buttonElement).not.toBeNull(); +}); + +test('renders primary button with overriden props', () => { + // You can override props and they will get merged with values from the story's args + render(Primary, { props: { label: 'Hello world' } }); + const buttonElement = screen.getByText(/Hello world/i); + expect(buttonElement).not.toBeNull(); +}); +``` diff --git a/docs/snippets/vue/portable-stories-jest-compose-story.ts.mdx b/docs/snippets/vue/portable-stories-jest-compose-story.ts.mdx new file mode 100644 index 00000000000..8141b5d1cb4 --- /dev/null +++ b/docs/snippets/vue/portable-stories-jest-compose-story.ts.mdx @@ -0,0 +1,19 @@ +```ts +// Button.test.ts +import { jest, test, expect } from '@jest/globals'; +import { render, screen } from '@testing-library/vue'; +import { composeStory } from '@storybook/vue3'; + +import meta, { Primary } from './Button.stories'; + +test('onclick handler is called', () => { + // Returns a story which already contains all annotations from story, meta and global levels + const PrimaryStory = composeStory(Primary, meta); + + const onClickSpy = jest.fn(); + render(Primary, { props: { onClick: onClickSpy } }); + const buttonElement = screen.getByRole('button'); + buttonElement.click(); + expect(onClickSpy).toHaveBeenCalled(); +}); +``` diff --git a/docs/snippets/vue/portable-stories-jest-override-globals.ts.mdx b/docs/snippets/vue/portable-stories-jest-override-globals.ts.mdx new file mode 100644 index 00000000000..6a18d79dc75 --- /dev/null +++ b/docs/snippets/vue/portable-stories-jest-override-globals.ts.mdx @@ -0,0 +1,24 @@ +```ts +// Button.test.ts +import { test } from '@jest/globals'; +import { render } from '@testing-library/vue'; +import { composeStory } from '@storybook/vue3'; + +import meta, { Primary } from './Button.stories'; + +test('renders in English', async () => { + const PrimaryStory = composeStory( + Primary, + meta, + { globals: { locale: 'en' } }, // 👈 Project annotations to override the locale + ); + + render(PrimaryStory); +}); + +test('renders in Spanish', async () => { + const PrimaryStory = composeStory(Primary, meta, { globals: { locale: 'es' } }); + + render(PrimaryStory); +}); +``` diff --git a/docs/snippets/vue/portable-stories-jest-with-loaders.ts.mdx b/docs/snippets/vue/portable-stories-jest-with-loaders.ts.mdx new file mode 100644 index 00000000000..99dff905971 --- /dev/null +++ b/docs/snippets/vue/portable-stories-jest-with-loaders.ts.mdx @@ -0,0 +1,18 @@ +```ts +// Button.test.ts +import { test } from '@jest/globals'; +import { render } from '@testing-library/vue'; +import { composeStory } from '@storybook/vue3'; + +import meta, { Primary } from './Button.stories'; + +test('applies the loaders and renders', async () => { + const PrimaryStory = composeStory(Primary, meta); + + // First, load the data for the story + await PrimaryStory.load(); + + // Then, render the story + render(PrimaryStory); +}); +``` diff --git a/docs/snippets/vue/portable-stories-jest-with-play-function.ts.mdx b/docs/snippets/vue/portable-stories-jest-with-play-function.ts.mdx new file mode 100644 index 00000000000..a901326a9cf --- /dev/null +++ b/docs/snippets/vue/portable-stories-jest-with-play-function.ts.mdx @@ -0,0 +1,18 @@ +```ts +// Button.test.ts +import { test } from '@jest/globals'; +import { render } from '@testing-library/vue'; +import { composeStory } from '@storybook/vue3'; + +import meta, { Primary } from './Button.stories'; + +test('renders and executes the play function', async () => { + const PrimaryStory = composeStory(Primary, meta); + + // First, render the story + render(PrimaryStory); + + // Then, execute the play function + await PrimaryStory.play(); +}); +``` diff --git a/docs/snippets/vue/portable-stories-playwright-ct.ts.mdx b/docs/snippets/vue/portable-stories-playwright-ct.ts.mdx new file mode 100644 index 00000000000..8421d82cdcd --- /dev/null +++ b/docs/snippets/vue/portable-stories-playwright-ct.ts.mdx @@ -0,0 +1,15 @@ +```ts +// Button.playwright.test.ts +import { createTest } from '@storybook/vue3/experimental-playwright'; +import { test as base } from '@playwright/experimental-ct-vue'; + +import stories from './Button.portable'; + +const test = createTest(base); + +test('renders primary button', async ({ mount }) => { + // The mount function will execute all the necessary steps in the story, + // such as loaders, render, and play function + await mount(stories.Primary); +}); +``` diff --git a/docs/snippets/vue/portable-stories-vitest-compose-stories.ts.mdx b/docs/snippets/vue/portable-stories-vitest-compose-stories.ts.mdx new file mode 100644 index 00000000000..7a518d2198a --- /dev/null +++ b/docs/snippets/vue/portable-stories-vitest-compose-stories.ts.mdx @@ -0,0 +1,26 @@ +```ts +// Button.test.ts +import { test, expect } from 'vitest'; +import { render, screen } from '@testing-library/vue'; +import { composeStories } from '@storybook/vue3'; + +// Import all stories and the component annotations from the stories file +import * as stories from './Button.stories'; + +// Every component that is returned maps 1:1 with the stories, +// but they already contain all annotations from story, meta, and project levels +const { Primary, Secondary } = composeStories(stories); + +test('renders primary button with default args', () => { + render(Primary); + const buttonElement = screen.getByText('Text coming from args in stories file!'); + expect(buttonElement).not.toBeNull(); +}); + +test('renders primary button with overriden props', () => { + // You can override props and they will get merged with values from the story's args + render(Primary, { props: { label: 'Hello world' } }); + const buttonElement = screen.getByText(/Hello world/i); + expect(buttonElement).not.toBeNull(); +}); +``` diff --git a/docs/snippets/vue/portable-stories-vitest-compose-story.ts.mdx b/docs/snippets/vue/portable-stories-vitest-compose-story.ts.mdx new file mode 100644 index 00000000000..9b8bf8ac6d3 --- /dev/null +++ b/docs/snippets/vue/portable-stories-vitest-compose-story.ts.mdx @@ -0,0 +1,19 @@ +```ts +// Button.test.ts +import { vi, test, expect } from 'vitest'; +import { render, screen } from '@testing-library/vue'; +import { composeStory } from '@storybook/vue3'; + +import meta, { Primary } from './Button.stories'; + +test('onclick handler is called', () => { + // Returns a story which already contains all annotations from story, meta and global levels + const PrimaryStory = composeStory(Primary, meta); + + const onClickSpy = vi.fn(); + render(Primary, { props: { onClick: onClickSpy } }); + const buttonElement = screen.getByRole('button'); + buttonElement.click(); + expect(onClickSpy).toHaveBeenCalled(); +}); +``` diff --git a/docs/snippets/vue/portable-stories-vitest-override-globals.ts.mdx b/docs/snippets/vue/portable-stories-vitest-override-globals.ts.mdx new file mode 100644 index 00000000000..ba69f6cd581 --- /dev/null +++ b/docs/snippets/vue/portable-stories-vitest-override-globals.ts.mdx @@ -0,0 +1,24 @@ +```ts +// Button.test.ts +import { test } from 'vitest'; +import { render } from '@testing-library/vue'; +import { composeStory } from '@storybook/vue3'; + +import meta, { Primary } from './Button.stories'; + +test('renders in English', async () => { + const PrimaryStory = composeStory( + Primary, + meta, + { globals: { locale: 'en' } }, // 👈 Project annotations to override the locale + ); + + render(PrimaryStory); +}); + +test('renders in Spanish', async () => { + const PrimaryStory = composeStory(Primary, meta, { globals: { locale: 'es' } }); + + render(PrimaryStory); +}); +``` diff --git a/docs/snippets/vue/portable-stories-vitest-with-loaders.ts.mdx b/docs/snippets/vue/portable-stories-vitest-with-loaders.ts.mdx new file mode 100644 index 00000000000..279d84bac65 --- /dev/null +++ b/docs/snippets/vue/portable-stories-vitest-with-loaders.ts.mdx @@ -0,0 +1,18 @@ +```ts +// Button.test.ts +import { test } from 'vitest'; +import { render } from '@testing-library/vue'; +import { composeStory } from '@storybook/vue3'; + +import meta, { Primary } from './Button.stories'; + +test('applies the loaders and renders', async () => { + const PrimaryStory = composeStory(Primary, meta); + + // First, load the data for the story + await PrimaryStory.load(); + + // Then, render the story + render(PrimaryStory); +}); +``` diff --git a/docs/snippets/vue/portable-stories-vitest-with-play-function.ts.mdx b/docs/snippets/vue/portable-stories-vitest-with-play-function.ts.mdx new file mode 100644 index 00000000000..1fe2a7a42ae --- /dev/null +++ b/docs/snippets/vue/portable-stories-vitest-with-play-function.ts.mdx @@ -0,0 +1,18 @@ +```ts +// Button.test.ts +import { test } from 'vitest'; +import { render } from '@testing-library/vue'; +import { composeStory } from '@storybook/vue3'; + +import meta, { Primary } from './Button.stories'; + +test('renders and executes the play function', async () => { + const PrimaryStory = composeStory(Primary, meta); + + // First, render the story + render(PrimaryStory); + + // Then, execute the play function + await PrimaryStory.play(); +}); +``` diff --git a/docs/toc.js b/docs/toc.js index b7ee152fb8c..270043ad59d 100644 --- a/docs/toc.js +++ b/docs/toc.js @@ -691,6 +691,29 @@ module.exports = { }, ], }, + { + title: 'Portable stories', + pathSegment: '', + type: 'menu', + children: [ + { + pathSegment: 'portable-stories-jest', + title: 'Jest', + type: 'link', + }, + { + pathSegment: 'portable-stories-playwright', + title: 'Playwright', + type: 'link', + }, + { + pathSegment: 'portable-stories-vitest', + title: 'Vitest', + type: 'link', + }, + ], + }, + { pathSegment: 'new-frameworks', title: 'Frameworks',