Developing an OpenLayers app from scratch in ES6 using Mocha, Webpack and Karma: Arctic sea-ice data visualisation

23 minute read

… where we finally visualise actual sea-ice concentration data and migrate our tests to karma.

Visualising Arctic sea-ice concentration data

Now we have enough building blocks in place to be able to display the sea-ice concentration data for the Arctic. We’ll do this by adding a PNG image layer where the pixel colours represent the sea-ice concentration value. Sea-ice concentration is a percentage measure which indicates how much ice covers a given area and is measured from space via satellite. The standard data set (e.g. from the Japan Aerospace Exploration Agency (JAXA)) uses 6km x 6km pixels: a value of 20% means that 20% of a 6km x 6km area is covered with ice.

The company I work for, Drift+Noise, takes low-level swath data from JAXA and combines the information into a running composite image (a continuously updated image containing data from the last roughly 28 satellite swaths). These images are available in various formats (e.g. GeoTIFF, HDF5, PNG); the format that is of interest for us here is the PNG format which can be downloaded via the API at https://svalnav.driftnoise.com. To find out the URL of the latest 6km sea-ice concentration image, one can use this API lookup:

https://svalnav.driftnoise.com/api/sea_ice_data_files/?sea_ice_data_product=sea-ice-concentration-6k&latest=true

The output is JSON and the url field contains the information we need. To download the latest image, we can use the following one-liner:

$ wget \
  $(curl -s 'https://svalnav.driftnoise.com/api/sea_ice_data_files/?sea_ice_data_product=sea-ice-concentration-6k&latest=true' \
    | tr ',' "\n" \
    | grep url \
    | cut -d '"' -f4) \
  -O src/sea-ice-concentration.png

which I’ve split over several lines to improve its readability in this text. This command uses curl to fetch the JSON metadata about the latest image, the -s option stops curl from showing some diagnostic output which we don’t need to see, we then translate all commas to newlines so that we can grep for the url field. Once we have the url field from the JSON, we cut the resulting string on double quotes and take the fourth field from the resulting output. This is then the URL of the latest sea-ice concentration image. The curl command is run in a subprocess, the result of which we pass to wget to download the file to src/sea-ice-concentration.png. This way we have a simple filename that we can use as the source data for the image layer we want to add to our OpenLayers map.

The sea-ice concentration image should look similar to this

Sea-ice concentration image

where pixels coinciding with landmasses or any sea-ice concentration below 15% have been made transparent. Looking at the image it’s possible to discern Greenland (at the bottom, in the middle) as well as the Bering Strait (top left-hand corner).

Our OpenLayers map currently has only one layer: the OpenStreetMap TileLayer. We therefore expect to have two layers in the map once the image layer has been added. Let’s add a test for this expectation to help drive the development. Add the following test to the map-test.js file:

    it('should have 2 layers', function () {
        const numMapLayers = map.getLayers().getArray().length;
        numMapLayers.should.equal(2);
    });

Running the tests fails (as we expect)

  1) Basic map
       should have 2 layers:

      AssertionError: expected 1 to equal 2
      + expected - actual

      -1
      +2

and with the error we expect (that there’s only one layer, but we wanted to see two).

To add the new layer we need to import the ImageLayer class, instantiate a new object from this class, and add this object to the list of layers in the map. Therefore, we need to add

import ImageLayer from 'ol/layer/Image';

to the list of imports; we create a variable sicLayer:

const sicLayer = new ImageLayer({});

and we extend the list of layers passed to the Map constructor:

    layers: [osmLayer, sicLayer],

The test suite now passes. Sweet!

  Basic map
    ✓ should have a view centred on Longyearbyen
    ✓ should have a default zoom of 6
    ✓ should have a default rotation of 45 degrees
    ✓ should have 2 layers

  EPSG:3413
    ✓ should use EPSG:3413 as its projection code
    ✓ should use the NSIDC north pole max sea ice extent


  6 passing (11ms)

However, we’ve not actually added the PNG image to the layer. To do that we need to add what’s called a “source” (in the OpenLayers nomenclature) to the image layer. We therefore expect there to be a source associated with the sicLayer object and we can test for this. Let’s start a new suite of tests describing the sea-ice concentration image layer. Add a new describe block to map-test.js:

describe('SIC layer', function () {
    it('should have a valid image source', function () {
        sicLayer.getSource().should.not.be.null;
    });
});

This test is based upon the information in the OpenLayers API docs for ImageLayer which says that the default source of a layer is null. It would be better to use a “positive” test here (i.e. one where we expect a definite value, not that we don’t expect a non-value) but this should get things going.

The tests show that the variable sicLayer isn’t defined.

  SIC layer
    1) should have a valid image source

  EPSG:3413
    ✓ should use EPSG:3413 as its projection code
    ✓ should use the NSIDC north pole extent


  6 passing (16ms)
  1 failing

  1) SIC layer
       should have a valid image source:
     ReferenceError: sicLayer is not defined

We’ll now export this variable so that we’ve got direct access to it in the tests. Change the last line in index.js to read:

export { map, sicLayer };

and import the sicLayer variable in map-test.js:

import {map, sicLayer} from '../src/js/index.js';

The tests fail and sort of how we would expect them to:

  SIC layer
    1) should have a valid image source

  EPSG:3413
    ✓ should use EPSG:3413 as its projection code
    ✓ should use the NSIDC north pole extent


  6 passing (23ms)
  1 failing

  1) SIC layer
       should have a valid image source:
     TypeError: Cannot read property 'should' of null

This output means that the test isn’t actually checking for null as we want it to do, however since the value we get back is null and hence the should part of the assertion fails, we still get the feedback we want from the test.

Let’s add a minimal image source. Import the Static class:

import Static from 'ol/source/ImageStatic';

instantiate an empty source object for the sicLayer (before the definition of sicLayer):

const sicSource = new Static({});

and pass this variable as the source parameter to the ImageLayer constructor:

const sicLayer = new ImageLayer({
    source: sicSource,
});

And the tests pass. Wicked!

  Basic map
    ✓ should have a view centred on Longyearbyen
    ✓ should have a default zoom of 6
    ✓ should have a default rotation of 45 degrees
    ✓ should have 2 layers

  SIC layer
    ✓ should have a valid image source

  EPSG:3413
    ✓ should use EPSG:3413 as its projection code
    ✓ should use the NSIDC north pole max sea ice extent


  7 passing (12ms)

Our image source should have three parameters defined to ensure that it has been correctly set up: the projection for the view, the image extent (defined by the lower-left and upper-right coordinates in the relevant projection units), and a URL pointing to the image file.

We’ll make sure we’re using the correct projection by checking the code of the source’s projection object. Add this test to the SIC layer test suite:

    it('should have a source which uses EPSG:3413', function () {
        const projectionCode = sicLayer.getSource().getProjection().getCode();
        projectionCode.should.equal('EPSG:3413');
    });

We can also remove the 'should have a valid image source' test because the new test will implicitly check that the source is non-null.

The tests fail with an error that looks sensible:

  SIC layer
    1) should have a source which uses EPSG:3413

  EPSG:3413
    ✓ should use EPSG:3413 as its projection code
    ✓ should use the NSIDC north pole extent


  6 passing (13ms)
  1 failing

  1) SIC layer
       should have a source which uses EPSG:3413:
     TypeError: Cannot read property 'getCode' of null

because the call to getCode() is made on a null object, which means that the projection hasn’t been defined. Adding the projection property to sicSource

const sicSource = new Static({
    projection: epsg3413,
});

gets the tests to pass again.

  Basic map
    ✓ should have a view centred on Longyearbyen
    ✓ should have a default zoom of 6
    ✓ should have a default rotation of 45 degrees
    ✓ should have 2 layers

  SIC layer
    ✓ should have a source which uses EPSG:3413

  EPSG:3413
    ✓ should use EPSG:3413 as its projection code
    ✓ should use the NSIDC north pole max sea ice extent


  7 passing (16ms)

Now add a test for the extent to the SIC layer test suite:

    it('should have a source with the JAXA Arctic extent', function () {
        const jaxaArcticExtent = [-3846875.000, -5353125.000, 3753125.000, 5846875.000];
        sicLayer.getSource().getImageExtent().should.deep.equal(jaxaArcticExtent);
    });

where the JAXA Arctic extent in projection coordinates was calculated from the AMSR2 data product manual (i.e. the manual for the instrument on the satellite from which the sea-ice concentration data is measured).

The tests fail because there isn’t yet an extent defined for this source

  SIC layer
    ✓ should have a source which uses EPSG:3413
    1) should have a source with the JAXA Arctic extent

  EPSG:3413
    ✓ should use EPSG:3413 as its projection code
    ✓ should use the NSIDC north pole extent


  7 passing (14ms)
  1 failing

  1) SIC layer
       should have a source with the JAXA Arctic extent:
     TypeError: Cannot read property 'should' of undefined

Setting the imageExtent property in the Static constructor to the appropriate value

    imageExtent: [-3846875.000, -5353125.000, 3753125.000, 5846875.000],

gets the tests to pass again.

  Basic map
    ✓ should have a view centred on Longyearbyen
    ✓ should have a default zoom of 6
    ✓ should have a default rotation of 45 degrees
    ✓ should have 2 layers

  SIC layer
    ✓ should have a source which uses EPSG:3413
    ✓ should have a source with the JAXA Arctic extent

  EPSG:3413
    ✓ should use EPSG:3413 as its projection code
    ✓ should use the NSIDC north pole max sea ice extent


  8 passing (13ms)

The test to check for the correct value of the url parameter is tricky. The reason it’s tricky is because of the bundling issue. One might naively think that one could pass the filename directly as the url parameter to the Static constructor, i.e. something like this:

    url: "../sea-ice-concentration.png",

Unfortunately, this doesn’t work because webpack won’t be able to find the file in order to bundle it and the path to the file won’t be correct. If you try this out, you’ll find that the bundle gets built, but there aren’t any PNG images added to it.

The solution is to use the file-loader loader in webpack; the first example in the file-loader documentation gives us the outline of a concrete solution. Before we can start, we need to install the file-loader package:

$ npm install --save-dev file-loader

Now we need to tell webpack to look for .png files, so we add the following code to the list of rules in webpack.config.js

            {
                test: /\.png$/,
                use: [
                    'file-loader',
                ],
            },

And now comes the trick: we load the image as if it were a module:

import sicImage from '../sea-ice-concentration.png';

and then we pass the variable sicImage as the url parameter in the Static constructor for the static image we want to load:

    url: sicImage,

Building the application bundle with make build, you’ll notice that a PNG image does get bundled as part of the application (see the Names section below):

Hash: d1b643d80b3078b0350a
Version: webpack 4.43.0
Time: 1385ms
Built at: 05/10/2020 11:15:34 PM
                               Asset       Size  Chunks             Chunk
Names
26c42ed1b81ea4d34d28813a35e40310.png    132 KiB          [emitted]
                           bundle.js   2.34 MiB    main  [emitted]  main
                          index.html  343 bytes          [emitted]
Entrypoint main = bundle.js
[./src/js/index.js] 958 bytes {main} [built]
[./src/js/projections.js] 529 bytes {main} [built]
[./src/sea-ice-concentration.png] 80 bytes {main} [built]
    + 312 hidden modules
Child HtmlWebpackCompiler:
     1 asset
    Entrypoint HtmlWebpackPlugin_0 = __child-HtmlWebpackPlugin_0
    [./node_modules/html-webpack-plugin/lib/loader.js!./src/index.html] 590
bytes {HtmlWebpackPlugin_0} [built]

But better than that, reloading the app in a browser, we see that the sea-ice concentration data is now being displayed! Brilliant!

Sea-ice concentration prototype display

Ok, given we know how to do this, let’s imagine that we’ve not coded any of this yet and we’ll guide the development using tests. Note that we know roughly how to procede because we’ve already thrown together a prototype solution in the code above.

So what do we test for? We can’t test that the url property is equal to a string that looks like 'sea-ice-concentration.png' because we’re assigning the variable sicImage to the url parameter and it’s not likely that the value of sicImage is going to be the same as the image filename.

That actually the begs the question: what is the value of sicImage after the bundle has been built? To work this out, we can implement the prototype code above as well as the file-loader setup and run

$ make build

and see what turns up in the dist/ output directory. If you now list the contents of the dist/ directory you should see something like this:

$ ls dist/
26c42ed1b81ea4d34d28813a35e40310.png  bundle.js  index.html

In other words, we see our automatically generated JavaScript bundle and the main HTML file as well as a PNG file with an automatically generated filename containing only hexadecimal digits. If you view this file you’ll see that it’s the same as our sea-ice-concentration.png file in the src/ directory. Let’s try logging the value of sicImage as loaded into our app by adding the following line after the import statements

console.log(sicImage);

Then, if we build the package (make build), reload the app in the browser and use F12 to see the console output, we’ll find that the value of sicImage is the filename of the bundled, automatically generated PNG file. That’s cool, because now we have a much better idea of what we can sensibly test.

Remove the console.log() code and add the following test to the ‘SIC layer’ test suite, which we expect to pass because we now know what to look for and we’ve already implemented a working solution:

    it('should have a url matching a PNG file', function () {
        sicLayer.getSource().getUrl().should.match(/[0-9a-f]+\.png$/);
    });

The purpose of this test is to check that the URL matches a string of hexadecimal digits and ends with the literal text .png. However, the test suite fails, which we didn’t expect to happen this time. Why? Well, if we think about things carefully, we’ll realise we’re testing the source code directly, not the code that the bundler generates.

Let’s take a step back and save the state we had after having a passing test for the Arctic extent. Therefore, let’s remove the 'should have a url matching a PNG file' test, remove the url parameter, and remove the import of the sicImage file. You should find that the tests are passing again. Let’s commit this state to the repo:

# mention that this code prepares the stage for static images to be loaded
$ git commit src/js/index.js test/map-test.js

We’re now in a bit of a conundrum: testing the source code directly doesn’t help us anymore because we actually need to test the bundled code, i.e. the code that the browser is going to be executing. Have you also noticed a mismatch here between the code we’re testing and the environment in which the code is going to be running? Note that we’re testing the code from within node which is basically a version of JavaScript for servers, however our code is intended to run within a browser. So this begs the question: why don’t we just run our tests in the browser? Can we have our nice mocha test framework as well as the ability to test and run our code in an environment that is much better matched to that which our users are actually going to be running? It turns out the answer is yes. What we need is karma.

Increasing our karma with JavaScript

karma is a test runner for JavaScript and allows unit tests to run on real devices and in real browsers. This is in contrast to the common way to run JavaScript tests: from within the Node.js environment. Running tests in node is a good idea if the production JavaScript code is going to run within a node environment (e.g. on a server as part of an app’s backend). We’ve now seen that in our case running tests in a different environment to that which we use for our production environment can have its limits.

To install karma, simply use npm:

$ npm install --save-dev karma

In the project we’re developing here, we’re also using mocha, chai and webpack, so we also have to install the relevant karma adapters for these libraries:

$ npm install --save-dev karma-mocha karma-chai karma-webpack

We also need some launchers for the browsers that we want to use (Google Chrome and Firefox), so let’s install the packages for them as well:

$ npm install --save-dev karma-chrome-launcher karma-firefox-launcher

We now need to configure karma; the configuration lives by default in a file called karma.conf.js; create a new file with this name in your favourite editor and add the following boilerplate code:

module.exports = function (config) {
    config.set({
    });
};

Karma needs to know which files it needs to look for so that it can run the appropriate code in the browser. This is configured via the files option, which we append to the options passed to config.set(). The browser needs to know about both the source and test files, hence we glob for all JavaScript files under src/ and test/:

module.exports = function (config) {
    config.set({
        files: [
            'src/**/*.js',
            'test/**/*.js',
        ],
    });
};

It doesn’t make much sense to run this yet, because we haven’t told karma which browsers we want to use for the tests. Let’s get things started by concentrating on Chrome first. We specify the browsers to use via the browsers option:

        browsers: ['ChromeHeadless'],

We can now run the tests by using the following command:

$ npx karma start --single-run --browsers ChromeHeadless karma.conf.js

You should see this command fail and it will give output like this:

05 05 2020 14:46:00.327:INFO [karma-server]: Karma v5.0.4 server started at
http://0.0.0.0:9876/
05 05 2020 14:46:00.330:INFO [launcher]: Launching browsers ChromeHeadless
with concurrency unlimited
05 05 2020 14:46:00.344:INFO [launcher]: Starting browser ChromeHeadless
05 05 2020 14:46:16.123:INFO [Chrome Headless 81.0.4044.129 (Linux x86_64)]:
Connected on socket GTDtsXVG5Sir-AkMAAAA with id 40140894
Chrome Headless 81.0.4044.129 (Linux x86_64) ERROR
  Uncaught SyntaxError: Cannot use import statement outside a module
  at src/js/index.js:1:1

  SyntaxError: Cannot use import statement outside a module

What this is trying to tell us is that we’ve not packaged the code before trying to run it: we need to package the code with webpack before it can be run correctly in the browser. To fix this issue, we need the preprocessors option in the karma configuration. We therefore get karma to preprocess the source and test files with webpack before running the tests:

        preprocessors: {
            'src/**/*.js': ['webpack'],
            'test/**/*.js': ['webpack'],
        },

Running the karma command again we get the following error (among others):

05 05 2020 14:55:54.754:ERROR [preprocess]: Can not load "webpack"!

To fix this issue, we need to set the webpack option with the value of our webpack configuration. We therefore require the webpack configuration file and assign its value to a variable which we can then pass to karma’s webpack option; add the following line to the top of karma.config.js:

const webpackConfig = require('./webpack.config.js');

and set the webpack option:

        webpack: webpackConfig,

You should now see output from the karma command similar to this:

ℹ 「wdm」: Hash: 8e56442c989820840bbc
Version: webpack 4.43.0
Time: 615ms
Built at: 05/10/2020 11:41:48 PM
                   Asset       Size                 Chunks             Chunk
Names
              index.html  598 bytes                         [emitted]
                 main.js   2.34 MiB                   main  [emitted]  main
         src/js/index.js   2.34 MiB           src/js/index  [emitted]
src/js/index
   src/js/projections.js    432 KiB     src/js/projections  [emitted]
src/js/projections
        test/map-test.js    2.7 MiB          test/map-test  [emitted]
test/map-test
test/projections-test.js    800 KiB  test/projections-test  [emitted]
test/projections-test
Entrypoint main = main.js
Entrypoint src/js/index = src/js/index.js
Entrypoint src/js/projections = src/js/projections.js
Entrypoint test/map-test = test/map-test.js
Entrypoint test/projections-test = test/projections-test.js
[./node_modules/chai/index.js] 40 bytes {test/map-test}
{test/projections-test} [built]
[./node_modules/chai/register-should.js] 40 bytes {test/map-test}
{test/projections-test} [built]
[./node_modules/ol/index.js] 1.6 KiB {main} {src/js/index} {test/map-test}
[built]
[./node_modules/ol/layer/Image.js] 1.71 KiB {main} {src/js/index}
{test/map-test} [built]
[./node_modules/ol/layer/Tile.js] 1.74 KiB {main} {src/js/index}
{test/map-test} [built]
[./node_modules/ol/ol.css] 490 bytes {main} {src/js/index} {test/map-test}
[./node_modules/ol/proj.js] 23.2 KiB {main} {src/js/index}
{src/js/projections} {test/map-test} {test/projections-test} [built]
[./node_modules/ol/proj/proj4.js] 1.65 KiB {main} {src/js/index}
{src/js/projections} {test/map-test} {test/projections-test} [built]
[./node_modules/ol/source/ImageStatic.js] 5.29 KiB {main} {src/js/index}
{test/map-test} [built]
[./node_modules/ol/source/OSM.js] 3.57 KiB {main} {src/js/index}
{test/map-test} [built]
[./node_modules/proj4/lib/index.js] 554 bytes {main} {src/js/index}
{src/js/projections} {test/map-test} {test/projections-test} [built]
[./src/js/index.js] 864 bytes {main} {src/js/index} {test/map-test} [built]
[./src/js/projections.js] 529 bytes {src/js/projections} {main}
{src/js/index} {test/map-test} {test/projections-test} [built]
[./test/map-test.js] 1.41 KiB {test/map-test} [built]
[./test/projections-test.js] 573 bytes {test/projections-test} [built]
    + 341 hidden modules
Child HtmlWebpackCompiler:
                          Asset      Size               Chunks  Chunk Names
    __child-HtmlWebpackPlugin_0  4.71 KiB  HtmlWebpackPlugin_0
HtmlWebpackPlugin_0
    Entrypoint HtmlWebpackPlugin_0 = __child-HtmlWebpackPlugin_0
    [./node_modules/html-webpack-plugin/lib/loader.js!./src/index.html] 590
bytes {HtmlWebpackPlugin_0} [built]
ℹ 「wdm」: Compiled successfully.
10 05 2020 23:41:49.017:INFO [karma-server]: Karma v5.0.5 server started at
http://0.0.0.0:9876/
10 05 2020 23:41:49.018:INFO [launcher]: Launching browsers ChromeHeadless
with concurrency unlimited
10 05 2020 23:41:49.037:INFO [launcher]: Starting browser ChromeHeadless
10 05 2020 23:41:49.302:INFO [Chrome Headless 81.0.4044.129 (Linux x86_64)]:
Connected on socket yUjGmvuw_r7l9KPvAAAA with id 38661624
Chrome Headless 81.0.4044.129 (Linux x86_64) ERROR
  Uncaught ReferenceError: describe is not defined
  at webpack:///./test/map-test.js?:13:1

  ReferenceError: describe is not defined
      at eval (webpack:///./test/map-test.js?:13:1)
      at Module../test/map-test.js (test/map-test.js:4305:1)
      at __webpack_require__ (test/map-test.js:20:30)
      at test/map-test.js:84:18
      at test/map-test.js:87:10

Wow! We got much further! You’ll notice that karma has used webpack to bundle our software and then has passed it on to the browser to run the test suite. The tests couldn’t run because the describe keyword isn’t defined; the main hint in all the noise of the error output is:

  ReferenceError: describe is not defined

The solution here is to tell karma which frameworks we’re using for the tests. We’re using mocha and chai, so we just need to set the frameworks option to the following value:

        frameworks: ['mocha', 'chai'],

Running the karma command again, we get (among other things):

Chrome Headless 81.0.4044.129 (Linux x86_64): Executed 8 of 8 SUCCESS (0.032 secs / 0.008 secs)
TOTAL: 8 SUCCESS

Yay! The tests are running in karma!

Unfortunately, the detailed output we got from running the tests through mocha no longer appears. To be able to see this information again we need to use the mocha reporter in karma. Let’s install that

$ npm install --save-dev karma-mocha-reporter

and tell karma to use this library to report the test output by setting the reporters option:

        reporters: ['mocha'],

Now, when we run the karma test runner, we get this output:

  Basic map
    ✔ should have a view centred on Longyearbyen
    ✔ should have a default zoom of 6
    ✔ should have a default rotation of 45 degrees
    ✔ should have 2 layers
  SIC layer
    ✔ should have a source which uses EPSG:3413
    ✔ should have a source with the JAXA Arctic extent
  EPSG:3413
    ✔ should use EPSG:3413 as its projection code
    ✔ should use the NSIDC north pole extent

Finished in 0.035 secs / 0.003 secs @ 19:23:55 GMT+0200 (Central European Summer Time)

SUMMARY:
✔ 8 tests completed

That’s pretty cool! Let’s now run the tests in Firefox as well; add 'Firefox' to the browsers option list:

        browsers: ['ChromeHeadless', 'Firefox'],

and mention Firefox in the karma command:

$ npx karma start --single-run --browsers ChromeHeadless,Firefox karma.conf.js

although we now see that 16 tests have been run:

SUMMARY:
✔ 16 tests completed

which means that we’re running the tests in both browsers! That’s excellent, and exactly what we hope to see here.

Unfortunately, a Firefox window appears and disappears while the tests are running. This could get a bit annoying when we run the tests, but more than that, this means that the Firefox tests probably won’t run in a “headless” environment such as Jenkins. We need to get Firefox to run in “headless” mode to stop the browser window from appearing, and to set this up in karma, we need to define a custom launcher 1:

        customLaunchers: {
            FirefoxHeadless: {
                base: 'Firefox',
                flags: ['--headless'],
            },
        },

Now, if we run karma with FirefoxHeadless as one of its browsers

$ npx karma start --single-run --browsers ChromeHeadless,FirefoxHeadless karma.conf.js

we find that we still have 16 tests completed (in other words the tests ran in both browsers) but we don’t get the extra Firefox window appearing. Let’s now make this our default test command in package.json:

    "test": "karma start --single-run --browsers ChromeHeadless,FirefoxHeadless karma.conf.js",

which means that we can now run the test suite simply by running

$ make test

Now would be a good time to save our work to the repository:

$ git add karma.conf.js
# mention use of karma for tests in commit message and why we wanted to do this
$ git commit package-lock.json package.json karma.conf.js

Picking up from where we left off

Now that we’ve got our tests running in the browser, we can go back and get our test of the sea-ice concentration image filename to work. Let’s reinstate our test for the image in map-test.js:

    it('should have a url matching a PNG file', function () {
        sicLayer.getSource().getUrl().should.match(/[0-9a-f]+\.png$/);
    });

The tests fail, which is what we now expect.

  Basic map
    ✔ should have a view centred on Longyearbyen
    ✔ should have a default zoom of 6
    ✔ should have a default rotation of 45 degrees
    ✔ should have 2 layers
  SIC layer
    ✔ should have a source which uses EPSG:3413
    ✔ should have a source with the JAXA Arctic extent
    ✖ should have a url matching a PNG file
  EPSG:3413
    ✔ should use EPSG:3413 as its projection code
    ✔ should use the NSIDC north pole max sea ice extent

Finished in 0.038 secs / 0.009 secs @ 23:48:39 GMT+0200 (Central European Summer Time)

SUMMARY:
✔ 16 tests completed
✖ 2 tests failed

FAILED TESTS:
  SIC layer
    ✖ should have a url matching a PNG file
      Chrome Headless 81.0.4044.129 (Linux x86_64)
      Firefox 76.0 (Linux x86_64)
    _src_js_index_js__WEBPACK_IMPORTED_MODULE_2__.sicLayer.getSource(...).getUrl(...) is undefined
    @webpack:///./test/map-test.js?:44:79

Note also that the summary mentions that two tests failed. This is because we’re using two browsers; in actual fact there’s only one test failing2.

We ensure that the file-loader webpack loader is set up:

            {
                test: /\.png$/,
                use: [
                    'file-loader',
                ],
            },

that the image is imported into the main file (src/js/index.js):

import sicImage from '../sea-ice-concentration.png';

and that the url option to the Static constructor is set:

    url: sicImage,

If we now run the test suite, we see that the tests pass. Yay!

  Basic map
    ✔ should have a view centred on Longyearbyen
    ✔ should have a default zoom of 6
    ✔ should have a default rotation of 45 degrees
    ✔ should have 2 layers
  SIC layer
    ✔ should have a source which uses EPSG:3413
    ✔ should have a source with the JAXA Arctic extent
    ✔ should have a url matching a PNG file
  EPSG:3413
    ✔ should use EPSG:3413 as its projection code
    ✔ should use the NSIDC north pole max sea ice extent

Finished in 0.033 secs / 0.008 secs @ 23:51:14 GMT+0200 (Central European Summer Time)

SUMMARY:
✔ 18 tests completed

That’s definitely time for a little celebration! Have another dance :smiley:

Time for a last git commit:

$ git add src/sea-ice-concentration.png
# mention addition of test for PNG image and why in commit message
$ git commit src/ test/ webpack.config.js

Now, if we run make build and load the application in a browser, we see that the application still works as we want it to

Final Svalbard sea-ice concentration map

namely that the sea-ice concentration data is displayed over a map centred over Svalbard.

You can explore the map by using the mouse to drag the map around; you can also zoom in and out with the + and - buttons in the top, left-hand corner of the screen.

Where to from here?

We could now work out a way to download the latest sea-ice concentration image from the upstream API and we could even add a button to the map to trigger download and display of new sea-ice concentration images. One could even think about using a different coastline map in the background, or possibly extend the app to use PWA technology so that the user could install the app locally on their own device. Basically, the sky’s the limit, however, I think that’ll do for now.

Recap and winding down

Wow! We did it! We have an OpenLayers app showing actual satellite data on a map and we managed to test the application and get them to run within both Google Chrome and Firefox. That’s quite an achievement and a pat on the back is order.

Hopefully you’ve also been able to see how to get mocha, webpack and karma working together on what is really a fairly simple application, and that you’ll be able to extend what you’ve learned here to more complex situations.

I wish you luck writing your next JavaScript app, and above all have fun!

  1. I spotted this tip on Meziantou’s blog

  2. I think it would be interesting to see a test pass in one browser, but fail in another, however adding this here would be going overboard. 

Support

If you liked this post and want to see more like this, please buy me a coffee!

buy me a coffee logo