/* global window */
import { STORY_RENDERED, STORY_UNCHANGED, SET_INDEX } from '@storybook/core-events';

import type { Store_ModuleExports, Path, Loadable } from '@storybook/types';
import { setGlobalRender } from '@storybook/client-api';
import global from 'global';
import {
  waitForRender,
  waitForEvents,
  waitForQuiescence,
  emitter,
  mockChannel,
} from './PreviewWeb.mockdata';

import { start as realStart } from './start';

jest.mock('global', () => ({
  // @ts-expect-error (jest is not happy with this)
  ...jest.requireActual('global'),
  history: { replaceState: jest.fn() },
  document: {
    location: {
      pathname: 'pathname',
      search: '?id=*',
    },
  },
  FEATURES: {
    breakingChangesV7: true,
  },
  DOCS_OPTIONS: {
    enabled: true,
  },
}));

jest.mock('@storybook/channel-postmessage', () => ({ createChannel: () => mockChannel }));
jest.mock('react-dom');

// for the auto-title test
jest.mock('@storybook/store', () => {
  const actualStore = jest.requireActual('@storybook/store');
  return {
    ...actualStore,
    userOrAutoTitle: (importPath: Path, specifier: any, userTitle?: string) =>
      userTitle || 'auto-title',
  };
});

jest.mock('@storybook/preview-web', () => {
  const actualPreviewWeb = jest.requireActual('@storybook/preview-web');

  class OverloadPreviewWeb extends actualPreviewWeb.PreviewWeb {
    constructor() {
      super();

      this.view = {
        ...Object.fromEntries(
          Object.getOwnPropertyNames(this.view.constructor.prototype).map((key) => [key, jest.fn()])
        ),
        prepareForDocs: jest.fn().mockReturnValue('docs-root'),
        prepareForStory: jest.fn().mockReturnValue('story-root'),
      };
    }
  }
  return {
    ...actualPreviewWeb,
    PreviewWeb: OverloadPreviewWeb,
  };
});

beforeEach(() => {
  mockChannel.emit.mockClear();
  // Preview doesn't clean itself up as it isn't designed to ever be stopped :shrug:
  emitter.removeAllListeners();
});

const start: typeof realStart = (...args) => {
  const result = realStart(...args);

  const configure: typeof result['configure'] = (
    framework: string,
    loadable: Loadable,
    m?: NodeModule,
    disableBackwardCompatibility = false
  ) => result.configure(framework, loadable, m, disableBackwardCompatibility);

  return {
    ...result,
    configure,
  };
};
afterEach(() => {
  // I'm not sure why this is required (it seems just afterEach is required really)
  mockChannel.emit.mockClear();
});

function makeRequireContext(importMap: Record<Path, Store_ModuleExports>) {
  const req = (path: Path) => importMap[path];
  req.keys = () => Object.keys(importMap);
  return req;
}

describe('start', () => {
  beforeEach(() => {
    global.DOCS_OPTIONS = { enabled: false };
  });
  describe('when configure is called with storiesOf only', () => {
    it('loads and renders the first story correctly', async () => {
      const renderToDOM = jest.fn();

      const { configure, clientApi } = start(renderToDOM);

      configure('test', () => {
        clientApi
          .storiesOf('Component A', { id: 'file1' } as NodeModule)
          .add('Story One', jest.fn())
          .add('Story Two', jest.fn());

        clientApi
          .storiesOf('Component B', { id: 'file2' } as NodeModule)
          .add('Story Three', jest.fn());
      });

      await waitForRender();

      expect(mockChannel.emit.mock.calls.find((call: [string, any]) => call[0] === SET_INDEX)[1])
        .toMatchInlineSnapshot(`
        Object {
          "entries": Object {
            "component-a--story-one": Object {
              "argTypes": Object {},
              "args": Object {},
              "componentId": "component-a",
              "id": "component-a--story-one",
              "importPath": "file1",
              "initialArgs": Object {},
              "name": "Story One",
              "parameters": Object {
                "__id": "component-a--story-one",
                "__isArgsStory": false,
                "fileName": "file1",
                "framework": "test",
              },
              "tags": Array [
                "story",
              ],
              "title": "Component A",
              "type": "story",
            },
            "component-a--story-two": Object {
              "argTypes": Object {},
              "args": Object {},
              "componentId": "component-a",
              "id": "component-a--story-two",
              "importPath": "file1",
              "initialArgs": Object {},
              "name": "Story Two",
              "parameters": Object {
                "__id": "component-a--story-two",
                "__isArgsStory": false,
                "fileName": "file1",
                "framework": "test",
              },
              "tags": Array [
                "story",
              ],
              "title": "Component A",
              "type": "story",
            },
            "component-b--story-three": Object {
              "argTypes": Object {},
              "args": Object {},
              "componentId": "component-b",
              "id": "component-b--story-three",
              "importPath": "file2",
              "initialArgs": Object {},
              "name": "Story Three",
              "parameters": Object {
                "__id": "component-b--story-three",
                "__isArgsStory": false,
                "fileName": "file2",
                "framework": "test",
              },
              "tags": Array [
                "story",
              ],
              "title": "Component B",
              "type": "story",
            },
          },
          "v": 4,
        }
      `);

      await waitForRender();
      expect(mockChannel.emit).toHaveBeenCalledWith(STORY_RENDERED, 'component-a--story-one');

      expect(renderToDOM).toHaveBeenCalledWith(
        expect.objectContaining({
          id: 'component-a--story-one',
        }),
        'story-root'
      );
    });

    it('deals with stories with "default" name', async () => {
      const renderToDOM = jest.fn();

      const { configure, clientApi } = start(renderToDOM);

      configure('test', () => {
        clientApi.storiesOf('Component A', { id: 'file1' } as NodeModule).add('default', jest.fn());
      });

      await waitForRender();

      expect(mockChannel.emit).toHaveBeenCalledWith(STORY_RENDERED, 'component-a--default');
    });

    it('deals with stories with camel-cased names', async () => {
      const renderToDOM = jest.fn();

      const { configure, clientApi } = start(renderToDOM);

      configure('test', () => {
        clientApi
          .storiesOf('Component A', { id: 'file1' } as NodeModule)
          .add('storyOne', jest.fn());
      });

      await waitForRender();

      expect(mockChannel.emit).toHaveBeenCalledWith(STORY_RENDERED, 'component-a--storyone');
    });

    it('deals with stories with spaces in the name', async () => {
      const renderToDOM = jest.fn();

      const { configure, clientApi } = start(renderToDOM);

      configure('test', () => {
        clientApi
          .storiesOf('Component A', { id: 'file1' } as NodeModule)
          .add('Story One', jest.fn());
      });

      await waitForRender();

      expect(mockChannel.emit).toHaveBeenCalledWith(STORY_RENDERED, 'component-a--story-one');
    });

    // https://github.com/storybookjs/storybook/issues/16303
    it('deals with stories with numeric names', async () => {
      const renderToDOM = jest.fn();

      const { configure, clientApi } = start(renderToDOM);

      configure('test', () => {
        clientApi.storiesOf('Component A', { id: 'file1' } as NodeModule).add('story0', jest.fn());
      });

      await waitForRender();

      expect(mockChannel.emit).toHaveBeenCalledWith(STORY_RENDERED, 'component-a--story0');
    });

    it('deals with storiesOf from the same file twice', async () => {
      const renderToDOM = jest.fn();

      const { configure, clientApi } = start(renderToDOM);

      configure('test', () => {
        clientApi.storiesOf('Component A', { id: 'file1' } as NodeModule).add('default', jest.fn());
        clientApi.storiesOf('Component B', { id: 'file1' } as NodeModule).add('default', jest.fn());
        clientApi.storiesOf('Component C', { id: 'file1' } as NodeModule).add('default', jest.fn());
      });

      await waitForRender();
      expect(mockChannel.emit).toHaveBeenCalledWith(STORY_RENDERED, 'component-a--default');

      const storiesOfData = mockChannel.emit.mock.calls.find(
        (call: any[]) => call[0] === SET_INDEX
      )[1];
      expect(Object.values(storiesOfData.entries).map((s: any) => s.parameters.fileName)).toEqual([
        'file1',
        'file1-2',
        'file1-3',
      ]);
    });

    it('allows setting compomnent/args/argTypes via a parameter', async () => {
      const renderToDOM = jest.fn(({ storyFn }) => storyFn());

      const { configure, clientApi } = start(renderToDOM);

      const component = {};
      configure('test', () => {
        clientApi
          .storiesOf('Component A', { id: 'file1' } as NodeModule)
          .addParameters({
            component,
            args: { a: 'a' },
            argTypes: { a: { type: 'string' } },
          })
          .add('default', jest.fn(), {
            args: { b: 'b' },
            argTypes: { b: { type: 'string' } },
          });
      });

      await waitForRender();

      expect(renderToDOM).toHaveBeenCalledWith(
        expect.objectContaining({
          storyContext: expect.objectContaining({
            component,
            args: { a: 'a', b: 'b' },
            argTypes: {
              a: { name: 'a', type: { name: 'string' } },
              b: { name: 'b', type: { name: 'string' } },
            },
          }),
        }),
        'story-root'
      );

      expect((window as any).IS_STORYBOOK).toBe(true);
    });

    it('supports forceRerender()', async () => {
      const renderToDOM = jest.fn(({ storyFn }) => storyFn());

      const { configure, clientApi, forceReRender } = start(renderToDOM);

      configure('test', () => {
        clientApi.storiesOf('Component A', { id: 'file1' } as NodeModule).add('default', jest.fn());
      });

      await waitForRender();
      expect(mockChannel.emit).toHaveBeenCalledWith(STORY_RENDERED, 'component-a--default');

      mockChannel.emit.mockClear();
      forceReRender();

      await waitForRender();
      expect(mockChannel.emit).toHaveBeenCalledWith(STORY_RENDERED, 'component-a--default');
    });

    it('supports HMR when a story file changes', async () => {
      const renderToDOM = jest.fn(({ storyFn }) => storyFn());

      const { configure, clientApi } = start(renderToDOM);

      let disposeCallback: () => void = () => {};
      const module = {
        id: 'file1',
        hot: {
          accept: jest.fn(),
          dispose(cb: () => void) {
            disposeCallback = cb;
          },
        },
      };
      const firstImplementation = jest.fn();
      configure('test', () => {
        clientApi.storiesOf('Component A', module as any).add('default', firstImplementation);
      });

      await waitForRender();
      expect(mockChannel.emit).toHaveBeenCalledWith(STORY_RENDERED, 'component-a--default');
      expect(firstImplementation).toHaveBeenCalled();
      expect(module.hot.accept).toHaveBeenCalled();
      expect(disposeCallback).toBeDefined();

      mockChannel.emit.mockClear();
      disposeCallback();
      const secondImplementation = jest.fn();
      clientApi.storiesOf('Component A', module as any).add('default', secondImplementation);

      await waitForRender();
      expect(mockChannel.emit).toHaveBeenCalledWith(STORY_RENDERED, 'component-a--default');
      expect(secondImplementation).toHaveBeenCalled();
    });

    it('re-emits SET_INDEX when a story is added', async () => {
      const renderToDOM = jest.fn(({ storyFn }) => storyFn());

      const { configure, clientApi, forceReRender } = start(renderToDOM);

      let disposeCallback: () => void = () => {};
      const module = {
        id: 'file1',
        hot: {
          accept: jest.fn(),
          dispose(cb: () => void) {
            disposeCallback = cb;
          },
        },
      };
      configure('test', () => {
        clientApi.storiesOf('Component A', module as any).add('default', jest.fn());
      });

      await waitForRender();

      mockChannel.emit.mockClear();
      disposeCallback();
      clientApi
        .storiesOf('Component A', module as any)
        .add('default', jest.fn())
        .add('new', jest.fn());

      await waitForEvents([SET_INDEX]);
      expect(mockChannel.emit.mock.calls.find((call: [string, any]) => call[0] === SET_INDEX)[1])
        .toMatchInlineSnapshot(`
        Object {
          "entries": Object {
            "component-a--default": Object {
              "argTypes": Object {},
              "args": Object {},
              "componentId": "component-a",
              "id": "component-a--default",
              "importPath": "file1",
              "initialArgs": Object {},
              "name": "default",
              "parameters": Object {
                "__id": "component-a--default",
                "__isArgsStory": false,
                "fileName": "file1",
                "framework": "test",
              },
              "tags": Array [
                "story",
              ],
              "title": "Component A",
              "type": "story",
            },
            "component-a--new": Object {
              "argTypes": Object {},
              "args": Object {},
              "componentId": "component-a",
              "id": "component-a--new",
              "importPath": "file1",
              "initialArgs": Object {},
              "name": "new",
              "parameters": Object {
                "__id": "component-a--new",
                "__isArgsStory": false,
                "fileName": "file1",
                "framework": "test",
              },
              "tags": Array [
                "story",
              ],
              "title": "Component A",
              "type": "story",
            },
          },
          "v": 4,
        }
      `);
    });

    it('re-emits SET_INDEX when a story file is removed', async () => {
      const renderToDOM = jest.fn(({ storyFn }) => storyFn());

      const { configure, clientApi } = start(renderToDOM);

      let disposeCallback: () => void = () => {};
      const moduleB = {
        id: 'file2',
        hot: {
          accept: jest.fn(),
          dispose(cb: () => void) {
            disposeCallback = cb;
          },
        },
      };
      configure('test', () => {
        clientApi.storiesOf('Component A', { id: 'file1' } as any).add('default', jest.fn());
        clientApi.storiesOf('Component B', moduleB as any).add('default', jest.fn());
      });

      await waitForEvents([SET_INDEX]);
      expect(mockChannel.emit.mock.calls.find((call: [string, any]) => call[0] === SET_INDEX)[1])
        .toMatchInlineSnapshot(`
        Object {
          "entries": Object {
            "component-a--default": Object {
              "argTypes": Object {},
              "args": Object {},
              "componentId": "component-a",
              "id": "component-a--default",
              "importPath": "file1",
              "initialArgs": Object {},
              "name": "default",
              "parameters": Object {
                "__id": "component-a--default",
                "__isArgsStory": false,
                "fileName": "file1",
                "framework": "test",
              },
              "tags": Array [
                "story",
              ],
              "title": "Component A",
              "type": "story",
            },
            "component-b--default": Object {
              "argTypes": Object {},
              "args": Object {},
              "componentId": "component-b",
              "id": "component-b--default",
              "importPath": "file2",
              "initialArgs": Object {},
              "name": "default",
              "parameters": Object {
                "__id": "component-b--default",
                "__isArgsStory": false,
                "fileName": "file2",
                "framework": "test",
              },
              "tags": Array [
                "story",
              ],
              "title": "Component B",
              "type": "story",
            },
          },
          "v": 4,
        }
      `);
      mockChannel.emit.mockClear();
      disposeCallback();

      await waitForEvents([SET_INDEX]);
      expect(mockChannel.emit.mock.calls.find((call: [string, any]) => call[0] === SET_INDEX)[1])
        .toMatchInlineSnapshot(`
        Object {
          "entries": Object {
            "component-a--default": Object {
              "argTypes": Object {},
              "args": Object {},
              "componentId": "component-a",
              "id": "component-a--default",
              "importPath": "file1",
              "initialArgs": Object {},
              "name": "default",
              "parameters": Object {
                "__id": "component-a--default",
                "__isArgsStory": false,
                "fileName": "file1",
                "framework": "test",
              },
              "tags": Array [
                "story",
              ],
              "title": "Component A",
              "type": "story",
            },
          },
          "v": 4,
        }
      `);
    });
  });

  const componentCExports = {
    default: {
      title: 'Component C',
      tags: ['component-tag', 'docsPage'],
    },
    StoryOne: {
      render: jest.fn(),
      tags: ['story-tag'],
    },
    StoryTwo: jest.fn(),
  };

  describe('when configure is called with CSF only', () => {
    it('loads and renders the first story correctly', async () => {
      const renderToDOM = jest.fn();

      const { configure } = start(renderToDOM);
      configure('test', () => [componentCExports]);

      await waitForRender();
      expect(mockChannel.emit.mock.calls.find((call: [string, any]) => call[0] === SET_INDEX)[1])
        .toMatchInlineSnapshot(`
        Object {
          "entries": Object {
            "component-c--story-one": Object {
              "argTypes": Object {},
              "args": Object {},
              "id": "component-c--story-one",
              "importPath": "exports-map-0",
              "initialArgs": Object {},
              "name": "Story One",
              "parameters": Object {
                "__isArgsStory": false,
                "fileName": "exports-map-0",
                "framework": "test",
              },
              "tags": Array [
                "story-tag",
                "story",
              ],
              "title": "Component C",
              "type": "story",
            },
            "component-c--story-two": Object {
              "argTypes": Object {},
              "args": Object {},
              "id": "component-c--story-two",
              "importPath": "exports-map-0",
              "initialArgs": Object {},
              "name": "Story Two",
              "parameters": Object {
                "__isArgsStory": false,
                "fileName": "exports-map-0",
                "framework": "test",
              },
              "tags": Array [
                "component-tag",
                "docsPage",
                "story",
              ],
              "title": "Component C",
              "type": "story",
            },
          },
          "v": 4,
        }
      `);

      await waitForRender();
      expect(mockChannel.emit).toHaveBeenCalledWith(STORY_RENDERED, 'component-c--story-one');

      expect(renderToDOM).toHaveBeenCalledWith(
        expect.objectContaining({
          id: 'component-c--story-one',
        }),
        'story-root'
      );
    });

    it('supports HMR when a story file changes', async () => {
      const renderToDOM = jest.fn(({ storyFn }) => storyFn());

      let disposeCallback: (data: object) => void = () => {};
      const module = {
        id: 'file1',
        hot: {
          data: {},
          accept: jest.fn(),
          dispose(cb: () => void) {
            disposeCallback = cb;
          },
        },
      };

      const { configure } = start(renderToDOM);
      configure('test', () => [componentCExports], module as any);

      await waitForRender();
      expect(mockChannel.emit).toHaveBeenCalledWith(STORY_RENDERED, 'component-c--story-one');
      expect(componentCExports.StoryOne.render).toHaveBeenCalled();
      expect(module.hot.accept).toHaveBeenCalled();
      expect(disposeCallback).toBeDefined();

      mockChannel.emit.mockClear();
      disposeCallback(module.hot.data);
      const secondImplementation = jest.fn();
      configure(
        'test',
        () => [{ ...componentCExports, StoryOne: secondImplementation }],
        module as any
      );

      await waitForRender();
      expect(mockChannel.emit).toHaveBeenCalledWith(STORY_RENDERED, 'component-c--story-one');
      expect(secondImplementation).toHaveBeenCalled();
    });

    it('re-emits SET_INDEX when a story is added', async () => {
      const renderToDOM = jest.fn(({ storyFn }) => storyFn());

      let disposeCallback: (data: object) => void = () => {};
      const module = {
        id: 'file1',
        hot: {
          data: {},
          accept: jest.fn(),
          dispose(cb: () => void) {
            disposeCallback = cb;
          },
        },
      };
      const { configure } = start(renderToDOM);
      configure('test', () => [componentCExports], module as any);

      await waitForRender();

      mockChannel.emit.mockClear();
      disposeCallback(module.hot.data);
      configure('test', () => [{ ...componentCExports, StoryThree: jest.fn() }], module as any);

      await waitForEvents([SET_INDEX]);
      expect(mockChannel.emit.mock.calls.find((call: [string, any]) => call[0] === SET_INDEX)[1])
        .toMatchInlineSnapshot(`
        Object {
          "entries": Object {
            "component-c--story-one": Object {
              "argTypes": Object {},
              "args": Object {},
              "id": "component-c--story-one",
              "importPath": "exports-map-0",
              "initialArgs": Object {},
              "name": "Story One",
              "parameters": Object {
                "__isArgsStory": false,
                "fileName": "exports-map-0",
                "framework": "test",
              },
              "tags": Array [
                "story-tag",
                "story",
              ],
              "title": "Component C",
              "type": "story",
            },
            "component-c--story-three": Object {
              "argTypes": Object {},
              "args": Object {},
              "id": "component-c--story-three",
              "importPath": "exports-map-0",
              "initialArgs": Object {},
              "name": "Story Three",
              "parameters": Object {
                "__isArgsStory": false,
                "fileName": "exports-map-0",
                "framework": "test",
              },
              "tags": Array [
                "component-tag",
                "docsPage",
                "story",
              ],
              "title": "Component C",
              "type": "story",
            },
            "component-c--story-two": Object {
              "argTypes": Object {},
              "args": Object {},
              "id": "component-c--story-two",
              "importPath": "exports-map-0",
              "initialArgs": Object {},
              "name": "Story Two",
              "parameters": Object {
                "__isArgsStory": false,
                "fileName": "exports-map-0",
                "framework": "test",
              },
              "tags": Array [
                "component-tag",
                "docsPage",
                "story",
              ],
              "title": "Component C",
              "type": "story",
            },
          },
          "v": 4,
        }
      `);
    });

    it('re-emits SET_INDEX when a story file is removed', async () => {
      const renderToDOM = jest.fn(({ storyFn }) => storyFn());

      let disposeCallback: (data: object) => void = () => {};
      const module = {
        id: 'file1',
        hot: {
          data: {},
          accept: jest.fn(),
          dispose(cb: () => void) {
            disposeCallback = cb;
          },
        },
      };
      const { configure } = start(renderToDOM);
      configure(
        'test',
        () => [componentCExports, { default: { title: 'Component D' }, StoryFour: jest.fn() }],
        module as any
      );

      await waitForEvents([SET_INDEX]);
      expect(mockChannel.emit.mock.calls.find((call: [string, any]) => call[0] === SET_INDEX)[1])
        .toMatchInlineSnapshot(`
        Object {
          "entries": Object {
            "component-c--story-one": Object {
              "argTypes": Object {},
              "args": Object {},
              "id": "component-c--story-one",
              "importPath": "exports-map-0",
              "initialArgs": Object {},
              "name": "Story One",
              "parameters": Object {
                "__isArgsStory": false,
                "fileName": "exports-map-0",
                "framework": "test",
              },
              "tags": Array [
                "story-tag",
                "story",
              ],
              "title": "Component C",
              "type": "story",
            },
            "component-c--story-two": Object {
              "argTypes": Object {},
              "args": Object {},
              "id": "component-c--story-two",
              "importPath": "exports-map-0",
              "initialArgs": Object {},
              "name": "Story Two",
              "parameters": Object {
                "__isArgsStory": false,
                "fileName": "exports-map-0",
                "framework": "test",
              },
              "tags": Array [
                "component-tag",
                "docsPage",
                "story",
              ],
              "title": "Component C",
              "type": "story",
            },
            "component-d--story-four": Object {
              "argTypes": Object {},
              "args": Object {},
              "id": "component-d--story-four",
              "importPath": "exports-map-1",
              "initialArgs": Object {},
              "name": "Story Four",
              "parameters": Object {
                "__isArgsStory": false,
                "fileName": "exports-map-1",
                "framework": "test",
              },
              "tags": Array [
                "story",
              ],
              "title": "Component D",
              "type": "story",
            },
          },
          "v": 4,
        }
      `);
      await waitForRender();

      mockChannel.emit.mockClear();
      disposeCallback(module.hot.data);
      configure('test', () => [componentCExports], module as any);

      await waitForEvents([SET_INDEX]);
      expect(mockChannel.emit.mock.calls.find((call: [string, any]) => call[0] === SET_INDEX)[1])
        .toMatchInlineSnapshot(`
        Object {
          "entries": Object {
            "component-c--story-one": Object {
              "argTypes": Object {},
              "args": Object {},
              "id": "component-c--story-one",
              "importPath": "exports-map-0",
              "initialArgs": Object {},
              "name": "Story One",
              "parameters": Object {
                "__isArgsStory": false,
                "fileName": "exports-map-0",
                "framework": "test",
              },
              "tags": Array [
                "story-tag",
                "story",
              ],
              "title": "Component C",
              "type": "story",
            },
            "component-c--story-two": Object {
              "argTypes": Object {},
              "args": Object {},
              "id": "component-c--story-two",
              "importPath": "exports-map-0",
              "initialArgs": Object {},
              "name": "Story Two",
              "parameters": Object {
                "__isArgsStory": false,
                "fileName": "exports-map-0",
                "framework": "test",
              },
              "tags": Array [
                "component-tag",
                "docsPage",
                "story",
              ],
              "title": "Component C",
              "type": "story",
            },
          },
          "v": 4,
        }
      `);

      await waitForEvents([STORY_UNCHANGED]);
    });

    it('allows you to override the render function in project annotations', async () => {
      const renderToDOM = jest.fn(({ storyFn }) => storyFn());
      const frameworkRender = jest.fn();

      const { configure } = start(renderToDOM, { render: frameworkRender });

      const projectRender = jest.fn();
      setGlobalRender(projectRender);
      configure('test', () => {
        return [
          {
            default: {
              title: 'Component A',
              component: jest.fn(),
            },
            StoryOne: {},
          },
        ];
      });

      await waitForRender();
      expect(mockChannel.emit).toHaveBeenCalledWith(STORY_RENDERED, 'component-a--story-one');

      expect(frameworkRender).not.toHaveBeenCalled();
      expect(projectRender).toHaveBeenCalled();
    });

    describe('docs', () => {
      beforeEach(() => {
        global.DOCS_OPTIONS = { enabled: true };
      });

      // NOTE: MDX files are only ever passed as CSF
      it('sends over docs only stories as entries', async () => {
        const renderToDOM = jest.fn();

        const { configure } = start(renderToDOM);

        configure(
          'test',
          makeRequireContext({
            './Introduction.stories.mdx': {
              default: { title: 'Introduction' },
              _Page: { name: 'Page', parameters: { docsOnly: true } },
            },
          })
        );

        await waitForEvents([SET_INDEX]);
        expect(mockChannel.emit.mock.calls.find((call: [string, any]) => call[0] === SET_INDEX)[1])
          .toMatchInlineSnapshot(`
          Object {
            "entries": Object {
              "introduction": Object {
                "id": "introduction",
                "importPath": "./Introduction.stories.mdx",
                "name": undefined,
                "standalone": false,
                "storiesImports": Array [],
                "tags": Array [
                  "docs",
                ],
                "title": "Introduction",
                "type": "docs",
              },
            },
            "v": 4,
          }
        `);

        // Wait a second to let the docs "render" finish (and maybe throw)
        await waitForQuiescence();
      });
    });
  });

  describe('when configure is called with a combination', () => {
    it('loads and renders the first story correctly', async () => {
      const renderToDOM = jest.fn();

      const { configure, clientApi } = start(renderToDOM);
      configure('test', () => {
        clientApi
          .storiesOf('Component A', { id: 'file1' } as NodeModule)
          .add('Story One', jest.fn())
          .add('Story Two', jest.fn());

        clientApi
          .storiesOf('Component B', { id: 'file2' } as NodeModule)
          .add('Story Three', jest.fn());

        return [componentCExports];
      });

      await waitForRender();
      expect(mockChannel.emit.mock.calls.find((call: [string, any]) => call[0] === SET_INDEX)[1])
        .toMatchInlineSnapshot(`
        Object {
          "entries": Object {
            "component-a--story-one": Object {
              "argTypes": Object {},
              "args": Object {},
              "componentId": "component-a",
              "id": "component-a--story-one",
              "importPath": "file1",
              "initialArgs": Object {},
              "name": "Story One",
              "parameters": Object {
                "__id": "component-a--story-one",
                "__isArgsStory": false,
                "fileName": "file1",
                "framework": "test",
              },
              "tags": Array [
                "story",
              ],
              "title": "Component A",
              "type": "story",
            },
            "component-a--story-two": Object {
              "argTypes": Object {},
              "args": Object {},
              "componentId": "component-a",
              "id": "component-a--story-two",
              "importPath": "file1",
              "initialArgs": Object {},
              "name": "Story Two",
              "parameters": Object {
                "__id": "component-a--story-two",
                "__isArgsStory": false,
                "fileName": "file1",
                "framework": "test",
              },
              "tags": Array [
                "story",
              ],
              "title": "Component A",
              "type": "story",
            },
            "component-b--story-three": Object {
              "argTypes": Object {},
              "args": Object {},
              "componentId": "component-b",
              "id": "component-b--story-three",
              "importPath": "file2",
              "initialArgs": Object {},
              "name": "Story Three",
              "parameters": Object {
                "__id": "component-b--story-three",
                "__isArgsStory": false,
                "fileName": "file2",
                "framework": "test",
              },
              "tags": Array [
                "story",
              ],
              "title": "Component B",
              "type": "story",
            },
            "component-c--story-one": Object {
              "argTypes": Object {},
              "args": Object {},
              "id": "component-c--story-one",
              "importPath": "exports-map-0",
              "initialArgs": Object {},
              "name": "Story One",
              "parameters": Object {
                "__isArgsStory": false,
                "fileName": "exports-map-0",
                "framework": "test",
              },
              "tags": Array [
                "story-tag",
                "story",
              ],
              "title": "Component C",
              "type": "story",
            },
            "component-c--story-two": Object {
              "argTypes": Object {},
              "args": Object {},
              "id": "component-c--story-two",
              "importPath": "exports-map-0",
              "initialArgs": Object {},
              "name": "Story Two",
              "parameters": Object {
                "__isArgsStory": false,
                "fileName": "exports-map-0",
                "framework": "test",
              },
              "tags": Array [
                "component-tag",
                "docsPage",
                "story",
              ],
              "title": "Component C",
              "type": "story",
            },
          },
          "v": 4,
        }
      `);

      await waitForRender();
      expect(mockChannel.emit).toHaveBeenCalledWith(STORY_RENDERED, 'component-a--story-one');

      expect(renderToDOM).toHaveBeenCalledWith(
        expect.objectContaining({
          id: 'component-a--story-one',
        }),
        'story-root'
      );
    });

    describe('docsPage', () => {
      beforeEach(() => {
        global.DOCS_OPTIONS = { enabled: true, docsPage: true, defaultName: 'Docs' };
      });

      it('adds stories for each component with docsPage tag', async () => {
        const renderToDOM = jest.fn();

        const { configure, clientApi } = start(renderToDOM);
        configure('test', () => {
          clientApi
            .storiesOf('Component A', { id: 'file1' } as NodeModule)
            .add('Story One', jest.fn())
            .add('Story Two', jest.fn());

          clientApi
            .storiesOf('Component B', { id: 'file2' } as NodeModule)
            .addParameters({ tags: ['docsPage'] })
            .add('Story Three', jest.fn());

          return [componentCExports];
        });

        await waitForRender();
        expect(mockChannel.emit.mock.calls.find((call: [string, any]) => call[0] === SET_INDEX)[1])
          .toMatchInlineSnapshot(`
          Object {
            "entries": Object {
              "component-a--story-one": Object {
                "argTypes": Object {},
                "args": Object {},
                "componentId": "component-a",
                "id": "component-a--story-one",
                "importPath": "file1",
                "initialArgs": Object {},
                "name": "Story One",
                "parameters": Object {
                  "__id": "component-a--story-one",
                  "__isArgsStory": false,
                  "fileName": "file1",
                  "framework": "test",
                },
                "tags": Array [
                  "story",
                ],
                "title": "Component A",
                "type": "story",
              },
              "component-a--story-two": Object {
                "argTypes": Object {},
                "args": Object {},
                "componentId": "component-a",
                "id": "component-a--story-two",
                "importPath": "file1",
                "initialArgs": Object {},
                "name": "Story Two",
                "parameters": Object {
                  "__id": "component-a--story-two",
                  "__isArgsStory": false,
                  "fileName": "file1",
                  "framework": "test",
                },
                "tags": Array [
                  "story",
                ],
                "title": "Component A",
                "type": "story",
              },
              "component-b--docs": Object {
                "componentId": "component-b",
                "id": "component-b--docs",
                "importPath": "file2",
                "name": "Docs",
                "standalone": false,
                "storiesImports": Array [],
                "tags": Array [
                  "docsPage",
                  "docs",
                ],
                "title": "Component B",
                "type": "docs",
              },
              "component-b--story-three": Object {
                "argTypes": Object {},
                "args": Object {},
                "componentId": "component-b",
                "id": "component-b--story-three",
                "importPath": "file2",
                "initialArgs": Object {},
                "name": "Story Three",
                "parameters": Object {
                  "__id": "component-b--story-three",
                  "__isArgsStory": false,
                  "fileName": "file2",
                  "framework": "test",
                },
                "tags": Array [
                  "docsPage",
                  "story",
                ],
                "title": "Component B",
                "type": "story",
              },
              "component-c--docs": Object {
                "id": "component-c--docs",
                "importPath": "exports-map-0",
                "name": "Docs",
                "standalone": false,
                "storiesImports": Array [],
                "tags": Array [
                  "component-tag",
                  "docsPage",
                  "docs",
                ],
                "title": "Component C",
                "type": "docs",
              },
              "component-c--story-one": Object {
                "argTypes": Object {},
                "args": Object {},
                "id": "component-c--story-one",
                "importPath": "exports-map-0",
                "initialArgs": Object {},
                "name": "Story One",
                "parameters": Object {
                  "__isArgsStory": false,
                  "fileName": "exports-map-0",
                  "framework": "test",
                },
                "tags": Array [
                  "story-tag",
                  "story",
                ],
                "title": "Component C",
                "type": "story",
              },
              "component-c--story-two": Object {
                "argTypes": Object {},
                "args": Object {},
                "id": "component-c--story-two",
                "importPath": "exports-map-0",
                "initialArgs": Object {},
                "name": "Story Two",
                "parameters": Object {
                  "__isArgsStory": false,
                  "fileName": "exports-map-0",
                  "framework": "test",
                },
                "tags": Array [
                  "component-tag",
                  "docsPage",
                  "story",
                ],
                "title": "Component C",
                "type": "story",
              },
            },
            "v": 4,
          }
        `);
      });
    });
  });

  describe('auto-title', () => {
    const componentDExports = {
      default: {
        component: 'Component D',
      },
      StoryOne: jest.fn(),
    };
    it('loads and renders the first story correctly', async () => {
      const renderToDOM = jest.fn();

      const { configure } = start(renderToDOM);
      configure('test', () => [componentDExports]);

      await waitForEvents([SET_INDEX]);
      expect(mockChannel.emit.mock.calls.find((call: [string, any]) => call[0] === SET_INDEX)[1])
        .toMatchInlineSnapshot(`
        Object {
          "entries": Object {
            "auto-title--story-one": Object {
              "argTypes": Object {},
              "args": Object {},
              "id": "auto-title--story-one",
              "importPath": "exports-map-0",
              "initialArgs": Object {},
              "name": "Story One",
              "parameters": Object {
                "__isArgsStory": false,
                "fileName": "exports-map-0",
                "framework": "test",
              },
              "tags": Array [
                "story",
              ],
              "title": "auto-title",
              "type": "story",
            },
          },
          "v": 4,
        }
      `);

      await waitForRender();
    });
  });
});
