- [From version 7.x to 8.0.0](#from-version-7x-to-800)
- - [Type change in `composeStories` API](#type-change-in-composestories-api)
+ - [Portable stories](#portable-stories)
+ - [Project annotations are now merged instead of overwritten in composeStory](#project-annotations-are-now-merged-instead-of-overwritten-in-composestory)
+ - [Type change in `composeStories` API](#type-change-in-composestories-api)
+ - [DOM structure changed in portable stories](#dom-structure-changed-in-portable-stories)
- [Tab addons are now routed to a query parameter](#tab-addons-are-now-routed-to-a-query-parameter)
- [Default keyboard shortcuts changed](#default-keyboard-shortcuts-changed)
- [Manager addons are now rendered with React 18](#manager-addons-are-now-rendered-with-react-18)
@@ -86,17 +89,17 @@
- [Tab addons cannot manually route, Tool addons can filter their visibility via tabId](#tab-addons-cannot-manually-route-tool-addons-can-filter-their-visibility-via-tabid)
- [Removed `config` preset](#removed-config-preset-1)
- [From version 7.5.0 to 7.6.0](#from-version-750-to-760)
- - [CommonJS with Vite is deprecated](#commonjs-with-vite-is-deprecated)
- - [Using implicit actions during rendering is deprecated](#using-implicit-actions-during-rendering-is-deprecated)
- - [typescript.skipBabel deprecated](#typescriptskipbabel-deprecated)
- - [Primary doc block accepts of prop](#primary-doc-block-accepts-of-prop)
- - [Addons no longer need a peer dependency on React](#addons-no-longer-need-a-peer-dependency-on-react)
+ - [CommonJS with Vite is deprecated](#commonjs-with-vite-is-deprecated)
+ - [Using implicit actions during rendering is deprecated](#using-implicit-actions-during-rendering-is-deprecated)
+ - [typescript.skipBabel deprecated](#typescriptskipbabel-deprecated)
+ - [Primary doc block accepts of prop](#primary-doc-block-accepts-of-prop)
+ - [Addons no longer need a peer dependency on React](#addons-no-longer-need-a-peer-dependency-on-react)
- [From version 7.4.0 to 7.5.0](#from-version-740-to-750)
- - [`storyStoreV6` and `storiesOf` is deprecated](#storystorev6-and-storiesof-is-deprecated)
- - [`storyIndexers` is replaced with `experimental_indexers`](#storyindexers-is-replaced-with-experimental_indexers)
+ - [`storyStoreV6` and `storiesOf` is deprecated](#storystorev6-and-storiesof-is-deprecated)
+ - [`storyIndexers` is replaced with `experimental_indexers`](#storyindexers-is-replaced-with-experimental_indexers)
- [From version 7.0.0 to 7.2.0](#from-version-700-to-720)
- - [Addon API is more type-strict](#addon-api-is-more-type-strict)
- - [Addon-controls hideNoControlsWarning parameter is deprecated](#addon-controls-hidenocontrolswarning-parameter-is-deprecated)
+ - [Addon API is more type-strict](#addon-api-is-more-type-strict)
+ - [Addon-controls hideNoControlsWarning parameter is deprecated](#addon-controls-hidenocontrolswarning-parameter-is-deprecated)
- [From version 6.5.x to 7.0.0](#from-version-65x-to-700)
- [7.0 breaking changes](#70-breaking-changes)
- [Dropped support for Node 15 and below](#dropped-support-for-node-15-and-below)
@@ -122,7 +125,7 @@
- [Deploying build artifacts](#deploying-build-artifacts)
- [Dropped support for file URLs](#dropped-support-for-file-urls)
- [Serving with nginx](#serving-with-nginx)
- - [Ignore story files from node\_modules](#ignore-story-files-from-node_modules)
+ - [Ignore story files from node_modules](#ignore-story-files-from-node_modules)
- [7.0 Core changes](#70-core-changes)
- [7.0 feature flags removed](#70-feature-flags-removed)
- [Story context is prepared before for supporting fine grained updates](#story-context-is-prepared-before-for-supporting-fine-grained-updates)
@@ -136,7 +139,7 @@
- [Addon-interactions: Interactions debugger is now default](#addon-interactions-interactions-debugger-is-now-default)
- [7.0 Vite changes](#70-vite-changes)
- [Vite builder uses Vite config automatically](#vite-builder-uses-vite-config-automatically)
- - [Vite cache moved to node\_modules/.cache/.vite-storybook](#vite-cache-moved-to-node_modulescachevite-storybook)
+ - [Vite cache moved to node_modules/.cache/.vite-storybook](#vite-cache-moved-to-node_modulescachevite-storybook)
- [7.0 Webpack changes](#70-webpack-changes)
- [Webpack4 support discontinued](#webpack4-support-discontinued)
- [Babel mode v7 exclusively](#babel-mode-v7-exclusively)
@@ -186,7 +189,7 @@
- [Dropped addon-docs manual babel configuration](#dropped-addon-docs-manual-babel-configuration)
- [Dropped addon-docs manual configuration](#dropped-addon-docs-manual-configuration)
- [Autoplay in docs](#autoplay-in-docs)
- - [Removed STORYBOOK\_REACT\_CLASSES global](#removed-storybook_react_classes-global)
+ - [Removed STORYBOOK_REACT_CLASSES global](#removed-storybook_react_classes-global)
- [7.0 Deprecations and default changes](#70-deprecations-and-default-changes)
- [storyStoreV7 enabled by default](#storystorev7-enabled-by-default)
- [`Story` type deprecated](#story-type-deprecated)
@@ -401,7 +404,24 @@
## From version 7.x to 8.0.0
-### Type change in `composeStories` API
+### Portable stories
+
+#### Project annotations are now merged instead of overwritten in composeStory
+
+When passing project annotations overrides via `composeStory` such as:
+
+```tsx
+const projectAnnotationOverrides = { parameters: { foo: "bar" } };
+const Primary = composeStory(
+ stories.Primary,
+ stories,
+ projectAnnotationOverrides
+);
+```
+
+they are now merged with the annotations passed via `setProjectAnnotations` rather than completely overwriting them. This was seen as a bug and it's now fixed. If you have a use case where you really need this, please open an issue to elaborate.
+
+#### Type change in `composeStories` API
There is a TypeScript type change in the `play` function returned from `composeStories` or `composeStory` in `@storybook/react` or `@storybook/vue3`, where before it was always defined, now it is potentially undefined. This means that you might have to make a small change in your code, such as:
@@ -418,6 +438,35 @@ await Primary.play!(...) // if you want a runtime error when the play function d
There are plans to make the type of the play function be inferred based on your imported story's play function in a near future, so the types will be 100% accurate.
+#### DOM structure changed in portable stories
+
+The portable stories API now adds a wrapper to your stories with a unique id based on your story id, such as:
+
+```html
+
+
+
+```
+
+This means that if you take DOM snapshots of your stories, they will be affected and you will have to update them.
+
+The id calculation is based on different heuristics based on your Meta title and Story name. When using `composeStories`, the id can be inferred automatically. However, when using `composeStory` and your story does not explicitly have a `storyName` property, the story name can't be inferred automatically. As a result, its name will be "Unnamed Story", resulting in a wrapper id like `"#storybook-story-button--unnamed-story"`. If the id matters to you and you want to fix it, you have to specify the `exportsName` property like so:
+
+```ts
+test("snapshots the story with custom id", () => {
+ const Primary = composeStory(
+ stories.Primary,
+ stories.default,
+ undefined,
+ // If you do not want the `unnamed-story` id, you have to pass the name of the story as a parameter
+ "Primary"
+ );
+
+ const { baseElement } = render();
+ expect(baseElement).toMatchSnapshot();
+});
+```
+
### Tab addons are now routed to a query parameter
The URL of a tab used to be: `http://localhost:6006/?path=/my-addon-tab/my-story`.
@@ -556,7 +605,6 @@ This means https://github.com/IanVS/vite-plugin-turbosnap is no longer necessary
Now that both Vite and Webpack support the `preview-stats.json` file, the flag has been renamed. The old flag will continue to work.
-
### Implicit actions can not be used during rendering (for example in the play function)
In Storybook 7, we inferred if the component accepts any action props,
diff --git a/code/lib/preview-api/src/index.ts b/code/lib/preview-api/src/index.ts
index e47cdaa0a0dd..63d45114dc23 100644
--- a/code/lib/preview-api/src/index.ts
+++ b/code/lib/preview-api/src/index.ts
@@ -56,6 +56,7 @@ export {
filterArgTypes,
sanitizeStoryContextUpdate,
setProjectAnnotations,
+ getPortableStoryWrapperId,
inferControls,
userOrAutoTitleFromSpecifier,
userOrAutoTitle,
diff --git a/code/lib/preview-api/src/modules/store/csf/portable-stories.test.ts b/code/lib/preview-api/src/modules/store/csf/portable-stories.test.ts
index af775f27360b..dbca4640b05b 100644
--- a/code/lib/preview-api/src/modules/store/csf/portable-stories.test.ts
+++ b/code/lib/preview-api/src/modules/store/csf/portable-stories.test.ts
@@ -1,10 +1,18 @@
// @vitest-environment node
import { describe, expect, vi, it } from 'vitest';
-import { composeStory, composeStories } from './portable-stories';
+import type {
+ ComponentAnnotations as Meta,
+ StoryAnnotationsOrFn as Story,
+ Store_CSFExports,
+} from '@storybook/types';
+
+import { composeStory, composeStories, setProjectAnnotations } from './portable-stories';
+
+type StoriesModule = Store_CSFExports & Record;
// Most integration tests for this functionality are located under renderers/react
describe('composeStory', () => {
- const meta = {
+ const meta: Meta = {
title: 'Button',
parameters: {
firstAddon: true,
@@ -15,13 +23,26 @@ describe('composeStory', () => {
},
};
- it('should return story with composed args and parameters', () => {
- const Story = () => {};
- Story.args = { primary: true };
- Story.parameters = {
+ it('should return story with composed annotations from story, meta and project', () => {
+ const decoratorFromProjectAnnotations = vi.fn((StoryFn) => StoryFn());
+ const decoratorFromStoryAnnotations = vi.fn((StoryFn) => StoryFn());
+ setProjectAnnotations([
+ {
+ parameters: { injected: true },
+ globalTypes: {
+ locale: { defaultValue: 'en' },
+ },
+ decorators: [decoratorFromProjectAnnotations],
+ },
+ ]);
+
+ const Story: Story = {
+ render: () => {},
+ args: { primary: true },
parameters: {
secondAddon: true,
},
+ decorators: [decoratorFromStoryAnnotations],
};
const composedStory = composeStory(Story, meta);
@@ -29,11 +50,16 @@ describe('composeStory', () => {
expect(composedStory.parameters).toEqual(
expect.objectContaining({ ...Story.parameters, ...meta.parameters })
);
+
+ composedStory();
+
+ expect(decoratorFromProjectAnnotations).toHaveBeenCalledOnce();
+ expect(decoratorFromStoryAnnotations).toHaveBeenCalledOnce();
});
it('should compose with a play function', async () => {
const spy = vi.fn();
- const Story = () => {};
+ const Story: Story = () => {};
Story.args = {
primary: true,
};
@@ -53,6 +79,80 @@ describe('composeStory', () => {
);
});
+ it('should merge parameters with correct precedence in all combinations', async () => {
+ const storyAnnotations = { render: () => {} };
+ const metaAnnotations: Meta = { parameters: { label: 'meta' } };
+ const projectAnnotations: Meta = { parameters: { label: 'projectOverrides' } };
+
+ const storyPrecedence = composeStory(
+ { ...storyAnnotations, parameters: { label: 'story' } },
+ metaAnnotations,
+ projectAnnotations
+ );
+ expect(storyPrecedence.parameters.label).toEqual('story');
+
+ const metaPrecedence = composeStory(storyAnnotations, metaAnnotations, projectAnnotations);
+ expect(metaPrecedence.parameters.label).toEqual('meta');
+
+ const projectPrecedence = composeStory(storyAnnotations, {}, projectAnnotations);
+ expect(projectPrecedence.parameters.label).toEqual('projectOverrides');
+
+ setProjectAnnotations({ parameters: { label: 'setProjectAnnotationsOverrides' } });
+ const setProjectAnnotationsPrecedence = composeStory(storyAnnotations, {}, {});
+ expect(setProjectAnnotationsPrecedence.parameters.label).toEqual(
+ 'setProjectAnnotationsOverrides'
+ );
+ });
+
+ it('should call and compose loaders data', async () => {
+ const loadSpy = vi.fn();
+ const args = { story: 'story' };
+ const LoaderStory: Story = {
+ args,
+ loaders: [
+ async (context) => {
+ loadSpy();
+ expect(context.args).toEqual(args);
+ return {
+ foo: 'bar',
+ };
+ },
+ ],
+ render: (_args, { loaded }) => {
+ expect(loaded).toEqual({ foo: 'bar' });
+ },
+ };
+
+ const composedStory = composeStory(LoaderStory, {});
+ await composedStory.load();
+ composedStory();
+ expect(loadSpy).toHaveBeenCalled();
+ });
+
+ it('should work with spies set up in loaders', async () => {
+ const spyFn = vi.fn();
+
+ const Story: Story = {
+ args: {
+ spyFn,
+ },
+ loaders: [
+ async () => {
+ spyFn.mockReturnValue('mockedData');
+ },
+ ],
+ render: (args) => {
+ const data = args.spyFn();
+ expect(data).toBe('mockedData');
+ },
+ };
+
+ const composedStory = composeStory(Story, {});
+ await composedStory.load();
+ composedStory();
+ expect(spyFn).toHaveBeenCalled();
+ });
+
it('should throw an error if Story is undefined', () => {
expect(() => {
// @ts-expect-error (invalid input)
@@ -62,7 +162,7 @@ describe('composeStory', () => {
describe('Id of the story', () => {
it('is exposed correctly when composeStories is used', () => {
- const module = {
+ const module: StoriesModule = {
default: {
title: 'Example/Button',
},
@@ -72,7 +172,7 @@ describe('composeStory', () => {
expect(Primary.id).toBe('example-button--csf-3-primary');
});
it('is exposed correctly when composeStory is used and exportsName is passed', () => {
- const module = {
+ const module: StoriesModule = {
default: {
title: 'Example/Button',
},
@@ -83,7 +183,7 @@ describe('composeStory', () => {
});
it("is not unique when composeStory is used and exportsName isn't passed", () => {
const Primary = composeStory({ render: () => {} }, {});
- expect(Primary.id).toContain('unknown');
+ expect(Primary.id).toContain('composedstory--unnamed-story');
});
});
});
@@ -93,7 +193,7 @@ describe('composeStories', () => {
const defaultAnnotations = { render: () => '' };
it('should call composeStoryFn with stories', () => {
const composeStorySpy = vi.fn((v) => v);
- const module = {
+ const module: StoriesModule = {
default: {
title: 'Button',
},
@@ -118,7 +218,7 @@ describe('composeStories', () => {
it('should not call composeStoryFn for non-story exports', () => {
const composeStorySpy = vi.fn((v) => v);
- const module = {
+ const module: StoriesModule = {
default: {
title: 'Button',
excludeStories: /Data/,
@@ -131,7 +231,7 @@ describe('composeStories', () => {
describe('non-story exports', () => {
it('should filter non-story exports with excludeStories', () => {
- const StoryModuleWithNonStoryExports = {
+ const StoryModuleWithNonStoryExports: StoriesModule = {
default: {
title: 'Some/Component',
excludeStories: /.*Data/,
@@ -149,7 +249,7 @@ describe('composeStories', () => {
});
it('should filter non-story exports with includeStories', () => {
- const StoryModuleWithNonStoryExports = {
+ const StoryModuleWithNonStoryExports: StoriesModule = {
default: {
title: 'Some/Component',
includeStories: /.*Story/,
diff --git a/code/lib/preview-api/src/modules/store/csf/portable-stories.ts b/code/lib/preview-api/src/modules/store/csf/portable-stories.ts
index 91e4cdc365e1..0974f0908526 100644
--- a/code/lib/preview-api/src/modules/store/csf/portable-stories.ts
+++ b/code/lib/preview-api/src/modules/store/csf/portable-stories.ts
@@ -1,3 +1,4 @@
+/* eslint-disable @typescript-eslint/naming-convention */
import { isExportStory } from '@storybook/csf';
import type {
Renderer,
@@ -12,7 +13,7 @@ import type {
Parameters,
ComposedStoryFn,
StrictArgTypes,
- ComposedStoryPlayContext,
+ PlayFunctionContext,
} from '@storybook/types';
import { HooksContext } from '../../../addons';
@@ -23,23 +24,28 @@ import { normalizeComponentAnnotations } from './normalizeComponentAnnotations';
import { getValuesFromArgTypes } from './getValuesFromArgTypes';
import { normalizeProjectAnnotations } from './normalizeProjectAnnotations';
-let GLOBAL_STORYBOOK_PROJECT_ANNOTATIONS = composeConfigs([]);
+let globalProjectAnnotations: ProjectAnnotations = {};
+
+export function getPortableStoryWrapperId(storyId: string) {
+ return `storybook-story-${storyId}`;
+}
export function setProjectAnnotations(
projectAnnotations: ProjectAnnotations | ProjectAnnotations[]
) {
const annotations = Array.isArray(projectAnnotations) ? projectAnnotations : [projectAnnotations];
- GLOBAL_STORYBOOK_PROJECT_ANNOTATIONS = composeConfigs(annotations);
+ globalProjectAnnotations = composeConfigs(annotations);
}
export function composeStory(
storyAnnotations: LegacyStoryAnnotationsOrFn,
componentAnnotations: ComponentAnnotations,
- projectAnnotations: ProjectAnnotations = GLOBAL_STORYBOOK_PROJECT_ANNOTATIONS as ProjectAnnotations,
- defaultConfig: ProjectAnnotations = {},
+ projectAnnotations?: ProjectAnnotations,
+ defaultConfig?: ProjectAnnotations,
exportsName?: string
): ComposedStoryFn> {
if (storyAnnotations === undefined) {
+ // eslint-disable-next-line local-rules/no-uncategorized-errors
throw new Error('Expected a story but received undefined.');
}
@@ -54,7 +60,7 @@ export function composeStory(
storyName,
@@ -62,10 +68,9 @@ export function composeStory({
- ...projectAnnotations,
- ...defaultConfig,
- });
+ const normalizedProjectAnnotations = normalizeProjectAnnotations(
+ composeConfigs([defaultConfig ?? {}, globalProjectAnnotations, projectAnnotations ?? {}])
+ );
const story = prepareStory(
normalizedStory,
@@ -73,11 +78,14 @@ export function composeStory = {
hooks: new HooksContext(),
- globals: defaultGlobals,
+ globals: {
+ ...globalsFromGlobalTypes,
+ ...normalizedProjectAnnotations.globals,
+ },
args: { ...story.initialArgs },
viewMode: 'story',
loaded: {},
@@ -86,28 +94,39 @@ export function composeStory>) =>
+ story.playFunction!({
+ ...context,
+ ...extraContext,
+ // if canvasElement is not provided, we default to the root element, which comes from a decorator
+ // the decorator has to be implemented in the defaultAnnotations of each integrator of portable stories
+ canvasElement:
+ extraContext?.canvasElement ??
+ globalThis.document?.getElementById(getPortableStoryWrapperId(context.id)),
+ })
+ : undefined;
+
const composedStory: ComposedStoryFn> = Object.assign(
- (extraArgs?: Partial) => {
- const finalContext: StoryContext = {
- ...context,
- args: { ...context.initialArgs, ...extraArgs },
+ function storyFn(extraArgs?: Partial) {
+ context.args = {
+ ...context.initialArgs,
+ ...extraArgs,
};
- return story.unboundStoryFn(prepareContext(finalContext));
+ return story.unboundStoryFn(prepareContext(context));
},
{
+ id: story.id,
storyName,
+ load: async () => {
+ const loadedContext = await story.applyLoaders(context);
+ context.loaded = loadedContext.loaded;
+ },
args: story.initialArgs as Partial,
parameters: story.parameters as Parameters,
argTypes: story.argTypes as StrictArgTypes,
- id: story.id,
- play: story.playFunction
- ? ((async (extraContext: ComposedStoryPlayContext) =>
- story.playFunction!({
- ...context,
- ...extraContext,
- })) as unknown as ComposedStoryPlayFn>)
- : undefined,
+ play: playFunction as ComposedStoryPlayFn | undefined,
}
);
@@ -119,7 +138,6 @@ export function composeStories(
globalConfig: ProjectAnnotations,
composeStoryFn: ComposeStoryFn
) {
- // eslint-disable-next-line @typescript-eslint/naming-convention
const { default: meta, __esModule, __namedExportsOrder, ...stories } = storiesImport;
const composedStories = Object.entries(stories).reduce((storiesMap, [exportsName, story]) => {
if (!isExportStory(exportsName, meta)) {
diff --git a/code/lib/types/src/modules/composedStory.ts b/code/lib/types/src/modules/composedStory.ts
index 5ce61bc678e8..b0a7bff6c374 100644
--- a/code/lib/types/src/modules/composedStory.ts
+++ b/code/lib/types/src/modules/composedStory.ts
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/naming-convention */
-import type { Renderer, StoryId, StrictArgTypes } from '@storybook/csf';
+import type { PlayFunction, Renderer, StoryId, StrictArgTypes } from '@storybook/csf';
import type {
AnnotatedStoryFn,
@@ -9,7 +9,6 @@ import type {
Parameters,
StoryAnnotations,
StoryAnnotationsOrFn,
- StoryContext,
} from './csf';
import type { ProjectAnnotations } from './story';
@@ -22,23 +21,6 @@ export type Store_CSFExports) // or PrimaryButton()
- * PrimaryButton.play({ canvasElement: container })
- */
-export type ComposedStoryPlayContext = Partial<
- StoryContext & Pick, 'canvasElement'>
->;
-
-export type ComposedStoryPlayFn = (
- context: ComposedStoryPlayContext
-) => Promise | void;
-
/**
* A story function with partial args, used internally by composeStory
*/
@@ -48,6 +30,14 @@ export type PartialArgsStoryFn = T extends (...args: infer P) => infer R
+ ? (...args: { [K in keyof P]?: Partial
}) => R
+ : never;
+
+export type ComposedStoryPlayFn<
+ TRenderer extends Renderer = Renderer,
+ TArgs = Args,
+> = MakeAllParametersOptional>>;
/**
* A story that got recomposed for portable stories, containing all the necessary data to be rendered in external environments
*/
@@ -55,9 +45,10 @@ export type ComposedStoryFn<
TRenderer extends Renderer = Renderer,
TArgs = Args,
> = PartialArgsStoryFn & {
- play: ComposedStoryPlayFn | undefined;
args: TArgs;
id: StoryId;
+ play?: ComposedStoryPlayFn;
+ load: () => Promise;
storyName: string;
parameters: Parameters;
argTypes: StrictArgTypes;
diff --git a/code/renderers/react/src/__test__/Button.stories.tsx b/code/renderers/react/src/__test__/Button.stories.tsx
index 6882b957b136..277f92ddde1f 100644
--- a/code/renderers/react/src/__test__/Button.stories.tsx
+++ b/code/renderers/react/src/__test__/Button.stories.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { within, userEvent } from '@storybook/testing-library';
+import { within, userEvent, fn, expect } from '@storybook/test';
import type { StoryFn as CSF2Story, StoryObj as CSF3Story, Meta } from '..';
import type { ButtonProps } from './Button';
@@ -33,14 +33,21 @@ const getCaptionForLocale = (locale: string) => {
return '안녕하세요!';
case 'pt':
return 'Olá!';
- default:
+ case 'en':
return 'Hello!';
+ default:
+ return undefined;
}
};
export const CSF2StoryWithLocale: CSF2Story = (args, { globals: { locale } }) => {
const caption = getCaptionForLocale(locale);
- return ;
+ return (
+ <>
+
`;
diff --git a/code/renderers/vue3/src/__tests__/composeStories/portable-stories.test.ts b/code/renderers/vue3/src/__tests__/composeStories/portable-stories.test.ts
index 4c541e1c4536..36aa7c2e9c56 100644
--- a/code/renderers/vue3/src/__tests__/composeStories/portable-stories.test.ts
+++ b/code/renderers/vue3/src/__tests__/composeStories/portable-stories.test.ts
@@ -10,39 +10,58 @@ import type Button from './Button.vue';
import { composeStories, composeStory, setProjectAnnotations } from '../../portable-stories';
// example with composeStories, returns an object with all stories composed with args/decorators
-const { CSF3Primary } = composeStories(stories);
+const { CSF3Primary, LoaderStory } = composeStories(stories);
// example with composeStory, returns a single story composed with args/decorators
const Secondary = composeStory(stories.CSF2Secondary, stories.default);
-it('renders primary button', () => {
- render(CSF3Primary({ label: 'Hello world' }));
- const buttonElement = screen.getByText(/Hello world/i);
- expect(buttonElement).toBeInTheDocument();
-});
+describe('renders', () => {
+ it('renders primary button', () => {
+ render(CSF3Primary({ label: 'Hello world' }));
+ const buttonElement = screen.getByText(/Hello world/i);
+ expect(buttonElement).toBeInTheDocument();
+ });
-it('reuses args from composed story', () => {
- render(Secondary());
- const buttonElement = screen.getByRole('button');
- expect(buttonElement.textContent).toEqual(Secondary.args.label);
-});
+ it('reuses args from composed story', () => {
+ render(Secondary());
+ const buttonElement = screen.getByRole('button');
+ expect(buttonElement.textContent).toEqual(Secondary.args.label);
+ });
-it('myClickEvent handler is called', async () => {
- const myClickEventSpy = vi.fn();
- render(Secondary({ onMyClickEvent: myClickEventSpy }));
- const buttonElement = screen.getByRole('button');
- buttonElement.click();
- expect(myClickEventSpy).toHaveBeenCalled();
-});
+ it('myClickEvent handler is called', async () => {
+ const myClickEventSpy = vi.fn();
+ render(Secondary({ onMyClickEvent: myClickEventSpy }));
+ const buttonElement = screen.getByRole('button');
+ buttonElement.click();
+ expect(myClickEventSpy).toHaveBeenCalled();
+ });
-it('reuses args from composeStories', () => {
- const { getByText } = render(CSF3Primary());
- const buttonElement = getByText(/foo/i);
- expect(buttonElement).toBeInTheDocument();
+ it('reuses args from composeStories', () => {
+ const { getByText } = render(CSF3Primary());
+ const buttonElement = getByText(/foo/i);
+ expect(buttonElement).toBeInTheDocument();
+ });
+
+ it('should call and compose loaders data', async () => {
+ await LoaderStory.load();
+ const { getByTestId } = render(LoaderStory());
+ expect(getByTestId('spy-data').textContent).toEqual('mockFn return value');
+ expect(getByTestId('loaded-data').textContent).toEqual('loaded data');
+ // spy assertions happen in the play function and should work
+ await LoaderStory.play!();
+ });
});
describe('projectAnnotations', () => {
it('renders with default projectAnnotations', () => {
+ setProjectAnnotations([
+ {
+ parameters: { injected: true },
+ globalTypes: {
+ locale: { defaultValue: 'en' },
+ },
+ },
+ ]);
const WithEnglishText = composeStory(stories.CSF2StoryWithLocale, stories.default);
const { getByText } = render(WithEnglishText());
const buttonElement = getByText('Hello!');
@@ -51,7 +70,7 @@ describe('projectAnnotations', () => {
it('renders with custom projectAnnotations via composeStory params', () => {
const WithPortugueseText = composeStory(stories.CSF2StoryWithLocale, stories.default, {
- globalTypes: { locale: { defaultValue: 'pt' } } as any,
+ globals: { locale: 'pt' },
});
const { getByText } = render(WithPortugueseText());
const buttonElement = getByText('Olá!');
@@ -127,12 +146,14 @@ describe('ComposeStories types', () => {
// Batch snapshot testing
const testCases = Object.values(composeStories(stories)).map((Story) => [Story.storyName, Story]);
it.each(testCases)('Renders %s story', async (_storyName, Story) => {
- if (typeof Story === 'string' || _storyName === 'CSF2StoryWithParamsAndDecorator') {
+ if (typeof Story === 'string' || _storyName === 'CSF2StoryWithLocale') {
return;
}
+ await Story.load();
+ const { container, baseElement } = await render(Story());
+ await Story.play?.({ canvasElement: container as HTMLElement });
await new Promise((resolve) => setTimeout(resolve, 0));
- const tree = await render(Story());
- expect(tree.baseElement).toMatchSnapshot();
+ expect(baseElement).toMatchSnapshot();
});
diff --git a/code/renderers/vue3/src/portable-stories.ts b/code/renderers/vue3/src/portable-stories.ts
index 4e009b25d672..aef26b39a5e7 100644
--- a/code/renderers/vue3/src/portable-stories.ts
+++ b/code/renderers/vue3/src/portable-stories.ts
@@ -2,6 +2,7 @@ import {
composeStory as originalComposeStory,
composeStories as originalComposeStories,
setProjectAnnotations as originalSetProjectAnnotations,
+ getPortableStoryWrapperId,
} from '@storybook/preview-api';
import type {
Args,
@@ -11,10 +12,24 @@ import type {
StoriesWithPartialProps,
} from '@storybook/types';
-import * as defaultProjectAnnotations from './render';
+import * as vueProjectAnnotations from './entry-preview';
import type { Meta } from './public-types';
import type { VueRenderer } from './types';
+const defaultProjectAnnotations: ProjectAnnotations = {
+ ...vueProjectAnnotations,
+ decorators: [
+ function addStorybookId(story, { id }) {
+ return {
+ components: { story },
+ template: `
+
+
`,
+ };
+ },
+ ],
+};
+
/** Function that sets the globalConfig of your Storybook. The global config is the preview module of your .storybook folder.
*
* It should be run a single time, so that your global config (e.g. decorators) is applied to your stories when using `composeStories` or `composeStory`.