UNPKG

orphic-cypress

Version:

Set of utilities and typescript transformers to cover storybook stories with cypress component tests

173 lines (163 loc) 5.31 kB
/** * 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; };