使用 playwright 进行 UI diff 测试

816 阅读4分钟

playwright 介绍

Playwright enables reliable end-to-end testing for modern web apps.

一个 E2E 的测试框架,启动一个类似于 puppeteer 的无头浏览器,但是他支持多种浏览器, 具体介绍看官网即可。

这里用他做一个快照的对比度测试,逻辑和代码非常简单,抛砖引玉,将 E2E 用起来,提升代码质量。

测试

1. 初始化

pnpm dlx create-playwright

2. package.json中增加命令

image.png 开启 trace 可以查看全程的执行过程,方便 debug

3. 编写 test 文件 match-snapshot.spec.ts

代码和思路很简单

  1. 启动我们的本地服务器
  2. 运行 test 时,会启动一个无头浏览器,我们拿到 page 对象,访问目标地址
  3. 对当前页面进行截图,截图需要考虑页面内容加载完成。
  4. 进行图片对比
import { test, expect, Page } from '@playwright/test';
async function waitPage(page: Page) {
    // 防止图片未加载完成
    await page.waitForLoadState('networkidle');
    
    // 滚动到底部,防止图片因为懒加载没有出现。当然 diff UI 时,图片不是重点。
    await page.evaluate(() => window.scrollTo(0, 999999));
    // 等待、防止部分图片未加载完成
    await page.waitForTimeout(1000);
}
// 目的 对比 UI 还原度~
test.describe('match snapshot', () => {
    test('test 1', async ({ page }: { page: Page }, test) => {
        await page.goto('newUrl or new code build image');
        await waitPage(page);
        const buffer = await page.screenshot({fullPage: true});
        await expect(buffer).toMatchSnapshot('target.png', {
            maxDiffPixels: 27, // allow no more than 27 different pixels.
            /**
            An acceptable ratio of pixels that are different to the total amount of pixels, between `0` and `1`. Default is configurable with `TestConfig.expect`. Unset by default.
            */
            maxDiffPixelRatio: 0,
            /**
            Snapshot name. If not passed, the test name and ordinals are used when called multiple times.
            */
            name: 'test',
            /**
            An acceptable perceived color difference in the [YIQ color space](https://en.wikipedia.org/wiki/YIQ) between the same pixel in compared images, between zero (strict) and one (lax), default is configurable with `TestConfig.expect`. Defaults to `0.2`.
            */
            threshold: 0.2
        });
        await page.waitForTimeout(1000);
    });
})

4. 修改 config

这个项目有点坑的地方在于,有很多默认配置,且配置在全局,最开始没发现,踩了很多坑~

ignoreSnapshots 是我调试完毕后加上的,仅在希望主动触发或 CI 时触发对比,这个自己看场景调整 (如果你不想代码提交不上去的话~)

image.png

执行

pnpm test
pnpm test:show-report

踩坑

全局 Config 带来的坑

其中 toMatchSnapshot 的参数是被对比的文件路径、但是这里有个坑

多次匹配永远都是通过,而且在本地生成了一张 buffer 图片。我要对比的是 buffer 和 target 图片的差异,多次测试发现传入的图片根本没有被使用,哪怕我删除了他都能通过~

稍微看一下 toMatchSnapshot 部分的源码

如果根据 path 没找到图片、则走missing、那么 missing 做了什么事情呢?

image.png

image.png 看文档中 updateSnapshots 的定义,updateSnapshots 默认是 missing、那么导致找不到对比的图片,就根据当前的 snapshot 生成一个图片、此时的结果居然是通过

image.png

image.png

所以可以看到、无论是 missing 还是 all、找不到文件时,都会校验通过且创建一个新的快照在对应文件下,只是 message 不一样。

我来这里是对比图片的,不是让你生成图片的~所以我选择 none。解决了对比时永远 pass 的情况。

那么第二个问题就是 helper.snapshotPath 到底是什么、为什么会找不到

他调用的是 snapshotPath ,我们查看文档

image.png 是个方法,代码中 bind 的内容是 testInfo,再看代码

image.png

image.png

他是读取 snapshotPathTemplate 然后做了 replace , 这玩意居然也是默认的

image.png

基本上知道了,toMatchSnapshot 的入参是路径不假,但你只是最后一层,你以为的路径名,和你传入的目标名称完全不一样

image.png

知道原因就好修复了,在这个目录下放设计稿原图,命名按他的规范来,或者修改全局配置。

image.png 再跑任务

pnpm test
pnpm test:show-report

大功告成!

image.png

toMatchSnapshot 图片对比

稍微看下 helper.comparator 做了什么事情

图片处理部分引用了 pngjsjpegjs

图片对比分了俩种情况,分别引用了 ssimpixelmatch

function compareImages(mimeType: string, actualBuffer: Buffer | string, expectedBuffer: Buffer, options: ImageComparatorOptions = {}): ComparatorResult {
  if (!actualBuffer || !(actualBuffer instanceof Buffer))
    return { errorMessage: 'Actual result should be a Buffer.' };
  // 当前图片
  let actual: ImageData = mimeType === 'image/png' ? PNG.sync.read(actualBuffer) : jpegjs.decode(actualBuffer, { maxMemoryUsageInMB: JPEG_JS_MAX_BUFFER_SIZE_IN_MB });
  // 目标对比的图片
  let expected: ImageData = mimeType === 'image/png' ? PNG.sync.read(expectedBuffer) : jpegjs.decode(expectedBuffer, { maxMemoryUsageInMB: JPEG_JS_MAX_BUFFER_SIZE_IN_MB });
  const size = { width: Math.max(expected.width, actual.width), height: Math.max(expected.height, actual.height) };
  let sizesMismatchError = '';
  // 调整图片宽高
  if (expected.width !== actual.width || expected.height !== actual.height) {
    sizesMismatchError = `Expected an image ${expected.width}px by ${expected.height}px, received ${actual.width}px by ${actual.height}px. `;
    actual = resizeImage(actual, size);
    expected = resizeImage(expected, size);
  }
  
  // 找到 diff 后生成的图片
  const diff = new PNG({ width: size.width, height: size.height });
  let count;
  
  if (options._comparator === 'ssim-cie94') {
    count = compare(expected.data, actual.data, diff.data, size.width, size.height, {
      // All ΔE* formulae are originally designed to have the difference of 1.0 stand for a "just noticeable difference" (JND).
      // See https://en.wikipedia.org/wiki/Color_difference#CIELAB_%CE%94E*
      maxColorDeltaE94: 1.0,
    });
  } else if ((options._comparator ?? 'pixelmatch') === 'pixelmatch') {
    count = pixelmatch(expected.data, actual.data, diff.data, size.width, size.height, {
      threshold: options.threshold ?? 0.2,
    });
  } else {
    throw new Error(`Configuration specifies unknown comparator "${options._comparator}"`);
  }
  // 拿到diff结果、和传入的参数,返回是否满足条件
  const maxDiffPixels1 = options.maxDiffPixels;
  const maxDiffPixels2 = options.maxDiffPixelRatio !== undefined ? expected.width * expected.height * options.maxDiffPixelRatio : undefined;
  let maxDiffPixels;
  if (maxDiffPixels1 !== undefined && maxDiffPixels2 !== undefined)
    maxDiffPixels = Math.min(maxDiffPixels1, maxDiffPixels2);
  else
    maxDiffPixels = maxDiffPixels1 ?? maxDiffPixels2 ?? 0;
  const ratio = Math.ceil(count / (expected.width * expected.height) * 100) / 100;
  const pixelsMismatchError = count > maxDiffPixels ? `${count} pixels (ratio ${ratio.toFixed(2)} of all image pixels) are different.` : '';
  if (pixelsMismatchError || sizesMismatchError)
    return { errorMessage: sizesMismatchError + pixelsMismatchError, diff: PNG.sync.write(diff) };
  return null;
}

github.com/mapbox/pixe…

The smallest, simplest and fastest JavaScript pixel-level image comparison library, originally created to compare screenshots in tests.

github.com/obartra/ssi…

🖼🔬 JavaScript Image Comparison

也可以用这个库: github.com/americanexp…

图片对比源码部分与 playwright 的部分几乎一致。

也可以用 playwright 它去生成网站快照,类似于 html2Image

test('save snapshot', async ({ page }: { page: Page }) => {
    await page.goto('https://www.baidu.com');
    await waitPage(page);
    
    await page.screenshot({fullPage: true, path: `${path.join(__dirname, '__snapshot__', 'target.png')}`});
    
    await page.waitForTimeout(500);
});