mirror of
https://github.com/storybookjs/storybook.git
synced 2025-04-08 00:52:12 +08:00
192 lines
5.4 KiB
TypeScript
192 lines
5.4 KiB
TypeScript
/// <reference types="@types/jest" />;
|
|
|
|
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<Store_TeardownRenderToDOM> = 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));
|