diff --git a/code/lib/preview-api/src/modules/preview-web/PreviewWeb.test.ts b/code/lib/preview-api/src/modules/preview-web/PreviewWeb.test.ts index e20af13752a6..0506167666b9 100644 --- a/code/lib/preview-api/src/modules/preview-web/PreviewWeb.test.ts +++ b/code/lib/preview-api/src/modules/preview-web/PreviewWeb.test.ts @@ -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(); diff --git a/code/lib/preview-api/src/modules/preview-web/render/StoryRender.test.ts b/code/lib/preview-api/src/modules/preview-web/render/StoryRender.test.ts index df4b37b840ae..57822b9de6dd 100644 --- a/code/lib/preview-api/src/modules/preview-web/render/StoryRender.test.ts +++ b/code/lib/preview-api/src/modules/preview-web/render/StoryRender.test.ts @@ -15,43 +15,18 @@ const entry = { importPath: './component.stories.ts', } as StoryIndexEntry; -const createGate = (): [Promise, (_?: any) => void] => { - let openGate = (_?: any) => {}; - const gate = new Promise((resolve) => { +const createGate = (): [Promise, () => void] => { + let openGate = () => {}; + const gate = new Promise((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, - 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', @@ -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, + 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(); + }); + }); + }); }); diff --git a/code/lib/preview-api/src/modules/preview-web/render/StoryRender.ts b/code/lib/preview-api/src/modules/preview-web/render/StoryRender.ts index 72b554d38cd2..f17759baf8f0 100644 --- a/code/lib/preview-api/src/modules/preview-web/render/StoryRender.ts +++ b/code/lib/preview-api/src/modules/preview-web/render/StoryRender.ts @@ -120,7 +120,8 @@ export class StoryRender implements Render implements Render