Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion code/core/src/csf/csf-factories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,9 @@ function defineStory<

const play =
mountDestructured(this.play) || mountDestructured(testFunction)
? async ({ context }: StoryContext<TRenderer>) => {
? // mount needs to be explicitly destructured
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async ({ mount, context }: StoryContext<TRenderer>) => {
await this.play?.(context);
await testFunction(context);
}
Expand Down
10 changes: 5 additions & 5 deletions code/core/src/preview-api/modules/store/csf/processCSFFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,16 +49,16 @@ export function processCSFFile<TRenderer extends Renderer>(
): CSFFile<TRenderer> {
const { default: defaultExport, __namedExportsOrder, ...namedExports } = moduleExports;

const firstStory = Object.values(namedExports)[0];
if (isStory<TRenderer>(firstStory)) {
const factoryStory = Object.values(namedExports).find((it) => isStory<TRenderer>(it));
if (factoryStory) {
Comment on lines +52 to +53
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change from Object.values(namedExports)[0] to Object.values(namedExports).find((it) => isStory<TRenderer>(it)) is a bug fix that ensures the code finds an actual story export rather than assuming the first export is a story. However, there appears to be no test coverage for this scenario in processCSFFile.test.ts. Consider adding a test case where the first named export is not a story (e.g., a helper function or constant) to verify this fix works correctly.

Copilot uses AI. Check for mistakes.
const meta: NormalizedComponentAnnotations<TRenderer> =
normalizeComponentAnnotations<TRenderer>(firstStory.meta.input, title, importPath);
normalizeComponentAnnotations<TRenderer>(factoryStory.meta.input, title, importPath);
checkDisallowedParameters(meta.parameters);

const csfFile: CSFFile<TRenderer> = { meta, stories: {}, moduleExports };

Object.keys(namedExports).forEach((key) => {
if (isExportStory(key, meta)) {
if (isExportStory(key, meta) && isStory<TRenderer>(namedExports[key])) {
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The additional isStory check on line 61 ensures that only actual Story objects (created by meta.story()) are processed in factory-based CSF files, not just any export that passes the isExportStory filter. This is a defensive check that prevents processing non-Story exports as stories. Consider adding test coverage for a scenario where a factory-based CSF file has a named export that passes isExportStory but is not a Story object (e.g., a helper function with a name that looks like a story name).

Copilot uses AI. Check for mistakes.
const story: Story<TRenderer> = namedExports[key];

const storyMeta = normalizeStory(key, story.input as any, meta);
Expand All @@ -82,7 +82,7 @@ export function processCSFFile<TRenderer extends Renderer>(
}
});

csfFile.projectAnnotations = firstStory.meta.preview.composed;
csfFile.projectAnnotations = factoryStory.meta.preview.composed;

return csfFile;
}
Expand Down
19 changes: 18 additions & 1 deletion code/renderers/react/src/csf-factories.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ describe('Args can be provided in multiple ways', () => {
args: { label: 'good' },
});
// @ts-expect-error disabled not provided ❌
const Basic = meta.story({});
const Basic = meta.story();
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused variable Basic.

Suggested change
const Basic = meta.story();
meta.story();

Copilot uses AI. Check for mistakes.
}
{
const meta = preview.meta({ component: Button });
Expand Down Expand Up @@ -384,3 +384,20 @@ describe('Composed getters', () => {
expect(renderSpy).toHaveBeenCalled();
});
});

it('meta.input also contains play', () => {
const meta = preview.meta({
/** Title, component, etc... */
play: async ({ canvas }) => {
/** Do some common interactions */
},
});

const ExtendedInteractionsStory = meta.story({
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused variable ExtendedInteractionsStory.

Suggested change
const ExtendedInteractionsStory = meta.story({
meta.story({

Copilot uses AI. Check for mistakes.
play: async ({ canvas, ...rest }) => {
await meta.input.play?.({ canvas, ...rest });

/** Do some extra interactions */
},
});
});
35 changes: 26 additions & 9 deletions code/renderers/react/src/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export function __definePreview<Addons extends PreviewAddon<never>[]>(
preview.meta = (_input) => {
const meta = defineMeta(_input);
const defineStory = meta.story.bind(meta);
// @ts-expect-error internal code that is hard to type
meta.story = (__input: any) => {
const story = defineStory(__input);
// TODO: [test-syntax] Are we sure we want this? the Component construct was for
Expand All @@ -52,6 +53,12 @@ export function __definePreview<Addons extends PreviewAddon<never>[]>(
return preview;
}

type InferArgs<
TArgs extends Args,
T extends AddonTypes,
Decorators extends DecoratorFunction<ReactTypes & T, any>,
> = Simplify<TArgs & Simplify<RemoveIndexSignature<DecoratorsArgs<ReactTypes & T, Decorators>>>>;

/** @ts-expect-error We cannot implement the meta faithfully here, but that is okay. */
export interface ReactPreview<T extends AddonTypes> extends Preview<ReactTypes & T> {
meta<
Expand All @@ -70,13 +77,13 @@ export interface ReactPreview<T extends AddonTypes> extends Preview<ReactTypes &
'decorators' | 'component' | 'args' | 'render'
>
): ReactMeta<
ReactTypes &
T & {
args: Simplify<
TArgs & Simplify<RemoveIndexSignature<DecoratorsArgs<ReactTypes & T, Decorators>>>
>;
},
{ args: Partial<TArgs> extends TMetaArgs ? {} : TMetaArgs }
ReactTypes & T & { args: InferArgs<TArgs, T, Decorators> },
Omit<
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a comment explaining what's going on?

ComponentAnnotations<ReactTypes & T & { args: InferArgs<TArgs, T, Decorators> }>,
'args'
> & {
args: Partial<TArgs> extends TMetaArgs ? {} : TMetaArgs;
}
>;
}

Expand All @@ -95,7 +102,7 @@ export interface ReactMeta<T extends ReactTypes, MetaInput extends ComponentAnno
render: () => ReactTypes['storyResult'];
}),
>(
story?: TInput
story: TInput
): ReactStory<T, TInput extends () => ReactTypes['storyResult'] ? { render: TInput } : TInput>;

story<
Expand All @@ -108,9 +115,19 @@ export interface ReactMeta<T extends ReactTypes, MetaInput extends ComponentAnno
>
>,
>(
story?: TInput
story: TInput
/** @ts-expect-error hard */
): ReactStory<T, TInput>;

// This overload matches meta.story(), but only when all args are optional
story(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a comment here to explain what's going on?

..._args: Partial<T['args']> extends SetOptional<
T['args'],
keyof T['args'] & keyof MetaInput['args']
>
? []
: [never]
): ReactStory<T, {}>;
}

export interface ReactStory<T extends ReactTypes, TInput extends StoryAnnotations<T, T['args']>>
Expand Down