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 all 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
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