New rendering tests
This commit is contained in:
@@ -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
11
rendering/.eslintrc
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": 2017
|
||||||
|
},
|
||||||
|
"globals": {
|
||||||
|
"render": false
|
||||||
|
}
|
||||||
|
}
|
||||||
1
rendering/.gitignore
vendored
Normal file
1
rendering/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
actual.png
|
||||||
BIN
rendering/cases/single-layer/expected.png
Normal file
BIN
rendering/cases/single-layer/expected.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
22
rendering/cases/single-layer/index.html
Normal file
22
rendering/cases/single-layer/index.html
Normal 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>
|
||||||
19
rendering/cases/single-layer/main.js
Normal file
19
rendering/cases/single-layer/main.js
Normal 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
193
rendering/test.js
Executable 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)));
|
||||||
|
}
|
||||||
36
rendering/webpack.config.js
Normal file
36
rendering/webpack.config.js
Normal 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'
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user