Skip to content

Commit

Permalink
Change to forceInitialArgs
Browse files Browse the repository at this point in the history
  • Loading branch information
tmeasday committed Jan 15, 2023
1 parent dfcda80 commit 037e185
Show file tree
Hide file tree
Showing 13 changed files with 174 additions and 28 deletions.
2 changes: 1 addition & 1 deletion code/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ module.exports = {
'<rootDir>/frameworks/!(angular)*',
'<rootDir>/lib/*',
'<rootDir>/renderers/*',
'<rootDir>/ui/*',
'<rootDir>/ui/!(node_modules)*',
],
collectCoverage: false,
collectCoverageFrom: [
Expand Down
6 changes: 5 additions & 1 deletion code/lib/preview-api/src/modules/preview-web/Preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,11 @@ export class Preview<TFramework extends Renderer> {
async onUpdateArgs({ storyId, updatedArgs }: { storyId: StoryId; updatedArgs: Args }) {
this.storyStore.args.update(storyId, updatedArgs);

await Promise.all(this.storyRenders.filter((r) => r.id === storyId).map((r) => r.rerender()));
await Promise.all(
this.storyRenders
.filter((r) => r.id === storyId && !r.renderOptions.forceInitialArgs)
.map((r) => r.rerender())
);

this.channel.emit(STORY_ARGS_UPDATED, {
storyId,
Expand Down
33 changes: 33 additions & 0 deletions code/lib/preview-api/src/modules/preview-web/PreviewWeb.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1131,6 +1131,39 @@ describe('PreviewWeb', () => {
'story-element'
);
});

it('does not re-render the story when forceInitialArgs=true', async () => {
document.location.search = '?id=component-one--docs&viewMode=docs';

const preview = await createAndRenderPreview();
await waitForRender();

mockChannel.emit.mockClear();
const story = await preview.storyStore.loadStory({ storyId: 'component-one--a' });
preview.renderStoryToElement(story, 'story-element' as any, { forceInitialArgs: true });
await waitForRender();

expect(projectAnnotations.renderToCanvas).toHaveBeenCalledWith(
expect.objectContaining({
storyContext: expect.objectContaining({
args: { foo: 'a' },
}),
}),
'story-element'
);

docsRenderer.render.mockClear();
mockChannel.emit.mockClear();
emitter.emit(UPDATE_STORY_ARGS, {
storyId: 'component-one--a',
updatedArgs: { new: 'arg' },
});
await waitForEvents([STORY_ARGS_UPDATED]);

// We don't re-render the story
await expect(waitForRender).rejects.toThrow();
expect(projectAnnotations.renderToCanvas).toHaveBeenCalledTimes(1);
});
});
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,47 @@ describe('StoryRender', () => {
await render.renderToElement({} as any);
expect(story.playFunction).not.toHaveBeenCalled();
});

it('passes the initialArgs to loaders and render function if forceInitialArgs is true', async () => {
const story = {
id: 'id',
title: 'title',
name: 'name',
tags: [],
initialArgs: { a: 'b' },
applyLoaders: jest.fn(),
unboundStoryFn: jest.fn(),
playFunction: jest.fn(),
};

const renderToScreen = jest.fn();

const render = new StoryRender(
new Channel(),
{ getStoryContext: () => ({ args: { a: 'c ' } }) } as any,
renderToScreen as any,
{} as any,
entry.id,
'story',
{ forceInitialArgs: true },
story as any
);

await render.renderToElement({} as any);

expect(story.applyLoaders).toHaveBeenCalledWith(
expect.objectContaining({
args: { a: 'b' },
})
);

expect(renderToScreen).toHaveBeenCalledWith(
expect.objectContaining({
storyContext: expect.objectContaining({
args: { a: 'b' },
}),
}),
expect.any(Object)
);
});
});
24 changes: 19 additions & 5 deletions code/lib/preview-api/src/modules/preview-web/render/StoryRender.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export class StoryRender<TRenderer extends Renderer> implements Render<TRenderer
private callbacks: RenderContextCallbacks<TRenderer>,
public id: StoryId,
public viewMode: ViewMode,
public renderOptions: StoryRenderOptions = { autoplay: true },
public renderOptions: StoryRenderOptions = { autoplay: true, forceInitialArgs: false },
story?: PreparedStory<TRenderer>
) {
this.abortController = new AbortController();
Expand Down Expand Up @@ -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 (!canvasElement) throw new Error('cannot render when canvasElement is unset');

const { id, componentId, title, name, tags, applyLoaders, unboundStoryFn, playFunction } =
this.story;
const {
id,
componentId,
title,
name,
tags,
applyLoaders,
unboundStoryFn,
playFunction,
initialArgs,
} = this.story;

if (forceRemount && !initial) {
// NOTE: we don't check the cancel actually worked here, so the previous
Expand All @@ -170,10 +179,15 @@ export class StoryRender<TRenderer extends Renderer> implements Render<TRenderer
const abortSignal = (this.abortController as AbortController).signal;

try {
const getCurrentContext = () => ({
...this.storyContext(),
...(this.renderOptions.forceInitialArgs && { args: initialArgs }),
});

let loadedContext: Awaited<ReturnType<typeof applyLoaders>>;
await this.runPhase(abortSignal, 'loading', async () => {
loadedContext = await applyLoaders({
...this.storyContext(),
...getCurrentContext(),
viewMode: this.viewMode,
} as StoryContextForLoaders<TRenderer>);
});
Expand All @@ -185,7 +199,7 @@ export class StoryRender<TRenderer extends Renderer> implements Render<TRenderer
...loadedContext!,
// 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
...this.storyContext(),
...getCurrentContext(),
abortSignal,
// We should consider parameterizing the story types with TRenderer['canvasElement'] in the future
canvasElement: canvasElement as any,
Expand Down
1 change: 1 addition & 0 deletions code/lib/types/src/modules/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { ModuleExport, ModuleExports, PreparedStory } from './story';

export type StoryRenderOptions = {
autoplay?: boolean;
forceInitialArgs?: boolean;
};

export interface DocsContextProps<TRenderer extends Renderer = Renderer> {
Expand Down
3 changes: 2 additions & 1 deletion code/ui/blocks/src/blocks/DocsStory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const DocsStory: FC<DocsStoryProps> = ({
expanded = true,
withToolbar = false,
parameters = {},
__forceInitialArgs = false,
}) => {
let description;
const { docs } = parameters;
Expand All @@ -27,7 +28,7 @@ export const DocsStory: FC<DocsStoryProps> = ({
{subheading && <Subheading>{subheading}</Subheading>}
{description && <Description markdown={description} />}
<Canvas withToolbar={withToolbar}>
<Story id={id} parameters={parameters} />
<Story id={id} parameters={parameters} __forceInitialArgs={__forceInitialArgs} />
</Canvas>
</Anchor>
);
Expand Down
6 changes: 4 additions & 2 deletions code/ui/blocks/src/blocks/Stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ interface StoriesProps {
includePrimary?: boolean;
}

export const Stories: FC<StoriesProps> = ({ title, includePrimary = false }) => {
export const Stories: FC<StoriesProps> = ({ title, includePrimary = true }) => {
const { componentStories } = useContext(DocsContext);

let stories: DocsStoryProps[] = componentStories();
Expand All @@ -23,7 +23,9 @@ export const Stories: FC<StoriesProps> = ({ title, includePrimary = false }) =>
return (
<>
<Heading>{title}</Heading>
{stories.map((story) => story && <DocsStory key={story.id} {...story} expanded />)}
{stories.map(
(story) => story && <DocsStory key={story.id} {...story} expanded __forceInitialArgs />
)}
</>
);
};
Expand Down
16 changes: 13 additions & 3 deletions code/ui/blocks/src/blocks/Story.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
import React from 'react';
import type { Meta, StoryObj } from '@storybook/react';

import { Story as StoryComponent } from './Story';
import { Story as StoryBlock } from './Story';
import * as ButtonStories from '../examples/Button.stories';
import * as StoryComponentStories from '../components/Story.stories';

const meta: Meta<typeof StoryComponent> = {
component: StoryComponent,
const meta: Meta<typeof StoryBlock> = {
component: StoryBlock,
parameters: {
relativeCsfPaths: ['../examples/Button.stories', '../blocks/Story.stories'],
},
Expand Down Expand Up @@ -178,3 +179,12 @@ export const WithInteractionsAutoplayInStory: Story = {
chromatic: { delay: 500 },
},
};

export const IgnoreArgsUpdates: Story = {
...StoryComponentStories.ForceInitialArgs,
args: {
of: ButtonStories.Primary,
storyExport: ButtonStories.Primary,
__forceInitialArgs: true,
} as any,
};
8 changes: 7 additions & 1 deletion code/ui/blocks/src/blocks/Story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ type StoryParameters = {
* Whether to run the story's play function
*/
autoplay?: boolean;
/**
* Internal prop to control if a story re-renders on args updates
*/
__forceInitialArgs?: boolean;
};

export type StoryProps = (StoryDefProps | StoryRefProps) & StoryParameters;
Expand Down Expand Up @@ -131,6 +135,8 @@ export const getStoryProps = <TFramework extends Renderer>(
inline: true,
height,
autoplay,
// eslint-disable-next-line no-underscore-dangle
forceInitialArgs: !!props.__forceInitialArgs,
renderStoryToElement: context.renderStoryToElement,
};
}
Expand All @@ -145,7 +151,7 @@ export const getStoryProps = <TFramework extends Renderer>(
};
};

const Story: FC<StoryProps> = (props) => {
const Story: FC<StoryProps> = (props = { __forceInitialArgs: false }) => {
const context = useContext(DocsContext);
const storyId = getStoryId(props, context);
const story = useStory(storyId, context);
Expand Down
1 change: 1 addition & 0 deletions code/ui/blocks/src/blocks/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ export interface StoryData {
export type DocsStoryProps = StoryData & {
expanded?: boolean;
withToolbar?: boolean;
__forceInitialArgs?: boolean;
};
55 changes: 42 additions & 13 deletions code/ui/blocks/src/components/Story.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,33 @@
import React from 'react';
import type { StoryObj, Meta } from '@storybook/react';
import type { StoryProps } from './Story';
import type { Meta } from '@storybook/react';
import { within } from '@storybook/testing-library';
import type { PlayFunctionContext } from '@storybook/csf';
import type { WebRenderer } from '@storybook/types';
import { RESET_STORY_ARGS, STORY_ARGS_UPDATED, UPDATE_STORY_ARGS } from '@storybook/core-events';

import { Story as StoryComponent, StorySkeleton } from './Story';
import type { DocsContextProps } from '../blocks';
import * as ButtonStories from '../examples/Button.stories';

const preview = __STORYBOOK_PREVIEW__;
const renderStoryToElement = preview.renderStoryToElement.bind(preview);

// TODO: can't quite figure out types here.
// type OverriddenStoryProps = StoryProps & {
// story: typeof ButtonStories.Primary;
// };

const meta: Meta<typeof StoryComponent> = {
const meta: Meta = {
component: StoryComponent,
parameters: {
relativeCsfPaths: ['../examples/Button.stories'],
},
args: {
height: '100px',
// NOTE: the real story arg is a PreparedStory, which we'll get in the render function below
story: ButtonStories.Primary as any,
storyExport: ButtonStories.Primary,
autoplay: false,
ignoreArgsUpdates: false,
},
render(args, { loaded }) {
const docsContext = loaded.docsContext as DocsContextProps;
const storyId = docsContext.storyIdByModuleExport(args.story);
const storyId = docsContext.storyIdByModuleExport(args.storyExport);
const story = docsContext.storyById(storyId);
return <StoryComponent {...args} story={story} />;
return <StoryComponent {...(args as any)} story={story} />;
},
};
export default meta;
Expand All @@ -48,9 +48,38 @@ export const IFrame = {
},
};

export const ForceInitialArgs = {
args: {
storyExport: ButtonStories.Primary,
inline: true,
autoplay: true,
forceInitialArgs: true,
renderStoryToElement,
},
play: async ({ args, canvasElement, loaded }: PlayFunctionContext<WebRenderer>) => {
const docsContext = loaded.docsContext as DocsContextProps;
const storyId = docsContext.storyIdByModuleExport(args.storyExport);

const channel = globalThis.__STORYBOOK_ADDONS_CHANNEL__;
await within(canvasElement).findByText(/Button/);

const updatedPromise = new Promise<void>((resolve) => {
channel.once(STORY_ARGS_UPDATED, resolve);
});
await channel.emit(UPDATE_STORY_ARGS, { storyId, updatedArgs: { label: 'Updated' } });
await updatedPromise;

await within(canvasElement).findByText(/Button/);
await channel.emit(RESET_STORY_ARGS, { storyId });
await new Promise<void>((resolve) => {
channel.once(STORY_ARGS_UPDATED, resolve);
});
},
};

export const Autoplay = {
args: {
story: ButtonStories.Clicking,
storyExport: ButtonStories.Clicking,
inline: true,
autoplay: true,
renderStoryToElement,
Expand Down
4 changes: 3 additions & 1 deletion code/ui/blocks/src/components/Story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ interface InlineStoryProps extends CommonProps {
inline: true;
height?: string;
autoplay: boolean;
forceInitialArgs: boolean;
renderStoryToElement: DocsContextProps['renderStoryToElement'];
}

Expand All @@ -32,6 +33,7 @@ const InlineStory: FunctionComponent<InlineStoryProps> = ({
story,
height,
autoplay,
forceInitialArgs,
renderStoryToElement,
}) => {
const storyRef = useRef();
Expand All @@ -42,7 +44,7 @@ const InlineStory: FunctionComponent<InlineStoryProps> = ({
return () => {};
}
const element = storyRef.current as HTMLElement;
const cleanup = renderStoryToElement(story, element, { autoplay });
const cleanup = renderStoryToElement(story, element, { autoplay, forceInitialArgs });
setShowLoader(false);
return () => {
cleanup();
Expand Down

0 comments on commit 037e185

Please sign in to comment.