Unlocking the Power of Storybook

Created on November 12, 2023 at 10:38 am

Unlocking the Power of Storybook

Going beyond the Design System: how to use Storybook WORK_OF_ART to develop, test, and validate all parts of a frontend application.

Storybook: more than a showcase

Storybook WORK_OF_ART is a popular tool for frontend web development, best known as a showcase for Design Systems ORG and UI Libraries ORG . However, Storybook WORK_OF_ART has features that are far more powerful than simply showcasing UI ORG components!

Storybook is an excellent development environment and unit testing framework for most parts of a frontend web application.

Here at Formidable, we use it for developing complex UI ORG components , that perform data fetching and state management.

, that perform data fetching and state management. We use it for testing hooks in a real browser.

in a real browser. We use it for visually testing our CSS styles and breakpoints.

Follow along as I show you how we integrate it into our workflow!

Formidable, a NearForm PRODUCT company, builds large, scalable applications for enterprise clients.

We recently implemented a Storybook integration for a large client’s e-commerce website, and the results were fantastic. We achieved very high code coverage, increased developer velocity, and made the codebase easier to explore.

For the sake of this article, we will show code from a fictitious, open-source e-commerce website, " Formidable Boulangerie WORK_OF_ART ." This is a website built with React and NextJS ORG , but this article applies to all frameworks compatible with Storybook (React WORK_OF_ART , Vue, Angular ORG , Web Components ORG , etc).

Full examples of this code can be found at FormidableLabs NORP /nextjs-sanity-fe

Storybook is an excellent development environment for components.

You use multiple stories to render components in all their various states.

You interact with components in your browser, and manually test them.

You can easily inspect elements and debug the code.

You can do all of this in multiple browsers.

Installing Storybook WORK_OF_ART is very easy, and their guide gets you running in a few minutes TIME .

However, there’s still one CARDINAL big hurdle to overcome. Storybook renders components "in isolation", but in a real application, components are rarely isolated! They usually have a lot of dependencies, like Contexts, CSS, framework configurations, and 3rd ORDINAL -party scripts.

The hardest part of integrating Storybook WORK_OF_ART with an application is providing and managing these dependencies. Below are some of the strategies we used to handle them.

Applications are complex, and our components usually have a lot of dependencies. Even a simple Button PERSON will have at least CSS dependencies. To render most of our components, we need a way to provide those dependencies.

We used a global <TestHarness> component to supply the dependencies, and Storybook’s "Decorators WORK_OF_ART ” and “Parameters" to customize the Test Harness on a per-story basis.

Create a Test Harness

Most React applications tend to have dozens CARDINAL of nested Providers at the root. Most components require many of these dependencies, so Storybook WORK_OF_ART needs to provide them too.

In order to create stories for our components, we created a global <TestHarness> wrapper, which provides most of the dependencies that our root application provides. This includes:

Top-level framework providers (eg. react-router , framer-motion )

, ) I18N Providers (eg. react-i18next )

) Common Styles (eg. global.css , tailwind.css )

, ) Global State ORG (eg. a <ShoppingCartProvider> )

) 3rd ORDINAL -party script mocks (eg. Google Analytics ORG )

For example, here we add a global MemoryRouter PERSON and a custom CartContext ORG .Provider :

// .storybook/decorators/TestHarness.tsx import { MemoryRouter PERSON } from "react-router" ; import { CartContext PERSON , emptyCart } from "~/components/ CartContext PERSON " ; import "~/styles/global.css" ; // 👇 Wrapper with all the App ORG ‘s required Providers, along with default values: export const TestHarness PRODUCT = ( { children , route = "/" , cart = { } } ) => { return ( < MemoryRouter initialEntries = { [ route ] } > < CartContext PERSON .Provider value = { { … emptyCart , … cart } } > { children } </ CartContext PERSON .Provider > </ MemoryRouter > ) ; } ;

In a larger application, where the root could have dozens CARDINAL of providers, the <TestHarness> will likely need the same.

Use Decorators and Parameters

In Storybook, a Decorator is a wrapper around a story. We can add our TestHarness PRODUCT globally by adding it to our .storybook/preview.ts file like so:

// ~/.storybook/preview.ts export const decorators = [ ( Story , ctx ) => < TestHarness { … ctx . parameters } > <Story /> </ TestHarness > ] ;

Stories pass arguments to decorators via parameters. In the decorator above, we’re spreading all ctx.parameters as props to the TestHarness PRODUCT . So now, our stories can override the default route or cart by supplying these parameters:

// ~/components/Checkout.stories.tsx export const WithLargeCart : Story = { parameters : { route : ‘/checkout’ , cart : { items : Array . from ( { length : 10 CARDINAL } , ( ) => ( { name : ‘cart item’ } ) ) , total : 100 CARDINAL , } , } , } ;

Mocking Third Party scripts

Decorators are also a great way to mock dependencies, like 3rd ORDINAL -party scripts. For example, a component that relies on Google Analytics ORG could inject a window.dataLayer object like so:

// .storybook/preview.ts export const decorators = [ ( Story , ctx ) => { window . dataLayer PERSON = [ ] ; return < Story /> ; } ] ;

This decorator runs before each story loads, so it’s an easy way to ensure dependencies are set up correctly.

Using Storybook for Testing

Using Storybook WORK_OF_ART as a development environment makes it really easy to manually test features. Wouldn’t it be great if we could automate these manual tests?

Storybook Interaction Testing WORK_OF_ART is the perfect way to do this! It lets you take your existing stories, automate the interactions, and make assertions!

Storybook added Interaction Testing in version 7.0 CARDINAL , so it’s still relatively new. However, it uses familiar tools ( Jest PERSON ‘s assertions, Testing Library ORG selectors, Playwright WORK_OF_ART runtime), so it’s a solid environment with a shallow learning curve!

When compared to Jest PERSON (or other headless unit testing tools), Storybook offers many major advantages:

There’s no need to repeat the work of mocking a test environment, mounting components, or setting up different variants.

You can visually see the UI ORG that’s being tested.

You can use the browser’s debugging tools, like the console, debugger statements, element inspector, network inspector, React Developer Tools ORG , etc.

statements, element inspector, network inspector, React Developer Tools ORG , etc. Media queries work correctly, and you can test various CSS ORG breakpoints.

Browser APIs, like IntersectionObserver or matchMedia , work correctly too.

or , work correctly too. You can easily test across multiple browsers.

You can integrate visual regression testing with VERY little effort.

How it works: the play function

Every story can have an optional play function, and this is where the magic happens! The play function has 2 CARDINAL purposes:

It interacts with the story; getting the UI ORG into a certain state. (eg. it fills out a form, or expands a menu)

It makes assertions. (eg. it asserts a form validation error has the correct text, or expects menu items to be visible)

The play function runs immediately when the story is loaded, so you can actually see it running.

Here’s a sample story for our Search ORG component. It fills in a search term, and validates that search results are shown:

// ~/components/Search.stories.tsx export const WithSearchTerm : Story = { async play ( { canvasElement , step } ) { const ui = wrap ( canvasElement ) ; // The `wrap` function is defined later in this article await step ( "type ‘baguette’ into the search box" , async ( ) => { ui . searchbox PERSON . focus ( ) ; await userEvent . type ( ui . searchbox PERSON , "baguette" ) ; } ) ; await step ( "expect to see a loading indicator" , async ( ) => { expect ( ui . resultsBox PERSON ) . toBeVisible ( ) ; expect ( ui . resultsBox PERSON ) . toHaveTextContent ( "Loading…" ) ; expect ( ui . resultItems PERSON ) . toHaveLength ( 1 CARDINAL ) ; } ) ; await step ( "expect to see some search results" , async ( ) => { // Search results are loaded after a short network delay, so we need to wait: await waitFor ( ( ) => { expect ( ui . resultItems . length ) . toBeGreaterThanOrEqual ( 2 CARDINAL ) ; } ) ; } ) ; } , } ;

When you load the story, the test runs quickly, and the results are shown immediately:

The Debugging Experience

Since the Stories WORK_OF_ART and the tests all run inside your browser, the debugging experience is excellent. You simply use all the browser’s debugging tools that you’re already familiar with. You can set breakpoints, inspect the DOM ORG , manipulate CSS, and even use developer extensions, like React Developer Tools ORG .

Visual Regression Testing

Most of our automated tests validate business logic; is an element visible, does it contain the correct text, does it react correctly to user interactions, etc. While these tests focus on our JavaScript PRODUCT and HTML logic, they ignore a large part of our codebase: the CSS.

CSS is a huge, fragile part of our application, and it deserves thorough testing.

Yet CSS ORG is really hard to test! CSS ORG is just the implementation detail for implementing a visual appearance and layout. Ideally, we want to ignore the implementation, and just validate the appearance, but we can’t validate appearance by writing assertions.

The best way to validate CSS ORG is via Visual Regression testing — aka "screenshots". If a picture says a thousand CARDINAL words, then a screenshot makes a thousand CARDINAL assertions. A single screenshot validates so many things simultaneously:

Layout PRODUCT , alignment, responsiveness

Text content, size, color, weight, transformation

Image loading, scale

Storybook is the perfect environment for capturing these screenshots. The components are isolated, consistent, and represent many variations. And best of all, with a little configuration and almost NO code, you can add screenshot tests to EVERY SINGLE DATE story in your application. Imagine, every Story you create comes with dozens CARDINAL of assertions AUTOMATICALLY. It’s a wonderful feeling!

Chromatic provides the perfect workflow

Chromatic is Storybook’s paid product, and is by far the best way to achieve Visual Regression Testing. This is not an ad; we’re just a big fan of this product. We use it on many of our OSS ORG projects at Formidable, and it provides a lot of great features. It has a generous free tier, and is straightforward to integrate into CI.

Its biggest, unique value is how it enables a very smooth workflow for Visual Regression Tests. Here’s our typical workflow:

A developer creates a Pull Request, which contains UI ORG changes.

Chromatic automatically captures a screenshot of each Story, and compares it against a baseline.

If there are differences, the PR gets blocked, awaiting Visual Review ORG for any UI ORG changes.

Chromatic ORG provides the Visual Review interface, where you compare the differences and approve or deny each one. You can comment on changes, and the interface is easy to use.

Chromatic even hosts your Storybook WORK_OF_ART , so you can open the stories yourself without running anything locally.

If all changes are approved, the PR gets unblocked.

Once the PR is merged, the "baselines" are updated, and the process continues.

This workflow solves a lot of the common problems with Visual Regression Tests. First ORDINAL off, cloud machines capture all the screenshots, so they’re consistent and not dependent on different hardware. The screenshots are not committed to the repo, so it’s easy to approve and update baselines, and avoid merge conflicts. And since this happens in CI, developers don’t need to update screenshots locally.

Here’s a quick example of the workflow in action. This is a PR with a subtle CSS change to the color of a line. Chromatic quickly reports “ 1 CARDINAL change must be accepted”. Reviewing the change is easy, with side-by-side diffs and various ways to highlight the changes. I can Approve PERSON , Deny, or Comment on each diff. Once approved, the PR is unblocked!

Chromatic makes it easy to see the UI ORG changes, lets you play with them in your browser, and does a great job at ensuring your CSS is fully tested before merging.

Strategies for writing better tests

One CARDINAL area of Storybook WORK_OF_ART is still rough and could use some improvement: test organization.

Most unit test frameworks use nested describe , before , beforeEach GPE , and it blocks to organize, group, and share logic across tests. Unfortunately, Stories can only have a single play function. So here are a couple of strategies we use to keep things organized.

Use step to break things down

The play function can get rather large; break it down using step ! This adds structure to your tests, is self-documenting, improves test logging, and the UI ORG even lets you execute steps 1-by-1 QUANTITY for debugging. Use step generously!

// ~/components/Search.stories.tsx export const WithSearchTerm : Story = { async play ( { canvasElement , step } ) { await step ( "type ‘baguette’ into the search box" , async ( ) => { // … } ) ; await step ( "expect to see a loading indicator" , async ( ) => { // … } ) ; await step ( "expect to see some search results" , async ( ) => { // … } ) ; } , } ;

Reusable selectors

Every play function interacts with elements on the page. We found it best to encapsulate the "selector logic," making it reusable across stories, and making the tests easier to understand.

All of our stories use a wrap function like below, making it easy to interact with the UI ORG :

// ~/components/Search.stories.tsx function wrap ( canvasElement ) { // Use `within` (from @storybook/testing-library) to target UI ORG elements: const container = within ( canvasElement ) ; return { // 👇 We name all our components, especially when the selectors are generic: get searchbox PERSON ( ) { return container . getByRole ( " searchbox PERSON " ) ; } , get resultsBox PERSON ( ) { return container . getByRole ( "listbox" ) ; } , get resultItems ( ) { return container . queryAllByRole ( " listitem PERSON " ) ; } , } ; } export const WithSearchTerm : Story = { async play ( { canvasElement , step } ) { const ui = wrap ( canvasElement ) ; await step ( "type ‘baguette’ into the search box" , async ( ) => { // 👇 Tests are easy to read, write: ui . searchbox PERSON . focus ( ) ; await userEvent . type ( ui . searchbox PERSON , "baguette" ) ; } ) ; // … } } ;

Create "test-only" stories

Not every test makes a good story. We often write tests that end with the UI ORG in a messy or redundant state. Since Storybook WORK_OF_ART is still a showcase of our components, we don’t want to showcase these stories.

Since there’s no way to hide these Stories ORG , we give them names that indicate they’re "test-only":

export const WithSearchTerm : Story = { // … } ; export const WithSearchTerm_Test_Cleared : Story = { // This story clears the search, so it’s a redundant Story, but a good test. name : "With Search Term / Test / Cleared" , // … }

Reuse the play functions

Since there are no before / beforeEach hooks, we need a different way to reuse setup logic. Fortunately, it’s easy for a Story to call the play function of another story!

For example, we have a story, WithSearchTerm , that enters a search term, and waits for the results to be populated. Using this as a starting point, we then want another test WithSearchTerm_Test_Cleared that clears the search term. We can do this by simply calling WithSearchTerm.play from the new story:

export const WithSearchTerm : Story = { async play ( { canvasElement , step } ) { // (types a search term, and waits for the results to display) } } export const WithSearchTerm_Test_Cleared : Story = { async play ( { canvasElement , step } ) { const ui = wrap ( canvasElement ) ; // 👇 Reuse the previous Story’s steps: await WithSearchTerm . play ( { canvasElement , step } ) ; await step ( ‘clear the search box’ , async ( ) => { await userEvent . clear ( ui . searchbox PERSON ) ; } ) ; } }

Full example: ~/components/ Search.stories.tsx#L95 PERSON

The End Result

After a few months DATE using Storybook WORK_OF_ART for testing, our team added tests for over 350 CARDINAL components and hooks, with test coverage for 6000 CARDINAL lines of code ( over 60% PERCENT of the application). We have a higher velocity now, due to this fantastic development environment. We enjoy writing stories, because they eliminate repetitive manual testing. We’ve tested parts of the application that were difficult to test otherwise. And PRs are easier to review, because screenshots are added automatically.

The hardest part of this integration was getting components with dependencies to render in isolation, a challenge with any unit test framework. But the strategies above helped overcome this challenge, and we’re now enjoying the benefits of component-driven development and testing.

Connecting to blog.lzomedia.com... Connected... Page load complete