playwright 介绍
Playwright enables reliable end-to-end testing for modern web apps.
一个 E2E 的测试框架,启动一个类似于 puppeteer 的无头浏览器,但是他支持多种浏览器, 具体介绍看官网即可。
这里用他做一个快照的对比度测试,逻辑和代码非常简单,抛砖引玉,将 E2E 用起来,提升代码质量。
测试
1. 初始化
pnpm dlx create-playwright
2. package.json中增加命令
开启 trace 可以查看全程的执行过程,方便 debug
3. 编写 test 文件 match-snapshot.spec.ts
代码和思路很简单
- 启动我们的本地服务器
- 运行 test 时,会启动一个无头浏览器,我们拿到 page 对象,访问目标地址。
- 对当前页面进行截图,截图需要考虑页面内容加载完成。
- 进行图片对比
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 时触发对比,这个自己看场景调整 (如果你不想代码提交不上去的话~)
执行
pnpm test
pnpm test:show-report
踩坑
全局 Config 带来的坑
其中 toMatchSnapshot 的参数是被对比的文件路径、但是这里有个坑
多次匹配永远都是通过,而且在本地生成了一张 buffer 图片。我要对比的是 buffer 和 target 图片的差异,多次测试发现传入的图片根本没有被使用,哪怕我删除了他都能通过~
稍微看一下 toMatchSnapshot 部分的源码
如果根据 path 没找到图片、则走missing、那么 missing 做了什么事情呢?
看文档中 updateSnapshots 的定义,updateSnapshots 默认是 missing、那么导致找不到对比的图片,就根据当前的 snapshot 生成一个图片、此时的结果居然是通过
所以可以看到、无论是 missing 还是 all、找不到文件时,都会校验通过且创建一个新的快照在对应文件下,只是 message 不一样。
我来这里是对比图片的,不是让你生成图片的~所以我选择 none。解决了对比时永远 pass 的情况。
那么第二个问题就是 helper.snapshotPath 到底是什么、为什么会找不到
他调用的是 snapshotPath ,我们查看文档
是个方法,代码中 bind 的内容是 testInfo,再看代码
他是读取 snapshotPathTemplate 然后做了 replace , 这玩意居然也是默认的
基本上知道了,toMatchSnapshot 的入参是路径不假,但你只是最后一层,你以为的路径名,和你传入的目标名称完全不一样
知道原因就好修复了,在这个目录下放设计稿原图,命名按他的规范来,或者修改全局配置。
再跑任务
pnpm test
pnpm test:show-report
大功告成!
toMatchSnapshot 图片对比
稍微看下 helper.comparator 做了什么事情
图片处理部分引用了 pngjs、jpegjs
图片对比分了俩种情况,分别引用了 ssim、pixelmatch
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;
}
The smallest, simplest and fastest JavaScript pixel-level image comparison library, originally created to compare screenshots in tests.
🖼🔬 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);
});