Implemented prepared context.

This commit is contained in:
Mauricio Rivera 2023-01-23 12:10:22 -05:00
parent 858aa9d49f
commit 466a547e6a
5 changed files with 97 additions and 28 deletions

View File

@ -59,6 +59,7 @@ describe('StoryRender', () => {
applyLoaders: jest.fn(), applyLoaders: jest.fn(),
unboundStoryFn: jest.fn(), unboundStoryFn: jest.fn(),
playFunction: jest.fn(), playFunction: jest.fn(),
prepareContext: jest.fn(),
}; };
const render = new StoryRender( const render = new StoryRender(
@ -85,6 +86,7 @@ describe('StoryRender', () => {
applyLoaders: jest.fn(), applyLoaders: jest.fn(),
unboundStoryFn: jest.fn(), unboundStoryFn: jest.fn(),
playFunction: jest.fn(), playFunction: jest.fn(),
prepareContext: jest.fn(),
}; };
const render = new StoryRender( const render = new StoryRender(

View File

@ -154,8 +154,17 @@ export class StoryRender<TRenderer extends Renderer> implements Render<TRenderer
if (!this.story) throw new Error('cannot render when not prepared'); if (!this.story) throw new Error('cannot render when not prepared');
if (!canvasElement) throw new Error('cannot render when canvasElement is unset'); if (!canvasElement) throw new Error('cannot render when canvasElement is unset');
const { id, componentId, title, name, tags, applyLoaders, unboundStoryFn, playFunction } = const {
this.story; id,
componentId,
title,
name,
tags,
applyLoaders,
unboundStoryFn,
playFunction,
prepareContext,
} = this.story;
if (forceRemount && !initial) { if (forceRemount && !initial) {
// NOTE: we don't check the cancel actually worked here, so the previous // NOTE: we don't check the cancel actually worked here, so the previous
@ -181,7 +190,7 @@ export class StoryRender<TRenderer extends Renderer> implements Render<TRenderer
return; return;
} }
const renderStoryContext: StoryContext<TRenderer> = { const renderStoryContext: StoryContext<TRenderer> = prepareContext({
...loadedContext!, ...loadedContext!,
// By this stage, it is possible that new args/globals have been received for this story // By this stage, it is possible that new args/globals have been received for this story
// and we need to ensure we render it with the new values // and we need to ensure we render it with the new values
@ -189,7 +198,7 @@ export class StoryRender<TRenderer extends Renderer> implements Render<TRenderer
abortSignal, abortSignal,
// We should consider parameterizing the story types with TRenderer['canvasElement'] in the future // We should consider parameterizing the story types with TRenderer['canvasElement'] in the future
canvasElement: canvasElement as any, canvasElement: canvasElement as any,
}; });
const renderContext: RenderContext<TRenderer> = { const renderContext: RenderContext<TRenderer> = {
componentId, componentId,
title, title,

View File

@ -468,8 +468,8 @@ describe('prepareStory', () => {
{ render: renderMock } { render: renderMock }
); );
const context = { args: story.initialArgs, ...story }; const context = story.prepareContext({ args: story.initialArgs, ...story } as any);
story.undecoratedStoryFn(context as any); story.undecoratedStoryFn(context);
expect(renderMock).toHaveBeenCalledWith( expect(renderMock).toHaveBeenCalledWith(
{ one: 'mapped', two: 2, three: 3 }, { one: 'mapped', two: 2, three: 3 },
expect.objectContaining({ args: { one: 'mapped', two: 2, three: 3 } }) expect.objectContaining({ args: { one: 'mapped', two: 2, three: 3 } })
@ -529,6 +529,50 @@ describe('prepareStory', () => {
hooks.clean(); hooks.clean();
}); });
it('prepared context is applied to decorators', () => {
const renderMock = jest.fn();
let ctx1;
let ctx2;
let ctx3;
const globalDecorator = jest.fn((fn, ctx) => {
ctx1 = ctx;
return fn();
});
const componentDecorator = jest.fn((fn, ctx) => {
ctx2 = ctx;
return fn();
});
const storyDecorator = jest.fn((fn, ctx) => {
ctx3 = ctx;
return fn();
});
const story = prepareStory(
{
id,
name,
argTypes: {
one: { name: 'one', type: { name: 'string' }, mapping: { 1: 'mapped-1' } },
},
args: { one: 1 },
decorators: [storyDecorator],
moduleExport,
},
{ id, title, decorators: [componentDecorator] },
{ render: renderMock, decorators: [globalDecorator] }
);
const hooks = new HooksContext();
const context = story.prepareContext({ args: story.initialArgs, hooks, ...story } as any);
story.unboundStoryFn(context);
expect(ctx1).toMatchObject({ args: { one: 'mapped-1' } });
expect(ctx2).toMatchObject({ args: { one: 'mapped-1' } });
expect(ctx3).toMatchObject({ args: { one: 'mapped-1' } });
hooks.clean();
});
}); });
describe('with `FEATURES.argTypeTargetsV7`', () => { describe('with `FEATURES.argTypeTargetsV7`', () => {
@ -549,11 +593,12 @@ describe('prepareStory', () => {
{ render: renderMock } { render: renderMock }
); );
firstStory.unboundStoryFn({ const context = firstStory.prepareContext({
args: firstStory.initialArgs, args: firstStory.initialArgs,
hooks: new HooksContext(), hooks: new HooksContext(),
...firstStory, ...firstStory,
} as any); } as any);
firstStory.unboundStoryFn(context);
expect(renderMock).toHaveBeenCalledWith( expect(renderMock).toHaveBeenCalledWith(
{ a: 1 }, { a: 1 },
expect.objectContaining({ args: { a: 1 }, allArgs: { a: 1, b: 2 } }) expect.objectContaining({ args: { a: 1 }, allArgs: { a: 1, b: 2 } })
@ -574,11 +619,12 @@ describe('prepareStory', () => {
{ render: renderMock } { render: renderMock }
); );
firstStory.unboundStoryFn({ const context = firstStory.prepareContext({
args: firstStory.initialArgs, args: firstStory.initialArgs,
hooks: new HooksContext(), hooks: new HooksContext(),
...firstStory, ...firstStory,
} as any); } as any);
firstStory.unboundStoryFn(context);
expect(renderMock).toHaveBeenCalledWith( expect(renderMock).toHaveBeenCalledWith(
{ a: 1 }, { a: 1 },
expect.objectContaining({ args: { a: 1 }, allArgs: { a: 1, b: 2 } }) expect.objectContaining({ args: { a: 1 }, allArgs: { a: 1, b: 2 } })
@ -599,11 +645,12 @@ describe('prepareStory', () => {
{ render: renderMock } { render: renderMock }
); );
firstStory.unboundStoryFn({ const context = firstStory.prepareContext({
args: firstStory.initialArgs, args: firstStory.initialArgs,
hooks: new HooksContext(), hooks: new HooksContext(),
...firstStory, ...firstStory,
} as any); } as any);
firstStory.unboundStoryFn(context);
expect(renderMock).toHaveBeenCalledWith( expect(renderMock).toHaveBeenCalledWith(
{ a: 1 }, { a: 1 },
expect.objectContaining({ argsByTarget: { [NO_TARGET_NAME]: { a: 1 }, foo: { b: 2 } } }) expect.objectContaining({ argsByTarget: { [NO_TARGET_NAME]: { a: 1 }, foo: { b: 2 } } })
@ -624,11 +671,12 @@ describe('prepareStory', () => {
{ render: renderMock } { render: renderMock }
); );
firstStory.unboundStoryFn({ const context = firstStory.prepareContext({
args: firstStory.initialArgs, args: firstStory.initialArgs,
hooks: new HooksContext(), hooks: new HooksContext(),
...firstStory, ...firstStory,
} as any); } as any);
firstStory.unboundStoryFn(context);
expect(renderMock).toHaveBeenCalledWith( expect(renderMock).toHaveBeenCalledWith(
{}, {},
expect.objectContaining({ argsByTarget: { foo: { b: 2 } } }) expect.objectContaining({ argsByTarget: { foo: { b: 2 } } })
@ -647,11 +695,12 @@ describe('prepareStory', () => {
{ render: renderMock } { render: renderMock }
); );
firstStory.unboundStoryFn({ const context = firstStory.prepareContext({
args: firstStory.initialArgs, args: firstStory.initialArgs,
hooks: new HooksContext(), hooks: new HooksContext(),
...firstStory, ...firstStory,
} as any); } as any);
firstStory.unboundStoryFn(context);
expect(renderMock).toHaveBeenCalledWith({}, expect.objectContaining({ argsByTarget: {} })); expect(renderMock).toHaveBeenCalledWith({}, expect.objectContaining({ argsByTarget: {} }));
}); });
}); });
@ -739,6 +788,7 @@ describe('prepareMeta', () => {
unboundStoryFn, unboundStoryFn,
undecoratedStoryFn, undecoratedStoryFn,
playFunction, playFunction,
prepareContext,
...expectedPreparedMeta ...expectedPreparedMeta
} = preparedStory; } = preparedStory;

View File

@ -71,23 +71,10 @@ export function prepareStory<TRenderer extends Renderer>(
}; };
const undecoratedStoryFn: LegacyStoryFn<TRenderer> = (context: StoryContext<TRenderer>) => { const undecoratedStoryFn: LegacyStoryFn<TRenderer> = (context: StoryContext<TRenderer>) => {
const mappedArgs = Object.entries(context.args).reduce((acc, [key, val]) => {
const mapping = context.argTypes[key]?.mapping;
acc[key] = mapping && val in mapping ? mapping[val] : val;
return acc;
}, {} as Args);
const includedArgs = Object.entries(mappedArgs).reduce((acc, [key, val]) => {
const argType = context.argTypes[key] || {};
if (includeConditionalArg(argType, mappedArgs, context.globals)) acc[key] = val;
return acc;
}, {} as Args);
const includedContext = { ...context, args: includedArgs };
const { passArgsFirst: renderTimePassArgsFirst = true } = context.parameters; const { passArgsFirst: renderTimePassArgsFirst = true } = context.parameters;
return renderTimePassArgsFirst return renderTimePassArgsFirst
? (render as ArgsStoryFn<TRenderer>)(includedContext.args, includedContext) ? (render as ArgsStoryFn<TRenderer>)(context.args, context)
: (render as LegacyStoryFn<TRenderer>)(includedContext); : (render as LegacyStoryFn<TRenderer>)(context);
}; };
// Currently it is only possible to set these globally // Currently it is only possible to set these globally
@ -109,8 +96,15 @@ export function prepareStory<TRenderer extends Renderer>(
if (!render) throw new Error(`No render function available for storyId '${id}'`); if (!render) throw new Error(`No render function available for storyId '${id}'`);
const decoratedStoryFn = applyHooks<TRenderer>(applyDecorators)(undecoratedStoryFn, decorators); const decoratedStoryFn = applyHooks<TRenderer>(applyDecorators)(undecoratedStoryFn, decorators);
const unboundStoryFn = (context: StoryContext<TRenderer>) => { const unboundStoryFn = (context: StoryContext<TRenderer>) => decoratedStoryFn(context);
// prepareContext is invoked at StoryRender.render()
// the context is prepared before invoking the render function, instead of here directly
// to ensure args don't loose there special properties set by the renderer
// eg. reactive proxies set by frameworks like SolidJS or Vue
const prepareContext = (context: StoryContext<TRenderer>) => {
let finalContext: StoryContext<TRenderer> = context; let finalContext: StoryContext<TRenderer> = context;
if (global.FEATURES?.argTypeTargetsV7) { if (global.FEATURES?.argTypeTargetsV7) {
const argsByTarget = groupArgsByTarget(context); const argsByTarget = groupArgsByTarget(context);
finalContext = { finalContext = {
@ -121,7 +115,19 @@ export function prepareStory<TRenderer extends Renderer>(
}; };
} }
return decoratedStoryFn(finalContext); const mappedArgs = Object.entries(finalContext.args).reduce((acc, [key, val]) => {
const mapping = finalContext.argTypes[key]?.mapping;
acc[key] = mapping && val in mapping ? mapping[val] : val;
return acc;
}, {} as Args);
const includedArgs = Object.entries(mappedArgs).reduce((acc, [key, val]) => {
const argType = finalContext.argTypes[key] || {};
if (includeConditionalArg(argType, mappedArgs, finalContext.globals)) acc[key] = val;
return acc;
}, {} as Args);
return { ...finalContext, args: includedArgs };
}; };
const play = storyAnnotations?.play || componentAnnotations.play; const play = storyAnnotations?.play || componentAnnotations.play;
@ -150,6 +156,7 @@ export function prepareStory<TRenderer extends Renderer>(
unboundStoryFn, unboundStoryFn,
applyLoaders, applyLoaders,
playFunction, playFunction,
prepareContext,
}; };
} }

View File

@ -84,6 +84,7 @@ export type PreparedStory<TRenderer extends Renderer = Renderer> =
context: StoryContextForLoaders<TRenderer> context: StoryContextForLoaders<TRenderer>
) => Promise<StoryContextForLoaders<TRenderer> & { loaded: StoryContext<TRenderer>['loaded'] }>; ) => Promise<StoryContextForLoaders<TRenderer> & { loaded: StoryContext<TRenderer>['loaded'] }>;
playFunction?: (context: StoryContext<TRenderer>) => Promise<void> | void; playFunction?: (context: StoryContext<TRenderer>) => Promise<void> | void;
prepareContext: (context: StoryContext<TRenderer>) => StoryContext<TRenderer>;
}; };
export type PreparedMeta<TRenderer extends Renderer = Renderer> = Omit< export type PreparedMeta<TRenderer extends Renderer = Renderer> = Omit<