diff --git a/code/addons/actions/src/runtime/action.ts b/code/addons/actions/src/runtime/action.ts index f6779d1a1a64..a647a8eb0d1b 100644 --- a/code/addons/actions/src/runtime/action.ts +++ b/code/addons/actions/src/runtime/action.ts @@ -103,6 +103,7 @@ export function action(name: string, options: ActionOptions = {}): HandlerFuncti channel.emit(EVENT_ID, actionDisplayToEmit); }; handler.isAction = true; + handler.implicit = options.implicit; return handler; } diff --git a/code/addons/interactions/src/preview.ts b/code/addons/interactions/src/preview.ts index d751cbf7bc58..e0cc0aeae87a 100644 --- a/code/addons/interactions/src/preview.ts +++ b/code/addons/interactions/src/preview.ts @@ -1,11 +1,11 @@ -/* eslint-disable no-underscore-dangle */ import type { - Args, - LoaderFunction, + ArgsEnhancer, PlayFunction, PlayFunctionContext, + Renderer, StepLabel, } from '@storybook/types'; +import { fn, isMockFunction } from '@storybook/test'; import { instrument } from '@storybook/instrumenter'; export const { step: runStep } = instrument( @@ -16,26 +16,47 @@ export const { step: runStep } = instrument( { intercept: true } ); -const instrumentSpies: LoaderFunction = ({ initialArgs }) => { - const argTypesWithAction = Object.entries(initialArgs).filter( - ([, value]) => - typeof value === 'function' && - '_isMockFunction' in value && - value._isMockFunction && - !value._instrumented - ); - - return argTypesWithAction.reduce((acc, [key, value]) => { - const instrumented = instrument({ [key]: () => value }, { retain: true })[key]; - acc[key] = instrumented(); - // this enhancer is being called multiple times - - value._instrumented = true; - return acc; - }, {} as Args); +const traverseArgs = (value: unknown, depth = 0, key?: string): any => { + // Make sure to not get in infinite loops with self referencing args + if (depth > 5) return value; + if (value == null) return value; + if (isMockFunction(value)) { + // Makes sure we get the arg name in the interactions panel + if (key) value.mockName(key); + return value; + } + + // wrap explicit actions in a spy + if ( + typeof value === 'function' && + 'isAction' in value && + value.isAction && + !('implicit' in value && value.implicit) + ) { + const mock = fn(value as any); + if (key) mock.mockName(key); + return mock; + } + + if (Array.isArray(value)) { + depth++; + return value.map((item) => traverseArgs(item, depth)); + } + + if (typeof value === 'object' && value.constructor === Object) { + depth++; + // We have to mutate the original object for this to survive HMR. + for (const [k, v] of Object.entries(value)) { + (value as Record)[k] = traverseArgs(v, depth, k); + } + return value; + } + return value; }; -export const argsEnhancers = [instrumentSpies]; +const wrapActionsInSpyFns: ArgsEnhancer = ({ initialArgs }) => traverseArgs(initialArgs); + +export const argsEnhancers = [wrapActionsInSpyFns]; export const parameters = { throwPlayFunctionExceptions: false, diff --git a/code/lib/instrumenter/src/instrumenter.ts b/code/lib/instrumenter/src/instrumenter.ts index 71713bc50a6a..3eeb6ea86ed8 100644 --- a/code/lib/instrumenter/src/instrumenter.ts +++ b/code/lib/instrumenter/src/instrumenter.ts @@ -432,7 +432,9 @@ export class Instrumenter { return { __element__: { prefix, localName, id, classNames, innerText } }; } if (typeof value === 'function') { - return { __function__: { name: value.name } }; + return { + __function__: { name: 'getMockName' in value ? value.getMockName() : value.name }, + }; } if (typeof value === 'symbol') { return { __symbol__: { description: value.description } };