Introduce screenshot tests

This patch introduces a goldentest.js. The tests inside
the file should rely on "golden" results rather then asserts.

For now, goldentest.js allows only image expectations. If the
actual result doesn't match the expected result, the two files
are created under `test/output` folder:
- The '-actual.png' contains the actual test result
- The '-diff.png' contains the diff between images
This commit is contained in:
Andrey Lushnikov 2017-06-16 14:33:34 -07:00
parent 28a3343d2c
commit 242a6a6e73
6 changed files with 187 additions and 4 deletions

1
.gitignore vendored
View File

@ -1,4 +1,5 @@
/node_modules/
/test/output
/.local-chromium/
/.dev_profile*
.DS_Store

View File

@ -5,8 +5,9 @@
"main": "index.js",
"scripts": {
"test-puppeteer": "jasmine test/test.js",
"test-golden": "jasmine test/goldentest.js",
"test-phantom": "python third_party/phantomjs/test/run-tests.py",
"test": "npm run test-puppeteer && npm run test-phantom",
"test": "npm run test-puppeteer && npm run test-golden && npm run test-phantom",
"install": "node install.js"
},
"author": "The Chromium Authors",
@ -22,9 +23,10 @@
"chromium_revision": "478524"
},
"devDependencies": {
"ncp": "^2.0.0",
"minimist": "^1.2.0",
"deasync": "^0.1.9",
"jasmine": "^2.6.0"
"jasmine": "^2.6.0",
"minimist": "^1.2.0",
"ncp": "^2.0.0",
"pixelmatch": "^4.0.2"
}
}

44
test/assets/grid.html Normal file
View File

@ -0,0 +1,44 @@
<script>
document.addEventListener('DOMContentLoaded', function() {
function generatePalette(amount) {
var result = [];
var hueStep = 360 / amount;
for (var i = 0; i < amount; ++i)
result.push('hsl(' + (hueStep * i) + ', 100%, 90%)');
return result;
}
var palette = generatePalette(100);
for (var i = 0; i < 1000; ++i) {
var box = document.createElement('div');
box.classList.add('box');
box.textContent = i;
box.style.setProperty('background-color', palette[i % palette.length]);
document.body.appendChild(box);
}
});
</script>
<style>
/* Hide scrollbar so that it is not captured on screenshots */
::-webkit-scrollbar {
display: none;
}
body {
margin: 0;
padding: 0;
}
.box {
font-family: arial;
display: inline-block;
margin: 0;
padding: 0;
width: 50px;
height: 50px;
box-sizing: border-box;
border: 1px solid darkgray;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

136
test/goldentest.js Normal file
View File

@ -0,0 +1,136 @@
/**
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var fs = require('fs');
var path = require('path');
var rm = require('rimraf').sync;
var Browser = require('../lib/Browser');
var StaticServer = require('./StaticServer');
var PNG = require('pngjs').PNG;
var pixelmatch = require('pixelmatch');
var PORT = 8907;
var STATIC_PREFIX = 'http://localhost:' + PORT;
var GOLDEN_DIR = path.join(__dirname, 'golden');
var OUTPUT_DIR = path.join(__dirname, 'output');
describe('GoldenTests', function() {
var browser;
var staticServer;
var page;
beforeAll(function() {
browser = new Browser();
staticServer = new StaticServer(path.join(__dirname, 'assets'), PORT);
if (fs.existsSync(OUTPUT_DIR))
rm(OUTPUT_DIR);
});
afterAll(function() {
browser.close();
staticServer.stop();
});
beforeEach(SX(async function() {
page = await browser.newPage();
}));
afterEach(function() {
page.close();
});
imageTest('screenshot-sanity.png', async function() {
await page.setViewportSize({width: 500, height: 500});
await page.navigate(STATIC_PREFIX + '/grid.html');
return page.screenshot('png');
});
imageTest('screenshot-clip-rect.png', async function() {
await page.setViewportSize({width: 500, height: 500});
await page.navigate(STATIC_PREFIX + '/grid.html');
return page.screenshot('png', {
x: 50,
y: 100,
width: 150,
height: 100
});
});
});
/**
* @param {string} fileName
* @param {function():!Promise} runner
*/
function imageTest(fileName, runner) {
var expectedPath = path.join(GOLDEN_DIR, fileName);
var actualPath = path.join(OUTPUT_DIR, fileName);
var expected = null;
if (fs.existsSync(expectedPath)) {
var buffer = fs.readFileSync(expectedPath);
expected = PNG.sync.read(buffer);
}
it(fileName, SX(async function() {
var imageBuffer = await runner();
if (!imageBuffer || !(imageBuffer instanceof Buffer)) {
fail(fileName + ' test did not return Buffer with image.');
return;
}
var actual = PNG.sync.read(imageBuffer);
if (!expected) {
ensureOutputDir();
fs.writeFileSync(addSuffix(actualPath, '-actual'), imageBuffer);
fail(fileName + ' is missing in golden results.');
return;
}
if (expected.width !== actual.width || expected.height !== actual.height) {
ensureOutputDir();
fs.writeFileSync(addSuffix(actualPath, '-actual'), imageBuffer);
fail(`Sizes differ: expected image ${expected.width}px X ${expected.height}px, but got ${actual.width}px X ${actual.height}px`);
return;
}
var diff = new PNG({width: expected.width, height: expected.height});
var count = pixelmatch(expected.data, actual.data, diff.data, expected.width, expected.height, {threshold: 0.1});
if (count > 0) {
ensureOutputDir();
fs.writeFileSync(addSuffix(actualPath, '-actual'), imageBuffer);
fs.writeFileSync(addSuffix(actualPath, '-diff'), PNG.sync.write(diff));
fail(fileName + ' mismatch!');
}
}));
}
function ensureOutputDir() {
if (!fs.existsSync(OUTPUT_DIR))
fs.mkdirSync(OUTPUT_DIR);
}
/**
* @param {string} filePath
* @param {string} suffix
* @return {string}
*/
function addSuffix(filePath, suffix) {
var dirname = path.dirname(filePath);
var ext = path.extname(filePath);
var name = path.basename(filePath, ext);
return path.join(dirname, name + suffix + ext);
}
// Since Jasmine doesn't like async functions, they should be wrapped
// in a SX function.
function SX(fun) {
return done => Promise.resolve(fun()).then(done).catch(done.fail);
}