Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Core: Reload the preview-iframe when tearing down a story during loading #26735

Merged
merged 6 commits into from
Apr 10, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
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'
);
});

tmeasday marked this conversation as resolved.
Show resolved Hide resolved
it('aborts render for initial story', async () => {
const [gate, openGate] = createGate();

Expand Down
183 changes: 152 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,17 @@ 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];
};

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 +79,151 @@ describe('StoryRender', () => {
await render.renderToElement({} as any);
expect(story.playFunction).not.toHaveBeenCalled();
});

describe('teardown', () => {
const teardownAndWaitForReload = (render: StoryRender<any>) => {
// 1. immediately teardown the story
render.teardown();

return new Promise<void>((resolve) => {
setInterval(() => {
try {
// 2. assert that the window is reloaded and move on
expect(window.location.reload).toHaveBeenCalledOnce();
resolve();
} catch {
// empty catch to ignore the assertion failing
}
}, 0);
});
};

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 () => {
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 render = new StoryRender(
new Channel({}),
store as any,
vi.fn() as any,
{} as any,
entry.id,
'story',
{ autoplay: true },
story as any
);

story.applyLoaders.mockImplementation(() => teardownAndWaitForReload(render));
JReinhold marked this conversation as resolved.
Show resolved Hide resolved

await render.renderToElement({} as any);

expect(story.applyLoaders).toHaveBeenCalledOnce();
expect(store.cleanupStory).toHaveBeenCalledOnce();
expect(window.location.reload).toHaveBeenCalledOnce();
});

it('reloads the page when tearing down during rendering', async () => {
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();

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

renderToScreen.mockImplementation(() => teardownAndWaitForReload(render));

await render.renderToElement({} as any);

expect(renderToScreen).toHaveBeenCalledOnce();
expect(store.cleanupStory).toHaveBeenCalledOnce();
expect(window.location.reload).toHaveBeenCalledOnce();
});

it('reloads the page when tearing down during playing', async () => {
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 render = new StoryRender(
new Channel({}),
store as any,
vi.fn() as any,
{} as any,
entry.id,
'story',
{ autoplay: true },
story as any
);

story.playFunction.mockImplementation(() => teardownAndWaitForReload(render));

await render.renderToElement({} as any);

expect(story.playFunction).toHaveBeenCalledOnce();
expect(store.cleanupStory).toHaveBeenCalledOnce();
expect(window.location.reload).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