From 7eb7e53b8b8a1631c7e8a9958a6d0f3b77b17c34 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Fri, 9 Dec 2016 13:51:41 -0500 Subject: [PATCH 1/5] Add the ability to test examples. Test the lines example. Add screenshot capabilities. This requires ImageMagick and expects the test to be run on the same computer or VM as the karma test server. --- .travis.yml | 3 +- CMakeLists.txt | 14 ++++- docs/developers.rst | 6 ++ examples/common/css/examples.css | 1 + examples/lines/main.js | 2 + karma-base.js | 43 +++++++++---- package.json | 2 + testing/test-data/base-images.tgz.md5 | 2 +- testing/test-data/base-images.tgz.url | 2 +- tests/all.js | 21 +++++-- tests/example-cases/lines.js | 24 +++++++ tests/gl-cases/choropleth.js | 2 +- tests/gl-cases/glLines.js | 4 +- tests/image-test.js | 91 ++++++++++++++++++++++----- tests/test-examples.js | 9 +++ tests/test-utils.js | 16 +++++ 16 files changed, 204 insertions(+), 38 deletions(-) create mode 100644 tests/example-cases/lines.js create mode 100644 tests/test-examples.js diff --git a/.travis.yml b/.travis.yml index e6b4d9a49d..52d571ef52 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,12 +31,13 @@ cache: - "$HOME/cache" before_install: + # Start xvfb with a specific resolution and pixel depth + - "/sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 1280x1024x24" - CACHE="${HOME}/cache" CMAKE_VERSION=3.5.0 CMAKE_SHORT_VERSION=3.5 source ./scripts/install_cmake.sh - npm prune before_script: - export DISPLAY=:99.0 - - sh -e /etc/init.d/xvfb start script: - npm run build diff --git a/CMakeLists.txt b/CMakeLists.txt index cf07568087..b47ff83faf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -111,6 +111,16 @@ if(FFHEADLESS_TESTS) set_property(TEST "total-coverage" APPEND PROPERTY DEPENDS "ffheadless") set_property(TEST "ffheadless" APPEND PROPERTY DEPENDS "get_data_files") + add_test( + NAME "examplesheadless" + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + COMMAND npm run examplesci + ) + set_property(TEST "examplesheadless" APPEND PROPERTY ENVIRONMENT "CTEST_IMAGE_PATH=${CMAKE_CURRENT_BINARY_DIR}/images") + set_property(TEST "examplesheadless" APPEND PROPERTY ENVIRONMENT "TEST_SAVE_IMAGE=${TEST_SAVE_IMAGE}") + set_property(TEST "total-coverage" APPEND PROPERTY DEPENDS "examplesheadless") + set_property(TEST "examplesheadless" APPEND PROPERTY DEPENDS "get_data_files") + configure_file( "${CMAKE_CURRENT_SOURCE_DIR}/testing/test-runners/baseline_images.py" "${CMAKE_CURRENT_BINARY_DIR}/test/baseline_images.py" @@ -124,8 +134,10 @@ add_custom_command(OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/base-images.tgz" COMMAND "${CMAKE_COMMAND}" --build ${CMAKE_BINARY_DIR} --target data_files # Run the ffheadless test, asking to save all images COMMAND TEST_SAVE_IMAGE=all npm run ffci + # Run the examplesheadless test, asking to save all images + COMMAND TEST_SAVE_IMAGE=all npm run examplesci # Make a tarball of all of the images - COMMAND tar -zcvf "${CMAKE_CURRENT_BINARY_DIR}/base-images.tgz" --exclude=*-test.png --exclude=*-diff.png --exclude=*-base.png -C "${CMAKE_CURRENT_BINARY_DIR}/images" . + COMMAND tar -zcvf "${CMAKE_CURRENT_BINARY_DIR}/base-images.tgz" --exclude=*-test.png --exclude=*-diff.png --exclude=*-base.png --exclude=*-screen.png -C "${CMAKE_CURRENT_BINARY_DIR}/images" . COMMENT "Create baseline images, then tar them into a single file" VERBATIM ) diff --git a/docs/developers.rst b/docs/developers.rst index 552f7b1835..a3d12ff117 100644 --- a/docs/developers.rst +++ b/docs/developers.rst @@ -109,6 +109,12 @@ test command or set this parameter in CMake. build correctly. Try running ``ccmake /path/to/geojs`` for a full list of configuration options. +Examples should be tested by creating an entry in the ``tests/example-cases/`` +directory. To run these tests in a normal browser, run +``npm run start`` and browse to ``_. +Since the browser's direct screen output is used, the browser must be running +on the same machine as the ``npm run start`` command. + Selenium testing ---------------- diff --git a/examples/common/css/examples.css b/examples/common/css/examples.css index 2804a78433..57c8c30091 100644 --- a/examples/common/css/examples.css +++ b/examples/common/css/examples.css @@ -16,4 +16,5 @@ html, body { #map { width: 100%; height: calc(100% - 60px); + overflow: hidden; } diff --git a/examples/lines/main.js b/examples/lines/main.js index 878b8a008d..4a50e0d27d 100644 --- a/examples/lines/main.js +++ b/examples/lines/main.js @@ -128,6 +128,7 @@ $(function () { * the line. */ function show_lines(rawdata) { + $('#map').removeClass('ready'); if (!rawdata) { return; } @@ -143,6 +144,7 @@ $(function () { lineFeature.draw(); var text = 'Shown: ' + segments; $('#lines-shown').text(text).attr('title', text); + $('#map').addClass('ready'); } /** diff --git a/karma-base.js b/karma-base.js index 9a19d6dbec..f903169426 100644 --- a/karma-base.js +++ b/karma-base.js @@ -53,6 +53,29 @@ function saveImage(name, image, always) { } } +/* Use ImageMagick's import tool to get a portion of the screen. The caller is + * responsible for identifying the useful portion of the screen. + * + * @param {string} name: base name for the image. + * @param {number} left: left screen coordinate + * @param {number} top: top screen coordinate + * @param {number} width: width in pixels of area to fetch. + * @param {number} height: height in pixels of area to fetch. + * @returns: a base64-encoded image. + */ +function getScreenImage(name, left, top, width, height) { + var child_process = require('child_process'); + var dest = path.resolve(image_path, name + '-screen.png'); + child_process.execSync( + 'import -window root ' + + '-crop ' + width + 'x' + height + (left >= 0 ? '+' : '') + left + + (top >= 0 ? '+' : '') + top + ' +repage ' + + '\'' + dest.replace(/'/g, "'\\''") + '\''); + var xvfbImage = new Buffer(fs.readFileSync(dest)).toString('base64'); + xvfbImage = 'data:image/png;base64,' + xvfbImage; + return xvfbImage; +} + /* Compare an image to a base image. If it violates a threshold, save the * image and a diff between it and the base image. Returns the resemble * results. @@ -64,17 +87,6 @@ function saveImage(name, image, always) { * @param {function} callback: a function to call when complete. */ function compareImage(name, image, threshold, callback) { - /* Note, we could read the xvfb frame buffer using imageMagick, which would - * get the entire browser display, including it's window border, tabs, search - * bar, and non-canvas elements. It might be worth install a kiosk extension - * to FireFox (or use Chrome in Kiosk mode), and exclude the portions of the - * window that are used for Karma information. - var child_process = require('child_process'); - var dest = path.resolve(image_path, name + '-xvfb.png'); - child_process.execSync('import -window root \'' + dest.replace(/'/g, "'\\''") + '\''); - var xvfbImage = new Buffer(fs.readFileSync(dest)).toString('base64'); - xvfbImage = 'data:image/png;base64,' + xvfbImage; - */ var resemble = require('node-resemble'); var src = path.resolve('dist/data/base-images', name + '.png'); if (!fs.existsSync(src)) { @@ -138,7 +150,13 @@ var notes_middleware = function (config) { if (request.method === 'PUT') { return getRawBody(request).then(function (body) { var name = query.name; - var image = '' + body; + var image; + if (query.screen === 'true') { + image = getScreenImage(name, query.left, query.top, + query.width, query.height); + } else { + image = '' + body; + } saveImage(name, image); if (query.compare === 'true') { compareImage(name, image, query.threshold, function (results) { @@ -172,6 +190,7 @@ module.exports = { {pattern: 'tests/data/**/*', included: false}, {pattern: 'tests/cases/**/*.js', included: false, served: false, watched: true}, {pattern: 'tests/gl-cases/**/*.js', included: false, served: false, watched: true}, + {pattern: 'tests/example-cases/**/*.js', included: false, served: false, watched: true}, {pattern: 'dist/data/**/*', included: false}, {pattern: 'dist/examples/**/*', included: false} ], diff --git a/package.json b/package.json index c1dec27c00..21f28826b9 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,9 @@ "start": "karma start karma.conf.js", "ci": "GEOJS_TEST_CASE=tests/test-unit.js karma start karma-cov.conf.js --single-run --browsers PhantomJS", "ffci": "GEOJS_TEST_CASE=tests/test-gl.js karma start karma-cov.conf.js --single-run --browsers Firefox", + "examplesci": "GEOJS_TEST_CASE=tests/test-examples.js karma start karma-cov.conf.js --single-run --browsers Firefox", "test-webgl": "GEOJS_TEST_CASE=tests/test-gl.js xvfb-run -s '-ac -screen 0 1280x1024x24' karma start karma-cov.conf.js --single-run --browsers Firefox", + "test-examples": "GEOJS_TEST_CASE=tests/test-examples.js xvfb-run -s '-ac -screen 0 1280x1024x24' karma start karma-cov.conf.js --single-run --browsers Firefox", "codecov": "cat lcov/*/lcov.info | codecov", "combine-coverage": "istanbul-combine -d dist/cobertura -r cobertura 'dist/coverage/json/**/coverage-final.json'", "examples": "webpack-dev-server --config webpack-examples.config.js --host ${HOST-127.0.0.1} --port ${PORT-8082} --content-base dist/", diff --git a/testing/test-data/base-images.tgz.md5 b/testing/test-data/base-images.tgz.md5 index 803d36d26f..59877a0f52 100644 --- a/testing/test-data/base-images.tgz.md5 +++ b/testing/test-data/base-images.tgz.md5 @@ -1 +1 @@ -5fc9100434a75382b93dc86db6a8c62e \ No newline at end of file +564b1810e1b404bbc758c10d2c72f604 \ No newline at end of file diff --git a/testing/test-data/base-images.tgz.url b/testing/test-data/base-images.tgz.url index 3b7d8bedf4..97b467ab7c 100644 --- a/testing/test-data/base-images.tgz.url +++ b/testing/test-data/base-images.tgz.url @@ -1 +1 @@ -https://data.kitware.com/api/v1/file/5858590d8d777f1e3428d5b0/download \ No newline at end of file +https://data.kitware.com/api/v1/file/586af8348d777f05f44a57ce/download \ No newline at end of file diff --git a/tests/all.js b/tests/all.js index a1b5e177cd..5738f1a0fa 100644 --- a/tests/all.js +++ b/tests/all.js @@ -2,8 +2,21 @@ * Entry point for all tests. */ +var query = require('./test-utils').getQuery(); + +/* By default, general and gl tests are run. Set the 'test' query parameter to + * 'all' to run all tests, or use a specific test group name. + */ var tests; -tests = require.context('./cases', true, /.*\.js$/); -tests.keys().forEach(tests); -tests = require.context('./gl-cases', true, /.*\.js$/); -tests.keys().forEach(tests); +if (query.test === 'all' || query.test === 'general' || query.test === undefined) { + tests = require.context('./cases', true, /.*\.js$/); + tests.keys().forEach(tests); +} +if (query.test === 'all' || query.test === 'gl' || query.test === undefined) { + tests = require.context('./gl-cases', true, /.*\.js$/); + tests.keys().forEach(tests); +} +if (query.test === 'all' || query.test === 'examples') { + tests = require.context('./example-cases', true, /.*\.js$/); + tests.keys().forEach(tests); +} diff --git a/tests/example-cases/lines.js b/tests/example-cases/lines.js new file mode 100644 index 0000000000..40c44e7126 --- /dev/null +++ b/tests/example-cases/lines.js @@ -0,0 +1,24 @@ +var $ = require('jquery'); + +describe('lines example', function () { + var imageTest = require('../image-test'); + var base$; + + beforeAll(function () { + imageTest.prepareIframeTest(); + }); + + it('basic', function (done) { + $('#map').attr('src', '/examples/lines/index.html?showmap=false'); + imageTest.imageTest('exampleLines', '#map', 0.0015, done, null, 0, 2, '#map.ready'); + }, 10000); + it('more lines', function (done) { + base$ = $('iframe#map')[0].contentWindow.$; + base$('#lines').val(100000).trigger('change'); + imageTest.imageTest('exampleLines100k', '#map', 0.0015, done, null, 0, 2, '#map.ready'); + }, 10000); + it('thin preset', function (done) { + base$('button.preset').eq(1).trigger('click'); + imageTest.imageTest('exampleLinesThin', '#map', 0.0015, done, null, 0, 2, '#map.ready'); + }, 10000); +}); diff --git a/tests/gl-cases/choropleth.js b/tests/gl-cases/choropleth.js index 09e4a8af2f..c66993f919 100644 --- a/tests/gl-cases/choropleth.js +++ b/tests/gl-cases/choropleth.js @@ -69,6 +69,6 @@ describe('choropleth', function () { .choropleth({}); myMap.draw(); - imageTest.imageTest('choropleth', 0.001, done, myMap.onIdle, 0, 2); + imageTest.imageTest('choropleth', null, 0.001, done, myMap.onIdle, 0, 2); }); }); diff --git a/tests/gl-cases/glLines.js b/tests/gl-cases/glLines.js index b9bfbd19f5..61e07dc0bb 100644 --- a/tests/gl-cases/glLines.js +++ b/tests/gl-cases/glLines.js @@ -83,7 +83,7 @@ describe('glLines', function () { .style(style); myMap.draw(); - imageTest.imageTest('glLines', 0.0015, done, myMap.onIdle, 0, 2); + imageTest.imageTest('glLines', null, 0.0015, done, myMap.onIdle, 0, 2); }); it('lines with different options', function (done) { @@ -140,7 +140,7 @@ describe('glLines', function () { myMap.draw(); - imageTest.imageTest('glLinesOpts', 0.0015, done, myMap.onIdle, 0, 2); + imageTest.imageTest('glLinesOpts', null, 0.0015, done, myMap.onIdle, 0, 2); }); }); diff --git a/tests/image-test.js b/tests/image-test.js index 303e82de71..8a22840417 100644 --- a/tests/image-test.js +++ b/tests/image-test.js @@ -19,9 +19,18 @@ function compareImage(name, canvas, threshold, callback) { if (threshold === undefined) { threshold = 0.001; } + var data, params = ''; + if (canvas.screenCoordinates) { + params = '&screen=true&left=' + encodeURIComponent(canvas.left) + + '&top=' + encodeURIComponent(canvas.top) + + '&width=' + encodeURIComponent(canvas.width) + + '&height=' + encodeURIComponent(canvas.height); + } else { + data = '' + canvas.toDataURL(); + } return $.ajax({ - url: '/testImage?compare=true&threshold=' + encodeURIComponent(threshold) + '&name=' + encodeURIComponent(name), - data: '' + canvas.toDataURL(), + url: '/testImage?compare=true&threshold=' + encodeURIComponent(threshold) + '&name=' + encodeURIComponent(name) + params, + data: data, method: 'PUT', contentType: 'image/png', dataType: 'json' @@ -41,14 +50,26 @@ module.exports.prepareImageTest = function () { window.contextPreserveDrawingBuffer = true; $('#map').remove(); var map = $('
').css({width: '800px', height: '600px'}); - $('body').append(map); + $('body').prepend(map); +}; + +module.exports.prepareIframeTest = function () { + window.contextPreserveDrawingBuffer = true; + $('#map').remove(); + var map = $('