Skip to content

Commit

Permalink
Merge pull request #26735 from storybookjs/jeppe/teardown-loading-reload
Browse files Browse the repository at this point in the history
Core: Reload the preview-iframe when tearing down a story during loading
  • Loading branch information
kasperpeulen authored Apr 10, 2024
2 parents 15cc239 + 45d4b32 commit 388516c
Show file tree
Hide file tree
Showing 3 changed files with 158 additions and 66 deletions.
33 changes: 0 additions & 33 deletions code/lib/preview-api/src/modules/preview-web/PreviewWeb.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2141,39 +2141,6 @@ describe('PreviewWeb', () => {
window.location = { ...originalLocation, reload: originalLocation.reload };
});

it('stops initial story after loaders if running', async () => {
const [gate, openGate] = createGate();
componentOneExports.default.loaders[0].mockImplementationOnce(async () => gate);

document.location.search = '?id=component-one--a';
await new PreviewWeb(importFn, getProjectAnnotations).ready();
await waitForRenderPhase('loading');

emitter.emit(SET_CURRENT_STORY, {
storyId: 'component-one--b',
viewMode: 'story',
});
await waitForSetCurrentStory();
await waitForRender();

// Now let the loader resolve
openGate({ l: 8 });
await waitForRender();

// Story gets rendered with updated args
expect(projectAnnotations.renderToCanvas).toHaveBeenCalledTimes(1);
expect(projectAnnotations.renderToCanvas).toHaveBeenCalledWith(
expect.objectContaining({
forceRemount: true,
storyContext: expect.objectContaining({
id: 'component-one--b',
loaded: { l: 7 },
}),
}),
'story-element'
);
});

it('aborts render for initial story', async () => {
const [gate, openGate] = createGate();

Expand Down
186 changes: 155 additions & 31 deletions code/lib/preview-api/src/modules/preview-web/render/StoryRender.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,43 +15,18 @@ const entry = {
importPath: './component.stories.ts',
} as StoryIndexEntry;

const createGate = (): [Promise<any | undefined>, (_?: any) => void] => {
let openGate = (_?: any) => {};
const gate = new Promise<any | undefined>((resolve) => {
const createGate = (): [Promise<void>, () => void] => {
let openGate = () => {};
const gate = new Promise<void>((resolve) => {
openGate = resolve;
});
return [gate, openGate];
};
const tick = () => new Promise((resolve) => setTimeout(resolve, 0));

describe('StoryRender', () => {
it('throws PREPARE_ABORTED if torndown during prepare', async () => {
const [importGate, openImportGate] = createGate();
const mockStore = {
loadStory: vi.fn(async () => {
await importGate;
return {};
}),
cleanupStory: vi.fn(),
};

const render = new StoryRender(
new Channel({}),
mockStore as unknown as StoryStore<Renderer>,
vi.fn(),
{} as any,
entry.id,
'story'
);

const preparePromise = render.prepare();

render.teardown();

openImportGate();

await expect(preparePromise).rejects.toThrowError(PREPARE_ABORTED);
});
window.location = { reload: vi.fn() } as any;

describe('StoryRender', () => {
it('does run play function if passed autoplay=true', async () => {
const story = {
id: 'id',
Expand Down Expand Up @@ -105,4 +80,153 @@ describe('StoryRender', () => {
await render.renderToElement({} as any);
expect(story.playFunction).not.toHaveBeenCalled();
});

describe('teardown', () => {
it('throws PREPARE_ABORTED if torndown during prepare', async () => {
const [importGate, openImportGate] = createGate();
const mockStore = {
loadStory: vi.fn(async () => {
await importGate;
return {};
}),
cleanupStory: vi.fn(),
};

const render = new StoryRender(
new Channel({}),
mockStore as unknown as StoryStore<Renderer>,
vi.fn(),
{} as any,
entry.id,
'story'
);

const preparePromise = render.prepare();

render.teardown();

openImportGate();

await expect(preparePromise).rejects.toThrowError(PREPARE_ABORTED);
});

it('reloads the page when tearing down during loading', async () => {
// Arrange - setup StoryRender and async gate blocking applyLoaders
const [loaderGate] = createGate();
const story = {
id: 'id',
title: 'title',
name: 'name',
tags: [],
applyLoaders: vi.fn(() => loaderGate),
unboundStoryFn: vi.fn(),
playFunction: vi.fn(),
prepareContext: vi.fn(),
};
const store = { getStoryContext: () => ({}), cleanupStory: vi.fn() };
const render = new StoryRender(
new Channel({}),
store as any,
vi.fn() as any,
{} as any,
entry.id,
'story',
{ autoplay: true },
story as any
);

// Act - render (blocked by loaders), teardown
render.renderToElement({} as any);
expect(story.applyLoaders).toHaveBeenCalledOnce();
expect(render.phase).toBe('loading');
render.teardown();

// Assert - window is reloaded
await vi.waitFor(() => {
expect(window.location.reload).toHaveBeenCalledOnce();
expect(store.cleanupStory).toHaveBeenCalledOnce();
});
});

it('reloads the page when tearing down during rendering', async () => {
// Arrange - setup StoryRender and async gate blocking renderToScreen
const [renderGate] = createGate();
const story = {
id: 'id',
title: 'title',
name: 'name',
tags: [],
applyLoaders: vi.fn(),
unboundStoryFn: vi.fn(),
playFunction: vi.fn(),
prepareContext: vi.fn(),
};
const store = { getStoryContext: () => ({}), cleanupStory: vi.fn() };
const renderToScreen = vi.fn(() => renderGate);

const render = new StoryRender(
new Channel({}),
store as any,
renderToScreen as any,
{} as any,
entry.id,
'story',
{ autoplay: true },
story as any
);

// Act - render (blocked by renderToScreen), teardown
render.renderToElement({} as any);
await tick(); // go from 'loading' to 'rendering' phase
expect(renderToScreen).toHaveBeenCalledOnce();
expect(render.phase).toBe('rendering');
render.teardown();

// Assert - window is reloaded
await vi.waitFor(() => {
expect(window.location.reload).toHaveBeenCalledOnce();
expect(store.cleanupStory).toHaveBeenCalledOnce();
});
});

it('reloads the page when tearing down during playing', async () => {
// Arrange - setup StoryRender and async gate blocking playing
const [playGate] = createGate();
const story = {
id: 'id',
title: 'title',
name: 'name',
tags: [],
applyLoaders: vi.fn(),
unboundStoryFn: vi.fn(),
playFunction: vi.fn(() => playGate),
prepareContext: vi.fn(),
};
const store = { getStoryContext: () => ({}), cleanupStory: vi.fn() };

const render = new StoryRender(
new Channel({}),
store as any,
vi.fn() as any,
{} as any,
entry.id,
'story',
{ autoplay: true },
story as any
);

// Act - render (blocked by playFn), teardown
render.renderToElement({} as any);
await tick(); // go from 'loading' to 'playing' phase
expect(story.playFunction).toHaveBeenCalledOnce();
expect(render.phase).toBe('playing');
render.teardown();

// Assert - window is reloaded
await vi.waitFor(() => {
expect(window.location.reload).toHaveBeenCalledOnce();
expect(store.cleanupStory).toHaveBeenCalledOnce();
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,8 @@ export class StoryRender<TRenderer extends Renderer> implements Render<TRenderer
}

isPending() {
return ['rendering', 'playing'].includes(this.phase as RenderPhase);
// TODO: add beforeRendering here when that is implemented
return ['loading', 'rendering', 'playing'].includes(this.phase as RenderPhase);
}

async renderToElement(canvasElement: TRenderer['canvasElement']) {
Expand Down Expand Up @@ -293,7 +294,7 @@ export class StoryRender<TRenderer extends Renderer> implements Render<TRenderer
// If the story has loaded, we need to cleanup
if (this.story) this.store.cleanupStory(this.story);

// Check if we're done rendering/playing. If not, we may have to reload the page.
// Check if we're done loading/rendering/playing. If not, we may have to reload the page.
// Wait several ticks that may be needed to handle the abort, then try again.
// Note that there's a max of 5 nested timeouts before they're no longer "instant".
for (let i = 0; i < 3; i += 1) {
Expand Down

0 comments on commit 388516c

Please sign in to comment.