From 67ee32fdea1ead3c153feb0746a97341c5053e7a Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Tue, 6 Nov 2018 15:58:45 -0700 Subject: [PATCH] New rendering tests --- package.json | 9 +- rendering/.eslintrc | 11 ++ rendering/.gitignore | 1 + rendering/cases/single-layer/expected.png | Bin 0 -> 26049 bytes rendering/cases/single-layer/index.html | 22 +++ rendering/cases/single-layer/main.js | 19 +++ rendering/test.js | 193 ++++++++++++++++++++++ rendering/webpack.config.js | 36 ++++ 8 files changed, 289 insertions(+), 2 deletions(-) create mode 100644 rendering/.eslintrc create mode 100644 rendering/.gitignore create mode 100644 rendering/cases/single-layer/expected.png create mode 100644 rendering/cases/single-layer/index.html create mode 100644 rendering/cases/single-layer/main.js create mode 100755 rendering/test.js create mode 100644 rendering/webpack.config.js diff --git a/package.json b/package.json index 0af8897237..4a46d031a1 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "scripts": { "lint": "eslint tasks test src/ol examples config", "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", "serve-examples": "webpack-dev-server --config examples/webpack/config.js --mode development --watch", "build-examples": "webpack --config examples/webpack/config.js --mode production", @@ -72,7 +73,9 @@ "mocha": "5.2.0", "mustache": "^3.0.0", "pixelmatch": "^4.0.2", + "pngjs": "^3.3.3", "proj4": "2.5.0", + "puppeteer": "^1.10.0", "rollup": "0.66.6", "sinon": "^6.0.0", "typescript": "^3.1.0-dev.20180905", @@ -81,7 +84,9 @@ "walk": "^2.3.9", "webpack": "4.25.1", "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": { "extends": "openlayers", diff --git a/rendering/.eslintrc b/rendering/.eslintrc new file mode 100644 index 0000000000..07e46d0c56 --- /dev/null +++ b/rendering/.eslintrc @@ -0,0 +1,11 @@ +{ + "env": { + "node": true + }, + "parserOptions": { + "ecmaVersion": 2017 + }, + "globals": { + "render": false + } +} diff --git a/rendering/.gitignore b/rendering/.gitignore new file mode 100644 index 0000000000..8b295d4969 --- /dev/null +++ b/rendering/.gitignore @@ -0,0 +1 @@ +actual.png \ No newline at end of file diff --git a/rendering/cases/single-layer/expected.png b/rendering/cases/single-layer/expected.png new file mode 100644 index 0000000000000000000000000000000000000000..c57b9f9067e1f3f0773ddfe3f052f91f9c3e649b GIT binary patch literal 26049 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 +Im~y%m;rQugnc}_^ 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 literal 0 HcmV?d00001 diff --git a/rendering/cases/single-layer/index.html b/rendering/cases/single-layer/index.html new file mode 100644 index 0000000000..8ee3c8d0a6 --- /dev/null +++ b/rendering/cases/single-layer/index.html @@ -0,0 +1,22 @@ + + + + + + +
+ + + + \ No newline at end of file diff --git a/rendering/cases/single-layer/main.js b/rendering/cases/single-layer/main.js new file mode 100644 index 0000000000..0fe6335004 --- /dev/null +++ b/rendering/cases/single-layer/main.js @@ -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(); diff --git a/rendering/test.js b/rendering/test.js new file mode 100755 index 0000000000..3e14aa8e5b --- /dev/null +++ b/rendering/test.js @@ -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(`
  • ${href}
  • `); + } + const markup = `
      ${items.join('')}
    `; + + 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))); +} diff --git a/rendering/webpack.config.js b/rendering/webpack.config.js new file mode 100644 index 0000000000..2541c394f5 --- /dev/null +++ b/rendering/webpack.config.js @@ -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' +};