Wait a tick before apply arg changes and other re-renders.

This allows users to update args or re-render the story from *inside* decorators or story functions, for better or worse.
This commit is contained in:
Tom Coleman 2021-09-08 15:02:34 +10:00
parent ec23d8c2f7
commit 23254408d1
3 changed files with 57 additions and 4 deletions

View File

@ -95,3 +95,4 @@ export const waitForRender = () =>
]);
export const waitForQuiescence = async () => new Promise((r) => setTimeout(r, 100));
export const waitForTick = async () => new Promise((r) => setTimeout(r, 0));

View File

@ -18,6 +18,7 @@ import {
waitForEvents,
waitForRender,
waitForQuiescence,
waitForTick,
} from './PreviewWeb.mockdata';
jest.mock('./WebView');
@ -48,7 +49,7 @@ const createGate = (): [Promise<any | undefined>, (_?: any) => void] => {
return [gate, openGate];
};
beforeEach(() => {
beforeEach(async () => {
document.location.search = '';
mockChannel.emit.mockClear();
emitter.removeAllListeners();
@ -64,6 +65,9 @@ beforeEach(() => {
logger.warn.mockClear();
addons.setChannel(mockChannel as any);
// Some events happen in the next tick
await waitForTick();
});
describe('PreviewWeb', () => {
@ -629,6 +633,7 @@ describe('PreviewWeb', () => {
storyId: 'component-one--a',
updatedArgs: { new: 'arg' },
});
await waitForTick();
// Now let the loader resolve
openGate({ l: 8 });
@ -659,6 +664,7 @@ describe('PreviewWeb', () => {
storyId: 'component-one--a',
updatedArgs: { new: 'arg' },
});
await waitForTick();
expect(logger.warn).toHaveBeenCalled();
// Now let the renderToDOM call resolve
@ -679,6 +685,44 @@ describe('PreviewWeb', () => {
);
});
it('works if it is called directly from inside non async renderToDOM', async () => {
document.location.search = '?id=component-one--a';
projectAnnotations.renderToDOM.mockImplementationOnce(() => {
emitter.emit(Events.UPDATE_STORY_ARGS, {
storyId: 'component-one--a',
updatedArgs: { new: 'arg' },
});
});
await new PreviewWeb({ getProjectAnnotations, importFn, fetchStoryIndex }).initialize();
await waitForRender();
mockChannel.emit.mockClear();
await waitForRender();
expect(logger.warn).not.toHaveBeenCalled();
expect(projectAnnotations.renderToDOM).toHaveBeenCalledTimes(2);
expect(projectAnnotations.renderToDOM).toHaveBeenCalledWith(
expect.objectContaining({
forceRemount: true,
storyContext: expect.objectContaining({
loaded: { l: 7 },
args: { foo: 'a' },
}),
}),
undefined // this is coming from view.prepareForStory, not super important
);
expect(projectAnnotations.renderToDOM).toHaveBeenCalledWith(
expect.objectContaining({
forceRemount: false,
storyContext: expect.objectContaining({
loaded: { l: 7 },
args: { foo: 'a', new: 'arg' },
}),
}),
undefined // this is coming from view.prepareForStory, not super important
);
});
it('warns and calls renderToDOM again if play function is running', async () => {
const [gate, openGate] = createGate();
componentOneExports.a.play.mockImplementationOnce(async () => gate);
@ -709,6 +753,7 @@ describe('PreviewWeb', () => {
storyId: 'component-one--a',
updatedArgs: { new: 'arg' },
});
await waitForTick();
expect(logger.warn).toHaveBeenCalled();
// The second call should emit STORY_RENDERED
@ -769,8 +814,10 @@ describe('PreviewWeb', () => {
storyId: 'component-one--a',
updatedArgs: { foo: 'new', new: 'value' },
});
await waitForRender();
mockChannel.emit.mockClear();
projectAnnotations.renderToDOM.mockClear();
emitter.emit(Events.RESET_STORY_ARGS, {
storyId: 'component-one--a',
argNames: ['foo'],
@ -802,8 +849,10 @@ describe('PreviewWeb', () => {
storyId: 'component-one--a',
updatedArgs: { foo: 'new', new: 'value' },
});
await waitForRender();
mockChannel.emit.mockClear();
projectAnnotations.renderToDOM.mockClear();
emitter.emit(Events.RESET_STORY_ARGS, {
storyId: 'component-one--a',
});

View File

@ -488,6 +488,9 @@ export class PreviewWeb<TFramework extends AnyFramework> {
}
this.channel.emit(Events.STORY_RENDERED, id);
};
// We wait a moment to re-render the story in case users are doing things like force
// rerender or updating args from inside story functions.
const rerenderStoryOnTick = () => setTimeout(rerenderStory, 0);
// Start the first render
// NOTE: we don't await here because we need to return the "cleanup" function below
@ -496,10 +499,10 @@ export class PreviewWeb<TFramework extends AnyFramework> {
initialRender().catch((err) => renderContextWithoutStoryContext.showException(err));
// Listen to events and re-render story
this.channel.on(Events.UPDATE_GLOBALS, rerenderStory);
this.channel.on(Events.FORCE_RE_RENDER, rerenderStory);
this.channel.on(Events.UPDATE_GLOBALS, rerenderStoryOnTick);
this.channel.on(Events.FORCE_RE_RENDER, rerenderStoryOnTick);
const rerenderStoryIfMatches = async ({ storyId }: { storyId: StoryId }) => {
if (storyId === story.id) rerenderStory();
if (storyId === story.id) rerenderStoryOnTick();
};
this.channel.on(Events.UPDATE_STORY_ARGS, rerenderStoryIfMatches);
this.channel.on(Events.RESET_STORY_ARGS, rerenderStoryIfMatches);