diff --git a/examples/common/index.pug b/examples/common/index.pug index 6e15fd694e..094f1f893b 100644 --- a/examples/common/index.pug +++ b/examples/common/index.pug @@ -8,17 +8,9 @@ html(lang="en") // include the main bundle script(type='text/javascript', src=bundle, charset='UTF-8') - // include example sources - block exampleHeader - // Include example specific files - each css in exampleCss - link(rel="stylesheet", href=css) - each src in exampleJs - if(typeof src === "string") - script(type="text/javascript", src=src) - else - script&attributes(src) - + // Include example specific css + each css in exampleCss + link(rel="stylesheet", href=css) body if !hideNavbar // Add a navbar @@ -70,3 +62,10 @@ html(lang="en") // Add the default main content element block mainContent #map + + // Include example specific scripts + each src in exampleJs + if(typeof src === "string") + script(type="text/javascript", src=src) + else + script&attributes(src) diff --git a/examples/contour/example.json b/examples/contour/example.json index db0b1db4ed..b3e5824d83 100644 --- a/examples/contour/example.json +++ b/examples/contour/example.json @@ -2,6 +2,23 @@ "title": "Contour plot", "exampleJs": ["main.js"], "about": { - "text": "This example shows how to add contour features to a map. Contours color a region based on an array of scalar values." - } + "text": "This example shows how to add contour features to a map. Contours color a region based on an array of scalar values. If stepped colors are used, these are called isobands." + }, + "tests": [{ + "description": "contour feature loads small dataset", + "query": "url=../../data/oahu.json", + "wait": ["example.ready"], + "tests": [ + "example.contour instanceof geo.contourFeature", + "example.contour._createContours().value.length === 1953" + ] + }, { + "description": "contour feature loads dense dataset", + "query": "url=../../data/oahu-dense.json", + "wait": ["example.ready"], + "tests": [ + "example.contour instanceof geo.contourFeature", + "example.contour._createContours().value.length === 194770" + ] + }] } diff --git a/examples/contour/main.js b/examples/contour/main.js index 58b6ce8b80..258e7c83d3 100644 --- a/examples/contour/main.js +++ b/examples/contour/main.js @@ -8,9 +8,9 @@ $(function () { // Define a function we will use to generate contours. function makeContour(data, layer) { - /* There are two example data sets. One has a position array which - * consists of objects each with x, y, z values. The other has a values - * array which just has our contour values. */ + // There are two example data sets. One has a position array which + // consists of objects each with x, y, z values. The other has a values + // array which just has our contour values. var contour = layer.createFeature('contour') .data(data.position || data.values) .style({ @@ -47,8 +47,8 @@ $(function () { value: function (d) { return d > -9999 ? d : null; } }) .contour({ - /* The geometry can be specified using 0-point coordinates and deltas - * since it is a regular grid. */ + // The geometry can be specified using 0-point coordinates and deltas + // since it is a regular grid. x0: data.x0, y0: data.y0, dx: data.dx, dy: data.dy }); } @@ -107,7 +107,18 @@ $(function () { success: function (data) { var contour = makeContour(data, contourLayer); contour.draw(); - /* After 10 second, load a denser data set */ + + // Make some values available in the global context to aid exploration + // and automated tests. + window.example = { + ready: true, + map: map, + contourLayer: contourLayer, + contour: contour, + data: data + }; + + // After 10 second, load a denser data set if (!query.url) { window.setTimeout(function () { $.ajax({ diff --git a/examples/isoline/example.json b/examples/isoline/example.json index fd2bc9fee5..dada5b6980 100644 --- a/examples/isoline/example.json +++ b/examples/isoline/example.json @@ -3,5 +3,13 @@ "exampleJs": ["main.js"], "about": { "text": "This example shows how to add isolines to a map." - } + }, + "tests": [{ + "description": "isoline feature is loaded and has text", + "wait": ["example.ready"], + "tests": [ + "example.iso instanceof geo.isolineFeature", + "example.isolineLayer.children()[2].features()[0] instanceof geo.textFeature" + ] + }] } diff --git a/examples/isoline/main.js b/examples/isoline/main.js index 610e295dc8..188f27b495 100644 --- a/examples/isoline/main.js +++ b/examples/isoline/main.js @@ -1,63 +1,61 @@ -// Run after the DOM loads -$(function () { - 'use strict'; - - // Create a map object with the OpenStreetMaps base layer. - var map = geo.map({ - node: '#map', - center: { - x: -157.965, - y: 21.482 - }, - zoom: 11 - }); +// Create a map object with the OpenStreetMaps base layer. +var map = geo.map({ + node: '#map', + center: { + x: -157.965, + y: 21.482 + }, + zoom: 11 +}); - // Add a faint osm layer - map.createLayer('osm', {opacity: 0.5}); +// Add a faint osm layer +map.createLayer('osm', {opacity: 0.5}); - // Create a feature layer that supports contours - var isolineLayer = map.createLayer('feature', { - features: ['isoline'] - }); +// Create a feature layer that supports contours +var isolineLayer = map.createLayer('feature', { + features: ['isoline'] +}); - // Load the data - $.get('../../data/oahu-dense.json').done(function (data) { - // Create an isoline feature - var iso = isolineLayer.createFeature('isoline', { - isoline: { - // Specify our grid data - gridWidth: data.gridWidth, - gridHeight: data.gridHeight, - x0: data.x0, - y0: data.y0, - dx: data.dx, - dy: data.dy, - // Don't plot any values less than zero - min: 0, - // Create a contour line every 50 meters - spacing: 50, - // Make every 4th line heavier and every 4*5 = 20th line heavier yet - levels: [4, 5] - }, - style: { - // The data uses -9999 to represent no value; modify it to return null - // instead. - value: function (d) { return d > -9999 ? d : null; }, - // level relates to the isoline importance, with 0 being the most - // common and, using the levels specified, a level of 1 being every - // fourth, and 2 every twentieth line. Color the lines differently - // depending on the level - strokeColor: function (v, vi, d) { - return ['grey', 'mediumblue', 'blue'][d.level]; - } +// Load the data +$.get('../../data/oahu-dense.json').done(function (data) { + // Create an isoline feature + var iso = isolineLayer.createFeature('isoline', { + isoline: { + // Specify our grid data + gridWidth: data.gridWidth, + gridHeight: data.gridHeight, + x0: data.x0, + y0: data.y0, + dx: data.dx, + dy: data.dy, + // Don't plot any values less than zero + min: 0, + // Create a contour line every 50 meters + spacing: 50, + // Make every 4th line heavier and every 4*5 = 20th line heavier yet + levels: [4, 5] + }, + style: { + // The data uses -9999 to represent no value; modify it to return null + // instead. + value: function (d) { return d > -9999 ? d : null; }, + // level relates to the isoline importance, with 0 being the most + // common and, using the levels specified, a level of 1 being every + // fourth, and 2 every twentieth line. Color the lines differently + // depending on the level + strokeColor: function (v, vi, d) { + return ['grey', 'mediumblue', 'blue'][d.level]; } - }).data(data.values).draw(); - // Make some values available in the global context so curious people can - // play with them. - window.example = { - map: map, - isolineLayer: isolineLayer, - iso: iso - }; - }); + } + }).data(data.values).draw(); + + // Make some values available in the global context to aid exploration and + // automated tests. + window.example = { + ready: true, + map: map, + isolineLayer: isolineLayer, + iso: iso + }; + }); diff --git a/tests/headed-cases/examples.js b/tests/headed-cases/examples.js new file mode 100644 index 0000000000..a7866dc435 --- /dev/null +++ b/tests/headed-cases/examples.js @@ -0,0 +1,138 @@ +/* In each examples//example.json, if there is a top-level key called + * "tests", this will run a test on that example. This key is an array of + * tests, each of which is an object with the following optional values: + * Partial example: + * "tests": [{ + * "description": "test name", // a description of the test + * "htmlvideo": false, // if true, the test browser must support + * // video, or the test is skipped. + * "idle": [ // a list of idle functions or deferreds + * "map.onIdle" // an example idle function that will be + * ], // passed a function to call when idle + * "wait": [ // a list of expressions that will block + * // until they don't throw errors and + * // evaluate to truthy + * "map.layers().length >= 1" // an example wait expression + * ], + * "tests": [ // a list of expressions that must evaluate + * // to truthy for the tests to pass. + * "map.layers()[0] instanceof geo.tileLayer" // an example of a test + * ] + * }] + */ + +/* Find all examples. */ +var examples = require.context('../../examples', true, /\/example.json$/); + +describe('examples', function () { + 'use strict'; + + var $ = require('jquery'); + + var imageTest = require('../image-test'); + + beforeEach(function () { + imageTest.prepareIframeTest(); + }); + + /* Test each example */ + examples.keys().forEach(function (examplePath) { + var example; + try { + example = $.ajax({url: '/examples/' + examplePath, dataType: 'json', async: false}).responseJSON; + } catch (err) { + return; + } + if (!example || !example.tests) { + return; + } + var exampleName = examplePath.split('/')[1]; + example.tests.forEach(function (test, idx) { + describe('Test ' + exampleName, function () { + /* Load the example in the test iframe */ + beforeEach(function (done) { + sinon.stub(console, 'warn', function () {}); + $('#map').one('load', function () { + /* allow logging to percolate through to our test environment */ + $('iframe#map')[0].contentWindow.console = console; + window.setTimeout(done, 1); + }); + $('#map').attr('src', '/examples/' + exampleName + '/index.html' + (test.query ? '?' + test.query : '')); + }, 150000); + afterEach(function () { + console.warn.restore(); + }); + it(test.description || ('Test ' + idx), function (done) { + /* Never complain if there are no explicit expect statements */ + expect().nothing(); + /* If a test requires html video and the current browser doesn't + * support video, skip the test. */ + if (test.htmlvideo && !$('iframe#map')[0].contentWindow.HTMLVideoElement) { + done(); + return; + } + var exampleWindow = $('iframe#map')[0].contentWindow, + ex$ = exampleWindow.$, + deferreds = []; + /* Description of the current example and test. */ + var desc = $('iframe#map')[0].contentWindow.document.title + (test.description ? ' - ' + test.description : ''); + /* Evaluate and wait for each idle function and promises. */ + (test.idle || []).forEach(function (idleFunc) { + var defer = ex$.Deferred(); + deferreds.push(defer); + var idle = exampleWindow.eval(idleFunc); + if (!ex$.isFunction(idle)) { + idle = idle.then || idle.done; + } + idle(function () { + defer.resolve(); + }); + }); + (test.wait || []).forEach(function (waitCondition) { + var defer = ex$.Deferred(); + deferreds.push(defer); + var interval; + interval = exampleWindow.setInterval(function () { + var result; + try { + result = exampleWindow.eval(waitCondition); + } catch (err) { } + if (result) { + exampleWindow.clearInterval(interval); + defer.resolve(); + } + }, 10); + }); + /* When all idle functions and wait conditions have resolved, + * evaluate each test in the tests list. */ + ex$.when.apply(ex$, deferreds).then(function () { + var subtestDeferreds = []; + (test.tests || []).forEach(function (testExp) { + /* The test expression can return a value or a promise. We + * use jQuery's when to generically get the results in a + * resolution function. A rejection is a failure. */ + try { + var testResult = exampleWindow.eval(testExp); + } catch (err) { + fail(desc + ' -> raised an error: ' + err); + return; + } + var subtestDefer = ex$.when(testResult).then(function (result) { + /* If the result isn't truthy, make sure our expect has a + * description telling which test block and specific test + * failed. */ + expect(result).toBeTruthy(desc + ' -> ' + testExp); + }, function () { + fail(desc + ' promise failed -> ' + testExp); + }); + subtestDeferreds.push(subtestDefer); + }); + ex$.when.apply(ex$, subtestDeferreds).then(done); + }, function () { + fail(desc + ' -> idle functions were rejected'); + }); + }, 150000); + }); + }); + }); +}); diff --git a/tests/tutorials.js b/tests/tutorials.js index e32a25ee75..bd21f0ca7e 100644 --- a/tests/tutorials.js +++ b/tests/tutorials.js @@ -21,7 +21,7 @@ describe('tutorials', function () { sinon.stub(console, 'warn', function () {}); $('#map').one('load', function () { window.setTimeout(done, 1); }); $('#map').attr('src', '/tutorials/' + tutorialName + '/index.html'); - }); + }, 150000); afterEach(function () { console.warn.restore(); }); diff --git a/website/package.json b/website/package.json index f98acae48a..5c799dc353 100644 --- a/website/package.json +++ b/website/package.json @@ -12,7 +12,7 @@ "hexo-renderer-marked": "^0.3.2", "hexo-renderer-scss": "^1.0.3", "hexo-renderer-stylus": "^0.3.1", - "hexo-server": "^0.2.0" + "hexo-server": "^0.3.1" }, "devDependencies": { "ejs-lint": "^0.3.0",