Skip to content

Commit

Permalink
Merge pull request #3559 from storybooks/tmeasday/refactor-transition…
Browse files Browse the repository at this point in the history
…al-decorator

Refactor transitional decorator from addon-notes
  • Loading branch information
Hypnosphi authored May 15, 2018
2 parents 38992be + a2e57cf commit f2019e3
Show file tree
Hide file tree
Showing 7 changed files with 191 additions and 33 deletions.
14 changes: 13 additions & 1 deletion addons/notes/src/__tests__/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import addons from '@storybook/addons';
import { withNotes } from '..';

jest.mock('@storybook/addons');
addons.getChannel = jest.fn();

describe('Storybook Addon Notes', () => {
it('should inject text from `notes` parameter', () => {
Expand All @@ -16,6 +16,18 @@ describe('Storybook Addon Notes', () => {
expect(getStory).toHaveBeenCalledWith(context);
});

it('should NOT inject text if no `notes` parameter', () => {
const channel = { emit: jest.fn() };
addons.getChannel.mockReturnValue(channel);

const getStory = jest.fn();
const context = {};

withNotes(getStory, context);
expect(channel.emit).not.toHaveBeenCalled();
expect(getStory).toHaveBeenCalledWith(context);
});

it('should inject markdown from `notes.markdown` parameter', () => {
const channel = { emit: jest.fn() };
addons.getChannel.mockReturnValue(channel);
Expand Down
47 changes: 17 additions & 30 deletions addons/notes/src/index.js
Original file line number Diff line number Diff line change
@@ -1,48 +1,35 @@
import addons from '@storybook/addons';
import addons, { makeDecorator } from '@storybook/addons';
import marked from 'marked';

function renderMarkdown(text, options) {
marked.setOptions({ ...marked.defaults, options });
return marked(text);
}

const decorator = options => {
const channel = addons.getChannel();
return (getStory, context) => {
const {
parameters: { notes },
} = context;
const storyOptions = notes || options;
export const withNotes = makeDecorator({
name: 'withNotes',
parameterName: 'notes',
skipIfNoParametersOrOptions: true,
wrapper: (getStory, context, { options, parameters }) => {
const channel = addons.getChannel();

if (storyOptions) {
const { text, markdown, markdownOptions } =
typeof storyOptions === 'string' ? { text: storyOptions } : storyOptions;
const storyOptions = parameters || options;

if (!text && !markdown) {
throw new Error('You must set of one of `text` or `markdown` on the `notes` parameter');
}
const { text, markdown, markdownOptions } =
typeof storyOptions === 'string' ? { text: storyOptions } : storyOptions;

channel.emit('storybook/notes/add_notes', text || renderMarkdown(markdown, markdownOptions));
if (!text && !markdown) {
throw new Error('You must set of one of `text` or `markdown` on the `notes` parameter');
}

return getStory(context);
};
};
channel.emit('storybook/notes/add_notes', text || renderMarkdown(markdown, markdownOptions));

const hoc = options => story => context => decorator(options)(story, context);
return getStory(context);
},
});

export const withMarkdownNotes = (text, options) =>
hoc({
withNotes({
markdown: text,
markdownOptions: options,
});

export const withNotes = (...args) => {
// Used without options as .addDecorator(withNotes)
if (typeof args[0] === 'function') {
return decorator()(...args);
}

// Input are options, ala .add('name', withNotes('note')(() => <Story/>))
return hoc(args[0]);
};
3 changes: 2 additions & 1 deletion lib/addons/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
},
"dependencies": {
"@storybook/channels": "4.0.0-alpha.6",
"global": "^4.3.2"
"global": "^4.3.2",
"util-deprecate": "^1.0.2"
}
}
1 change: 1 addition & 0 deletions lib/addons/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import global from 'global';

export mockChannel from './storybook-channel-mock';
export { makeDecorator } from './make-decorator';

export class AddonStore {
constructor() {
Expand Down
51 changes: 51 additions & 0 deletions lib/addons/src/make-decorator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import deprecate from 'util-deprecate';

// Create a decorator that can be used both in the (deprecated) old "hoc" style:
// .add('story', decorator(options)(() => <Story />));
//
// And in the new, "parameterized" style:
// .addDecorator(decorator)
// .add('story', () => <Story />, { name: { parameters } });
//
// *And* in the older, but not deprecated, "pass options to decorator" style:
// .addDecorator(decorator(options))

export const makeDecorator = ({
name,
parameterName,
wrapper,
skipIfNoParametersOrOptions = false,
}) => {
const decorator = options => (getStory, context) => {
const parameters = context.parameters && context.parameters[parameterName];

if (skipIfNoParametersOrOptions && !options && !parameters) {
return getStory(context);
}
return wrapper(getStory, context, {
options,
parameters,
});
};

return (...args) => {
// Used without options as .addDecorator(decorator)
if (typeof args[0] === 'function') {
return decorator()(...args);
}

return (...innerArgs) => {
// Used as [.]addDecorator(decorator(options))
if (innerArgs.length > 1) {
return decorator(...args)(...innerArgs);
}

// Used to wrap a story directly .add('story', decorator(options)(() => <Story />))
// This is now deprecated:
return deprecate(
context => decorator(...args)(innerArgs[0], context),
`Passing stories directly into ${name}() is deprecated, instead use addDecorator(${name}) and pass options with the '${parameterName}' parameter`
);
};
};
};
106 changes: 106 additions & 0 deletions lib/addons/src/make-decorator.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import deprecate from 'util-deprecate';
import { makeDecorator } from './make-decorator';
import { defaultDecorateStory } from '../../../lib/core/src/client/preview/client_api';

jest.mock('util-deprecate');
let deprecatedFns = [];
deprecate.mockImplementation((fn, warning) => {
const deprecatedFn = jest.fn(fn);
deprecatedFns.push({
deprecatedFn,
warning,
});
return deprecatedFn;
});

describe('makeDecorator', () => {
it('returns a decorator that passes parameters on the parameters argument', () => {
const wrapper = jest.fn();
const decorator = makeDecorator({ wrapper, name: 'test', parameterName: 'test' });
const story = jest.fn();
const decoratedStory = defaultDecorateStory(story, [decorator]);

const context = { parameters: { test: 'test-val' } };
decoratedStory(context);

expect(wrapper).toHaveBeenCalledWith(expect.any(Function), context, { parameters: 'test-val' });
});

it('passes options added at decoration time', () => {
const wrapper = jest.fn();
const decorator = makeDecorator({ wrapper, name: 'test', parameterName: 'test' });
const story = jest.fn();
const options = 'test-val';
const decoratedStory = defaultDecorateStory(story, [decorator(options)]);

const context = {};
decoratedStory(context);

expect(wrapper).toHaveBeenCalledWith(expect.any(Function), context, { options: 'test-val' });
});

it('passes both options *and* parameters at the same time', () => {
const wrapper = jest.fn();
const decorator = makeDecorator({ wrapper, name: 'test', parameterName: 'test' });
const story = jest.fn();
const options = 'test-val';
const decoratedStory = defaultDecorateStory(story, [decorator(options)]);

const context = { parameters: { test: 'test-val' } };
decoratedStory(context);

expect(wrapper).toHaveBeenCalledWith(expect.any(Function), context, {
options: 'test-val',
parameters: 'test-val',
});
});

it('passes nothing if neither are supplied', () => {
const wrapper = jest.fn();
const decorator = makeDecorator({ wrapper, name: 'test', parameterName: 'test' });
const story = jest.fn();
const decoratedStory = defaultDecorateStory(story, [decorator]);

const context = {};
decoratedStory(context);

expect(wrapper).toHaveBeenCalledWith(expect.any(Function), context, {});
});

it('calls the story directly if neither are supplied and skipIfNoParametersOrOptions is true', () => {
const wrapper = jest.fn();
const decorator = makeDecorator({
wrapper,
name: 'test',
parameterName: 'test',
skipIfNoParametersOrOptions: true,
});
const story = jest.fn();
const decoratedStory = defaultDecorateStory(story, [decorator]);

const context = {};
decoratedStory(context);

expect(wrapper).not.toHaveBeenCalled();
expect(story).toHaveBeenCalled();
});

it('passes options added at story time, but with a deprecation warning', () => {
deprecatedFns = [];
const wrapper = jest.fn();
const decorator = makeDecorator({ wrapper, name: 'test', parameterName: 'test' });
const options = 'test-val';
const story = jest.fn();
const decoratedStory = decorator(options)(story);
expect(deprecatedFns).toHaveLength(1);
expect(deprecatedFns[0].warning).toMatch('addDecorator(test)');

const context = {};
decoratedStory(context);

expect(wrapper).toHaveBeenCalledWith(expect.any(Function), context, {
options: 'test-val',
});
expect(deprecatedFns[0].deprecatedFn).toHaveBeenCalled();
});
});
2 changes: 1 addition & 1 deletion lib/core/src/client/preview/client_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { logger } from '@storybook/client-logger';

import StoryStore from './story_store';

const defaultDecorateStory = (getStory, decorators) =>
export const defaultDecorateStory = (getStory, decorators) =>
decorators.reduce(
(decorated, decorator) => context => decorator(() => decorated(context), context),
getStory
Expand Down

0 comments on commit f2019e3

Please sign in to comment.