All files / src actions.ts

96.15% Statements 25/26
84.09% Branches 74/88
100% Functions 6/6
96% Lines 24/25

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173                                                    113x 1368x 1306x 1306x 1306x     113x       60x 60x                             113x         1726x 65x     65x                                                                                                                         113x         1726x 1726x   1726x         1726x   1726x           1726x     4966x             1726x     1726x                       1726x 1726x    
/**
 * Utilities to provide cypress stubs for storybook actions.
 * This will mock all explicitly defined argTypes in any location.
 *
 * Happens automatically for `executeCyTests`, but would be executed
 * manually for external test files.
 *
 * @module
 */
import type { ArgTypes } from "@storybook/react";
 
import type {
  ComponentStoryCy,
  ComponentStoryObjCy,
  StoryFileCy,
} from "./types";
 
/**
 * Object of function name keys to stubbed actions values.
 * Might be more likely that you'd access these stubs via `cy.get("@actions")`
 */
export type WrappedActions = {
  [fnName: string]: ReturnType<typeof cy.stub | typeof cy.spy>;
};
 
/** Quick util to stub or spy and alias, which isn't consistent in cypress API */
const mockAs = (alias: string, toSpy?: any) => {
  if (toSpy) return cy.spy(toSpy).as(alias);
  const stub = cy.stub();
  stub.as(alias);
  return stub;
};
 
const addAlias = (
  stubOrSpy: ReturnType<typeof cy.stub | typeof cy.spy>,
  alias: string
) => {
  stubOrSpy.as(alias);
  return stubOrSpy;
};
 
/**
 * Wrap argTypes in cy.stubs. Unit test framework from storybook at this point doesn't do
 * anything with these argTypes, nor does it add props/stubs for actions.argTypesRegex.
 * As such, its recommended to manually specify crucial argTypes, or write `.cy` tests
 * which provide mocks.
 *
 * In executeCyTests will operate on `export default { argTypes: { some: { action: 'some' } } }`
 * and combine that with any action argTypes defined on the story level.
 * Will be available at `cy.get("@actions")` or `this.actions` within tests.
 *
 * @private
 */
const stubArgTypeActions = (
  args: ComponentStoryCy<any>["args"] | ComponentStoryObjCy<any>["args"],
  argTypes?: Partial<ArgTypes<any>>,
  seed?: WrappedActions
): WrappedActions =>
  Object.entries(argTypes ?? {}).reduce((acc, [key, value]) => {
    if (value && value.action) {
      // alias the stub with the name given to the action. shows up really well in cypress
      // as a stub with call count and in assertion names etc.
      return {
        ...acc,
        [key]: acc[key]
          ? addAlias(acc[key], value.action)
          : mockAs(value.action, args?.[key]),
      };
    }
    return acc;
  }, seed ?? {});
 
/**
 * Object of either component obj of function
 * @private
 */
export type Stories = {
  [name: string]: (ComponentStoryCy<any> | ComponentStoryObjCy<any>) & {
    /** this seems to be the accurate storyName for the component */
    storyName: string;
  };
};
 
/**
 * Get argTypes from both the default export and the individual story.
 * Useful for a per-component beforeEach or top-of-test declaration.
 * Note that you'll want to return undefined from `beforeEach`
 *
 * ```ts
 * describe("SomeComponent", () => {
 *   beforeEach(() => {
 *     stubStoryActions(SomeComponent, stories);
 *   });
 *
 *   it("should render ok and call someAction on init", () => {
 *     cy.mount(<SomeComponent {...this.actions} />);
 *     cy.get("@actions").its("someAction").should("be.calledWith", "");
 *   });
 * });
 * ```
 *
 * ```ts
 * it("should do something", () => {
 *   // could just be `const actions = { someAction: cy.stub(), ... }`
 *   const actions = stubStoryActions(SomeStory, stories);
 *   cy.mount(<SomeStory {...actions} />);
 *   cy.dataCy("something").click().then(() => {
 *     expect(actions.someAction).to.have.callCount(1);
 *   });
 *   // or without the promise
 *   cy.dataCy("something").click();
 *   cy.get("@actions").its("someAction").should("have.callCount", 2);
 * });
 * ```
 *
 * Mostly an internal detail: precedence order, 3 through end will have essentially the same effect
 * 1) explicitly provided args/props for story which will become spies
 * 2) explicitly provided args at default export which will become spies
 * 3) local argTypes action definition
 * 4) global argTypes action definition
 * 5) local argTypes regex definition
 * 6) global argTypes regex definition
 */
export const stubStoryActions = <T extends StoryFileCy>(
  composedStory: ComponentStoryCy<any> | ComponentStoryObjCy<any>,
  stories: T,
  seed?: WrappedActions
): WrappedActions => {
  const { argTypes, storyName, parameters, args } = composedStory;
  const argTypesRegex = parameters?.actions?.argTypesRegex;
 
  const docgenInfo = (
    stories.default?.component as any as {
      __docgenInfo?: { props: { [key: string]: unknown } };
    }
  )?.__docgenInfo;
  const asRegex = new RegExp(argTypesRegex);
  // start with args and props, unique
  const argKeys = [
    ...new Set([
      ...Object.keys(args ?? {}),
      ...Object.keys(docgenInfo?.props ?? {}),
    ]),
  ];
  const toAutoMock = argTypesRegex
    ? Object.fromEntries(
        argKeys.flatMap((key) =>
          asRegex.test(key)
            ? [[key, mockAs(`argTypesRegex.${key}`, args?.[key])]]
            : []
        )
      )
    : {};
 
  const argTypesFromStoryObj = storyName
    ? (stories as any as Stories)[storyName]?.argTypes
    : null;
  const actions = stubArgTypeActions(
    { ...(stories.default?.args ?? {}), ...composedStory.args },
    {
      ...(stories.default?.argTypes ?? {}),
      ...(argTypes ?? {}),
      ...(argTypesFromStoryObj ?? {}),
    },
    {
      ...toAutoMock,
      ...(seed ?? {}),
    }
  );
  cy.wrap(actions).as("actions");
  return actions;
};