Use Karma, Mocha, Chai and Coverage to run headless unit tests and generate lcov code coverage

Github: https://github.com/railsstudent/image-gallery-native-js

1) Install gulp, mocha, chai, puppeteer, http-server as dev-dependencies

yarn add gulp mocha chai puppeteer http-server -D

2) Install all karma dependencies as dev-dependencies.

yarn add karma karma-chai karma-mocha karma-chrome-launcher mocha chai -D

3) Create test/bootstrap.karma.js file to share global variables among unit test cases.

'use strict';
 
const expect = chai.expect;
const assert = chai.assert;

4) Run karma command to generate karma.conf.js to specify Karma configuration.

./node_modules/.bin/karma init
// Karma configuration
// Generated on Sun Aug 26 2018 09:31:46 GMT+0800 (HKT)
 
process.env.CHROME_BIN = require('puppeteer').executablePath();
module.exports = function(config) {
  config.set({
 
    // base path that will be used to resolve all patterns (eg. files, exclude)
    basePath: '',
 
    plugins: [
      "karma-chai",
      "karma-chrome-launcher",
      "karma-mocha",
      "karma-coverage"
    ],
 
    // frameworks to use
    // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
    frameworks: ['mocha', 'chai'],
 
    // list of files / patterns to load in the browser
    files: [
      'src/**/*.js',
      'test/bootstrap.karma.js',
      'test/**.test.js'
    ],
 
    // list of files / patterns to exclude
    exclude: [
      'test/gallery.test.js'
    ],
 
    // preprocess matching files before serving them to the browser
    // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
    preprocessors: {
      'src/**/!(gallery).js': 'coverage'
    },
 
    // optionally, configure the reporter
    coverageReporter: {
      type : 'lcov',
      dir : 'coverage/',
      subdir: '.'
    },
 
    // test results reporter to use
    // possible values: 'dots', 'progress'
    // available reporters: https://npmjs.org/browse/keyword/karma-reporter
    reporters: ['progress', 'coverage'],
 
    // enable / disable watching file and executing tests whenever any file changes
    autoWatch: false,
 
    // start these browsers
    // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
    browsers: ['ChromeHeadless'],
 
    // Continuous Integration mode
    // if true, Karma captures browsers, runs the tests and exits
    singleRun: true,
  })
}

Explicitly list karma plugins for chai, browser launcher, mocha and code coverage.

plugins: [
   "karma-chai",
   "karma-chrome-launcher",
   "karma-mocha",
  "karma-coverage"
 ]

Include mocha and chai required by the unit test cases.

// frameworks to use
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
frameworks: ['mocha', 'chai'],

Load src/, test/bootstrap.karma.js and test/**/*.test.js in browser. Exclude test/gallery.test.js since this test file contains automated UI test cases

// list of files / patterns to load in the browser
files: [
  'src/**/*.js',
  'test/bootstrap.karma.js',
  'test/**.test.js'
],
 
// list of files / patterns to exclude
exclude: [
  'test/gallery.test.js'
]

Use Chromium instance of puppeteer to execute headless browser test

process.env.CHROME_BIN = require('puppeteer').executablePath();
browsers: ['ChromeHeadless'],

Add lcov code coverage. Code coverage results are kept in coverage/lcov-report directory

preprocessors: {
 'src/**/!(gallery).js': 'coverage'
},
 
// optionally, configure the reporter
coverageReporter: {
   type : 'lcov',
   dir : 'coverage/',
   subdir: '.'
}

Add reporters for headless browser testing and code coverage

reporters: ['progress', 'coverage']

5) Install gulp-karma to integrate karma with gulp

yarn add gulp-karma -D

Add gulp test task to run karma test runner

/**
 * Run test once and exit
 */
gulp.task('test', function (done) {
    new KarmaServer({
        configFile: __dirname + '/karma.conf.js',
        singleRun: true
    }, done).start();
});

6) Set up npm script commands in package.json.

"scripts": {
   "test": "gulp test",
   "serve:coverage": "gulp test && http-server coverage/lcov-report/ -p 8001"
}

Test unit test cases in headless browser

yarn test

Serve lcov code coverage results at http://localhost:8001

yarn serve:coverage

7) Write mocha test case in test file.

'use strict';
 
describe('slideshow test', function() {
  let first, middle;
  let imageUrls = [];
  let slideshow = null;
  before(function() {
    first = 0;
    middle = 1;
  });
 
  describe('single image slideshow', () => {
    before(function() {
      // runs before all tests in this block
      imageUrls = [
        'image1.jpg'
      ];
      slideshow = new SlideShow(imageUrls);
      slideshow.setCurrentIndex(first);
    });
 
    it('slideshow has 1 image', function() {
      assert.strictEqual(slideshow.totalCount(), imageUrls.length);
    });
 
    it('current url should be the first url', function() {
      assert.strictEqual(slideshow.currentUrl(), 'image1.jpg');
    });
 
    it('initial index is always 0', function() {
      assert.strictEqual(slideshow.currentIndex(), first);
    });
 
    it('Slide show with 1 image is always the first image', function() {
      assert.strictEqual(slideshow.isFirstImage(), true);
    });
 
    it('Slide show with 1 image is always last image', function() {
      assert.strictEqual(slideshow.isLastImage(), true);
    });
 
    it('Show next image does nothing when there is 1 image', function() {
      assert.strictEqual(slideshow.showNext(), false);
      assert.strictEqual(slideshow.currentIndex(), first);
    });
 
    it('Slide prev image with 1 image is always last image', function() {
      assert.strictEqual(slideshow.showPrev(), false);
      assert.strictEqual(slideshow.currentIndex(), first);
    });
  });
});

Finally, we are done.

Automate UI testing with Mocha and Puppeteer (Updated)

Github: https://github.com/railsstudent/image-gallery-native-js

1) yarn add puppeteer mocha chai

yarn add puppeteer mocha chai

2) Create bootstrap.js file to share global variables among tests. Expose chai.expect, chai.assert and an instance of browser

'use strict';
 
const puppeteer = require('puppeteer');
const chai = require('chai');
const expect = chai.expect;
const globalVariables = { 
    browser: global.browser,
    expect: global.expect
};
 
// puppeteer options
const opts = {
  headless: true,
  slowMo: 150,
  timeout: 50000
};
 
// expose variables
before (async function () {
  global.expect = expect;
  global.browser = await puppeteer.launch(opts);
});
 
// close browser and reset global variables
after (function () {
  global.browser.close();
 
  global.browser = globalVariables.browser;
  global.expect = globalVariables.expect;
});

3) Puppeteer creates a headless browser that timeout after 50 seconds and slow down operation by 150 milliseconds.

// puppeteer options
const opts = {
  headless: true,
  slowMo: 150,
  timeout: 50000
};

4) Set up npm script commands in package.json to load the variables in bootstrap.js and execute all UI test cases in test/gallery.test.js directory

 "scripts": {
    "serve:test": "gulp build && http-server dist/ -p 8000",
    "puppeteer": "rm ./test/*.png; SCREEN_SHOT=false mocha test/bootstrap.js test/gallery.test.js",
    "puppeteer-screenshot": "rm ./test/*.png; SCREEN_SHOT=true mocha test/bootstrap.js test/gallery.test.js"
 }

5) Write mocha test case in test file.

describe('gallery test', function() {
  let page;
  this.timeout(TIMEOUT);
 
  before(async () => {
    try {
      page = await browser.newPage();
      await page.goto('http://localhost:8000/');
    } catch (e) {
      console.error(e);
    }
  });
 
  after(async () => {
    await page.close();
  });
 
 it('Shows first image src is not blank and has correct caption and visible buttons', 
     async () => {
    try {
      await page.waitForSelector('.image:nth-child(1)', { visible: true, timeout: 0 });
      await page.screenshot( {
        path: './test/test4.png'
      });
 
      const imageElement = await page.$('.image:nth-child(1)');
      await imageElement.click(); 
      await page.screenshot( {
        path: './test/test5.png'
      });
 
      const modalHandle = await page.$('.modal.show');
      const imageSrc = await page.evaluate((modal) => 
                modal.querySelector('#modal-image').src, modalHandle);
      const caption = await page.evaluate((modal) => 
                modal.querySelector('#caption').innerText, modalHandle);
      const [btnCloseDisplay, btnCloseOpacity,
        btnLeftDisplay, btnLeftOpacity,
        btnRightDisplay, btnRightOpacity
      ] = await page.evaluate((modal) => { 
        const { opacity: closeOpacity, display: closeDisplay } 
             = window.getComputedStyle(modal.querySelector('.close'));
        const { opacity: leftOpacity, display: leftDisplay } 
             = window.getComputedStyle(modal.querySelector('.left-arrow'));
        const { opacity: rightOpacity, display: rightDisplay } 
             = window.getComputedStyle(modal.querySelector('.right-arrow'));
        return [
          closeDisplay, closeOpacity,  
          leftDisplay, leftOpacity,
          rightDisplay, rightOpacity 
        ];
      }, modalHandle);
 
      expect(imageSrc).to.not.equal('');
      expect(caption).to.equal(`1 of ${NUM_IMAGES}`);
      expect(btnCloseDisplay).to.equal('inline-block');
      expect(btnRightDisplay).to.equal('inline-block');
      expect(btnLeftDisplay).to.be.equal('block');
      expect(btnCloseOpacity).to.equal('1');
      expect(btnRightOpacity).to.equal('1');
      expect(btnLeftOpacity).to.equal('0');
 
      const closeHandle = await page.$('.modal.show .close');
      await closeHandle.click();      
      closeHandle.dispose();
      modalHandle.dispose();
    } catch (e) {
      console.error(e);
      throw e;
    }
  });
});

6) Build development version and serve website at http://localhost:8000

yarn serve:test

7) Run UI test cases without creating screen shots

yarn puppeteer

8) Run UI test cases that creates screen shots

yarn puppeteer-screenshot

Finally, we are done.

Articles I want to read

Tensorflow

https://towardsdatascience.com/how-to-build-a-gesture-controlled-web-based-game-using-tensorflow-object-detection-api-587fb7e0f907

Ngrx

https://medium.com/@bo.vandersteene/advanced-pagination-with-ngrx-store-and-angular-5-f26ca4761cef
https://medium.com/@vlado.tesanovic/handling-keyboard-shortcuts-in-angular-with-redux-ngrx-c88907f17ca8

Angular
https://blog.angularindepth.com/angular-ivy-change-detection-execution-are-you-prepared-ab68d4231f2c
https://medium.com/frontend-coach/self-or-optional-host-the-visual-guide-to-angular-di-decorators-73fbbb5c8658
https://medium.com/@davidibl/advanced-reusable-custom-angular-validator-9ca5febef583
https://malcoded.com/posts/web-assembly-angular?utm_source=mybridge&utm_medium=blog&utm_campaign=read_more

JS
https://blog.logrocket.com/using-trampolines-to-manage-large-recursive-loops-in-javascript-d8c9db095ae3

React Native
https://medium.freecodecamp.org/after-building-my-first-react-native-app-im-now-convinced-it-s-the-future-d3c5e74f8fa8

Week of August 23 to August 31, 2014

Coursera:
1) Programming Cloud Services for Android Handheld Systems: Submitted Quiz 6 and Assignment 2 in the past Sunday. The new assignment looked challenging at first after reading the README file and going through the source codes. Fortunately, code examples provided oAuth2 package that could directly use in the Gradle project. Furthermore, no effort to write CRUD methods to retrieve records from database because Spring Framework’s CRUDRespository class generates them automatically. My solution was create a Spring Controller to expose methods to receive HTTP requests from users. Then, the requests were delegated to CRUDRespository subclass to retrieve data and return it to users. Very simple!!!!

Programming:

2) Start my third AngularJS project that adds photo albums to store pictures taken in Open source Workshops and local meetup groups. This time, the project is bootstrap by Yeoman and Gulp because Matthew told me Gulp is better build system than Grunt. After using angular-gulp-generator to generate the skeleton project, I have to agree with his assessment.

Week of August 11 to August 22, 2014

Coursera:
1) Reproducible Research: Enroll Signature Track to earn my third Verified Certificate in Data Science Specialization.
2) Pattern-Oriented Software Architectures: Programming Mobile Services for Android Handheld Systems: Earn my second Verified Certificate in Mobile Cloud Computing with Android Specialization.
3) Advanced Competitive Strategy: Completed all 7 quizzes and open book final examination.
4) Programming Cloud Services for Android Handheld Systems: Submitted 5 quizzes and assignment 1. Assignment 2 was released but I cannot start until I watch all the videos of week 6. This course uses Spring framework to implement cloud services and instructor has covered Spring controller, Spring repository, Spring security, Spring data and Spring Rest.

Programming:

5) Deploy my second AngularJS size project to http://quiet-chamber-1998:herokuapp.com.
Features include:

  1. Show custom markers of pick-up locations on Google Map.
  2. Call Google Map Direction Service to render route.
  3. Allow user to input unlimited addresses to draw route to Hong Kong Science Park
  4. Define simple Restful API in NodeJS + Express Router
  5. Use Yeoman and Grunt to bootstrap AngularJS project.

Week of May 19 to May 31, 2014

Coursera:
1) Getting and Cleaning Data: Submitted Quiz 3 (15/15), Quiz 4 (15/15) and final course project.
2) Exploratory Data Analysis: Submitted Quiz 2 and Course Project 2.
3) Pattern-Oriented Software Architectures: Programming Mobile Services for Android Handheld Systems: Completed Quiz 1 to 3 and assignment 1 to 4. Unfortunately, wrong version of assignment 1 was submitted for peer-grading; therefore, I don’t expect to get good mark on it. Video lectures of week 4 are downloaded and will watch down when taking shuttle bus from Mei Foo to Hong Kong Science Park.

Programming:

4) Developing my first AngularJS project. Struggling to make multi-selection checkboxes working in my pet project. TODO item: make translation work at real time. When a button is clicked, labels change to either English or Traditional Chinese depending on user selection.

Week of May 5, 2014 to May 11, 2014

Coursera:
1) R Programming: Statement of Accomplishment with Distinction issued on May 9, 2014.
2) Getting and Cleaning Data: Watched week 1 and 2 video lectures. Submitted Quiz 1 (12/15) and Quiz 2 (15/15).
3) Exploratory Data Analysis: Watched week 1 video lectures. Submitted Quiz 1 (15/15) and Course Project 1.

Programming:

4) Developing my first AngularJS project. Using AngularJS, AngularJS UI and Bootstrap to display accessible facilities in Hong Kong.  http://localhost:8000/app/index.html