/// ; import { EventEmitter } from 'events'; import { DOCS_RENDERED, STORY_ERRORED, STORY_MISSING, STORY_RENDERED, STORY_RENDER_PHASE_CHANGED, STORY_THREW_EXCEPTION, } from '@storybook/core-events'; import type { Store_StoryIndex, Store_TeardownRenderToDOM } from '@storybook/types'; import type { RenderPhase } from './render/StoryRender'; export const componentOneExports = { default: { title: 'Component One', argTypes: { foo: { type: { name: 'string' } }, }, loaders: [jest.fn()], parameters: { docs: { page: jest.fn(), container: jest.fn() }, }, }, a: { args: { foo: 'a' }, play: jest.fn() }, b: { args: { foo: 'b' }, play: jest.fn() }, }; export const componentTwoExports = { default: { title: 'Component Two' }, c: { args: { foo: 'c' } }, }; export const standaloneDocsExports = { default: jest.fn(), }; // If a second file defines stories for componentOne export const extraComponentOneExports = { default: { title: 'Component One', parameters: { docs: { page: jest.fn() }, }, }, e: {}, }; export const importFn = jest.fn( async (path: string) => ({ './src/ComponentOne.stories.js': componentOneExports, './src/ComponentTwo.stories.js': componentTwoExports, './src/Introduction.mdx': standaloneDocsExports, './src/ExtraComponentOne.stories.js': extraComponentOneExports, }[path]) ); export const docsRenderer = { render: jest.fn().mockImplementation((context, parameters, element, cb) => cb()), unmount: jest.fn(), }; export const teardownRenderToDOM: jest.Mock = jest.fn(); export const projectAnnotations = { globals: { a: 'b' }, globalTypes: {}, decorators: [jest.fn((s) => s())], render: jest.fn(), renderToDOM: jest.fn().mockReturnValue(teardownRenderToDOM), parameters: { docs: { renderer: () => docsRenderer } }, }; export const getProjectAnnotations = jest.fn(() => projectAnnotations as any); export const storyIndex: Store_StoryIndex = { v: 4, entries: { 'component-one--docs': { type: 'docs', id: 'component-one--docs', title: 'Component One', name: 'Docs', importPath: './src/ComponentOne.stories.js', storiesImports: ['./src/ExtraComponentOne.stories.js'], standalone: false, }, 'component-one--a': { type: 'story', id: 'component-one--a', title: 'Component One', name: 'A', importPath: './src/ComponentOne.stories.js', }, 'component-one--b': { type: 'story', id: 'component-one--b', title: 'Component One', name: 'B', importPath: './src/ComponentOne.stories.js', }, 'component-one--e': { type: 'story', id: 'component-one--e', title: 'Component One', name: 'E', importPath: './src/ExtraComponentOne.stories.js', }, 'component-two--docs': { type: 'docs', id: 'component-two--docs', title: 'Component Two', name: 'Docs', importPath: './src/ComponentTwo.stories.js', storiesImports: [], standalone: false, }, 'component-two--c': { type: 'story', id: 'component-two--c', title: 'Component Two', name: 'C', importPath: './src/ComponentTwo.stories.js', }, 'introduction--docs': { type: 'docs', id: 'introduction--docs', title: 'Introduction', name: 'Docs', importPath: './src/Introduction.mdx', storiesImports: ['./src/ComponentTwo.stories.js'], standalone: true, }, }, }; export const getStoryIndex = () => storyIndex; export const emitter = new EventEmitter(); export const mockChannel = { on: emitter.on.bind(emitter), off: emitter.off.bind(emitter), removeListener: emitter.off.bind(emitter), emit: jest.fn(emitter.emit.bind(emitter)), // emit: emitter.emit.bind(emitter), }; export const waitForEvents = ( events: string[], predicate: (...args: any[]) => boolean = () => true, debugLabel?: string ) => { // We've already emitted a render event. NOTE if you want to test a second call, // ensure you call `mockChannel.emit.mockClear()` before `waitFor...` if ( mockChannel.emit.mock.calls.find( (call: string[]) => events.includes(call[0]) && predicate(...call.slice(1)) ) ) { return Promise.resolve(null); } return new Promise((resolve, reject) => { const listener = (...args: any[]) => { if (!predicate(...args)) return; events.forEach((event) => mockChannel.off(event, listener)); resolve(null); }; events.forEach((event) => mockChannel.on(event, listener)); // Don't wait too long waitForQuiescence().then(() => reject(new Error(`Event was not emitted in time: ${debugLabel || events}`)) ); }); }; // The functions on the preview that trigger rendering don't wait for // the async parts, so we need to listen for the "done" events export const waitForRender = () => waitForEvents([ STORY_RENDERED, DOCS_RENDERED, STORY_THREW_EXCEPTION, STORY_ERRORED, STORY_MISSING, ]); export const waitForRenderPhase = (phase: RenderPhase) => { const label = `${STORY_RENDER_PHASE_CHANGED} to ${phase}`; return waitForEvents([STORY_RENDER_PHASE_CHANGED], ({ newPhase }) => newPhase === phase, label); }; // A little trick to ensure that we always call the real `setTimeout` even when timers are mocked const realSetTimeout = setTimeout; export const waitForQuiescence = async () => new Promise((r) => realSetTimeout(r, 100));