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 2ad7f7500f5b..8e0452cc41fc 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,9 +1,16 @@
import { describe, expect, vi, it } from 'vitest';
+import type {
+ ComponentAnnotations as Meta,
+ StoryAnnotationsOrFn as Story,
+ Store_CSFExports,
+} from '@storybook/types';
import { composeStory, composeStories } 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,
@@ -14,8 +21,57 @@ describe('composeStory', () => {
},
};
+ 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();
+ expect(loadSpy).toHaveBeenCalled();
+ composedStory();
+ });
+
+ 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 return story with composed args and parameters', () => {
- const Story = () => {};
+ const Story: Story = () => {};
Story.args = { primary: true };
Story.parameters = {
parameters: {
@@ -32,7 +88,7 @@ describe('composeStory', () => {
it('should compose with a play function', async () => {
const spy = vi.fn();
- const Story = () => {};
+ const Story: Story = () => {};
Story.args = {
primary: true,
};
@@ -61,7 +117,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',
},
@@ -71,7 +127,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',
},
@@ -92,7 +148,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',
},
@@ -117,7 +173,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/,
@@ -130,7 +186,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/,
@@ -148,7 +204,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..de45ab6af59b 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
@@ -87,16 +87,20 @@ export function composeStory> = 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));
},
{
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,
diff --git a/code/lib/types/src/modules/composedStory.ts b/code/lib/types/src/modules/composedStory.ts
index 5ce61bc678e8..24bf673cabe6 100644
--- a/code/lib/types/src/modules/composedStory.ts
+++ b/code/lib/types/src/modules/composedStory.ts
@@ -55,6 +55,7 @@ export type ComposedStoryFn<
TRenderer extends Renderer = Renderer,
TArgs = Args,
> = PartialArgsStoryFn & {
+ load: () => Promise;
play: ComposedStoryPlayFn | undefined;
args: TArgs;
id: StoryId;
diff --git a/code/renderers/react/src/__test__/Button.stories.tsx b/code/renderers/react/src/__test__/Button.stories.tsx
index 6882b957b136..859a02ea9d1f 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';
@@ -84,7 +84,36 @@ export const CSF3InputFieldFilled: CSF3Story = {
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);
await step('Step label', async () => {
- await userEvent.type(canvas.getByTestId('input'), 'Hello world!');
+ const inputEl = canvas.getByTestId('input');
+ await userEvent.type(inputEl, 'Hello world!');
+ await expect(inputEl).toHaveValue('Hello world!');
});
},
};
+
+const spyFn = fn();
+export const LoaderStory: CSF3Story<{ spyFn: (val: string) => string }> = {
+ args: {
+ spyFn,
+ },
+ loaders: [
+ async () => {
+ spyFn.mockReturnValueOnce('baz');
+ return {
+ value: 'bar',
+ };
+ },
+ ],
+ render: (args, { loaded }) => {
+ const data = args.spyFn('foo');
+ return (
+
+
{loaded.value}
+
{String(data)}
+
+ );
+ },
+ play: async () => {
+ expect(spyFn).toHaveBeenCalledWith('foo');
+ },
+};
diff --git a/code/renderers/react/src/__test__/__snapshots__/portable-stories.test.tsx.snap b/code/renderers/react/src/__test__/__snapshots__/portable-stories.test.tsx.snap
index 2b92b1d68424..018406c3581d 100644
--- a/code/renderers/react/src/__test__/__snapshots__/portable-stories.test.tsx.snap
+++ b/code/renderers/react/src/__test__/__snapshots__/portable-stories.test.tsx.snap
@@ -94,3 +94,22 @@ exports[`Renders CSF3Primary story 1`] = `
+
+
+
+