Skip to content

Commit

Permalink
Ensure if a docs render is torndown during preparation, it throws.
Browse files Browse the repository at this point in the history
Implementing #17599 for DocsRenderrs
  • Loading branch information
tmeasday committed Sep 1, 2022
1 parent fb4b783 commit 05e51a1
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 56 deletions.
188 changes: 144 additions & 44 deletions code/lib/preview-web/src/PreviewWeb.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { jest, jest as mockJest, it, describe, beforeEach, afterEach, expect } from '@jest/globals';
import global from 'global';
import merge from 'lodash/merge';
import {
Expand Down Expand Up @@ -29,8 +30,8 @@ import { logger } from '@storybook/client-logger';
import { addons, mockChannel as createMockChannel } from '@storybook/addons';
import type { AnyFramework } from '@storybook/csf';
import type { ModuleImportFn, WebProjectAnnotations } from '@storybook/store';
import { expect } from '@jest/globals';
import { mocked } from 'ts-jest/utils';
import jestMock from 'jest-mock';

import { PreviewWeb } from './PreviewWeb';
import {
Expand Down Expand Up @@ -58,8 +59,8 @@ const mockStoryIndex = jest.fn(() => storyIndex);

let mockFetchResult;
jest.mock('global', () => ({
...(jest.requireActual('global') as any),
history: { replaceState: jest.fn() },
...(mockJest.requireActual('global') as any),
history: { replaceState: mockJest.fn() },
document: {
location: {
pathname: 'pathname',
Expand All @@ -68,7 +69,7 @@ jest.mock('global', () => ({
},
window: {
location: {
reload: jest.fn(),
reload: mockJest.fn(),
},
},
FEATURES: {
Expand Down Expand Up @@ -132,7 +133,7 @@ beforeEach(() => {
projectAnnotations.render.mockClear();
projectAnnotations.decorators[0].mockClear();
docsRenderer.render.mockClear();
(logger.warn as jest.Mock<typeof logger.warn>).mockClear();
(logger.warn as jestMock.Mock<typeof logger.warn>).mockClear();
mockStoryIndex.mockReset().mockReturnValue(storyIndex);

addons.setChannel(mockChannel as any);
Expand Down Expand Up @@ -347,8 +348,12 @@ describe('PreviewWeb', () => {
});

describe('after selection changes', () => {
beforeEach(() => jest.useFakeTimers());
afterEach(() => jest.useRealTimers());
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});

it('DOES NOT try again if CSF file changes', async () => {
document.location.search = '?id=component-one--missing';
Expand Down Expand Up @@ -1561,49 +1566,140 @@ describe('PreviewWeb', () => {
expect(teardownRenderToDOM).not.toHaveBeenCalled();
});

// For https://github.com/storybookjs/storybook/issues/17214
it('does NOT render a second time if preparing', async () => {
document.location.search = '?id=component-one--a';
describe('while preparing', () => {
// For https://github.com/storybookjs/storybook/issues/17214
it('does NOT render a second time in story mode', async () => {
document.location.search = '?id=component-one--a';

const [gate, openGate] = createGate();
const [importedGate, openImportedGate] = createGate();
importFn
.mockImplementationOnce(async (...args) => {
await gate;
return importFn(...args);
})
.mockImplementationOnce(async (...args) => {
// The second time we `import()` we open the "imported" gate
openImportedGate();
await gate;
return importFn(...args);
const [gate, openGate] = createGate();
const [importedGate, openImportedGate] = createGate();
importFn
.mockImplementationOnce(async (...args) => {
await gate;
return importFn(...args);
})
.mockImplementationOnce(async (...args) => {
// The second time we `import()` we open the "imported" gate
openImportedGate();
await gate;
return importFn(...args);
});

const preview = new PreviewWeb();
// We can't wait for the initialize function, as it waits for `renderSelection()`
// which prepares, but it does emit `CURRENT_STORY_WAS_SET` right before that
preview.initialize({ importFn, getProjectAnnotations });
await waitForEvents([CURRENT_STORY_WAS_SET]);

mockChannel.emit.mockClear();
projectAnnotations.renderToDOM.mockClear();
emitter.emit(SET_CURRENT_STORY, {
storyId: 'component-one--a',
viewMode: 'story',
});
await importedGate;
// We are blocking import so this won't render yet
expect(projectAnnotations.renderToDOM).not.toHaveBeenCalled();

const preview = new PreviewWeb();
// We can't wait for the initialize function, as it waits for `renderSelection()`
// which prepares, but it does emit `CURRENT_STORY_WAS_SET` right before that
preview.initialize({ importFn, getProjectAnnotations });
await waitForEvents([CURRENT_STORY_WAS_SET]);
mockChannel.emit.mockClear();
openGate();
await waitForRender();

mockChannel.emit.mockClear();
projectAnnotations.renderToDOM.mockClear();
emitter.emit(SET_CURRENT_STORY, {
storyId: 'component-one--a',
viewMode: 'story',
// We should only render *once*
expect(projectAnnotations.renderToDOM).toHaveBeenCalledTimes(1);

// We should not show an error either
expect(preview.view.showErrorDisplay).not.toHaveBeenCalled();
});
await importedGate;
// We are blocking import so this won't render yet
expect(projectAnnotations.renderToDOM).not.toHaveBeenCalled();

mockChannel.emit.mockClear();
openGate();
await waitForRender();
// For https://github.com/storybookjs/storybook/issues/19015
it('does NOT render a second time in template docs mode', async () => {
document.location.search = '?id=component-one--docs&viewMode=docs';

// We should only render *once*
expect(projectAnnotations.renderToDOM).toHaveBeenCalledTimes(1);
const [gate, openGate] = createGate();
const [importedGate, openImportedGate] = createGate();
importFn
.mockImplementationOnce(async (...args) => {
await gate;
return importFn(...args);
})
.mockImplementationOnce(async (...args) => {
// The second time we `import()` we open the "imported" gate
openImportedGate();
await gate;
return importFn(...args);
});

const preview = new PreviewWeb();
// We can't wait for the initialize function, as it waits for `renderSelection()`
// which prepares, but it does emit `CURRENT_STORY_WAS_SET` right before that
preview.initialize({ importFn, getProjectAnnotations });
await waitForEvents([CURRENT_STORY_WAS_SET]);

// We should not show an error either
expect(preview.view.showErrorDisplay).not.toHaveBeenCalled();
mockChannel.emit.mockClear();
projectAnnotations.renderToDOM.mockClear();
emitter.emit(SET_CURRENT_STORY, {
storyId: 'component-one--docs',
viewMode: 'docs',
});
await importedGate;
// We are blocking import so this won't render yet
expect(docsRenderer.render).not.toHaveBeenCalled();

mockChannel.emit.mockClear();
openGate();
await waitForRender();

// We should only render *once*
expect(docsRenderer.render).toHaveBeenCalledTimes(1);

// We should not show an error either
expect(preview.view.showErrorDisplay).not.toHaveBeenCalled();
});

it('does NOT render a second time in standalone docs mode', async () => {
document.location.search = '?id=introduction--docs&viewMode=docs';

const [gate, openGate] = createGate();
const [importedGate, openImportedGate] = createGate();
importFn
.mockImplementationOnce(async (...args) => {
await gate;
return importFn(...args);
})
.mockImplementationOnce(async (...args) => {
// The second time we `import()` we open the "imported" gate
openImportedGate();
await gate;
return importFn(...args);
});

const preview = new PreviewWeb();
// We can't wait for the initialize function, as it waits for `renderSelection()`
// which prepares, but it does emit `CURRENT_STORY_WAS_SET` right before that
preview.initialize({ importFn, getProjectAnnotations });
await waitForEvents([CURRENT_STORY_WAS_SET]);

mockChannel.emit.mockClear();
projectAnnotations.renderToDOM.mockClear();
emitter.emit(SET_CURRENT_STORY, {
storyId: 'introduction--docs',
viewMode: 'docs',
});
await importedGate;
// We are blocking import so this won't render yet
expect(docsRenderer.render).not.toHaveBeenCalled();

mockChannel.emit.mockClear();
openGate();
await waitForRender();

// We should only render *once*
expect(docsRenderer.render).toHaveBeenCalledTimes(1);

// We should not show an error either
expect(preview.view.showErrorDisplay).not.toHaveBeenCalled();
});
});
});

Expand Down Expand Up @@ -2713,8 +2809,12 @@ describe('PreviewWeb', () => {
});

describe('if it was previously rendered', () => {
beforeEach(() => jest.useFakeTimers());
afterEach(() => jest.useRealTimers());
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it('is reloaded when it is re-selected', async () => {
document.location.search = '?id=component-one--a';
const preview = await createAndRenderPreview();
Expand Down
3 changes: 2 additions & 1 deletion code/lib/preview-web/src/PreviewWeb.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ import { MaybePromise, Preview } from './Preview';

import { UrlStore } from './UrlStore';
import { WebView } from './WebView';
import { PREPARE_ABORTED, StoryRender } from './render/StoryRender';
import { PREPARE_ABORTED } from './render/Render';
import { StoryRender } from './render/StoryRender';
import { TemplateDocsRender } from './render/TemplateDocsRender';
import { StandaloneDocsRender } from './render/StandaloneDocsRender';

Expand Down
2 changes: 2 additions & 0 deletions code/lib/preview-web/src/render/Render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,5 @@ export interface Render<TFramework extends AnyFramework> {
torndown: boolean;
renderToElement: (canvasElement: HTMLElement, renderStoryToElement?: any) => Promise<void>;
}

export const PREPARE_ABORTED = new Error('prepareAborted');
14 changes: 11 additions & 3 deletions code/lib/preview-web/src/render/StandaloneDocsRender.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { CSFFile, ModuleExports, StoryStore } from '@storybook/store';
import { Channel, IndexEntry } from '@storybook/addons';
import { DOCS_RENDERED } from '@storybook/core-events';

import { Render, RenderType } from './Render';
import { Render, RenderType, PREPARE_ABORTED } from './Render';
import type { DocsContextProps } from '../docs-context/DocsContextProps';
import type { DocsRenderFunction } from '../docs-context/DocsRenderFunction';
import { DocsContext } from '../docs-context/DocsContext';
Expand All @@ -26,7 +26,7 @@ export class StandaloneDocsRender<TFramework extends AnyFramework> implements Re

public rerender?: () => Promise<void>;

public teardown?: (options: { viewModeChanged?: boolean }) => Promise<void>;
public teardownRender?: (options: { viewModeChanged?: boolean }) => Promise<void>;

public torndown = false;

Expand All @@ -51,6 +51,9 @@ export class StandaloneDocsRender<TFramework extends AnyFramework> implements Re
async prepare() {
this.preparing = true;
const { entryExports, csfFiles = [] } = await this.store.loadEntry(this.id);

if (this.torndown) throw PREPARE_ABORTED;

this.csfFiles = csfFiles;
this.exports = entryExports;

Expand Down Expand Up @@ -96,12 +99,17 @@ export class StandaloneDocsRender<TFramework extends AnyFramework> implements Re
};

this.rerender = async () => renderDocs();
this.teardown = async ({ viewModeChanged }: { viewModeChanged?: boolean } = {}) => {
this.teardownRender = async ({ viewModeChanged }: { viewModeChanged?: boolean } = {}) => {
if (!viewModeChanged || !canvasElement) return;
renderer.unmount(canvasElement);
this.torndown = true;
};

return renderDocs();
}

async teardown({ viewModeChanged }: { viewModeChanged?: boolean } = {}) {
this.teardownRender?.({ viewModeChanged });
this.torndown = true;
}
}
6 changes: 2 additions & 4 deletions code/lib/preview-web/src/render/StoryRender.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
STORY_RENDERED,
PLAY_FUNCTION_THREW_EXCEPTION,
} from '@storybook/core-events';
import { Render, RenderType } from './Render';
import { Render, RenderType, PREPARE_ABORTED } from './Render';

const { AbortController } = global;

Expand Down Expand Up @@ -60,8 +60,6 @@ export type RenderContextCallbacks<TFramework extends AnyFramework> = Pick<
'showMain' | 'showError' | 'showException'
>;

export const PREPARE_ABORTED = new Error('prepareAborted');

export class StoryRender<TFramework extends AnyFramework> implements Render<TFramework> {
public type: RenderType = 'story';

Expand Down Expand Up @@ -275,7 +273,7 @@ export class StoryRender<TFramework extends AnyFramework> implements Render<TFra
this.abortController?.abort();
}

async teardown(options: {} = {}) {
async teardown() {
this.torndown = true;
this.cancelRender();

Expand Down
14 changes: 10 additions & 4 deletions code/lib/preview-web/src/render/TemplateDocsRender.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { CSFFile, Story, StoryStore } from '@storybook/store';
import { Channel, IndexEntry } from '@storybook/addons';
import { DOCS_RENDERED } from '@storybook/core-events';

import { Render, RenderType } from './Render';
import { Render, RenderType, PREPARE_ABORTED } from './Render';
import type { DocsContextProps } from '../docs-context/DocsContextProps';
import type { DocsRenderFunction } from '../docs-context/DocsRenderFunction';
import { DocsContext } from '../docs-context/DocsContext';
Expand All @@ -29,7 +29,7 @@ export class TemplateDocsRender<TFramework extends AnyFramework> implements Rend

public rerender?: () => Promise<void>;

public teardown?: (options: { viewModeChanged?: boolean }) => Promise<void>;
public teardownRender?: (options: { viewModeChanged?: boolean }) => Promise<void>;

public torndown = false;

Expand Down Expand Up @@ -70,6 +70,8 @@ export class TemplateDocsRender<TFramework extends AnyFramework> implements Rend
const primaryStoryId = Object.keys(primaryCsfFile.stories)[0];
this.story = this.store.storyFromCSFFile({ storyId: primaryStoryId, csfFile: primaryCsfFile });

if (this.torndown) throw PREPARE_ABORTED;

this.csfFiles = [primaryCsfFile, ...csfFiles];

this.preparing = false;
Expand Down Expand Up @@ -112,12 +114,16 @@ export class TemplateDocsRender<TFramework extends AnyFramework> implements Rend
};

this.rerender = async () => renderDocs();
this.teardown = async ({ viewModeChanged }: { viewModeChanged?: boolean } = {}) => {
this.teardownRender = async ({ viewModeChanged }: { viewModeChanged?: boolean }) => {
if (!viewModeChanged || !canvasElement) return;
renderer.unmount(canvasElement);
this.torndown = true;
};

return renderDocs();
}

async teardown({ viewModeChanged }: { viewModeChanged?: boolean } = {}) {
this.teardownRender?.({ viewModeChanged });
this.torndown = true;
}
}

0 comments on commit 05e51a1

Please sign in to comment.