storybookjs / storybook

Storybook is the industry standard workshop for building, documenting, and testing UI components in isolation
https://storybook.js.org
MIT License
84.55k stars 9.3k forks source link

renderStory() function in jest/node environment #17769

Open KevinMind opened 2 years ago

KevinMind commented 2 years ago

I would like to render stories in jest and assert the dom during and after rendering. There is already support for this, but I feel some things are missing.

The way I see it there are actually a number of different problems, and a potentially a number of different solutions. I'd like to document here the problems I see trying to render stories in jest/node.

1. Story objects won't work with jest. If I define a story the "classical" way, I can import this story, which is a react component and render it in jest. But with CSF3.0 I can define a story as a plain object. This is great, simplifies the api and makes story writing much easier, but currently these stories won't render in jest.

Button.stories.tsx

import { ComponentProps } from "react";
import { ComponentMeta, StoryObj } from "@storybook/react";
import { Button } from "./Button";

export default {
  component: Button,
} as ComponentMeta<typeof Button>;

export const Default: StoryObj<ComponentProps<typeof Button>> = {
  args: {
    onClick: () => console.log("clicked"),
    children: "Click Me!",
  },
};

Button.test.tsx

import { render } from "@testing-library/react";

import { Default } from "./Button.stories";

test("should render button", async () => {
   render(<Default {...Default.args} />); // Does not work because Default is an object and not a component
});

Is there a way to render story objects in jest or non-storybook controlled environments? How would this look like?

2. How can I trigger .play() functions in jest. With interactiosn I can easily trigger use interaction logic in my story. It would be great to not only import and render the story in jest, but to trigger the logic as well. If I can achieve this, then the only logic my test is really concerned with is assertion logic. This also makes my stories renderable in a number of different test frameworks with ease.

Assuming I make my story renderable (avoiding the 1st problem)

Button.stories.tsx

import { ComponentProps } from "react";
import { ComponentMeta, StoryObj } from "@storybook/react";
import { within, userEvent } from "@storybook/testing-library";
import { Button } from "./Button";

export default {
  component: Button,
} as ComponentMeta<typeof Button>;

const Template: Story<ComponentProps<typeof Button>> = (args) => <Button {...args} />;

export const Default = Template.bind({});

Default.args = {
  args: {
    onClick: () => console.log("clicked"),
    children: "Click Me!",
  },
};

Default.play = async ({ canvasElement }) => {
  const canvas = within(canvasElement);

  await userEvent.click(canvas.getByRole("button"));
};

Button.test.tsx

import { render } from "@testing-library/react";

import { Default } from "./Button.stories";

test("handles click events", async () => {
  const spy = jest.fn();

  const wrapper = render(<Default {...Default.args} />);

  await Default.play({ canvasElement: wrapper.container }); // this works, but it is very hacky, and is missing a lot of the properties .play functions expect

  expect(spy).toHaveBeenCalled();

});

I would like first class support for rendering stories in jest because jest is lightweight, no browser process needed. Jest is also a widely used tool and support for it will be meaningful for continued integration of storybook and the practice of story writing.

What would you like to see added to Storybook to solve problem?

I took a short at a small POC factory function for rendering stories.

import React, { JSXElementConstructor, ComponentProps } from "react";
import { ComponentMeta, Story } from "@storybook/react";
import { render } from "@testing-library/react";

interface Props {
  skipPlay: boolean;
}

export function createStoryFactory(
  Component: keyof JSX.IntrinsicElements | JSXElementConstructor<any>,
  meta: ComponentMeta<typeof Component>
) {
  return async function renderStory(
    StoryComponent: Story<ComponentProps<typeof Component>>,
    props: typeof StoryComponent.args = StoryComponent.args,
    { skipPlay = false }: Props = { skipPlay: false }
  ) {
    const markdown = <StoryComponent {...props} />;
    const wrapper = render(markdown);

    if (!skipPlay) {
      // @TODO: we need to be able to pass the return value of render() to play functions
      // for stories so we can trigger interactions designated for a given story
      // @ts-ignore
      await StoryComponent.play?.({
        canvasElement: wrapper.baseElement,
      });
    }

    return wrapper;
  };
}

There are many things missing. How can we create or access the story context to be able to trigger play functions. How would composition look in jest? Would there be a way of accessing the story context in jest? maybe if you wanted to spy on some context method or mock some data somewhere?

Ideally I would like a solution that accepts a story, and renders it, returning the story context and the wrapper element to assert on. I should also be able to control some of the initial rendering behaviour, e.g. to trigger play automatically or delay and trigger manually later.

Describe alternatives you've considered

I could just use a different framework than jest. I could use cypress,playwrite,webdriverIO and run the test in the browser where I already have storybook support. As mentioned above, I would like to continue supporting jest as it is an important tool especially for CI.

I could try to mock the story context object, but this I feel might be the wrong approach since this object impacts rendering, and I don't want multiple sources of truth.

Are you able to assist to bring the feature to reality?

I would love to assist in bringing this feature to life. I need help understanding more about how storybook actually works internally but am happy and eager to work on the solution with someone who can bring that context.

Additional context

I have the working prototype on this repo: https://github.com/KevinMind/blog/pull/11 on the linked PR branch. instructions are in the PR comment.

KevinMind commented 2 years ago

Seems like this is the answer. https://storybook.js.org/blog/interaction-testing-with-storybook/