最近在工作中开发Canvas相关功能的工具库,需要写测试用例保证每次迭代不影响最终渲染效果。用关键词“canvas e2e test” 在线搜索相关资料后,看到网友们E2E测试方法八仙过海,各显神通,但都满足不了验证渲染结果的目的。后来换个角度去思考,研究了Three.js的E2E在github源码里测试用例,总结出该篇文章。
在浏览了网上大部分关于前端E2E测试文章后,归纳出方案分为两种。
很明显第一种不能彻底验证渲染结果,第二种比较重,还需要开启浏览器验证,对后续自动化集成有些不友好。思来想去后,觉得可能是搜索的姿势不对,Canvas的E2E测试应该是存在比较稳定丝滑的方案,要不然开源社区那些基于Canvas的大型可视化项目怎么保证渲染结果的质量呢?
提起社区基于Canvas的大型Web项目,很容易可以想出 Three.js ,是基于 Canvas 来执行WebGL的3D渲染。
翻了一下 Three.js 在Github上的官方仓库 https://github.com/mrdoob/three.js ,找到e2e的测试源码的目录 ./test/e2e 发现 Three.js 的E2E 的测试步骤主要有两步
const path = require('path');
const http = require('http');
const jimp = require('jimp');
const puppeteer = require('puppeteer');
const serveHandler = require('serve-handler');
const port = 3001;
const width = 400;
const height = 400;
const snapshotPicPath = path.join(__dirname, 'snapshot', 'expect.png');
async function main() {
const server = http.createServer((req, res) => serveHandler(req, res, {
// 这里需要改成所需静态资源目录
public: path.join(__dirname, '..', 'src'),
}));
server.listen(port, async () => {
try {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setViewport( { width: width, height: height } );
await page.goto(`http://127.0.0.1:${port}/index.html`);
const buf = await page.screenshot();
await browser.close();
server.close();
(await jimp.read(buf)).scale(1).quality(100).write(snapshotPicPath);
console.log('create snapshot of screen scuccess!')
} catch (err) {
server.close();
console.error(err);
process.exit(-1);
}
});
server.on('SIGINT', () => process.exit(1) );
}
main();
const fs = require('fs');
const path = require('path');
const http = require('http');
const assert = require('assert');
const jimp = require('jimp');
const pixelmatch = require('pixelmatch');
const puppeteer = require('puppeteer');
const serveHandler = require('serve-handler');
const port = 3001;
const width = 400;
const height = 400;
const snapshotPicPath = path.join(__dirname, 'snapshot', 'expect.png');
let server = null;
let browser = null;
describe('E2E Testing', function() {
before(function(done) {
server = http.createServer((req, res) => serveHandler(req, res, {
// 这里需要改成所需静态资源目录
public: path.join(__dirname, '..', 'src'),
}));
server.listen(port, done);
server.on('SIGINT', () => process.exit(1) );
});
it('testing...', function(done){
this.timeout(1000 * 60);
const expectDiffRate = 0.005;
new Promise(async (resolve) => {
browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setViewport( { width: width, height: height } );
await page.goto(`http://127.0.0.1:${port}/index.html`);
const buf = await page.screenshot();
const actual = (await jimp.read(buf)).scale(1).quality(100).bitmap;
const expected = (await jimp.read(fs.readFileSync(snapshotPicPath))).bitmap;
const diff = actual;
const failPixel = pixelmatch(expected.data, actual.data, diff.data, actual.width, actual.height);
const failRate = failPixel / (width * height);
resolve(failRate);
}).then((failRate) => {
assert.ok(failRate < expectDiffRate);
done();
}).catch(done);
});
after(function() {
browser && browser.close();
server && server.close();
});
});
修改后的E2E测试代码如下
const fs = require('fs');
const path = require('path');
const http = require('http');
const assert = require('assert');
const jimp = require('jimp');
const pixelmatch = require('pixelmatch');
const puppeteer = require('puppeteer');
const serveHandler = require('serve-handler');
const port = 3001;
const width = 400;
const height = 400;
const snapshotPicPath = path.join(__dirname, 'snapshot', 'expect.png');
const diffPicPath = path.join(__dirname, 'snapshot', 'diff.png');
let server = null;
let browser = null;
describe('E2E Testing', function() {
before(function(done) {
server = http.createServer((req, res) => serveHandler(req, res, {
public: path.join(__dirname, '..', 'src'),
}));
server.listen(port, done);
server.on('SIGINT', () => process.exit(1) );
});
it('testing...', function(done){
this.timeout(1000 * 60);
const expectDiffRate = 0.005;
new Promise(async (resolve) => {
browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setViewport( { width: width, height: height } );
await page.goto(`http://127.0.0.1:${port}/index.html`);
const buf = await page.screenshot();
const actual = (await jimp.read(buf)).scale(1).quality(100).bitmap;
const expected = (await jimp.read(fs.readFileSync(snapshotPicPath))).bitmap;
const diff = actual;
const failPixel = pixelmatch(expected.data, actual.data, diff.data, actual.width, actual.height);
const failRate = failPixel / (width * height);
if (failRate >= expectDiffRate) {
(await jimp.read(diff)).scale(1).quality(100).write(diffPicPath);
console.log(`create diff image at: ${diffPicPath}`)
}
resolve(failRate);
}).then((failRate) => {
assert.ok(failRate < expectDiffRate);
done();
}).catch(done);
});
after(function() {
browser && browser.close();
server && server.close();
});
});
修改后的链路流程图大致如下
经过上述一番折腾后,顺便学习了一下Three.js所有的测试用例,总结出前端可视化的质量验证一般有以下三个方面。