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.
MODIFY
.travis.yml
.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

MODIFY
CMakeLists.txt
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
)

MODIFY
docs/developers.rst
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 `<http://localhost:9876/debug.html?test=all>`_.
+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
----------------

MODIFY
examples/common/css/examples.css
examples/common/css/examples.css
@@ -16,4 +16,5 @@ html, body {
#map {
width: 100%;
height: calc(100% - 60px);
+ overflow: hidden;
}

MODIFY
examples/lines/main.js
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');
}
/**

MODIFY
karma-base.js
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}
],

MODIFY
package.json
package.json
"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/",

MODIFY
testing/test-data/base-images.tgz.md5
testing/test-data/base-images.tgz.md5
-5fc9100434a75382b93dc86db6a8c62e
\ No newline at end of file
+564b1810e1b404bbc758c10d2c72f604
\ No newline at end of file

MODIFY
testing/test-data/base-images.tgz.url
testing/test-data/base-images.tgz.url
-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

MODIFY
tests/all.js
tests/all.js
* 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;
+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);
+}

ADD
tests/example-cases/lines.js
+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);
+});

MODIFY
tests/gl-cases/choropleth.js
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);
});
});

MODIFY
tests/gl-cases/glLines.js
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);
});
});

MODIFY
tests/image-test.js
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 = $('<div id="map"/>').css({width: '800px', height: '600px'});
- $('body').append(map);
+ $('body').prepend(map);
+};
+
+module.exports.prepareIframeTest = function () {
+ window.contextPreserveDrawingBuffer = true;
+ $('#map').remove();
+ var map = $('<iframe id="map"/>').css({
+ width: '800px', height: '600px', border: 0});
+ $('body').prepend(map);
};
/**
- * Compare a composite of all canvas elements with a base image.
+ * Compare a screen region or a composite of all canvas elements with a base
+ * image.
*
* @param {string} name: name of the base image. This is probably the name of
* the test.
+ * @param {string|null} elemSelector: if present, ask the server to take a
+ * screenshot of the current display and crop it to the bounds of the
+ * specified element. If falsy, make a composite of all canvas elements.
* @param {number} threshold: allowed difference between this image and the
* base image.
* @param {function} doneFunc: a function to call when complete. Optional.
@@ -58,12 +79,16 @@ module.exports.prepareImageTest = function () {
* @param {number} delay: additional delay in milliseconds to wait after idle.
* @param {integer} rafCount: additional number of renderAnimationFrames to
* wait after the delay.
+ * @param {string|null} elemSelector: if present, wait until this selector
+ * selects at least one existing element.
*/
-module.exports.imageTest = function (name, threshold, doneFunc, idleFunc, delay, rafCount) {
+module.exports.imageTest = function (name, elemSelector, threshold, doneFunc, idleFunc, delay, rafCount, readySelector) {
var deferred = $.Deferred();
var readyFunc = function () {
- var result = $('<canvas>')[0];
+ var result;
+ if (!elemSelector) {
+ result = $('<canvas>')[0];
result.width = $('canvas')[0].width;
result.height = $('canvas')[0].height;
var context = result.getContext('2d');
@@ -72,6 +97,22 @@ module.exports.imageTest = function (name, threshold, doneFunc, idleFunc, delay,
$('canvas').each(function () {
context.drawImage($(this)[0], 0, 0);
});
+ } else {
+ var innerScreenX = window.mozInnerScreenX !== undefined ?
+ window.mozInnerScreenX :
+ (window.outerWidth - window.innerWidth) / 2 + window.screenX,
+ innerScreenY = window.mozInnerScreenY !== undefined ?
+ window.mozInnerScreenY :
+ window.outerHeight - window.innerHeight -
+ (window.outerWidth - window.innerWidth) / 2 + window.screenY;
+ result = {
+ screenCoordinates: true,
+ left: $(elemSelector).offset().left + innerScreenX,
+ top: $(elemSelector).offset().top + innerScreenY,
+ width: $(elemSelector).outerWidth(true),
+ height: $(elemSelector).outerHeight(true)
+ };
+ }
compareImage(name, result, threshold, function () {
if (doneFunc) {
doneFunc();
@@ -99,7 +140,27 @@ module.exports.imageTest = function (name, threshold, doneFunc, idleFunc, delay,
if (delay) {
var delayFunc = readyFunc;
- readyFunc = window.setTimeout(delayFunc, delay);
+ readyFunc = function () {
+ window.setTimeout(delayFunc, delay);
+ };
+ }
+
+ if (readySelector) {
+ var selFunc = readyFunc;
+ var selWaitFunc;
+ selWaitFunc = function () {
+ var base$ = $,
+ base = $('iframe#map');
+ if (base.length) {
+ base$ = base[0].contentWindow.$;
+ }
+ if (!base$ || !base$(readySelector).length) {
+ window.setTimeout(selWaitFunc, 10);
+ return;
+ }
+ selFunc();
+ };
+ readyFunc = selWaitFunc;
}
if (idleFunc) {

ADD
tests/test-examples.js
+/**
+ * Entry point for all tests in ./gl-cases/*
+ * This is here to prevent webpack from creating a seperate bundle for each
+ * test case. See: https://github.com/webpack/karma-webpack/issues/23
+ */
+
+var tests = require.context('./example-cases', true, /.*\.js$/);
+tests.keys().forEach(tests);
+

MODIFY
tests/test-utils.js
tests/test-utils.js
@@ -411,3 +411,19 @@ module.exports.unmockDate = function () {
module.exports.advanceDate = function (delta) {
startDate += delta;
};
+
+/* Decode query components into a dictionary of values.
+ *
+ * @returns {object}: the query parameters as a dictionary.
+ */
+module.exports.getQuery = function () {
+ var query = document.location.search.replace(/(^\?)/, '').split(
+ '&').map(function (n) {
+ n = n.split('=');
+ if (n[0]) {
+ this[decodeURIComponent(n[0].replace(/\+/g, '%20'))] = decodeURIComponent(n[1].replace(/\+/g, '%20'));
+ }
+ return this;
+ }.bind({}))[0];
+ return query;
+};

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.