目录:网上冲浪指南

开发 UI 组件 —— 视觉回归测试

2021/12/28

在开发 UI 组件库时,需要使用视觉回归测试来保证在 Design Token 变更的情况下,组件库中的组件没有被意外地修改。

以一个 Button 组件为例,其 props 的类型如下:

export type ButtonType = 'primary' | 'secondary' | 'text';
export type ButtonSize = 'small' | 'medium' | 'large';
export type ButtonColor = 'default' | 'danger' | 'success' | 'warning';

export interface ButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
  /**
   * The button type.
   * @default `secondary`
   */
  type?: ButtonType;
  /**
   * The button color.
   * @default `default`
   */
  color?: ButtonColor;
  /**
   * The button size.
   * @default `medium`
   */
  size?: ButtonSize;
  disabled?: boolean;
  onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
  icon?: React.ReactNode;
  loading?: boolean;
  htmlType?: 'button' | 'submit' | 'reset';
}

这样的一个 Button 展示在最终用户面前的样子将会有 1728 种。

type * color * size * disable * loading * (default + hover + focus + active) * (icon + icon/text + text)
= 3 * 4 * 3 * 2 * 2 * 4 * 3
= 1728

这些可能情况如果要人手工来进行 UI 检查是不切实际的,所以必须得借助自动化工具的力量:

  • 能够方便的枚举出组件所有可能的状态

  • 能够对浏览器中的内容截屏

  • 能够对比每次运行测试时截屏的差异

经过我的一番面向搜索引擎调研,最终确定了我的方案:Storybook + Playwright。

使用 Playwright 实现浏览器自动化

Playwright 是一款非常易于使用的浏览器自动化工具。相较于我之前使用的 Cypress 完全没有黑 Cypress 的意思,它有如下几个感知非常强烈的优势:

  1. 零配置,不需要配置 Playwright 就能很好地运行起来

  2. 无限制,能够 真正地执行鼠标 hover、move 等操作

  3. 自带代码生成器,通过运行 playwright codegen http://example.com 就可以将操作录制为代码

  4. 自带 截屏对比功能

  5. 自带 TypeScript 支持,且无需配置

使用 Storybook 来设置组件的状态

一提到写团队内部的组件库,我就会想到 Stroybook,这次我就用到了它提供的 通过 URL 参数来设置组件 Props 的功能

只要我们提供能够通过控件调整 Props 的 Story:

export default {
  component: Button,
  title: 'Components/Button',
  args: {
    disabled: false,
    loading: false,
  },
  argTypes: {
    icon: {
      control: false,
    },
    children: {
      control: false,
    },
    loading: {
      control: { type: 'boolean' },
      defaultValue: false,
    },
  },
} as ComponentMeta<typeof Button>;

const Template: ComponentStory<typeof Button> = (args: ButtonProps) => <Button {...args}>Press Me</Button>;

export const Default = Template.bind({});

Default.args = {
  type: 'secondary',
  color: 'default',
  children: 'Press Me',
};

我们就可以通过修改 URL 的 query string 来控制组件的 Props,例如:

http://localhost:9011/iframe.html?id=components-button--default&args=type:primary;color:success&viewMode=story

这个地址将会打开一个只渲染组件的页面,而且会自动地将组件的 props 为 type=primary color=success

“编写” 1728 个测试用例

如上所述,组件测试用例是通过多个 props 取不同的值排列组合而成的,所以我们其实并不用老老实实地将所有的排列组合手写出来。我采用的是嵌套 forof 循环的方式:

const stories = ['default', 'icon-text-button', 'icon-button'];
const types = ['primary', 'secondary', 'text'];
const sizes = ['small', 'medium', 'large'];
const colors = ['default', 'success', 'warning', 'danger'];
const loadings = [true, false];
const disabledStates = [true, false];

test.describe.parallel(`button`, () => {
  for (const story of stories) {
    for (const type of types) {
      for (const color of colors) {
        for (const size of sizes) {
          for (const loading of loadings) {
            for (const disabled of disabledStates) {
              test('write your test here', () => {
                await page.goto(
                  `http://localhost:9011/iframe.html?id=components-button--${story}&args=type:${type};color:${color};size:${size};loading:${loading};disabled:${disabled}`
                );
              });
            }
          }
        }
      }
    }
  }
});

这样上千个测试用例就很容易编写出来了。

禁用 CSS 动画

为了加快测试的执行,并禁用掉 loading 状态的动画,在 Playwright 中可以通过下面的方法来禁用掉 css 动画。

export class FigmaFramePageObject {
  root: Locator;

  private _disableCssAnimationStyleTag: ElementHandle | undefined;

  constructor(public page: Page) {
    this.root = page.locator('#root');
  }

  async disableCssAnimation() {
    this._disableCssAnimationStyleTag = await this.page.addStyleTag({
      content: '* {transition: none !important; animation: none !important;}',
    }); (1)
  }

  async enableCssAnimation() {
    if (this._disableCssAnimationStyleTag) {
      await this._disableCssAnimationStyleTag.evaluate((tag) => tag.parentNode?.removeChild(tag)); (2)
      await this._disableCssAnimationStyleTag.dispose();
      this._disableCssAnimationStyleTag = undefined;
    }
  }
}
1 向页面中注入一个带有 CSS 代码的 Style 标签,禁用掉所有元素的 CSS 动画
2 移除掉上面注入的 Style 标签

触发 ':active' 选择器

对于按钮类型的组件来说,按下鼠标时,组件的样式会相应的发生改变。在 Cypress 中想要测试这种场景是很困难的,因为 Cypress 没法真实的模拟鼠标动作。不过,这对于 Playwright 来说不算什么:

const box = await element.boundingBox();
const { x, y, height, width } = box!; (1)
await page.mouse.move(x + width / 2, y + height / 2); (2)
await page.mouse.down(); (3)
// write your assertion here
await page.mouse.up(); (4)
1 获取组件的位置
2 将鼠标移动到组件元素中间
3 按下鼠标但不松开,这样 :active 状态就会保持触发的状态
4 松开鼠标

提高截图清晰度

Playwright 默认不会模拟 Hi-DPI 屏幕,这会导致两个问题

  1. 组件比较小的时候,细节部分的样式无法清楚的渲染出来

  2. snapshot 截图清晰度很低

可以参考 这篇文档 来设置 Playwright,使其输出 Hi-DPI 屏幕的截图。

内容导航

本网站所展示的文章由 Zeeko Zhu 采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可

Zeeko's blog, Powered by ASP.NET Core 🐳