Skip to content

Commit

Permalink
Merge pull request #14901 from storybookjs/14579-arg-enhancers
Browse files Browse the repository at this point in the history
Core: Add args enhancers + use in addon-actions
  • Loading branch information
shilman authored May 18, 2021
2 parents e78a021 + 8644131 commit f692020
Show file tree
Hide file tree
Showing 13 changed files with 201 additions and 79 deletions.
16 changes: 16 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
- [From version 6.2.x to 6.3.0](#from-version-62x-to-630)
- [6.3 deprecations](#63-deprecations)
- [Deprecated scoped blocks imports](#deprecated-scoped-blocks-imports)
- [Deprecated argType.defaulValue](#deprecated-argtype-defaultValue)
- [From version 6.1.x to 6.2.0](#from-version-61x-to-620)
- [MDX pattern tweaked](#mdx-pattern-tweaked)
- [6.2 Angular overhaul](#62-angular-overhaul)
Expand Down Expand Up @@ -173,6 +174,21 @@ import { Meta, Story } from '@storybook/addon-docs/blocks';
import { Meta, Story } from '@storybook/addon-docs';
```

#### Deprecated `argType.defaultValue`

Previously, unset `args` were set to the `argType.defaultValue` if set or inferred from the component's prop types (etc.). In 6.3 we no longer infer default values and instead set arg values to `undefined` when unset, allowing the framework to supply the default value.

If you were using `argType.defaultValue` to fix issues with the above inference, it should no longer be necessary, you can remove that code. If you were using it to set a default value for an arg, there is a simpler way; simply set a value for the arg at the component level:

```js
export default {
component: MyComponent,
args: {
argName: 'default-value',
},
};
```

## From version 6.1.x to 6.2.0

### MDX pattern tweaked
Expand Down
2 changes: 1 addition & 1 deletion addons/actions/src/preset/addArgs.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { addActionsFromArgTypes, inferActionsFromArgTypesRegex } from './addArgsHelpers';

export const argTypesEnhancers = [addActionsFromArgTypes, inferActionsFromArgTypesRegex];
export const argsEnhancers = [addActionsFromArgTypes, inferActionsFromArgTypesRegex];
87 changes: 39 additions & 48 deletions addons/actions/src/preset/addArgsHelpers.test.ts
Original file line number Diff line number Diff line change
@@ -1,76 +1,67 @@
import { StoryContext } from '@storybook/addons';
import { inferActionsFromArgTypesRegex, addActionsFromArgTypes } from './addArgsHelpers';

const withDefaultValue = (argTypes) =>
Object.keys(argTypes).filter((key) => !!argTypes[key].defaultValue);

describe('actions parameter enhancers', () => {
describe('actions.argTypesRegex parameter', () => {
const baseParameters = {
argTypes: { onClick: {}, onFocus: {}, somethingElse: {} },
actions: { argTypesRegex: '^on.*' },
};
const parameters = { actions: { argTypesRegex: '^on.*' } };
const argTypes = { onClick: {}, onFocus: {}, somethingElse: {} };

it('should add actions that match a pattern', () => {
const parameters = baseParameters;
const argTypes = inferActionsFromArgTypesRegex({ parameters } as StoryContext);
expect(withDefaultValue(argTypes)).toEqual(['onClick', 'onFocus']);
const args = inferActionsFromArgTypesRegex(({
argTypes,
parameters,
} as unknown) as StoryContext);
expect(args).toEqual({
onClick: expect.any(Function),
onFocus: expect.any(Function),
});
});

it('should override pre-existing argTypes', () => {
const parameters = {
...baseParameters,
const args = inferActionsFromArgTypesRegex(({
parameters,
argTypes: {
onClick: { defaultValue: 'pre-existing value' },
},
};
const argTypes = inferActionsFromArgTypesRegex({ parameters } as StoryContext);
expect(withDefaultValue(argTypes)).toEqual(['onClick']);
expect(argTypes.onClick.defaultValue).not.toBeNull();
expect(argTypes.onClick.defaultValue).not.toEqual('pre-existing value');
} as unknown) as StoryContext);
expect(args).toEqual({
onClick: expect.any(Function),
});
});

it('should do nothing if actions are disabled', () => {
const parameters = {
...baseParameters,
actions: { ...baseParameters.actions, disable: true },
};
const result = inferActionsFromArgTypesRegex({ parameters } as StoryContext);
expect(result).toEqual(parameters.argTypes);
const args = inferActionsFromArgTypesRegex(({
parameters: {
...parameters,
actions: { ...parameters.actions, disable: true },
},
argTypes,
} as unknown) as StoryContext);
expect(args).toEqual({});
});
});

describe('argTypes.action parameter', () => {
const baseParameters = {
argTypes: {
onClick: { action: 'clicked!' },
onBlur: { action: 'blurred!' },
},
const argTypes = {
onClick: { action: 'clicked!' },
onBlur: { action: 'blurred!' },
};
it('should add actions based on action.args', () => {
const parameters = baseParameters;
const argTypes = addActionsFromArgTypes({ parameters } as StoryContext);
expect(withDefaultValue(argTypes)).toEqual(['onClick', 'onBlur']);
});

it('should override pre-existing args', () => {
const parameters = {
...baseParameters,
argTypes: {
onClick: { defaultValue: 'pre-existing value', action: 'onClick' },
onBlur: { action: 'onBlur' },
},
};
const argTypes = addActionsFromArgTypes({ parameters } as StoryContext);
expect(withDefaultValue(argTypes)).toEqual(['onClick', 'onBlur']);
expect(argTypes.onClick.defaultValue).not.toBeNull();
expect(argTypes.onClick.defaultValue).not.toEqual('pre-existing value');
expect(
addActionsFromArgTypes(({ argTypes, parameters: {} } as unknown) as StoryContext)
).toEqual({
onClick: expect.any(Function),
onBlur: expect.any(Function),
});
});

it('should do nothing if actions are disabled', () => {
const parameters = { ...baseParameters, actions: { disable: true } };
const result = addActionsFromArgTypes({ parameters } as StoryContext);
expect(result).toEqual(parameters.argTypes);
expect(
addActionsFromArgTypes(({
argTypes,
parameters: { actions: { disable: true } },
} as unknown) as StoryContext)
).toEqual({});
});
});
});
51 changes: 29 additions & 22 deletions addons/actions/src/preset/addArgsHelpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import mapValues from 'lodash/mapValues';
import { ArgTypesEnhancer } from '@storybook/client-api';
import { Args } from '@storybook/addons';
import { ArgsEnhancer } from '@storybook/client-api';
import { action } from '../index';

// interface ActionsParameter {
Expand All @@ -12,35 +12,42 @@ import { action } from '../index';
* matches a regex, such as `^on.*` for react-style `onClick` etc.
*/

export const inferActionsFromArgTypesRegex: ArgTypesEnhancer = (context) => {
const { actions, argTypes } = context.parameters;
export const inferActionsFromArgTypesRegex: ArgsEnhancer = (context) => {
const {
parameters: { actions },
argTypes,
} = context;
if (!actions || actions.disable || !actions.argTypesRegex || !argTypes) {
return argTypes;
return {};
}

const argTypesRegex = new RegExp(actions.argTypesRegex);
return mapValues(argTypes, (argType, name) => {
if (!argTypesRegex.test(name)) {
return argType;
}
return { ...argType, defaultValue: action(name) };
});
const argTypesMatchingRegex = Object.entries(argTypes).filter(
([name]) => !!argTypesRegex.test(name)
);

return argTypesMatchingRegex.reduce((acc, [name, argType]) => {
acc[name] = action(name);
return acc;
}, {} as Args);
};

/**
* Add action args for list of strings.
*/

export const addActionsFromArgTypes: ArgTypesEnhancer = (context) => {
const { argTypes, actions } = context.parameters;
export const addActionsFromArgTypes: ArgsEnhancer = (context) => {
const {
argTypes,
parameters: { actions },
} = context;
if (actions?.disable || !argTypes) {
return argTypes;
return {};
}

return mapValues(argTypes, (argType, name) => {
if (!argType.action) {
return argType;
}
const message = typeof argType.action === 'string' ? argType.action : name;
return { ...argType, defaultValue: action(message) };
});
const argTypesWithAction = Object.entries(argTypes).filter(([name, argType]) => !!argType.action);

return argTypesWithAction.reduce((acc, [name, argType]) => {
acc[name] = action(typeof argType.action === 'string' ? argType.action : name);
return acc;
}, {} as Args);
};
1 change: 1 addition & 0 deletions addons/storyshots/storyshots-core/src/frameworks/Loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export interface ClientApi extends ClientStoryApi<unknown> {
getStorybook: ClientApiThing['getStorybook'];
setAddon: ClientApiThing['setAddon'];
raw: ClientApiThing['raw'];
addArgsEnhancer: ClientApiThing['addArgsEnhancer'];
addArgTypesEnhancer: ClientApiThing['addArgTypesEnhancer'];
}

Expand Down
16 changes: 12 additions & 4 deletions addons/storyshots/storyshots-core/src/frameworks/configure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import path from 'path';
import { toRequireContext } from '@storybook/core-common';
import registerRequireContextHook from 'babel-plugin-require-context-hook/register';
import global from 'global';
import { ArgTypesEnhancer, DecoratorFunction } from '@storybook/client-api';
import { ArgsEnhancer, ArgTypesEnhancer, DecoratorFunction } from '@storybook/client-api';

import { ClientApi } from './Loader';
import { StoryshotsOptions } from '../api/StoryshotsOptions';
Expand Down Expand Up @@ -85,16 +85,24 @@ function configure(

if (preview) {
// This is essentially the same code as lib/core/src/server/preview/virtualModuleEntry.template
const { parameters, decorators, globals, globalTypes, argTypesEnhancers } = jest.requireActual(
preview
);
const {
parameters,
decorators,
globals,
globalTypes,
argsEnhancers,
argTypesEnhancers,
} = jest.requireActual(preview);

if (decorators) {
decorators.forEach((decorator: DecoratorFunction) => storybook.addDecorator(decorator));
}
if (parameters || globals || globalTypes) {
storybook.addParameters({ ...parameters, globals, globalTypes });
}
if (argsEnhancers) {
argsEnhancers.forEach((enhancer: ArgsEnhancer) => storybook.addArgsEnhancer(enhancer));
}
if (argTypesEnhancers) {
argTypesEnhancers.forEach((enhancer: ArgTypesEnhancer) =>
storybook.addArgTypesEnhancer(enhancer)
Expand Down
11 changes: 10 additions & 1 deletion lib/builder-webpack4/src/preview/virtualModuleEntry.template.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
/* eslint-disable import/no-unresolved */
import { addDecorator, addParameters, addLoader, addArgTypesEnhancer } from '{{clientApi}}';
import {
addDecorator,
addParameters,
addLoader,
addArgsEnhancer,
addArgTypesEnhancer,
} from '{{clientApi}}';
import { logger } from '{{clientLogger}}';
import * as config from '{{configFilename}}';

Expand All @@ -22,6 +28,9 @@ Object.keys(config).forEach((key) => {
case 'argTypesEnhancers': {
return value.forEach((enhancer) => addArgTypesEnhancer(enhancer));
}
case 'argsEnhancers': {
return value.forEach((enhancer) => addArgsEnhancer(enhancer));
}
case 'globals':
case 'globalTypes': {
const v = {};
Expand Down
11 changes: 10 additions & 1 deletion lib/builder-webpack5/src/preview/virtualModuleEntry.template.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
/* eslint-disable import/no-unresolved */
import { addDecorator, addParameters, addLoader, addArgTypesEnhancer } from '{{clientApi}}';
import {
addDecorator,
addParameters,
addLoader,
addArgsEnhancer,
addArgTypesEnhancer,
} from '{{clientApi}}';
import { logger } from '{{clientLogger}}';
import * as config from '{{configFilename}}';

Expand All @@ -22,6 +28,9 @@ Object.keys(config).forEach((key) => {
case 'argTypesEnhancers': {
return value.forEach((enhancer) => addArgTypesEnhancer(enhancer));
}
case 'argsEnhancers': {
return value.forEach((enhancer) => addArgsEnhancer(enhancer));
}
case 'globals':
case 'globalTypes': {
const v = {};
Expand Down
12 changes: 12 additions & 0 deletions lib/client-api/src/client_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
DecoratorFunction,
ClientApiAddons,
StoryApi,
ArgsEnhancer,
ArgTypesEnhancer,
} from './types';
import { applyHooks } from './hooks';
Expand Down Expand Up @@ -65,6 +66,13 @@ export const addLoader = (loader: LoaderFunction, deprecationWarning = true) =>
singleton.addLoader(loader);
};

export const addArgsEnhancer = (enhancer: ArgsEnhancer) => {
if (!singleton)
throw new Error(`Singleton client API not yet initialized, cannot call addArgsEnhancer`);

singleton.addArgsEnhancer(enhancer);
};

export const addArgTypesEnhancer = (enhancer: ArgTypesEnhancer) => {
if (!singleton)
throw new Error(`Singleton client API not yet initialized, cannot call addArgTypesEnhancer`);
Expand Down Expand Up @@ -136,6 +144,10 @@ export default class ClientApi {
this._storyStore.addGlobalMetadata({ loaders: [loader] });
};

addArgsEnhancer = (enhancer: ArgsEnhancer) => {
this._storyStore.addArgsEnhancer(enhancer);
};

addArgTypesEnhancer = (enhancer: ArgTypesEnhancer) => {
this._storyStore.addArgTypesEnhancer(enhancer);
};
Expand Down
2 changes: 2 additions & 0 deletions lib/client-api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import ClientApi, {
addDecorator,
addParameters,
addLoader,
addArgsEnhancer,
addArgTypesEnhancer,
} from './client_api';
import { defaultDecorateStory } from './decorators';
Expand All @@ -24,6 +25,7 @@ export * from './inferControls';
export type { PropDescriptor } from './filterArgTypes';

export {
addArgsEnhancer,
addArgTypesEnhancer,
addDecorator,
addLoader,
Expand Down
30 changes: 30 additions & 0 deletions lib/client-api/src/story_store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,36 @@ describe('preview.story_store', () => {
});
});

describe('argsEnhancer', () => {
it('allows you to add args', () => {
const store = new StoryStore({ channel });

const enhancer = jest.fn((context) => ({ c: 'd' }));
store.addArgsEnhancer(enhancer);

addStoryToStore(store, 'a', '1', (args: any) => 0, { args: { a: 'b' } });

expect(enhancer).toHaveBeenCalledWith(expect.objectContaining({ args: { a: 'b' } }));
expect(store.getRawStory('a', '1').args).toEqual({ a: 'b', c: 'd' });
});

it('does not pass result of earlier enhancers into subsequent ones, but composes their output', () => {
const store = new StoryStore({ channel });

const enhancerOne = jest.fn((context) => ({ c: 'd' }));
store.addArgsEnhancer(enhancerOne);

const enhancerTwo = jest.fn((context) => ({ e: 'f' }));
store.addArgsEnhancer(enhancerTwo);

addStoryToStore(store, 'a', '1', (args: any) => 0, { args: { a: 'b' } });

expect(enhancerOne).toHaveBeenCalledWith(expect.objectContaining({ args: { a: 'b' } }));
expect(enhancerTwo).toHaveBeenCalledWith(expect.objectContaining({ args: { a: 'b' } }));
expect(store.getRawStory('a', '1').args).toEqual({ a: 'b', c: 'd', e: 'f' });
});
});

describe('argTypesEnhancer', () => {
it('records when the given story processes args', () => {
const store = new StoryStore({ channel });
Expand Down
Loading

0 comments on commit f692020

Please sign in to comment.