前端工程师应该如何正确面对 UI 组件视觉回归测试

2 阅读16分钟

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!

引言

「基础组件」通常也被称作原子组件,所谓的原子组件也就意味着具备一定不易变性和普适性的微小组件。

通常具有一定规模的前端团队都会自己搭建一套满足私有定制化内容的基础组件库,公司内的各个 Web 站点也正是基于这一套组件库繁衍而来。

由于组件性质不同于页面,很多时候人工测试无法专注于单个独立组件。尽管我们可以通过单元测试来防止单个组件在功能方面出现意外行为,但对于原子组件的 UI 展示可测性,许多前端工程师常常感到无从下手。

在这篇文章中,我将与大家讨论如何通过自动化的方式,在基础类组件的开发阶段防止意外视觉内容变更。

UI 自动化是否有必要?

在文章的正文开始之前,我们先来聊聊应该如何客观看待 UI 自动化这件事。

衡量一件事情是否有必要做的最直接标准往往是投入和产出是否对称。

如果对于 UI 频繁变更的业务页面来说,UI 自动化的必要性我也保持怀疑态度:由于业务频繁变更页面样式导致自动化的结果不具备太多参考价值的话自然这种背景下我并不推荐去做偏业务侧的 UI 自动化。

但是换一种场景,这里我们讨论的更加偏向基础方向的组件库。越原子化的东西无论在易变性还是可测性上的困难都会随着量级的增加呈线性增长,想象以下几种场景:

  • 当你修改了 A 组件的样式后,正常状态下的 A 组件看起来一起 OK 但是等业务方(使用组件的同学)接入后发现组件 focus 后的样式发生了意外变更。oh shit ,我们又得重新来一遍开发接入、提测流程了。

  • 又或者当你修改了 B 组件后,B 组件一切正常。但是造成 C 组件样式意外变更,上线后影响到了 D 页面的正常展示(因为 QA 同学收到的也只是回归使用到了 B 组件的 C 页面),这次更难了直接演变成为线上事故。

等等诸如此类问题,我相信大家已经深恶痛绝。如果你正在面临这些问题,相信我视觉回归测试一定可以大幅度提升你的工作质量。

jest-image-snapshot

简介

jest-image-snapshot是一款基于 jest 的视觉回归测试工具。

它默认使用 pixelmatch 执行图像比较,提供了类似 Jest snapshots 的简单方式来为我们的组件库执行视觉回归测试。

当我们给定一个 png 图像(Buffer 格式数据),通过 jest-image-snapshot 全局拓展 expect 方法后我们可以简单的通过 expect(image).toMatchImageSnapshot(); 来快速生成逐像素的图像对比。

当第一次运行 jest 测试时, jest-image-snapshot 会将第一次生成的图片默认存储在给定的目录中。后续继续运行测试命令时,jest-image-snapshot 会帮助我们自动对比 expect 中传递的图片与已存储图片之间的差异。

用法

首先我们可以使用 jest 的官方文档来快速创建一个基础的 Jest 项目。

按照文档要求我们使用 babel-jest 安装并配置一系列 babel 的配置:

module.exports = {
  presets: [
    ['@babel/preset-env', { targets: { node: 'current' } }],
    '@babel/preset-typescript'
  ]
};

同时不要忘记修改 pacakge.json 中的 test 命令:

// package.json

  // ...
  "scripts": {
    "test": "jest --config ./jest.config.js --no-cache"
  },
  // ...

之后让我们在按照 jest-image-snapshot 的文章安装 jest-image-snapshot:

pnpm add -D jest-image-snapshot

参考 configuration jest 章节,我们在项目中创建一个 jest.config.js 文件作为 jest 的配置文件:

/** @returns {Promise<import('jest').Config>} */
module.exports = async () => {
  return {
    transform: {
      '\\.(j|t)sx?': 'babel-jest',
      '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
        '<rootDir>/transform/image.js'
    },
    setupFilesAfterEnv: ['./setup.ts']
  };
};

jest.config.js 中我们设置了两个配置:

  • setupFilesAfterEnv: 这里配置的文件会在每个测试文件执行前,Jest 环境准备完毕后执行。通常用于初始化 Jest 的全局配置文件。

  • transfrom: 我们通过 transfrom 选项告诉 jest 如何识别对应后缀的文件,类似于 webpack 中 loader 的作用。

这里,我们在配置文件中声明了 transfrom 用来识别图片文件以及对于 jsx、tsx、js、ts 文件使用 babel-jest 来识别它们。

之后,按照 jest-image-snapshot 文档我们需要在 ./setup.ts 为 jest 扩展支持 jest-image-snapshot:

// ./setup.ts
const { toMatchImageSnapshot } = require('jest-image-snapshot');

expect.extend({ toMatchImageSnapshot });

经过上述简单的配置之后,我们就可以对于图片类型文件进行视觉对比。让我们来编写一个简单的 jest 测试用例:

import image from '../assets/image.png';
import fs from 'fs';

describe('test image', () => {
  it('test image', () => {
    // image 的导出内容上述我们配置过 transform 配置,在 transfrom 中我们将测试文件中导入的 .png 后缀文件变成绝对路径
    // 这里的 image 变量也就是图片所在的绝对路径
    // 使用 fs 将图片文件读取为 buffer
    const imageBuffer = fs.readFileSync(image);
    expect(imageBuffer).toMatchImageSnapshot({
      // 自定义生成的 snapshots 名称
      customSnapshotIdentifier: 'image'
    });
  });
});

同时,我们需要在 assets 目录下放置一个 image.png:

image-1.png

当在项目中首次运行 npm run test 时,默认会在测试文件目录中生成一个 baseline 的基准图片:

__tests__/__image_snapshots__ 目录中会存在:

image.png

当再次运行 npm run test 时,jest-image-snapshot 将 test 文件中的 image 将原本 baseline 的图片进行逐像素对比:

image.png

由于 image.png 图片并没有任何变化,所以本次测试会完美通过。假设当我们将 image.png 替换成为一个完全不同的图片时,再次运行 npm run test:

image.png

替换后的 image.png 文件。

image.png

可以清晰的看到此时测试用例并不会通过,同时控制下输出了:

See diff for details: /Users/ccsa/Desktop/ image-test/tests/image_snapshots/diff_output/image-diff. png

jest-image-snapshot 贴心的帮我们生成了前后两张图片的 diff 图:

image.png

左侧为 baseline 右侧为 target 中间部分为两者的 diff 图片。

当然你也可以自定义 jest 的 reporter 配置,在 reporter 中选择将存在差异的文件部分按照你的需求进行操作就比如 jest-image-snapshot with s3 的示例。

总之,通过 jest-image-snapshot 我们可以在 jest 测试用例中对于 png 类型的文件进行视觉回归。

这章节的 Example 代码你可以在这里看到

jest-puppeteer

简介

上一步我们简单了解了 jest-image-snapshot 的作用和用法。

jest-image-snapshot 可以在 jest 单元测试文件中通过类编程的测试方式(expect)验证 png 类型的图片前后差异变更。

但通常我们在日常开发中总不至于对于每一个组件每一次变更都要手动生成一张 png 图片吧,当我们想要自动化这一过程时 puppeteer 登场了。

puppeteer 是一个 Node.js 库,它提供了一个高级 API,用于控制 Chrome 或 Chromium 浏览器的操作,通常 puppeteer 可以用于自动化网站的交互和测试,也可以用于爬取网站数据等。

简单来说 puppeteer 可以帮助我们在 nodejs 中通过编程的方式打开一个无头浏览器执行渲染我们的代码从而进行一系列截屏等交互操作。

而 jest 中 同样提供了 jest-puppeteer 的预设,帮助我们通过简单的配置即可在测试用例中使用 Puppeteer 进行端到端测试。

这里,我们需要在 jest 中配置 jest-puppeteer 来自动化的为我们的组件捕获屏幕截图从而将生成的图片提供给上一步的 jest-image-snapshot 。

用法

配置 React

此时,我们的项目中还没有任何相关 React 组件,首先让我们先让 jest 支持 React 组件的渲染。

上文提到过,我们在 jest.config.js 中通过 jest-babel 来编译 tsx 文件。要让 jest 认识 React 组件,我们需要在 babel 配置文件中增加 React 的预设:

// babel.config.js
module.exports = {
  presets: [
    ['@babel/preset-env', { targets: { node: 'current' } }],
    '@babel/preset-react',
    '@babel/preset-typescript'
  ]
};

完成 babel 配置后,我们就可以在测试文件中使用 React t/jsx 了。

接下来让我们来新建一个原子化的 Button 组件:

// src/components/Button.tsx
import React from 'react';

export function Button() {
  return <div className="ctrip-button">Hello I'm a Button.</div>;
}

// src/components/button.css
.ctrip-button {
  background-color: blue;
  color: #fff;
}

.ctrip-button:hover {
  background-color: yellow;
  color: #000;
}

我们创建了一个简单的原子化 Button 组件,组件的内容为了方便起见我们固定为 Hello I'm a Button.

同时 Button 组件具有两种状态的样式呈现:

  • 默认情况下,Button 为蓝底白字的展示。
  • 当 hover Button 组件时,它会呈现为黄底黑字的展示。

配置 jest-puppeteer

jest-puppeteer 预设

Jest 配置文件中提供了 preset 选项方便的为我们的来预设一系列需要的环境,jest-puppeteer 便 preset 的其中一种。

仅需要在 jest.config.js 中通过简单的配置便可以快速在我们的测试用例中使用 puppeteer:

// jest.config.js
/** @returns {Promise<import('jest').Config>} */
module.exports = async () => {
  return {
    verbose: true,
    transform: {
      '\\.(j|t)sx?': 'babel-jest',
      '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
        '<rootDir>/transform/image.js'
    },
    setupFilesAfterEnv: ['./setup.ts'],
    preset: 'jest-puppeteer'
  };
};

同时 jest-puppeteer 支持配置文件的方式来给 puppeteer 传递执行参数。

这里,让我们在根目录下新建一个 jest-puppeteer.config.js:

module.exports = {
  launch: {
    ignoreDefaultArgs: ['--disable-extensions'],
    args: [
      '--no-sandbox',
      '--disable-setuid-sandbox',
      // This will write shared memory files into /tmp instead of /dev/shm,
      // because Docker’s default for /dev/shm is 64MB
      '--disable-dev-shm-usage'
    ]
  }
};

launch 表示 puppeteer 启动一个可执行的 Chrome 浏览器实例,说人话也就是打开一个新的 Chrome 。自然,我们在 launch 中传递的参数就表示在打开 Chrome 实例时传递的参数。

  • args 顾名思义表示启动参数:
    • --no-sandbox 在默认情况下,Chrome 浏览器在 Linux 环境下会以沙箱模式运行,这是为了增加浏览器的安全性。沙箱模式可以限制浏览器进程的权限,从而减少潜在的风险。然而,在某些情况下,沙箱模式可能会导致 Puppeteer 在某些 Linux 系统上无法正常运行。通过添加 --no-sandbox 参数,可以禁用 Chrome 浏览器的沙箱模式。
    • --disable-setuid-sandbox 参数可以禁用 Chrome 浏览器的沙箱模式中的 setuid 功能,从而使浏览器在 Linux 系统上能够正常运行。这个参数的作用是告诉 Chrome 浏览器不要使用 setuid 功能来限制浏览器进程的权限,而是以普通用户的身份运行浏览器进程。
    • --disable-dev-shm-usage 在默认情况下,Chrome 浏览器在 Linux 环境下会使用 /dev/shm 目录来存储一些临时文件,例如浏览器的共享内存文件等。然而,在某些情况下,/dev/shm 目录可能会被限制了大小,从而导致 Chrome 浏览器无法正常运行。

这两个参数都是为了保证在 linux 系统下可以正常使用 puppeteer 同时获取所有访问权限,如果你有兴趣了解它们你可以参照 puppeteer 官方文档

通常关闭沙盒环境都是为了在 gitlabCI/githubAction/Docker 中可以正常使用 puppeteer。

puppeteer 前置梳理

要在测试文件中使用 puppeteer 前,我们先要了解对于单个组件的 UI 自动化测试我们希望通过 puppeteer 来实现什么样的目的。

  • 首先,由于原子化的组件即使转化为 HTML 结构后也只是一段 HTML 片段无法独立的在页面中进行展示,所以我们需要一个 html 结构来承载原子化组件的渲染。

  • 其次,我们需要借助一些工具方法将 React Tsx 的组件代码渲染成为浏览器可以识别的 HTML 片段。

  • 之后,我们将第二步得到的 HTML 片段插入到第一步的 HTML 页面中,在通过 puppeteer 导航到对应 HTML 页面进行截屏即可实现对于原子化组件的图像生成。

  • 最后,通过上一个章节的 jest-image-snapshot 拓展的 toMatchImageSnapshot 方法结合测试文件来进行图像 diff 从而执行原子化组件的视觉回归用例。

整个实现过程可以用一张图来概括:

image.png

所以在这里我们主要用于 puppeteer :

  • 渲染 HTML 页面(承载 JSX Component 的片段化 HTML)。
  • 对于渲染的 HTML 页面生成屏幕截图(用于结合测试文件生成视觉 Diff)。

了解了我们应该去做什么样的事情后,接下来就让我们从编码上一步一步来实现它。

首先,让我们先在根目录下创建一个 html 文件来作为 puppeteer 开启浏览器的导航载体:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Ctrip Visual Regression</title>
  <style>
    body {
      height: auto !important;
    }
  </style>
</head>

<body>
  <div id="root"></div>
</body>

</html>

之后再来让我们安装一系列需要使用到的 npm package:

pnpm add -D @types/jest-environment-puppeteer jest-puppeteer @testing-library/react puppeteer puppeteer-core jsdom
  • jest-puppeteer puppeteer-core puppeteer 这三者作为我们在 jest 中使用 puppeteer 的必备条件。

  • @types/jest-environment-puppeteer 为 jest-puppeteer 的全局类型声明文件。

  • @testing-library/react 我们使用 @testing-library/react 来将我们的 JSX Component 来渲染成为 HTML 片段(当然这里的选择非常多,我选择了一种 JSDOM 环境下常用的 React Package)。

  • jsdom 是一个在 NodeJs 环境下模拟 DOM 环境的工具包,在渲染 UI Component 为 HTML 片段时我们会用到它,稍后我们结合用例再来详细聊它的作用。

编写测试用例

1. 将 Button 渲染为 HTML 片段

完成了前置的准备后,让我们来着手编写 Button 的视觉测试用例。

顺着上边的思路,首先我们需要将原子化的 React Button 组件渲染成为浏览器可以识别的 HTML 片段。

这里我们需要使用到 @testing-library/react 的 render 方法。

render 方法正是将 React Component 渲染为 HTML 片段的方法,同时在渲染完成后他会返回一个 container 属性,我们可以通过返回的 container.innerHTML 获得 Button 的 HTML 片段。

// /__tests__/button/button.test.tsx
import React from 'react';
import { Button } from '../../src/components/Button';
import { render } from '@testing-library/react';

describe('Test button', () => {
  it('normal', async () => {
    let element = <Button />;
    // 渲染 button 组件同时获得 container 元素
    const { unmount, container } = render(element);
    // 获取 Button 组件渲染后的 HTML 字符串
    const html = container.innerHTML;
    unmount();
    console.log(html);
  });
});

此时当我们运行 npm run test 执行测试用例时:

image.png

看起来像是 Jest 在 NodeJs 下需要使用 testEnvironment: 'jsdom' 来模拟 DOM 环境,似乎我们只需要在 jest.config.js 中增加 testEnvironment: 'jsdom' 的环境预设即可。

但是,在 jest-puppeteer 的预设文档中有一句不是那么显眼的引言:

image.png

Note Ensure you remove any existing testEnvironment options from your Jest configuration

这也就意味着当我们使用 jest-puppeteer 预设时,是没法指定 testEnvironment 的。这会和 puppeteer 冲突造成 jest-puppeteer 无法正常运行 puppeteer。

当然,在文档的后续 jest-puppeteer 也提供了基于 jest-puppeteer 自定义的 environment 的拓展。

有兴趣的同学可以自行尝试拓展 jest-puppeteer 和 js-dom 结合的 environment,这里为了方便起见我就直接在测试文件中实现一套 DOM Api 了。

上边我们也提到过,我们在使用 jest-puppeteer preset 的情况下是无法直接指定 jsdom 环境来使用 DOM API 的。

那么,既然无法通过配置来实现 DOM Api 我们自己模拟一套 Nodejs 环境下的 DOM Api 不就可以了吗,的确是这样的。

上边我们提到的 jsdom 的作用正是在 NodeJs 环境下模拟 DOM 环境,它允许我们在 Node.js 环境中使用类似于浏览器中的 DOM API,进行 HTML 解析、操作和 JavaScript 执行,使用 jsdom 我可以快速在 nodejs 环境下模拟一套 DOM api 从而满足 render 方法的调用环境。

import React from 'react';
import { Button } from '../../src/components/Button';
import { render } from '@testing-library/react';
import { JSDOM } from 'jsdom';

describe('Test button', () => {
  let doc: Document;
  let container: HTMLDivElement;

  beforeAll(() => {
    // 创建 jsdom 对象,指定 window.location 为 http://localhost/
    const dom = new JSDOM('<!DOCTYPE html><body></body></p>', {
      url: 'http://localhost/'
    });
    const win = dom.window;
    doc = win.document;
    // 为 nodejs global 下添加全局 window 对象
    (global as any).window = win;

    // 检测 global 下不存在的全局 DOM Api
    const keys = [
      ...Object.keys(win),
      'HTMLElement',
      'SVGElement',
      'ShadowRoot',
      'Element',
      'File',
      'Blob'
    ].filter((key) => !(global as any)[key]);
    // 逐次为 global 添加对应  DOM api
    keys.forEach((key) => {
      (global as any)[key] = win[key];
    });
  });

  beforeEach(() => {
    // 在开启每一个测试之前,为 doc 的 body 中添加内容
    doc.body.innerHTML = `<div id="root"></div>`;
    // 获取 doc 中添加的 dom 节点
    container = doc.querySelector<HTMLDivElement>('#root')!;
  });
});

通过上述的前置配置条件,我们可以在 jest 的 node 执行环境下模拟出一套浏览器的 DOM api 从而保证在 node 执行环境中存在浏览器下的 DOM Api。

配置 puppeteer 生成组件截屏

满足了测试用例的环境配置后,让我们来着手与测试用例的编写。

关于 Button 组件,在视觉上有两种状态,分别在 normal 状态和 hover 状态下存在不同的样式展示,所以我们要分别为两种不同的状态渲染成为 HTML 并且使用 puppeteer 生成不同的屏幕截图。

import React from 'react';
import { Button } from '../../src/components/Button';
import { render } from '@testing-library/react';
import { JSDOM } from 'jsdom';

describe('Test button', () => {
  let doc: Document;
  let container: HTMLDivElement;

  beforeAll(() => {
    // 创建一个 js dom 对象
    const dom = new JSDOM('<!DOCTYPE html><body></body></p>', {
      url: 'http://localhost/'
    });
    const win = dom.window;
    doc = win.document;

    (global as any).window = win;

    // 这里相当于模拟了一个 window 的 JSDOM 环境,因为 render 方法需要用到
    const keys = [
      ...Object.keys(win),
      'HTMLElement',
      'SVGElement',
      'ShadowRoot',
      'Element',
      'File',
      'Blob'
    ].filter((key) => !(global as any)[key]);

    keys.forEach((key) => {
      (global as any)[key] = win[key];
    });
  });

  beforeEach(() => {
    // container 为 root Dom 节点
    doc.body.innerHTML = `<div id="root"></div>`;
    container = doc.querySelector<HTMLDivElement>('#root')!;
  });

  it('normal', async () => {
    // 重置页面
    await jestPuppeteer.resetPage();
    // 导航到对应页面
    await page.goto(`file://${process.cwd()}/index.html`);
    // 添加组件样式内容
    await page.addStyleTag({
      path: `${process.cwd()}/src/components/button.css`
    });

    let html: string;

    let element = <Button />;

    // 将 Button 组件渲染到 container 中
    const { unmount } = render(element, {
      container
    });
    // 获取 Button 组件渲染后的 HTML 字符串
    html = container.innerHTML;
    unmount();

    // 更新 puppeteer 开启的 HTML 中的内容
    await page.evaluate((innerHTML) => {
      document.querySelector('#root')!.innerHTML = innerHTML;
    }, html);

    // 对于 Button 截屏
    const image = await page.screenshot();

    // 对于截屏内容进行视觉回归
    expect(image).toMatchImageSnapshot();
  });

  it('hover', async () => {
    // 重置页面
    await jestPuppeteer.resetPage();
    // 导航到对应页面
    await page.goto(`file://${process.cwd()}/index.html`);
    // 添加组件样式内容
    await page.addStyleTag({
      path: `${process.cwd()}/src/components/button.css`
    });

    let html: string;

    let element = <Button />;

    // 将 Button 组件渲染到 container 中
    const { unmount } = render(element, {
      container
    });
    // 获取 Button 组件渲染后的 HTML 字符串
    html = container.innerHTML;
    unmount();

    // 更新 puppeteer 开启的 HTML 中的内容
    await page.evaluate((innerHTML) => {
      document.querySelector('#root')!.innerHTML = innerHTML;
    }, html);

    // hover button
    await page.hover('.ctrip-button');

    // 对于 Button 截屏
    const image = await page.screenshot();

    // 对于截屏内容进行视觉回归
    expect(image).toMatchImageSnapshot();
  });
});

完整的视觉测试用例代码我贴在了上方,主要还是按照我们之前的设计思路来实现:

  • 首先,在执行每一个单元测试时调用 jestPuppeteer.resetPage() 来重置全局的 page 对象。

  • 同时我们调用 page.goto 方法打开我们在上边新建的 HTML 文件,同时为 HTML 页面添加对应的样式文件。

  • 随后,我们调用了 @testing-library/react 的 render 方法来讲 Button 组件渲染为 HTML 片段。

  • 在将 Button 渲染为字符串后,我们调用了 page.evaluate 方法在 puppeteer 创建的页面上下文执行传入的 JS 脚本:将渲染后的 Button HTML 加入到对应页面。

  • 最后,我们调用 page.screenshot 对于 puppeteer 进行截屏,唯一不同的是在 hover 状态下我们优先使用了 page.hover 方法保证 button 组件的 hover 态。

关于 puppeteer 的相关 Api 你可以参考他的官方文档

执行测试用例生成组件视觉回归

按照我们的实现思路,我们已经完成了编码的环节。接下来让我们来调用 npm run test 来验证一下我们的成果:

image.png

随着测试用例全部通过,在 /__tests__/button/__image_snapshots__ 也生成了两种不同的 Button 组件视觉图片:

image.png

接下来让我们稍微来做一些修改在试试呢,我们将 Button 组件 hover 时的文字颜色从黑色变成红色:

.ctrip-button {
  background-color: blue;
  color: #fff;
}

.ctrip-button:hover {
  background-color: yellow;
- color: #000;
+ color: red;
}

再次运行 npm run test 执行我们的测试用例,此时控制台会提示我们 hover 状态下的视觉回归不通过啦:

image.png

同时,让我们查看 /__tests__/button/__image_snapshots__/__diff_output__ 路径会 jest-image-snapshot 会帮我们生成对应的 diff 对比文件:

image.png

看起来一切良好,一个完整的视觉回归流程我们已经基本实现了。其实并没有想象中的那么复杂对吧。

这一章节的源代码你可以在这里查看

自动化

上边的章节我们使用 jest-puppeteer 、jest-image-snapshot 已经在本地实现了原子组件的视觉回归测试。

不过上边的视线看起来更像是一种半自动化,需要我们每次手工去运行 npm run test 以及对于每个组件仍然是需要编写大量的重复代码(比如 puppeteer、jsdom 的相关 Api 调用)。

这里,我们就来聊聊如何让视觉回归测试更加智能、更加自动化。

所谓自动化的方式更多是想让组件开发、维护同学无需过多关心测试用例的编写和环境处理,而是在每次 publish or build 在不同的环节去自动化的触发视觉自动化流程从而为开发人员生成直观的报告来防止本次改动造成的意外行为。

嗯没错,我们可以将这一流程结合到 Github Action、Gitlab Pipeline 中让组件视觉回归更加智能、更加自动化。

目的

视觉回归也好、自动化也好我们的目的主要就是为了在原子化的组件开发阶段发现意外的视觉内容变更,所以我们应该如何借助视觉自动化来发现意外内容的变更呢?我们先来参考一些大型组件维护团队是如何实现这一效果。

pass:

image.png

fail:

image.png

上图是我在 ant-design 的 PR 中截取的几张视觉自动化图片(这里特别感谢 zombieJ 以及 miracles1919 两位大佬提供的思路)。

本质上我们希望在开发阶段实现自动化的目的,可以归纳为:

  • 稳定分支上特定条件触发上传组件视觉内容。比如在一些稳定分支上(release、master)上将每一次稳定分支在触发 push event 时将视觉报告上传到远端。

  • 非稳定分支特定条件下触发 visual-regression-diff 。 比如在一些 feat、fix 分支上每次存在 PR、MR Event 触发时,将当前分支的视觉内容和 Target 分支视觉内容进行对比从而生成 report 。

通常,通过结合稳定分支和非稳定分支的 visual regression diff report 就可以较为完美的在开发阶段保证原子组件的视觉稳定性。

思路

因为 Github Action、Gitlab Pipeline 中实现这一目的需要依赖不同的环境设置,这里我就不再展开对应详细的实现过程。

条条大路通罗马,本质上实现思路上都是一致的,唯一不同的只是不同的 Api 以及细微的对比差异而已。

要实现视觉自动化这一过程,我们可以用一张图来归纳下简单的实现思路:

image.png

当然,这一过程中还会涉及一些对应分支下如何存储最新的视觉回归内容(无论是通过当前 commit sha 还是其他唯一标识符都可以)。

同样,关于 report 你可以直接使用 jest-image-snapshot 的 report 图片也自定义 jest 的 reporter 亦或是仅选择和 ant-design 类似使用 jest-image-snapshot 来生成视觉图片而自己使用命令来处理 report 内容。

以及关于重复的 pupeteer、jsdom 初始化方法我们也通过多种方式来精简成为通用方法来减少组件开发同学的编写 Case 时的心智负担。

其实只要我们掌握了原子化组件执行视觉回归的思路接下来实现自动化的过程更多像是按部就班,不过需要留意的是通常我们需要结合一个远端的存储地址去保存我们在每次 workflow 中生成的组件视觉内容。

结尾

实现前端视觉回归测试的方式有多种框架可供选择。文章中的内容更多是希望起到一种抛砖引玉的作用,从另一个角度为大家讲述如何确保基础类组件的独立可测性。

希望这些内容能够对大家有所帮助,愿视觉回归测试能够成为每位前端工程师的得力工具。