Testing utilities that allow you to reuse your stories in your unit tests
You are using Storybook for your components and want to use your stories as fixtures in your tests.
With Storybook you are able to define the necessary boilerplate (theming, routing, state management, etc.) to make all of your components render correctly, and describe various scenarios that your components should render in. When you're writing tests you often end up needing to do the same thing. With the Storybook component story format (CSF) you are able to write your stories in a way that allows them to easily be consumed by other tools, including your tests! However some things don't come for free, such as applying decorators, merging args / default values and so on.
@storybook/marko
makes it easy to consume your Storybook stories as test fixtures while supporting many of the shorthands and features not taken care of automatically. Features such as args, decorators / global decorators, and meta from your story will be composed by this library and returned to you in a component which you can render directly or using @marko/testing-library
.
This module ships with @storybook/marko
as shown in the examples.
This library requires you to be using Storybook version 6, Component Story Format (CSF) and hoisted CSF annotations, which is the recommended way to write stories since Storybook 6.
Essentially, if you use Storybook 6 and your stories look similar to this, you're good to go!
import Button from "./button.marko";
// CSF: default export (meta) + named exports (stories)
export default {
title: "Example/Button",
component: Button,
};
export const Primary = {
args: { primary: true },
};
This is an optional step. If you don't have global decorators, there's no need to do this. However, if you do, this is a necessary step for your global decorators to be applied.
If you have global decorators/parameters/etc and want them applied to your stories when testing them, you first need to set this up. You'll typically want to do this in a setup file for your favorite test framework.
import { setProjectAnnotations } from "@storybook/marko";
import projectAnnotations from "./.storybook/preview"; // path of your preview.js file
setProjectAnnotations(projectAnnotations);
composeStories
will process all stories from the component you specify, compose args/decorators in all of them and return an object containing the composed stories.
If you use the composed story (e.g. PrimaryButton), once the returned component is rendered it will automatically merge in args
as input
that are passed in the story. Any additional input
provided when rendering the component will overwrite args
in the story.
import { render, screen } from "@testing-library/marko";
import { composeStories } from "@storybook/marko";
import * as stories from "./Button.stories"; // import all stories from the stories file
// Every component that is returned maps 1:1 with the stories, but they
// already contain all decorators from story level, meta level and global level.
// When the component is rendered, args are also merged in.
const { Primary, Secondary } = composeStories(stories);
test("renders primary button with default args", async () => {
await render(Primary);
const buttonElement = screen.getByText(
/Text coming from args in stories file!/i,
);
expect(buttonElement).toBeInTheDocument();
});
test("renders primary button with overriden props", async () => {
await render(Primary, { label: "Hello world" }); // you can override props and they will get merged with values from the Story's args
const buttonElement = screen.getByText(/Hello world/i);
expect(buttonElement).toBeInTheDocument();
});
You can use composeStory
if you wish to apply it for a single story rather than all of your stories. You need to pass the meta (default export) as well.
import { render, screen } from "@marko/testing-library";
import { composeStory } from "@storybook/marko";
import Meta, { Primary as PrimaryStory } from "./Button.stories";
// Returns a component that already contain all decorators from story level, meta level and global level.
// When the component is rendered, args are also merged in.
const Primary = composeStory(PrimaryStory, Meta);
test("onclick handler is called", async () => {
const { emitted } = await render(Primary);
const buttonElement = screen.getByRole("button");
buttonElement.click();
expect(emitted("click")).toHaveLength(1);
});
For the types to be automatically picked up, your stories must be typed. See an example:
import type { Story, Meta } from "@storybook/marko";
import Button, { type Input } from "./Button.marko";
export default {
title: "Components/Button",
component: Button,
} as Meta<Input>;
// Story<Input> is the key piece needed for typescript validation
export const Primary = {
args: {
label: "Hello Marko!",
},
} as Story<Input>;