Storycap

Storycap 抓取你的 Storybook 并截取图片。它主要负责为诸如 reg-suit 的视觉测试生成所需的图片。
特性
- :camera: 截取每个故事的截图。通过 Puppeteer。
- :zap: 速度极快。
- :package: 零配置。
- :rocket: 提供灵活的截图选项。
- :tada: 独立于任何 UI 框架(React、Angular、Vue 等)
安装
$ npm install storycap
或者
$ npm install storycap puppeteer
安装 puppeteer 是可选的。请参阅 Chromium 版本 部分以获取更多详细信息。
入门
Storycap 有两种运行模式。一种是“简单模式”,另一种是“管理模式”。
在简单模式下,您无需配置 Storybook。您只需提供 Storybook 的 URL,例如
$ npx storycap https://:9001
您可以通过 --serverCmd 选项启动您的服务器。
$ storycap --serverCmd "start-storybook -p 9001" https://:9001
当然,您可以使用预构建的 Storybook
$ build-storybook -o dist-storybook
$ storycap --serverCmd "npx http-server dist-storybook -p 9001" https://:9001
此外,Storycap 还可以抓取已构建并托管的 Storybook 页面
$ storycap https://next--storybookjs.netlify.app/vue-kitchen-sink/
管理模式
设置 Storybook
如果您想控制故事如何被捕获(时机、大小等),请使用管理模式。
首先,将 storycap 添加到您的 Storybook 配置文件中
/* .storybook/main.js */
module.exports = {
  stories: ['../src/**/*.stories.@(js|mdx)'],
  addons: [
    '@storybook/addon-actions',
    '@storybook/addon-links',
    'storycap', // <-- Add storycap
  ],
};
接下来,使用 withScreenshot decorator 来告诉 Storycap 如何捕获您的故事。
/* .storybook/preview.js */
import { withScreenshot } from 'storycap';
export const decorators = [
  withScreenshot, // Registration the decorator is required
];
export const parameters = {
  // Global parameter is optional.
  screenshot: {
    // Put global screenshot parameters(e.g. viewport)
  },
};
注意:您可以使用 addParameters 和 screenshot 键来设置截图配置。
注意:Storycap 也支持旧版 Storybook decorator 的写法,例如 addDecorator(withScreenshot({/* 一些选项 */}))。但将 decorator 作为函数使用已被弃用且不推荐。如果您想了解更多详情,请参阅 Storybook 的 迁移指南。
设置你的故事(可选)
并且您可以通过 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',
    },
  },
};
当然,Storycap 也很好地支持 CSF 3.0 写法。
import React from 'react';
import MyComponent from './MyComponent';
export default {
  title: 'MyComponent',
  component: MyComponent,
  parameters: {
    screenshot: {
      delay: 200,
    },
  },
};
export const Normal = {};
export const Small = {
  args: {
    text: 'small',
  },
  parameters: {
    screenshot: {
      viewport: 'iPhone 5',
    },
  },
};
运行 storycap 命令
$ npx start-storybook -p 9009
$ npx storycap https://:9009
或者您可以通过 --serverCmd 选项使用一行命令执行
$ npx storycap https://:9009 --serverCmd "start-storybook -p 9009"
API
withScreenshot
withScreenshot(opt?: ScreenshotOptions): Function;
一个 Storybook decorator,用于通知 Storycap 捕获故事。
注意:将 withScreenshot 作为函数使用已被弃用。如果您想提供截图选项,请使用 addParameters。
type ScreenshotOptions
ScreenshotOptions 对象可作为 addParameters 参数或 withScreenshot 参数中 screenshot 键的值使用。
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 ""
  click?: string;                           // default ""
  skip?: boolean;                           // default false
  viewport?: Viewport;
  viewports?: string[] | { [variantName]: Viewport };
  variants?: Variants;
  waitImages?: boolean;                     // default true
  omitBackground?: boolean;                 // default false
  captureBeyondViewport?: boolean;          // default true
  clip?: { x: number; y: number; width: number; height: number } | null; // default null
}
- delay:捕获前的等待时间 [毫秒]。
- waitAssets:如果设置为 true,Storycap 会等待故事请求的所有资源(如- <img>或 CSS 背景图片)完成加载。
- waitFor:如果您设置一个返回- Promise的函数,Storycap 会等待该 Promise 被解决。您也可以设置一个返回- Promise的全局函数的名称。
- fullPage:如果设置为 true,Storycap 会捕获整个故事页面。
- focus:如果设置一个有效的 CSS 选择器字符串,Storycap 会在聚焦匹配选择器的元素后进行捕获。
- hover:如果设置一个有效的 CSS 选择器字符串,Storycap 会在悬停匹配选择器的元素后进行捕获。
- click:如果设置一个有效的 CSS 选择器字符串,Storycap 会在点击匹配选择器的元素后进行捕获。
- skip:如果设置为 true,Storycap 会取消捕获相应的故事。
- viewport,- viewports:请参阅下面的 Viewport 类型部分。
- variants:请参阅下面的 Variants 类型部分。
- waitImages:已弃用。请使用- waitAssets。如果设置为 true,Storycap 会等待故事中的- <img>加载完成。
- omitBackground:如果设置为 true,Storycap 会忽略页面背景,允许透明截图。请注意,storybook 主题也需要设置为透明。
- captureBeyondViewport:如果设置为 true,Storycap 会捕获视口之外的截图。另请参阅 Puppeteer API 文档。
- clip:如果设置,Storycap 只会捕获由 x/y/width/height 限定的屏幕区域。
type Variants
Variants 用于从1个故事生成多个 PNG。
type Variants = {
  [variantName: string]: {
    extends?: string | string[]; // default: ""
    delay?: number;
    waitAssets?: boolean;
    waitFor?: string | () => Promise<void>;
    fullPage?: boolean;
    hover?: string;
    focus?: string;
    click?: string;
    skip?: boolean;
    viewport?: Viewport;
    waitImages?: boolean;
    omitBackground?: boolean;
    captureBeyondViewport?: boolean;
    clip?: { x: number; y: number; width: number; height: number } | null;
  };
};
- extends:如果设置为其他变体的名称(或其名称数组),则此变体会继承其他变体的选项。并且此变体生成一个带有后缀的 PNG 文件,例如- _${parentVariantName}_${thisVariantName}。
type Viewport
Viewport 与 Puppeteer 视口接口兼容。
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,
    };
注意:如果设置字符串,您应该选择一个有效的设备名称。
Viewport 值可以在 viewports 字段中使用,例如
addParameters({
  screenshot: {
    viewports: {
      large: {
        width: 1024,
        height: 768,
      },
      small: {
        width: 375,
        height: 668,
      },
      xsmall: {
        width: 320,
        height: 568,
      },
    },
  },
});
function isScreenshot
function isScreenshot(): boolean;
返回当前进程是否在 Storycap 浏览器中运行。这对于仅在 Storycap 中改变您的故事行为很有用(例如,禁用 JavaScript 动画)。
命令行选项
usage: storycap [options] storybook_url
Options:
      --help                       Show help                                                                   [boolean]
      --version                    Show version number                                                         [boolean]
  -o, --outDir                     Output directory.                               [string] [default: "__screenshots__"]
  -p, --parallel                   Number of browsers to screenshot.                               [number] [default: 4]
  -f, --flat                       Flatten output filename.                                   [boolean] [default: false]
  -i, --include                    Including stories name rule.                                    [array] [default: []]
  -e, --exclude                    Excluding stories name rule.                                    [array] [default: []]
      --delay                      Waiting time [msec] before screenshot for each story.           [number] [default: 0]
  -V, --viewport                   Viewport.                                              [array] [default: ["800x600"]]
      --disableCssAnimation        Disable CSS animation and transition.                       [boolean] [default: true]
      --disableWaitAssets          Disable waiting for requested assets                       [boolean] [default: false]
      --trace                      Emit Chromium trace files per screenshot.                  [boolean] [default: false]
      --silent                                                                                [boolean] [default: false]
      --verbose                                                                               [boolean] [default: false]
      --forwardConsoleLogs         Forward in-page console logs to the user's console.        [boolean] [default: false]
      --serverCmd                  Command line to launch Storybook server.                       [string] [default: ""]
      --serverTimeout              Timeout [msec] for starting Storybook server.               [number] [default: 60000]
      --shard                      The sharding options for this run. In the format <shardNumber>/<totalShards>.
                                   <shardNumber> is a number between 1 and <totalShards>. <totalShards> is the total
                                   number of computers working.                                [string] [default: "1/1"]
      --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]
  -C, --chromiumChannel            Channel to search local Chromium. One of "puppeteer", "canary", "stable", "*"
                                                                                                 [string] [default: "*"]
      --chromiumPath               Executable Chromium path.                                      [string] [default: ""]
      --puppeteerLaunchConfig      JSON string of launch config for Puppeteer.
               [string] [default: "{ "args": ["--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage"] }"]
Examples:
  storycap https://:9009
  storycap https://:9009 -V 1024x768 -V 320x568
  storycap https://:9009 -i "some-kind/a-story"
  storycap http://example.com/your-storybook -e "**/default" -V iPad
  storycap --serverCmd "start-storybook -p 3000" https://:3000
从1个故事生成多个 PNG
默认情况下,storycap 从1个故事生成1张截图。如果您想为1个故事生成多个 PNG(例如,不同的视口、元素状态变化等),请使用 variants。
基本用法
例如
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',
        },
      },
    },
  },
};
上述配置生成2个 PNG:
- MyButton/normal.png
- MyButton/normal_hovered.png
变体键,在上述示例中为 hovered,用作生成的 PNG 文件名的后缀。并且几乎所有的 ScreenshotOptions 字段都可以作为变体值的字段使用。
注意:variants 本身和 viewports 禁止作为变体的字段。
变体组合
您可以通过 extends 字段组合多个变体。
normal.story = {
  parameters: {
    screenshot: {
      variants: {
        small: {
          viewport: 'iPhone 5',
        },
        hovered: {
          extends: 'small',
          hover: 'button.my-button',
        },
      },
    },
  },
};
上面的示例生成以下内容:
- MyButton/normal.png(默认)
- MyButton/normal_small.png(派生自- small变体)
- MyButton/normal_hovered.png(派生自- hovered变体)
- MyButton/normal_small_hovered.png(派生自- hovered和- small变体)
注意:您可以扩展 viewports 选项中的一些视口,因为 viewports 字段在内部会扩展为变体。
跨多台计算机并行化
为了跨多台计算机并行处理更多故事,可以使用 shard 参数。
shard 参数是一个字符串,格式为:<分片编号>/<总分片数>。<分片编号> 是一个介于 1 和 <总分片数> 之间(包含两端)的数字。<总分片数> 是运行的总计算机数量。
例如,使用 --shard 1/1 运行被认为是单台计算机上的默认行为。两台计算机分别运行 --shard 1/2 和 --shard 2/2,会将故事分摊到两台计算机上。
故事按其 ID 排序后,以轮询方式分布在分片中。如果一系列“紧密相连”的故事截图速度比其他故事慢,它们应该被均匀分布。
技巧
使用 Docker 运行
使用 regviz/node-xcb。
或者创建你的 Docker 基础镜像,例如
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
完全控制截图时机
有时您可能希望完全管理执行截图的时机。如果您有此想法,请使用 waitFor 选项。此字符串参数应指向一个返回 Promise 的全局函数。
例如,以下设置告诉 storycap 等待 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',
  },
});
Chromium 版本
自 v3.0.0 起,Storycap 不再直接使用 Puppeteer。相反,Storycap 按以下顺序搜索 Chromium 二进制文件:
- 已安装的 Puppeteer 包(如果您明确安装了)
- 本地安装的 Canary Chrome
- 本地安装的 Stable Chrome
您可以使用 --chromiumChannel 选项更改搜索通道,或使用 --chromiumPath 选项设置可执行 Chromium 文件路径。
Storybook 兼容性
Storybook 版本
Storycap 已在以下版本中测试:
- 简单模式- Storybook v5.x
- Storybook v6.x
 
- 管理模式- Storybook v5.x
- Storybook v6.x
 
另请参阅 examples 目录中的包。
UI 框架
Storycap(包括简单模式和管理模式)对特定的 UI 框架(例如 React、Angular、Vue.js 等)是无感知的。因此,您可以将其与您自己喜欢的框架一起用于 Storybook :smile:。
迁移
如果您已经使用 storybook-chrome-screenshot 或 zisui,请参阅迁移指南。
工作原理
Storycap 使用 Puppeteer 访问已启动的页面。
TODO
以下任务待完成。欢迎贡献 :smiley
- 升级 v2
- 将爬虫提取为 NPM 包。
- 更多单元测试。
- 捕获 JS/CSS 覆盖率。
贡献
请参阅 CONTRIBUTING.md。