Skip to content

Commit

Permalink
Merge pull request #26033 from storybookjs/kasper/spy-on-explicit-act…
Browse files Browse the repository at this point in the history
…ions

Actions: Add spy to action for explicit actions
  • Loading branch information
kasperpeulen authored Feb 15, 2024
2 parents e6b89d7 + 95c7048 commit be84a02
Show file tree
Hide file tree
Showing 3 changed files with 46 additions and 22 deletions.
1 change: 1 addition & 0 deletions code/addons/actions/src/runtime/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
63 changes: 42 additions & 21 deletions code/addons/interactions/src/preview.ts
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -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<string, unknown>)[k] = traverseArgs(v, depth, k);
}
return value;
}
return value;
};

export const argsEnhancers = [instrumentSpies];
const wrapActionsInSpyFns: ArgsEnhancer<Renderer> = ({ initialArgs }) => traverseArgs(initialArgs);

export const argsEnhancers = [wrapActionsInSpyFns];

export const parameters = {
throwPlayFunctionExceptions: false,
Expand Down
4 changes: 3 additions & 1 deletion code/lib/instrumenter/src/instrumenter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 } };
Expand Down

0 comments on commit be84a02

Please sign in to comment.