New rendering tests

This commit is contained in:
Tim Schaub
2018-11-06 15:58:45 -07:00
parent aab9ff023c
commit 67ee32fdea
8 changed files with 289 additions and 2 deletions

View File

@@ -12,7 +12,8 @@
"scripts": { "scripts": {
"lint": "eslint tasks test src/ol examples config", "lint": "eslint tasks test src/ol examples config",
"pretest": "npm run lint", "pretest": "npm run lint",
"test": "npm run karma -- --single-run --log-level error", "test-rendering": "node rendering/test.js",
"test": "npm run karma -- --single-run --log-level error && npm run test-rendering",
"karma": "karma start test/karma.config.js", "karma": "karma start test/karma.config.js",
"serve-examples": "webpack-dev-server --config examples/webpack/config.js --mode development --watch", "serve-examples": "webpack-dev-server --config examples/webpack/config.js --mode development --watch",
"build-examples": "webpack --config examples/webpack/config.js --mode production", "build-examples": "webpack --config examples/webpack/config.js --mode production",
@@ -72,7 +73,9 @@
"mocha": "5.2.0", "mocha": "5.2.0",
"mustache": "^3.0.0", "mustache": "^3.0.0",
"pixelmatch": "^4.0.2", "pixelmatch": "^4.0.2",
"pngjs": "^3.3.3",
"proj4": "2.5.0", "proj4": "2.5.0",
"puppeteer": "^1.10.0",
"rollup": "0.66.6", "rollup": "0.66.6",
"sinon": "^6.0.0", "sinon": "^6.0.0",
"typescript": "^3.1.0-dev.20180905", "typescript": "^3.1.0-dev.20180905",
@@ -81,7 +84,9 @@
"walk": "^2.3.9", "walk": "^2.3.9",
"webpack": "4.25.1", "webpack": "4.25.1",
"webpack-cli": "^3.0.8", "webpack-cli": "^3.0.8",
"webpack-dev-server": "^3.1.4" "webpack-dev-middleware": "^3.4.0",
"webpack-dev-server": "^3.1.10",
"yargs": "^12.0.2"
}, },
"eslintConfig": { "eslintConfig": {
"extends": "openlayers", "extends": "openlayers",

11
rendering/.eslintrc Normal file
View File

@@ -0,0 +1,11 @@
{
"env": {
"node": true
},
"parserOptions": {
"ecmaVersion": 2017
},
"globals": {
"render": false
}
}

1
rendering/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
actual.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<head>
<style>
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
}
#map {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div id="map"></div>
<script src="main.js"></script>
</body>
</script>
</html>

View File

@@ -0,0 +1,19 @@
import Map from '../../../src/ol/Map.js';
import View from '../../../src/ol/View.js';
import TileLayer from '../../../src/ol/layer/Tile.js';
import OSM from '../../../src/ol/source/OSM.js';
new Map({
layers: [
new TileLayer({
source: new OSM()
})
],
target: 'map',
view: new View({
center: [0, 0],
zoom: 0
})
});
render();

193
rendering/test.js Executable file
View File

@@ -0,0 +1,193 @@
#! /usr/bin/env node
const puppeteer = require('puppeteer');
const webpack = require('webpack');
const config = require('./webpack.config');
const middleware = require('webpack-dev-middleware');
const http = require('http');
const path = require('path');
const png = require('pngjs');
const fs = require('fs');
const fse = require('fs-extra');
const pixelmatch = require('pixelmatch');
const yargs = require('yargs');
const compiler = webpack(Object.assign({mode: 'development'}, config));
const handler = middleware(compiler, {
lazy: true,
logLevel: 'error'
});
function getHref(entry) {
return path.dirname(entry).slice(1) + '/';
}
function notFound(res) {
return () => {
const items = [];
for (const key in config.entry) {
const href = getHref(config.entry[key]);
items.push(`<li><a href="${href}">${href}</a></li>`);
}
const markup = `<!DOCTYPE html><body><ul>${items.join('')}</ul></body>`;
res.writeHead(404, {
'Content-Type': 'text/html'
});
res.end(markup);
};
}
function serve(port) {
return new Promise((resolve, reject) => {
const server = http.createServer((req, res) => {
handler(req, res, notFound(res));
});
server.listen(port, err => {
if (err) {
return reject(err);
}
resolve(() => server.close());
});
});
}
function getActualScreenshotPath(entry) {
return path.join(__dirname, path.dirname(entry), 'actual.png');
}
function getExpectedScreenshotPath(entry) {
return path.join(__dirname, path.dirname(entry), 'expected.png');
}
function parsePNG(filepath) {
return new Promise((resolve, reject) => {
const stream = fs.createReadStream(filepath);
stream.on('error', err => {
if (err.code === 'ENOENT') {
return reject(new Error(`File not found: ${filepath}`));
}
reject(err);
});
const image = stream.pipe(new png.PNG());
image.on('parsed', () => resolve(image));
image.on('error', reject);
});
}
async function match(actual, expected) {
const actualImage = await parsePNG(actual);
const expectedImage = await parsePNG(expected);
const width = expectedImage.width;
const height = expectedImage.height;
if (actualImage.width != width) {
throw new Error(`Unexpected width for ${actual}: expected ${width}, got ${actualImage.width}`);
}
if (actualImage.height != height) {
throw new Error(`Unexpected height for ${actual}: expected ${height}, got ${actualImage.height}`);
}
const count = pixelmatch(actualImage.data, expectedImage.data, null, width, height);
return count / (width * height);
}
async function assertScreenshotsMatch(entry) {
const actual = getActualScreenshotPath(entry);
const expected = getExpectedScreenshotPath(entry);
let mismatch, error;
try {
mismatch = await match(actual, expected);
} catch (err) {
error = err;
}
if (error) {
return error;
}
if (mismatch) {
return new Error(`${entry} mistmatch: ${mismatch}`);
}
}
function exposeRender(page) {
return new Promise((resolve, reject) => {
const innerPromise = new Promise(innerResolve => {
page.exposeFunction('render', innerResolve).then(() => resolve(() => innerPromise), reject);
});
});
}
async function renderPage(page, entry, options) {
const href = getHref(entry);
const renderCalled = await exposeRender(page);
await page.goto(`http://localhost:${options.port}${href}`, {waitUntil: 'networkidle2'});
await renderCalled();
await page.screenshot({path: getActualScreenshotPath(entry)});
}
async function copyActualToExpected(entry) {
const actual = getActualScreenshotPath(entry);
const expected = getExpectedScreenshotPath(entry);
await fse.copy(actual, expected);
}
async function renderEach(page, entries, options) {
let fail = false;
for (const entry of entries) {
await renderPage(page, entry, options);
if (options.fix) {
await copyActualToExpected(entry);
continue;
}
const error = await assertScreenshotsMatch(entry);
if (error) {
process.stderr.write(`${error.message}\n`);
fail = true;
}
}
return fail;
}
async function render(entries, options) {
const browser = await puppeteer.launch();
let fail = false;
try {
const page = await browser.newPage();
await page.setViewport({width: 256, height: 256});
fail = await renderEach(page, entries, options);
} finally {
browser.close();
}
if (fail) {
throw new Error('RENDERING TESTS FAILED');
}
}
async function main(entries, options) {
const done = await serve(options.port);
try {
await render(entries, options);
} finally {
done();
}
}
if (require.main === module) {
const options = yargs.
option('fix', {
describe: 'Accept all screenshots as accepted',
default: false
}).
option('port', {
describe: 'The port for serving rendering cases',
default: 3000
}).
parse();
const entries = Object.keys(config.entry).map(key => config.entry[key]);
main(entries, options).catch(err => process.stderr.write(`${err.message}\n`, () => process.exit(1)));
}

View File

@@ -0,0 +1,36 @@
const CopyPlugin = require('copy-webpack-plugin');
const fs = require('fs');
const path = require('path');
const cases = path.join(__dirname, 'cases');
const caseDirs = fs.readdirSync(cases);
const entry = {};
caseDirs.forEach(c => {
entry[`cases/${c}/main`] = `./cases/${c}/main.js`;
});
module.exports = {
context: __dirname,
target: 'web',
entry: entry,
module: {
rules: [{
use: {
loader: 'buble-loader'
},
test: /\.js$/,
include: [
path.join(__dirname, '..', 'src')
]
}]
},
plugins: [
new CopyPlugin([
{from: '../src/ol/ol.css', to: 'css'},
{from: 'cases/**/*.html'}
])
],
devtool: 'source-map'
};