/storycap

A Storybook Addon, Save the screenshot image of your stories :camera: via puppeteer.

Primary LanguageTypeScriptMIT LicenseMIT

Storycap

DEMO

npm CircleCI

A Storybook Addon, Save the screenshot image of your stories 📷 via Puppeteer.

Storycap crawls your Storybook and takes screenshot images. It is primarily responsible for image generation necessary for Visual Testing such as reg-suit.

Features

  • 📷 Take screenshots of each stories. via Puppeteer.
  • âš¡ Extremely fast.
  • 📦 Zero configuration.
  • 🚀 Provide flexible screenshot shooting options.
  • 🎉 Independent of any UI framework(React, Angular, Vue, etc...)

Install

$ npm install storycap

Getting Started

Storycap runs with 2 modes. One is "simple" and another is "managed".

With the simple mode, you don't need to configure your Storybook. All you need is give Storybook's URL, such as:

$ npx storycap http://localhost:9001

You can launch your server via --serverCmd option.

$ storycap --serverCmd "start-storybook -p 9001" http://localhost:9001

Of course, you can use pre-built Storybook:

$ build-storybook -o dist-storybook
$ storycap --serverCmd "npx http-server dist-storybook -p 9001" http://localhost:9001 

Also, Storycap can crawls built and hosted Storybook pages:

$ storycap https://storybookjs-next.now.sh/vue-kitchen-sink

Managed mode

Setup Storybook

If you want to control how stories are captured (timing or size or etc...), use managed mode.

First, register storycap addon.

/* .storybook/addons.js */

// Other addons...
import 'storycap/register';

Next, use withScreenshot decorator to tell how Storycap captures your stories.

/* .storybook/config.js */
import { addDecorator, addParameters } from '@storybook/react';
import { withScreenshot } from 'storycap';

addDecorator(withScreenshot);
addParameters({
  screenshot: {
    // Some screenshot options...
  },
});

Note: You can set configuration of screenshot with addParameters and screenshot key.

Note: Storycap also supports notation of legacy Storybook decorator such as addDecorator(withScreenshot({/* some options */}). But using decorator as function is deprecated and not recommended. See Storybook's migration guide if you want more details.

Setup your stories(optional)

And you can overwrite the global screenshot options in specific stories file via parameters.

import React from 'react';
import MyComponent from './MyComponent';

export default {
  title: 'MyComponent',
  parameters: {
    screenshot: {
      delay: 200,
    },
  },
};

export const normal = () => <MyComponent />;
export const small = () => <MyComponent text="small" />;
small.story = {
  parameters: {
    screenshot: {
      viewport: 'iPhone 5',
    },
  },
};

Of course Storycap works well with storiesOf notation:

import React from 'react';
import MyComponent from './MyComponent';
import { storiesOf } from '@storybook/react';

storiesOf('MyComponent')
  .addParameters({
    screenshot: {
      delay: 200,
    },
  })
  .add('normal', () => <MyComponent />)
  .add('small', () => <MyComponent text="small" />, {
    screenshot: {
      viewport: 'iPhone 5',
    },
  });

Run storycap Command

$ npm run storybook -p 9009
$ npx run storycap http://localhost:9009

Or you can exec with one-liner via --serverCmd option:

$ npx run storycap http://localhost:9009 --serverCmd "storybook -p 9009"

API

withScreenshot

withScreenshot(opt?: ScreenshotOptions): Function;

A Storybook decorator to notify Storycap to captures stories.

Note: Using withScreenshot as function is deprecated. Use addParameters if you give screenshot options.

type ScreenshotOptions

ScreenshotOptions object is available as the value of the key screenshot of addParameters argument or withScreenshot argument.

interface ScreenshotOptions {
  delay?: number;                           // default 0 msec
  waitAssets?: boolean;                     // default true
  waitFor?: string | () => Promise<void>;   // default ""
  fullPage?: boolean;                       // default true
  hover?: string;                           // default ""
  focus?: string;                           // default ""
  skip?: boolean;                           // default false
  viewport?: Viewport;
  viewports?: string[] | { [variantName]: Viewport };
  variants?: Variants;
  waitImages?: boolean;                     // default true
}
  • delay: Waiting time [msec] before capturing.
  • waitAssets: If set true, Storycap waits until all resources requested by the story, such as <img> or CSS background images, are finished.
  • waitFor : If you set a function to return Promise, Storycap waits the promise is resolved. You can also set a name of global function that returns Promise.
  • fullPage: If set true, Storycap captures the entire page of stories.
  • focus: If set a valid CSS selector string, Storycap captures after focusing the element matched by the selector.
  • hover: If set a valid CSS selector string, Storycap captures after hovering the element matched by the selector.
  • skip: If set true, Storycap cancels capturing corresponding stories.
  • viewport, viewports: See type Viewport section below.
  • variants: See type Variants section below.
  • waitImages: Deprecated. Use waitAssets. If set true, Storycap waits until <img> in the story are loaded.

type Variants

Variants is used to generate multiple PNGs from 1 story.

type Variants = {
  [variantName: string]: {
    extends?: string | string[]; // default: ""
    delay?: number;
    waitAssets?: boolean;
    waitFor?: string | () => Promise<void>;
    fullPage?: boolean;
    hover?: string;
    focus?: string;
    skip?: boolean;
    viewport?: Viewport;
    waitImages?: boolean;
  };
};
  • extends: If set other variant's name(or an array of names of them), this variant extends the other variant options. And this variant generates a PNG file with suffix such as _${parentVariantName}_${thisVariantName}.

type Viewport

Viewport is compatible for Puppeteer viewport interface.

type Viewport = string | {
  width: number;              // default: 800
  height: number;             // default: 600
  deviceScaleFactor:?number;  // default: 1,
  isMobile?: boolean;         // default: false,
  hasTouch?: boolean;         // default: false,
  isLandscape?: boolean;      // default: false,
};

Note: You should choose a valid device name if set string.

Viewport values are available in viewports field such as:

addParameters({
  screenshot: {
    viewports: {
      large: {
        width: 1024,
        height: 768,
      },
      small: {
        width: 375,
        height: 668,
      },
      xsmall: {
        width: 320,
        height: 568,
      },
    },
  },
});

function isScreenshot

function isScreenshot(): boolean;

Returns whether current process runs in Storycap browser. It's useful to change your stories' behavior only in Storycap (e.g. disable JavaScript animation).

Command Line Options

usage: storycap [options] storybook_url

Options:
  --help                       Show help                                                                       [boolean]
  --version                    Show version number                                                             [boolean]
  --outDir, -o                 Output directory.                                   [string] [default: "__screenshots__"]
  --parallel, -p               Number of browsers to screenshot.                                   [number] [default: 4]
  --flat, -f                   Flatten output filename.                                       [boolean] [default: false]
  --include, -i                Including stories name rule.                                        [array] [default: []]
  --exclude, -e                Excluding stories name rule.                                        [array] [default: []]
  --delay                      Waiting time [msec] before screenshot for each story.               [number] [default: 0]
  --viewport, -V               Viewport.                                                  [array] [default: ["800x600"]]
  --disableCssAnimation        Disable CSS animation and transition.                           [boolean] [default: true]
  --disableWaitAssets          Disable waiting for requested assets                           [boolean] [default: false]
  --silent                                                                                    [boolean] [default: false]
  --verbose                                                                                   [boolean] [default: false]
  --serverCmd                  Command line to launch Storybook server.                           [string] [default: ""]
  --serverTimeout              Timeout [msec] for starting Storybook server.                   [number] [default: 20000]
  --captureTimeout             Timeout [msec] for capture a story.                              [number] [default: 5000]
  --captureMaxRetryCount       Number of count to retry to capture.                                [number] [default: 3]
  --metricsWatchRetryCount     Number of count to retry until browser metrics stable.           [number] [default: 1000]
  --viewportDelay              Delay time [msec] between changing viewport and capturing.        [number] [default: 300]
  --reloadAfterChangeViewport  Whether to reload after viewport changed.                      [boolean] [default: false]
  --stateChangeDelay           Delay time [msec] after changing element's state.                   [number] [default: 0]
  --listDevices                List available device descriptors.                             [boolean] [default: false]
  --puppeteerLaunchConfig      JSON string of launch config for Puppeteer.
               [string] [default: "{ "args": ["--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage"] }"]

Examples:
  storycap http://localshot:9009
  storycap http://localshot:9009 -V 1024x768 -V 320x568
  storycap http://localshot:9009 -i "some-kind/a-story"
  storycap http://example.com/your-storybook -e "**/default" -V iPad
  storycap --serverCmd "start-storybook -p 3000" http://localshot:3000

Multiple PNGs from 1 story

By default, storycap generates 1 screenshot image from 1 story. Use variants if you want multiple PNGs(e.g. viewports, element's states variation, etc...) for 1 story.

Basic usage

For example:

import React from 'react';
import MyComponent from './MyButton';

export default {
  title: 'MyButton',
};

export const normal = () => <MyButton />;
normal.story = {
  parameters: {
    screenshot: {
      variants: {
        hovered: {
          hover: 'button.my-button',
        },
      },
    },
  },
};

The above configuration generates 2 PNGs:

  • MyButton/normal.png
  • MyButton/normal_hovered.png

The variant key, hovered in the above example, is used as suffix of the generated PNG file name. And the almost all ScreenshotOptions fields are available as fields of variant value.

Note: variants itself and viewports are prohibited as variant's field.

Variants composition

You can composite multiple variants via extends field.

normal.story = {
  parameters: {
    screenshot: {
      variants: {
        small: {
          viewport: 'iPhone 5',
        },
        hovered: {
          extends: 'small',
          hover: 'button.my-button',
        },
      },
    },
  },
};

The above example generates the following:

  • MyButton/normal.png (default
  • MyButton/normal_small.png (derived from the small variant
  • MyButton/normal_hovered.png (derived from the hovered variant
  • MyButton/normal_small_hovered.png (derived from the hovered and small variant

Note: You can extend some viewports with keys of viewports option because the viewports field is expanded to variants internally.

Tips

Run with Docker

Use regviz/node-xcb.

Or create your Docker base image such as:

FROM node:12

RUN apt-get update -y
RUN apt-get install -yq gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 \
    libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 \
    libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 \
    libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 \
    ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget

Full control the screenshot timing

Sometimes you may want to full-manage the timing of performing screenshot. Use the waitFor option if you think so. This string parameter should points a global function to return Promise.

For example, the following setting tells storycap to wait for resolving of fontLoading:

<!-- ./storybook/preview-head.html -->
<link rel="preload" href="/some-heavy-asset.woff" as="font" onload="this.setAttribute('loaded', 'loaded')" />
<script>
  function fontLoading() {
    const loaded = () => !!document.querySelector('link[rel="preload"][loaded="loaded"]');
    if (loaded()) return Promise.resolve();
    return new Promise((resolve, reject) => {
      const id = setInterval(() => {
        if (!loaded()) return;
        clearInterval(id);
        resolve();
      }, 50);
    });
  }
</script>
/* .storybook/config.js */
import { addParameters, addDecorator } from '@storybook/react';
import { withScreenshot } from 'storycap';

addDecorator(withScreenshot);
addParameters({
  screenshot: {
    waitFor: 'fontLoading',
  },
});

Storybook compatibility

Storybook versions

Storycap is tested with the followings versions:

  • Simple mode:
    • Storybook v4.x
    • Storybook v5.x
  • Managed mode:
    • Storybook v4.x
    • Storybook v5.x

See also packages in examples directory.

UI frameworks

Storycap (with both simple and managed mode) is agnostic for specific UI frameworks(e.g. React, Angular, Vue.js, etc...). So you can use it with Storybook with your own favorite framework 😄 .

Migration

See migration guide if you already use storybook-chrome-screenshot or zisui.

How it works

Storycap accesses the launched page using Puppeteer.

TODO

The following tasks remain. Contributes are welcome 😃

  • Upgrade v2
  • Extract crawler as a NPM package.
  • More unit testing.
  • Capture with JS/CSS coverages.

Contributing

See CONTRIBUTING.md.

License

MIT © reg-viz