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..12dd085bc9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -99,6 +99,16 @@ add_custom_target( ) add_test(NAME get_data_files COMMAND "${CMAKE_COMMAND}" --build ${CMAKE_BINARY_DIR} --target data_files) +add_custom_command(OUTPUT "${GEOJS_DEPLOY_DIR}/examples/bundle.js" + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + COMMAND npm run build-examples + COMMENT "Build examples" + VERBATIM +) +add_custom_target(examples DEPENDS "${GEOJS_DEPLOY_DIR}/examples/bundle.js") +add_test(NAME build_examples COMMAND "${CMAKE_COMMAND}" --build ${CMAKE_BINARY_DIR} --target examples) +set_property(TEST "build_examples" APPEND PROPERTY DEPENDS "get_data_files") + if(FFHEADLESS_TESTS) find_program(NPM_EXECUTABLE npm) add_test( @@ -111,6 +121,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 "build_examples") + configure_file( "${CMAKE_CURRENT_SOURCE_DIR}/testing/test-runners/baseline_images.py" "${CMAKE_CURRENT_BINARY_DIR}/test/baseline_images.py" @@ -124,8 +144,12 @@ 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 + # Build examples to make sure that they are available. + COMMAND npm run build-examples + # 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/docs/provisioning.rst b/docs/provisioning.rst index 6f7307bd01..44099589a6 100644 --- a/docs/provisioning.rst +++ b/docs/provisioning.rst @@ -24,6 +24,7 @@ convenience in configuring CMake options) :: sudo apt-get install --yes \ cmake \ firefox \ + imagemagick \ git \ libjpeg8-dev \ libpango1.0-dev \ 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..0a3846ec7e 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,9 @@ $(function () { lineFeature.draw(); var text = 'Shown: ' + segments; $('#lines-shown').text(text).attr('title', text); + map.onIdle(function () { + $('#map').addClass('ready'); + }); } /** diff --git a/karma-base.js b/karma-base.js index 9a19d6dbec..7c21470962 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) { @@ -165,49 +183,84 @@ var notes_middleware = function (config) { }; }; -module.exports = { - autoWatch: false, - files: [ - test_case, - {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: 'dist/data/**/*', included: false}, - {pattern: 'dist/examples/**/*', included: false} - ], - proxies: { - '/testdata/': '/base/tests/data/', - '/data/': '/base/dist/data/', - '/examples/': '/base/dist/examples/' - }, - browsers: [ - 'PhantomJS' - ], - browserNoActivityTimeout: 30000, - reporters: [ - 'progress', - 'kjhtml' - ], - middleware: [ - 'notes' - ], - plugins: [ - {'middleware:notes': ['factory', notes_middleware]}, - 'karma-*' - ], - preprocessors: {}, - frameworks: [ - 'jasmine', 'sinon' - ], - webpack: { - cache: true, - devtool: 'inline-source-map', - module: { - loaders: webpack_config.module.loaders - }, - resolve: webpack_config.resolve, - plugins: webpack_config.exposed_plugins - } +/** + * Express style middleware to handle REST requests for OSM tiles on the test + * server. + */ +var osmtiles_middleware = function (config) { + return function (request, response, next) { + var match = request.url.match(/.*http:\/\/[a-c]\.tile.openstreetmap.org\/([0-9]+\/[0-9]+\/[0-9]+.png)$/); + /* Serve tiles if they have been proxied */ + if (match && request.method === 'GET') { + var imagePath = 'dist/data/tiles/' + match[1]; + var img = new Buffer(fs.readFileSync(imagePath)); + response.setHeader('Content-Type', 'image/png'); + response.setHeader('Content-Length', img.length); + response.setHeader('Access-Control-Allow-Origin', '*'); + response.writeHead(200); + return response.end(img); + } + next(); + }; }; -module.exports.preprocessors[test_case] = ['webpack', 'sourcemap']; +module.exports = function (config) { + var newConfig = { + autoWatch: false, + files: [ + test_case, + {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} + ], + proxies: { + '/testdata/': '/base/tests/data/', + '/data/': '/base/dist/data/', + '/examples/': '/base/dist/examples/' + }, + browsers: [ + 'PhantomJS' + ], + customLaunchers: { + FirefoxWithProxy: { + base: 'Firefox', + prefs: { + 'network.proxy.type': 2, + 'network.proxy.autoconfig_url': config.protocol + '//' + config.hostname + ':' + config.port + '/testdata/proxy-for-tests.pac' + } + } + }, + browserNoActivityTimeout: 30000, + reporters: [ + 'progress', + 'kjhtml' + ], + middleware: [ + 'notes', + 'osmtiles' + ], + plugins: [ + {'middleware:notes': ['factory', notes_middleware]}, + {'middleware:osmtiles': ['factory', osmtiles_middleware]}, + 'karma-*' + ], + preprocessors: {}, + frameworks: [ + 'jasmine', 'sinon' + ], + webpack: { + cache: true, + devtool: 'inline-source-map', + module: { + loaders: webpack_config.module.loaders + }, + resolve: webpack_config.resolve, + plugins: webpack_config.exposed_plugins + } + }; + newConfig.preprocessors[test_case] = ['webpack', 'sourcemap']; + return newConfig; +}; diff --git a/karma-cov.conf.js b/karma-cov.conf.js index 684ee40ba6..35f525ee14 100644 --- a/karma-cov.conf.js +++ b/karma-cov.conf.js @@ -2,7 +2,6 @@ // with coverage support. var path = require('path'); -var karma_config = require('./karma-base'); /** * Return URL friendly browser string @@ -11,24 +10,26 @@ function browser(b) { return b.toLowerCase().split(/[ /-]/)[0]; } -karma_config.reporters = ['progress', 'coverage']; -karma_config.coverageReporter = { - reporters: [ - {type: 'html', dir: 'dist/coverage/', subdir: browser}, - {type: 'cobertura', dir: 'dist/cobertura/', file: 'coverage.xml', subdir: browser}, - {type: 'json', dir: 'dist/coverage/json/', subdir: browser}, - {type: 'lcovonly', dir: 'lcov', subdir: browser}, - {type: 'text'} - ] -}; -karma_config.webpack.module.preLoaders = [ - { - test: /\.js$/, - include: path.resolve('src/'), - loader: 'istanbul-instrumenter' - } -]; - module.exports = function (config) { + var karma_config = require('./karma-base')(config); + + karma_config.reporters = ['progress', 'coverage']; + karma_config.coverageReporter = { + reporters: [ + {type: 'html', dir: 'dist/coverage/', subdir: browser}, + {type: 'cobertura', dir: 'dist/cobertura/', file: 'coverage.xml', subdir: browser}, + {type: 'json', dir: 'dist/coverage/json/', subdir: browser}, + {type: 'lcovonly', dir: 'lcov', subdir: browser}, + {type: 'text'} + ] + }; + karma_config.webpack.module.preLoaders = [ + { + test: /\.js$/, + include: path.resolve('src/'), + loader: 'istanbul-instrumenter' + } + ]; + config.set(karma_config); }; diff --git a/karma.conf.js b/karma.conf.js index ebf334549c..26bc813a6a 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -1,5 +1,3 @@ -var karma_config = require('./karma-base'); - module.exports = function (config) { - config.set(karma_config); + config.set(require('./karma-base')(config)); }; diff --git a/package.json b/package.json index c1dec27c00..ee63ed621e 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "express": "^4.13.4", "file-loader": "^0.8.5", "forever": "^0.15.2", + "fs-extra": "^1.0.0", "gl-mat3": "^1.0.0", "gl-mat4": "^1.1.4", "gl-vec2": "^1.0.0", @@ -61,7 +62,6 @@ "karma-sourcemap-loader": "^0.3.7", "karma-webpack": "^1.7.0", "mousetrap": "^1.6.0", - "node-fs-extra": "^0.8.1", "node-resemble": "^1.1.3", "phantomjs-prebuilt": "^2.1.5", "proj4": "^2.3.14", @@ -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 FirefoxWithProxy", "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 FirefoxWithProxy", "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..0e365a4194 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 +914356846f8c541a813bb7e53efc6c57 \ 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..5f1e29b242 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/586d1ff18d777f05f44a5c75/download \ No newline at end of file diff --git a/testing/test-data/tiles.tgz.md5 b/testing/test-data/tiles.tgz.md5 index 87ea977455..a0da2d8b27 100644 --- a/testing/test-data/tiles.tgz.md5 +++ b/testing/test-data/tiles.tgz.md5 @@ -1 +1 @@ -73d3e6e8800e5d82d5f668413379373a \ No newline at end of file +910b4894cc906dc3762dc9e314fd4025 diff --git a/testing/test-data/tiles.tgz.url b/testing/test-data/tiles.tgz.url index 0a8bcc380b..ac4e8d8f97 100644 --- a/testing/test-data/tiles.tgz.url +++ b/testing/test-data/tiles.tgz.url @@ -1 +1 @@ -https://data.kitware.com/api/v1/file/560a89528d777f7bfaadd3f8/download +https://data.kitware.com/api/v1/file/586d1e958d777f05f44a5c72/download 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/data/proxy-for-tests.pac b/tests/data/proxy-for-tests.pac new file mode 100644 index 0000000000..c2f9327b0b --- /dev/null +++ b/tests/data/proxy-for-tests.pac @@ -0,0 +1,15 @@ +/* global dnsDomainIs */ + +function FindProxyForURL(url, host) { + // Don't serve certain remote addresses + if (dnsDomainIs(host, 'fonts.googleapis.com')) { + // If we use a testing address such as 192.0.2.0, requests will take a + // long time to fail. Using an address starting with 0 fails promptly. + return 'PROXY 0.0.0.1'; + } + // Redirect tiles to our test server + if (dnsDomainIs(host, '.tile.openstreetmap.org')) { + return 'PROXY 127.0.0.1:9876'; + } + return 'DIRECT'; +} diff --git a/tests/example-cases/lines.js b/tests/example-cases/lines.js new file mode 100644 index 0000000000..eb233e72ae --- /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'); + imageTest.imageTest('exampleLines', '#map', 0.0015, done, null, 0, 2, '#map.ready'); + }, 10000); + it('more lines', function (done) { + base$ = $('iframe#map')[0].contentWindow.jQuery; + 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..64905870c5 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 = $('