Skip to main content
Version: Current

Storybook

The happo/storybook module makes it easy to integrate your Storybook app with Happo.

Installation​

In your project, install the happo package.

npm install --save-dev happo

Configuration​

Add the following to your happo.config.ts configuration file:

happo.config.ts
import { defineConfig } from 'happo';

export default defineConfig({
integration: {
type: 'storybook',
configDir: '.storybook',
},
// ... rest of config
});

That is the whole setup. The happo CLI puts its client runtime into the Storybook package it builds, so there is nothing to register by hand.

You can also import the runtime yourself:

.storybook/preview.js
import 'happo/storybook/register';

This is optional — it is how you reach setThemeSwitcher, forceHappoScreenshot and the other helpers below.

note

Before v6.19.1, this import was required. Without it the built package carried no client runtime, and the run failed with Timed out while waiting for window.happo.

Add a happo script to package.json:

package.json
{
"scripts": { "happo": "happo" }
}

The Happo addons panel​

You can add a Happo panel to your Storybook UI. This is optional, but it makes it easier to test hooks and see Happo parameters for your stories.

note

Before v6.19.1, the decorator below only worked under @storybook/react. On any other renderer it replaced every story with a React element the renderer could not draw, so every screenshot in the report came out as the text [object Object]. Upgrade before adding it, or leave it out.

Add this to .storybook/main.js:

.storybook/main.js
module.exports = {
addons: ['happo/storybook/preset'],
};

Add this to .storybook/preview.js:

.storybook/preview.js
import happoDecorator from 'happo/storybook/decorator';

export const decorators = [happoDecorator];

Options​

These options are available to the integration field in happo.config.ts:

  • configDir specify the name of the Storybook configuration directory. The default is '.storybook'.
  • outputDir the name of the directory where compiled files are saved. The default is '.out'.
  • staticDir directory where to load static files from, comma-separated list.
  • usePrebuiltPackage set to true to skip building storybook and instead use an already built package. It's important that the outputDir matches the place where the prebuilt package is located. Default is false.
  • previewOnly build the preview without the Storybook manager UI, which makes the uploaded package much smaller. Default is true. Set it to false if you download built packages and browse them locally.
  • navigatePerStory set to true to give every story a fresh page load instead of paging through stories client-side. Slower, but helps when state leaks between stories. Default is false.

These options are mostly the same ones used for the build-storybook CLI command. See https://storybook.js.org/configurations/cli-options/#for-build-storybook

Running​

To execute the test suite, run

npm run happo

Partial runs​

Happo supports running only a subset of your stories using the --only and --skip CLI options (available since v6.10.0). This is useful for reducing quota usage and speeding up your test suite — similar to how --onlyStoryFiles works with other providers.

Using --only for inclusion​

Pass --only with a JSON array of components or story files to render. All other stories will be excluded from the run but included in the Happo report. You can mix component and storyFile entries, though most runs will stick to one type:

npm run happo --only '[{"component":"Card"},{"storyFile":"./src/stories/Button.stories.js"}]'

Using --skip for exclusion​

Pass --skip with a JSON array of components or story files to exclude from the run. Everything else will be rendered as normal:

npm run happo --skip '[{"component":"Card"},{"storyFile":"./src/stories/Button.stories.js"}]'

Both options accept the same entry format — see the CLI reference for the full list of entry forms.

How it works​

When you run Happo with --only or --skip, Happo will:

  1. Find a recent baseline report by traversing the git history backwards from the merge-base with the base branch (for PRs) or simply backwards in git history (for pushes to the main/default branch).
  2. Statically determine which stories were excluded from the run using the index.json file built by Storybook.
  3. Send information about the excluded stories to Happo, along with a reference to the baseline report.
  4. Generate fresh screenshots for the included stories only.
  5. Combine the new screenshots with the matching screenshots from the baseline report to produce a full comparison report. Only the new screenshots are counted against your Happo quota.

CI setup​

To use partial runs in CI, set up triggers to run Happo on pushes to PRs as well as pushes to the main/default branch. Happo will perform partial runs for PRs and full baseline runs for the main branch.

Edge cases​

  • Deleted stories — even when using --only, Happo always tells the server which stories were excluded. This means deleted stories still appear correctly in comparison reports.
  • Pending baseline reports — if the baseline report isn't ready yet (e.g. you merged while a previous report was still being created), Happo will wait for it before finalizing the new report. Happo will make a best effort to filter out the excluded stories, but if the story files are ill-formatted or otherwise unresolvable, it will fall back to generating a full Happo report.

Reading partial run reports​

In Happo comparison reports, the sidebar shows the effects of a partial run. You will see something like:

6,809 snapshots · 2,233 quota used · 113 components

Clicking the "quota used" link takes you to a page showing which snapshots were freshly rendered and which were carried over from the baseline report. For debugging purposes, it's good practice to log the value of your --only (or --skip) filter in your CI logs so you can verify what was included in a given run.

Tips and Tricks​

If you want to have better control over what addons and/or decorators get loaded you can make use of the isHappoRun function exported by happo/storybook/register:

.storybook/preview.js
import { isHappoRun } from 'happo/storybook/register';

if (!isHappoRun()) {
// load some addons/decorators that happo won't use
} else {
// load some addons/decorators that happo will use
}

Disabling a story​

If some of your stories aren't well suited for Happo, you can disable them by setting a happo: false parameter. This can be done in the default export to globally disable all stories in the same file, or individually on certain stories.

components/FooComponent.stories.js
export default {
title: 'FooComponent',
parameters: {
happo: false, // this will disable all `FooComponent` stories
},
};

const WithBorder = () => <FooComponent bordered />;

WithBorder.parameters = {
happo: false, // this will disable the `WithBorder` story
};

export { WithBorder };

Dark mode and themes​

If you want to take screenshots in more than one theme, you can make Happo automatically render stories in several themes. This is great if you for instance want to make sure that your components look right in both dark mode and light mode.

Start by adding a happo.themes parameter to one or more of your stories:

components/Foo.stories.js
const Foo = () => <FooExample />;
Foo.parameters = {
happo: {
themes: ['light', 'dark'],
},
};
export { Foo };

Additionally, you also need to provide Happo with a "theme switcher" function. The happo/storybook/register import will export a setThemeSwitcher function that will allow you to control theme switching. Here's an example that makes use of storybook-dark-mode:

.storybook/preview.js
import { setThemeSwitcher } from 'happo/storybook/register';
import { DARK_MODE_EVENT_NAME } from 'storybook-dark-mode';

setThemeSwitcher((theme, channel) => {
return new Promise(resolve => {
const isDarkMode = theme === 'dark';

// Listen for dark mode to change and resolve.
channel.once(DARK_MODE_EVENT_NAME, resolve);
// Change the theme.
channel.emit(DARK_MODE_EVENT_NAME, isDarkMode);
});
});

The theme passed to your theme switcher function is the name of the theme that Happo wants to switch to. If we use the Foo example from above, it will be either the string 'light' or the string 'dark'.

The channel parameter passed to your theme switcher as the second argument is the addons channel. You can use this to subscribe to and send events.

If you want to set happo.themes globally for all stories, the best way is through the parameters export in .storybook/preview.js:

.storybook/preview.js
export const parameters = {
happo: { themes: ['light', 'dark'] },
};

Limiting targets​

If you want to avoid rendering an example in all targets, you can use a targets array defined for an example. The example will then be rendered in the specified targets exclusively.

components/FooComponent.stories.js
export default {
title: 'FooComponent',
parameters: {
happo: {
targets: ['chrome-small'],
},
},
};

In the example above, the FooComponent > Default story will only be rendered in the target named chrome-small (defined in happo.config.ts).

Waiting for content​

In some cases, examples might not be ready by the time Happo takes the screenshot. Adding a delay might help, but only if the asynchronous event is consistently timed. In these cases the waitForContent parameter might help. Let's assume that PaymentForm in the example below loads some third-party iframe that you have no control over, loading a credit card form. In order to wait for the iframe to finish, we can add a waitForContent parameter with some unique string in the iframe.

If the content does not appear within the render timeout, the snap fails by default. See failOnWaitForTimeout if you need to opt out of that behavior.

components/PaymentForm.stories.js
const Basic = () => <PaymentForm />;
Basic.parameters = {
happo: {
waitForContent: 'Credit card',
},
};
export { Basic };

Waiting for a condition to be truthy​

To make Happo wait with the screenshot until a condition has been met, use the waitFor option. Specify a function that returns true (or anything truthy) when the time is right to take the screenshot.

If the condition does not become truthy within the render timeout, the snap fails by default. See failOnWaitForTimeout if you need to opt out of that behavior.

Here's an example that waits for a specific element (.credit-card) to appear:

components/PaymentForm.stories.js
const Basic = () => <PaymentForm />;
Basic.parameters = {
happo: {
waitFor: () => document.querySelector('.credit-card'),
},
};
export { Basic };

Here's another example that waits for a specific number of elements:

components/PaymentForm.stories.js
const Basic = () => <PaymentForm />;
Basic.parameters = {
happo: {
waitFor: () => document.querySelectorAll('.validation-output').length === 5,
},
};
export { Basic };

To test this function you can use the Happo panel. There will be an "Invoke" button next to the waitFor parameter. Click it to run the function. Check the JavaScript console for details on the execution.

Setting delay for a story​

Use delays only as a last resort. They slow down your test suite and rarely get to the bottom of the issue.

Happo will make its best to wait for your stories to render, but at times you might need a little more control in the form of delays. Use the happo.delay parameter to set an individual delay for a story:

components/FooComponent.stories.js
export default {
title: 'FooComponent',
parameters: {
happo: {
delay: 200, // set a 200ms delay for all FooComponent stories
},
},
};

const WithBorder = () => <FooComponent bordered />;

WithBorder.parameters = {
happo: {
delay: 1000, // Set a 1000ms delay for the WithBorder story
},
};

export { WithBorder };

Overriding the default render timeout​

By default, Happo will wait up to 2 seconds for a story to complete. In some cases, you might have to increase this timeout to allow certain things to finish up properly. An example could be if you have a story with a play function using userEvent.type with a delay.

components/Interactive.stories.js
import { userEvent } from '@storybook/testing-library';

export const InteractiveStory = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.type(canvas.getByRole('textbox'), 'some longer text', {
delay: 200,
});
},
};

This story would take over 3 seconds to finish. To make happo wait that long, you can use setRenderTimeoutMs to increase the timeout.

.storybook/preview.js
import { setRenderTimeoutMs } from 'happo/storybook/register';

setRenderTimeoutMs(5000);

Setting a longer timeout won't affect rendering times for fast/regular stories. It is only in effect if you use the play function to do interactions, or if you use waitFor or waitForContent.

The beforeScreenshot hook​

If you need to interact with the DOM before a screenshot is taken you can use the beforeScreenshot option. This parameter, expected to be a function, is called right before Happo takes the screenshot. You can use this to e.g. click a button, enter text in an input field, remove certain elements, etc.

Here's an example where a button is clicked to open a modal:

components/ModalExample.stories.js
const BasicModal = () => <ModalExample />;
BasicModal.parameters = {
happo: {
beforeScreenshot: () => {
const clickEvent = new MouseEvent('click', {
view: window,
bubbles: true,
cancelable: false,
});
document.querySelector('button.open-modal').dispatchEvent(clickEvent);
},
},
};
export { BasicModal };

You can use async here as well:

components/ModalExample.stories.js
const BasicModal = () => <ModalExample />;
BasicModal.parameters = {
happo: {
beforeScreenshot: async () => {
await doSomethingAsync();
},
},
};
export { BasicModal };

To test this function you can use the Happo panel. There will be an "Invoke" button next to the beforeScreenshot parameter. Click it to run the function. Check the JavaScript console for details on the execution.

The afterScreenshot hook​

Similar to beforeScreenshot, this hook can be used to clean up things from the DOM after a story has been fully processed.

Here's an example where a lingering DOM element is removed.

components/Foo.stories.js
const Foo = () => <FooExample />;
Foo.parameters = {
happo: {
afterScreenshot: () => {
document.querySelector('.some-selector').remove();
},
},
};
export { Foo };

Same as for beforeScreenshot, you can use async as well.

To test this function you can use the Happo panel. There will be an "Invoke" button next to the afterScreenshot parameter. Click it to run the function. Check the JavaScript console for details on the execution.

Using forceHappoScreenshot​

If you are using the play function and the Interactions addon you can force Happo to take screenshots of different steps along the way. Here's an example of a Dropdown story that we open and close in two different steps:

components/Dropdown.stories.js
import { forceHappoScreenshot } from 'happo/storybook/register';

export const Dropdown = {
play: async ({ args, canvasElement, step }) => {
const canvas = within(canvasElement);

await step('open', async () => {
await userEvent.click(canvas.getByRole('button'));
await expect(canvas.getByText('Edit item')).toBeInTheDocument();
await forceHappoScreenshot('open');
});

await step('closed', async () => {
await userEvent.click(canvas.getByRole('button'));
await expect(canvas.getByText('Edit item')).not.toBeInTheDocument();
await forceHappoScreenshot('closed');
});
},
};

The forceHappoScreenshot function takes a string argument which will be used to identify the story in the Happo report. In the above example, you will see these snapshots:

  • Dropdown > Default-open
  • Dropdown > Default-closed
  • Dropdown > Default

Apart from taking all the "forced" screenshot, Happo also takes one screenshot of the "finished" state of the play function. This means that you could potentially omit the last step in the play execution, since it will be part of the Happo report anyway.

Under the hood, forceHappoScreenshot throws an error that gets picked up by Happo. This means that the play function will be invoked several times, restarting execution from the beginning (until Happo finds a step that it hasn't seen before). When the play function is executed outside of Happo (e.g. when you're using the Storybook UI), the forceHappoScreenshot call will simply be ignored.

Debugging​

If you want to debug your test suite similar to how Happo workers process jobs, you can follow these steps:

  1. In a browser, go to the storybook URL. E.g. http://localhost:3000
  2. The URL will change to something like http://localhost:3000/?selectedKind=foo&selectedStory=default
  3. Change the URL to point to /iframe.html, e.g. http://localhost:3000/iframe.html
  4. Open the JavaScript console
  5. Paste this JavaScript snippet and hit enter: happo.nextExample().then((item) => console.log(item))
  6. Run that code again repeatedly to step through each example (use the arrow up key to reuse the last command)

To quickly run through all examples, follow steps 1-4, then paste this script instead:

var renderIter = function () {
window.happo.nextExample().then(function (a) {
if (!a) {
return;
}
console.log(a);
renderIter();
});
};
renderIter();

Troubleshooting​

  • Getting Timed out while waiting for window.happo? The error names the most likely cause; see the debugging docs for what each one means. Before v6.19.1, the usual fix is adding import 'happo/storybook/register' to your .storybook/preview.js file.
  • Getting spurious diffs from fonts not loading? Happo workers will wait for fonts to load before taking the screenshot, but it assumes that fonts it has already seen are already available. Make sure the @font-face declaration is declared globally and not part of the stories themselves.
  • Seeing a warning that your Storybook was built with features.developmentModeForBuild? That flag makes Storybook define process.env.NODE_ENV as "development" in a production build, so the built package ships the development build of your framework. For React that is roughly twice the bundle, and considerably more than twice the render cost. Happo renders every story, so the cost lands on every snapshot in the report: expect slower jobs and a higher chance of timeouts. Remove the flag from your .storybook/main config to build in production mode.