diff --git a/code/lib/preview-api/src/modules/store/csf/testing-utils/index.ts b/code/lib/preview-api/src/modules/store/csf/testing-utils/index.ts index 40fcddcb6e35..10468737e6dc 100644 --- a/code/lib/preview-api/src/modules/store/csf/testing-utils/index.ts +++ b/code/lib/preview-api/src/modules/store/csf/testing-utils/index.ts @@ -10,7 +10,7 @@ import type { Store_CSFExports, StoryContext, Parameters, - PreparedStoryFn, + ComposedStoryFn, } from '@storybook/types'; import { HooksContext } from '../../../addons'; @@ -36,7 +36,7 @@ export function composeStory = GLOBAL_STORYBOOK_PROJECT_ANNOTATIONS as ProjectAnnotations, defaultConfig: ProjectAnnotations = {}, exportsName?: string -): PreparedStoryFn> { +): ComposedStoryFn> { if (storyAnnotations === undefined) { throw new Error('Expected a story but received undefined.'); } @@ -73,22 +73,25 @@ export function composeStory) => { - const context: Partial = { - ...story, - hooks: new HooksContext(), - globals: defaultGlobals, - args: { ...story.initialArgs, ...extraArgs }, - }; - - return story.unboundStoryFn(prepareContext(context as StoryContext)); - }; - - composedStory.storyName = storyName; - composedStory.args = story.initialArgs as Partial; - composedStory.play = story.playFunction as ComposedStoryPlayFn>; - composedStory.parameters = story.parameters as Parameters; - composedStory.id = story.id; + const composedStory: ComposedStoryFn> = Object.assign( + (extraArgs?: Partial) => { + const context: Partial = { + ...story, + hooks: new HooksContext(), + globals: defaultGlobals, + args: { ...story.initialArgs, ...extraArgs }, + }; + + return story.unboundStoryFn(prepareContext(context as StoryContext)); + }, + { + storyName, + args: story.initialArgs as Partial, + play: story.playFunction as ComposedStoryPlayFn>, + parameters: story.parameters as Parameters, + id: story.id, + } + ); return composedStory; } diff --git a/code/lib/types/src/modules/composedStory.ts b/code/lib/types/src/modules/composedStory.ts index ce31138bac20..f02a30187a38 100644 --- a/code/lib/types/src/modules/composedStory.ts +++ b/code/lib/types/src/modules/composedStory.ts @@ -8,8 +8,8 @@ import type { ComponentAnnotations, Parameters, StoryAnnotations, + StoryAnnotationsOrFn, StoryContext, - StoryFn, } from './csf'; import type { ProjectAnnotations } from './story'; @@ -22,6 +22,15 @@ export type Store_CSFExports) // or PrimaryButton() + * PrimaryButton.play({ canvasElement: container }) + */ export type ComposedStoryPlayContext = Partial< StoryContext & Pick, 'canvasElement'> >; @@ -30,40 +39,52 @@ export type ComposedStoryPlayFn ) => Promise | void; -export type PreparedStoryFn = AnnotatedStoryFn< - TRenderer, - TArgs -> & { play: ComposedStoryPlayFn; args: TArgs; id: StoryId }; - -export type ComposedStory = - | StoryFn - | StoryAnnotations; +/** + * A story function with partial args, used internally by composeStory + */ +export type PartialArgsStoryFn = ( + args?: TArgs +) => (TRenderer & { + T: TArgs; +})['storyResult']; /** - * T represents the whole ES module of a stories file. K of T means named exports (basically the Story type) - * 1. pick the keys K of T that have properties that are Story - * 2. infer the actual prop type for each Story - * 3. reconstruct Story with Partial. Story -> Story> + * A story that got recomposed for portable stories, containing all the necessary data to be rendered in external environments + */ +export type ComposedStoryFn< + TRenderer extends Renderer = Renderer, + TArgs = Args +> = PartialArgsStoryFn & { + play: ComposedStoryPlayFn; + args: TArgs; + id: StoryId; + storyName: string; + parameters: Parameters; +}; +/** + * Based on a module of stories, it returns all stories within it, filtering non-stories + * Each story will have partial props, as their props should be handled when composing stories */ export type StoriesWithPartialProps = { - // @TODO once we can use Typescript 4.0 do this to exclude nonStory exports: - // replace [K in keyof TModule] with [K in keyof TModule as TModule[K] extends ComposedStory ? K : never] - [K in keyof TModule]: TModule[K] extends ComposedStory - ? PreparedStoryFn> + // T represents the whole ES module of a stories file. K of T means named exports (basically the Story type) + // 1. pick the keys K of T that have properties that are Story + // 2. infer the actual prop type for each Story + // 3. reconstruct Story with Partial. Story -> Story> + [K in keyof TModule as TModule[K] extends StoryAnnotationsOrFn + ? K + : never]: TModule[K] extends StoryAnnotationsOrFn + ? ComposedStoryFn> : unknown; }; +/** + * Type used for integrators of portable stories, as reference when creating their own composeStory function + */ export interface ComposeStoryFn { ( storyAnnotations: AnnotatedStoryFn | StoryAnnotations, componentAnnotations: ComponentAnnotations, projectAnnotations: ProjectAnnotations, exportsName?: string - ): { - (extraArgs: Partial): TRenderer['storyResult']; - storyName: string; - args: Args; - play: ComposedStoryPlayFn; - parameters: Parameters; - }; + ): ComposedStoryFn; } diff --git a/code/renderers/react/src/testing-api.ts b/code/renderers/react/src/testing-api.ts index da061e64ef04..545147fab2db 100644 --- a/code/renderers/react/src/testing-api.ts +++ b/code/renderers/react/src/testing-api.ts @@ -6,7 +6,7 @@ import { import type { Args, ProjectAnnotations, - ComposedStory, + StoryAnnotationsOrFn, Store_CSFExports, StoriesWithPartialProps, } from '@storybook/types'; @@ -81,13 +81,13 @@ const defaultProjectAnnotations: ProjectAnnotations = { * @param [exportsName] - in case your story does not contain a name and you want it to have a name. */ export function composeStory( - story: ComposedStory, + story: StoryAnnotationsOrFn, componentAnnotations: Meta, projectAnnotations?: ProjectAnnotations, exportsName?: string ) { return originalComposeStory( - story as ComposedStory, + story as StoryAnnotationsOrFn, componentAnnotations, projectAnnotations, defaultProjectAnnotations,