diff --git a/addons/actions/README.md b/addons/actions/README.md index 4d6d3ea15834..dfaf82011937 100644 --- a/addons/actions/README.md +++ b/addons/actions/README.md @@ -35,10 +35,12 @@ Import the `action` function and use it to create actions handlers. When creatin ```js import { storiesOf } from '@storybook/react'; -import { action } from '@storybook/addon-actions'; +import { action, configureActions } from '@storybook/addon-actions'; import Button from './button'; +action('button-click') + storiesOf('Button', module) .add('default view', () => ( )) ``` + +## Configuration + +Arguments which are passed to the action call will have to be serialized while be "transfered" +over the channel. + +This is not very optimal and can cause lag when large objects are being logged, for this reason it is possible +to configure a maximum depth. + +To apply the configuration globally use the `configureActions` function in your `config.js` file. + +```js +import { configureActions } from '@storybook/addon-actions'; + +configureActions({ + depth: 100 +}) +``` + +To apply the configuration per action use: +```js +action('my-action', { + depth: 5 +}) +``` + +### Available Options + +|Name|Type|Description|Default| +|---|---|---|---| +|`depth`|Number|Configures the transfered depth of any logged objects.|`10`| diff --git a/addons/actions/src/index.js b/addons/actions/src/index.js index 2634da6e8a3b..e6823820166a 100644 --- a/addons/actions/src/index.js +++ b/addons/actions/src/index.js @@ -1,6 +1,8 @@ +import { action, configureActions, decorateAction } from './preview'; + // addons, panels and events get unique names using a prefix export const ADDON_ID = 'storybook/actions'; export const PANEL_ID = `${ADDON_ID}/actions-panel`; export const EVENT_ID = `${ADDON_ID}/action-event`; -export { action, decorateAction } from './preview'; +export { action, configureActions, decorateAction }; diff --git a/addons/actions/src/lib/decycle.js b/addons/actions/src/lib/decycle.js index c6a3e9ec44f8..c233e463be4c 100644 --- a/addons/actions/src/lib/decycle.js +++ b/addons/actions/src/lib/decycle.js @@ -1,25 +1,25 @@ import { DecycleError } from './errors'; -import { getPropertiesList, typeReplacer } from './util'; +import { getPropertiesList, typeReplacer, omitProperty } from './util'; import { CYCLIC_KEY } from './'; import { objectType } from './types'; +import { DEPTH_KEY } from './types/object/configureDepth'; + +const { hasOwnProperty } = Object.prototype; + export default function decycle(object, depth = 10) { const objects = new WeakMap(); let isCyclic = false; - const res = (function derez(value, path, _depth) { + const res = (function derez(value, path, _depth, _branchDepthMax) { let oldPath; let obj; - if (Object(value) === value && _depth > depth) { - const name = value.constructor ? value.constructor.name : typeof value; - - return `[${name}...]`; - } + let maxDepth = _branchDepthMax; const result = typeReplacer(value); @@ -51,19 +51,40 @@ export default function decycle(object, depth = 10) { if (Array.isArray(value)) { obj = []; for (let i = 0; i < value.length; i += 1) { - obj[i] = derez(value[i], `${path}[${i}]`, _depth + 1); + obj[i] = derez(value[i], `${path}[${i}]`, _depth + 1, maxDepth); } } else { obj = objectType.serialize(value); - getPropertiesList(value).forEach(name => { - try { - obj[name] = derez(value[name], `${path}[${JSON.stringify(name)}]`, _depth + 1); - } catch (error) { - console.error(error); // eslint-disable-line no-console - obj[name] = new DecycleError(error.message); + let newDepth; + if (hasOwnProperty.call(obj, DEPTH_KEY)) { + if (_depth + 1 < maxDepth) { + const depthKey = obj[DEPTH_KEY]; + + newDepth = depthKey === 0 ? 0 : _depth + depthKey; + maxDepth = newDepth >= depth ? depth : newDepth; } - }); + + delete obj[DEPTH_KEY]; + } + + if (_depth <= maxDepth) { + getPropertiesList(value).forEach(name => { + if (!omitProperty(name)) { + try { + obj[name] = derez( + value[name], + `${path}[${JSON.stringify(name)}]`, + _depth + 1, + maxDepth + ); + } catch (error) { + console.error(error); // eslint-disable-line no-console + obj[name] = new DecycleError(error.message); + } + } + }); + } } if (_depth === 0 && value instanceof Object && isCyclic) { @@ -74,7 +95,7 @@ export default function decycle(object, depth = 10) { } return value; - })(object, '$', 0); + })(object, '$', 0, depth); return res; } diff --git a/addons/actions/src/lib/types/object/__tests__/index.js b/addons/actions/src/lib/types/object/__tests__/index.js index 56341c9d11b2..04caf09e5d04 100644 --- a/addons/actions/src/lib/types/object/__tests__/index.js +++ b/addons/actions/src/lib/types/object/__tests__/index.js @@ -1,11 +1,15 @@ import objectType from '../'; +import { DEPTH_KEY } from '../configureDepth'; describe('Object', () => { it('Serializes Object', () => { function C() {} const c = new C(); - expect(objectType.serialize(c)).toEqual({ [objectType.KEY]: 'C' }); + expect(objectType.serialize(c)).toEqual({ + [DEPTH_KEY]: 2, + [objectType.KEY]: 'C', + }); }); it('Deserializes Object', () => { diff --git a/addons/actions/src/lib/types/object/configureDepth.js b/addons/actions/src/lib/types/object/configureDepth.js new file mode 100644 index 000000000000..8481e84b3364 --- /dev/null +++ b/addons/actions/src/lib/types/object/configureDepth.js @@ -0,0 +1,7 @@ +export const DEPTH_KEY = '$___storybook.depthKey'; + +export default function configureDepth(obj, depth = 0) { + obj[DEPTH_KEY] = depth; // eslint-disable-line no-param-reassign + + return obj; +} diff --git a/addons/actions/src/lib/types/object/index.js b/addons/actions/src/lib/types/object/index.js index fdaee125c7b7..42b0871955f7 100644 --- a/addons/actions/src/lib/types/object/index.js +++ b/addons/actions/src/lib/types/object/index.js @@ -1,12 +1,21 @@ import createNamedObject from './createNamedObject'; import getObjectName from './getObjectName'; +import configureDepth from './configureDepth'; +const maxDepth = 2; const KEY = '$___storybook.objectName'; const objectType = { KEY, // is: (value) => , // not used - serialize: value => ({ [KEY]: getObjectName(value) }), + serialize: value => { + const objectName = getObjectName(value); + if (objectName === 'Object') { + return { [KEY]: objectName }; + } + + return configureDepth({ [KEY]: objectName }, maxDepth); + }, deserialize: value => createNamedObject(value, KEY), }; diff --git a/addons/actions/src/lib/util/index.js b/addons/actions/src/lib/util/index.js index f10fd4416c63..af41b84ccca5 100644 --- a/addons/actions/src/lib/util/index.js +++ b/addons/actions/src/lib/util/index.js @@ -5,3 +5,4 @@ export muteProperty from './muteProperty'; export prepareArguments from './prepareArguments'; export typeReviver from './typeReviver'; export typeReplacer from './typeReplacer'; +export omitProperty from './omitProperty'; diff --git a/addons/actions/src/lib/util/omitProperty.js b/addons/actions/src/lib/util/omitProperty.js new file mode 100644 index 000000000000..41fa1b2c10a6 --- /dev/null +++ b/addons/actions/src/lib/util/omitProperty.js @@ -0,0 +1,3 @@ +export default function omitProperty(name) { + return name.startsWith('__') || name.startsWith('STORYBOOK_'); +} diff --git a/addons/actions/src/lib/util/prepareArguments.js b/addons/actions/src/lib/util/prepareArguments.js index ed441336a895..8789edce1bc7 100644 --- a/addons/actions/src/lib/util/prepareArguments.js +++ b/addons/actions/src/lib/util/prepareArguments.js @@ -1,12 +1,8 @@ import { decycle } from '../index'; -export default function prepareArguments(arg) { - if (arg && typeof arg.preventDefault !== 'undefined') { - return JSON.stringify(`[${arg.constructor.name}]`); - } - +export default function prepareArguments(arg, depth) { try { - return JSON.stringify(decycle(arg)); + return JSON.stringify(decycle(arg, depth)); } catch (error) { return error.toString(); // IE still cyclic. } diff --git a/addons/actions/src/preview/__tests__/action.test.js b/addons/actions/src/preview/__tests__/action.test.js new file mode 100644 index 000000000000..fd5c301bb59f --- /dev/null +++ b/addons/actions/src/preview/__tests__/action.test.js @@ -0,0 +1,90 @@ +import addons from '@storybook/addons'; +import { action, configureActions } from '../../'; + +jest.mock('@storybook/addons'); + +const getChannelData = channel => + channel.emit.mock.calls[channel.emit.mock.calls.length - 1][1].data; + +describe('Action', () => { + const channel = { emit: jest.fn() }; + addons.getChannel.mockReturnValue(channel); + + it('with one argument', () => { + action('test-action')('one'); + + expect(getChannelData(channel).args[0]).toEqual('"one"'); + }); + + it('with multiple arguments', () => { + action('test-action')('one', 'two', 'three'); + + expect(getChannelData(channel).args).toEqual(['"one"', '"two"', '"three"']); + }); + + it('with global depth configuration', () => { + const depth = 1; + + configureActions({ + depth, + }); + + action('test-action')({ + root: { + one: { + two: 'foo', + }, + }, + }); + + expect(getChannelData(channel).args[0]).toEqual( + JSON.stringify({ + '$___storybook.objectName': 'Object', + root: { + '$___storybook.objectName': 'Object', + one: { + '$___storybook.objectName': 'Object', + }, + }, + }) + ); + }); + + it('per action depth option overrides global config', () => { + configureActions({ + depth: 1, + }); + + action('test-action', { depth: 3 })({ + root: { + one: { + two: { + three: { + four: { + five: 'foo', + }, + }, + }, + }, + }, + }); + + expect(getChannelData(channel).args[0]).toEqual( + JSON.stringify({ + '$___storybook.objectName': 'Object', + root: { + '$___storybook.objectName': 'Object', + one: { + '$___storybook.objectName': 'Object', + two: { + '$___storybook.objectName': 'Object', + three: { + '$___storybook.objectName': 'Object', + }, + }, + }, + }, + }) + ); + }); +}); diff --git a/addons/actions/src/preview/__tests__/configureActions.test.js b/addons/actions/src/preview/__tests__/configureActions.test.js new file mode 100644 index 000000000000..299d8f802c3b --- /dev/null +++ b/addons/actions/src/preview/__tests__/configureActions.test.js @@ -0,0 +1,16 @@ +import { config } from '../configureActions'; +import { configureActions } from '../../'; + +describe('Configure Actions', () => { + it('can configure actions', () => { + const depth = 100; + + configureActions({ + depth, + }); + + expect(config).toEqual({ + depth, + }); + }); +}); diff --git a/addons/actions/src/preview.test.js b/addons/actions/src/preview/__tests__/preview.test.js similarity index 97% rename from addons/actions/src/preview.test.js rename to addons/actions/src/preview/__tests__/preview.test.js index d96cef9576eb..b6e562d5a58b 100644 --- a/addons/actions/src/preview.test.js +++ b/addons/actions/src/preview/__tests__/preview.test.js @@ -1,7 +1,7 @@ import addons from '@storybook/addons'; import uuid from 'uuid/v1'; -import { action } from './preview'; -import { undefinedType, symbolType } from './lib/types'; +import { action } from '../'; +import { undefinedType, symbolType } from '../../lib/types'; jest.mock('uuid/v1'); jest.mock('@storybook/addons'); diff --git a/addons/actions/src/preview.js b/addons/actions/src/preview/action.js similarity index 52% rename from addons/actions/src/preview.js rename to addons/actions/src/preview/action.js index 1df9c39c291b..3c036da7d4df 100644 --- a/addons/actions/src/preview.js +++ b/addons/actions/src/preview/action.js @@ -1,12 +1,18 @@ -import addons from '@storybook/addons'; import uuid from 'uuid/v1'; -import { EVENT_ID } from './'; -import { canConfigureName, prepareArguments } from './lib/util'; +import addons from '@storybook/addons'; +import { EVENT_ID } from '../'; +import { canConfigureName, prepareArguments } from '../lib/util'; +import { config } from './configureActions'; + +export default function action(name, options = {}) { + const actionOptions = { + ...config, + ...options, + }; -export function action(name) { // eslint-disable-next-line no-shadow const handler = function action(..._args) { - const args = _args.map(prepareArguments); + const args = _args.map(arg => prepareArguments(arg, actionOptions.depth)); const channel = addons.getChannel(); const id = uuid(); channel.emit(EVENT_ID, { @@ -20,13 +26,3 @@ export function action(name) { } return handler; } - -export function decorateAction(decorators) { - return name => { - const callAction = action(name); - return (..._args) => { - const decorated = decorators.reduce((args, fn) => fn(args), _args); - callAction(...decorated); - }; - }; -} diff --git a/addons/actions/src/preview/configureActions.js b/addons/actions/src/preview/configureActions.js new file mode 100644 index 000000000000..4c76554b6a3e --- /dev/null +++ b/addons/actions/src/preview/configureActions.js @@ -0,0 +1,7 @@ +export const config = { + depth: 10, +}; + +export function configureActions(options = {}) { + Object.assign(config, options); +} diff --git a/addons/actions/src/preview/decorateAction.js b/addons/actions/src/preview/decorateAction.js new file mode 100644 index 000000000000..90ed85d870d2 --- /dev/null +++ b/addons/actions/src/preview/decorateAction.js @@ -0,0 +1,11 @@ +import { action } from '../preview'; + +export default function decorateAction(decorators) { + return (name, options) => { + const callAction = action(name, options); + return (..._args) => { + const decorated = decorators.reduce((args, fn) => fn(args), _args); + callAction(...decorated); + }; + }; +} diff --git a/addons/actions/src/preview/index.js b/addons/actions/src/preview/index.js new file mode 100644 index 000000000000..ea207b3fd4d9 --- /dev/null +++ b/addons/actions/src/preview/index.js @@ -0,0 +1,3 @@ +export { default as action } from './action'; +export { configureActions } from './configureActions'; +export { default as decorateAction } from './decorateAction'; diff --git a/examples/official-storybook/stories/__snapshots__/addon-actions.stories.storyshot b/examples/official-storybook/stories/__snapshots__/addon-actions.stories.storyshot index 14d27e909bd3..9abce3a828c7 100644 --- a/examples/official-storybook/stories/__snapshots__/addon-actions.stories.storyshot +++ b/examples/official-storybook/stories/__snapshots__/addon-actions.stories.storyshot @@ -67,6 +67,11 @@ exports[`Storyshots Addons|Actions All types 1`] = ` > Plain Object + `; + +exports[`Storyshots Addons|Actions configureActions 1`] = ` + +`; diff --git a/examples/official-storybook/stories/addon-actions.stories.js b/examples/official-storybook/stories/addon-actions.stories.js index b353ddaf3014..83fbffff1f1b 100644 --- a/examples/official-storybook/stories/addon-actions.stories.js +++ b/examples/official-storybook/stories/addon-actions.stories.js @@ -1,7 +1,7 @@ /* global window */ import React from 'react'; import { storiesOf } from '@storybook/react'; -import { action, decorateAction } from '@storybook/addon-actions'; +import { action, configureActions, decorateAction } from '@storybook/addon-actions'; import { setOptions } from '@storybook/addon-options'; import { Button } from '@storybook/react/demo'; import { File } from 'global'; @@ -66,7 +66,16 @@ storiesOf('Addons|Actions', module) > Multiple - + + @@ -75,4 +84,17 @@ storiesOf('Addons|Actions', module) ); + }) + .add('configureActions', () => { + configureActions({ + depth: 2, + }); + + return ( + + ); });