NightWatch ...

NightWatch ...

Great title for a film or book maybe, but in this case I'm talking about Nightwatch.js and automated testing. In the past I've always picked up on code written by coders and DevOPS'd it, so I wasn't totally sure what to expect when approaching it from scratch .. little bit of work but pleasantly surprised with the results.
In principle, run a "selenium" server, which provides a conduit between a testing / scripting language and a browser instance, then use the scripting language to code up a bunch of tests to simulate everything a user might do, and check the results of everything that happens, including the "correctness" of resulting displays from both a data and presentation perspective.

Upload that ...

First problem I hit was file uploading. I'm using Crossbar's upload facility which in turn relies on a Javascript widget, Resumable.js. This is great, but is doesn't behave as one might expect in terms of Nightwatch's documented way of dealing with "files". Turns out the answer was fairly simple, Resumable creates a dynamic "input" element with an "id" of the enclosing element, plus "-input", so simply filling this field provides the upload functionality, AND activates the UI progress bar into the bargain. So;

<span id="resumable-container">
.. resumable.js code
</span>

Ends up tacking the following to the base element;

<input id="resumable-container-input" />

And in Nightwatch, all we need is;

.setValue('#resumable-container-input', require('path').resolve(__dirname + 'myfilename')) 

This is complete example which posts a new Vacancy to the application;

module.exports = {
  '@tags': ['requests'],
  'NAC Requests Test' : function(client) {   
    client
      .url('https://localhost:8443')
      .waitForElementVisible('body',5000)
      .waitForElementVisible('#start-button',5000)
      .click('#start-button')
      .click('#start-button')
      .waitForElementVisible('input[name="username"]',5000);
    client.expect.element('#dialog2-save').to.be.enabled;
    client.expect.element('#dialog2-cancel').to.be.enabled;
    client.expect.element('#dialog2-help').to.be.enabled;
    client
      .setValue('input[name="username"]',['myuser',client.Keys.ENTER])
      .setValue('input[name="password"]',['mypass',client.Keys.ENTER])
      .pause(500)
      .click('#dialog1-chk');      
   client
      .waitForElementVisible('#base-dashboard-new_request',5000)
      .click('#base-dashboard-new_request')
      .waitForElementVisible('#request-title',5000)
      .setValue('#request-title','Super Hero Job # 1')
      .setValue('#request-ref','SHJ123')
      .setValue('#request-visible','Private')
      .setValue('#request-feetype','Fixed')
      .setValue('#request-deal','1000')
      .setValue('#request-location','Earth')
      .setValue('#request-deadline','01/01/2020')
      .setValue('#request-essential-tokenfield',['strong',client.Keys.ENTER])
      .setValue('#request-email','myEmail')
      .setValue('#s-button-input',require('path').resolve(__dirname+'spec1.pdf'))
      client.click('#request-Submit')
      .waitForElementVisible('#popup-yes',5000)
      .click('#popup-yes');     
  }  
}
How Big?!

The only other real issue was verifying uploaded images. Following for example a profile photo upload, I need to be able to verify that various parts of the screen are updated dynamically with the new and correct profile photo - which appears twice for example on the profile edit screen. (on the upload box, and against the user menu in the top right hand corner)

Nightwatch has no inbuilt facility for this.
Enter "custom commands", very powerful but very poorly documented (!)

So, here is my custom command to verify the size of an uploaded file, whether it be an image or a document, I guess I could check various attributes, but the correct size will tell me if the update has happened or not. In a NightWatch script, all I need to do is;

.getAttribute('#preferences-photo','src',
    function(result){client.check(result.value,5435);}
)

This will extract the "src" attribute from the IMG element with tag "preferences-photo" and call the inline function, which can extract the URL from result.value. This is then passed to the custom-command check which is defined as follows;

var request = require("request");
exports.command = function(url, size, callback) {
    var self = this;
    this.execute(function(){ 
        return true;
    },[],function(result) {
        var options = {url:url, rejectUnauthorized: false};
        request.get(options, function(error, response, body) {
            if (!error && response.statusCode == 200) {
                self.assert.equal(response.headers["content-length"],
                                          size, 'checking file size');
            } else {
                console.log(error);
            };
        });
        if (typeof callback === "function") {
            callback.call(self, result);
        }
    });
    return this;
};

This is so-to-speak a game of two halves and VERY poorly explained in the documentation, but very obvious once you work out what's going on. Custom commands come in two halves, the first half operates in the browser, the second half operates on the test server .. so the first thing to bear in mind is that it's not worth executing console.log (or much else) in the first half (hence return true) because it's going to appear in the browser console, and you won't be looking there. So all the good stuff needs to happen in the second half. Bit like watching Wales play Rugby.
Anyway, second half, run a request against the URL supplied by Nightwatch, extract the size from the header, then call NightWach's assert to make sure the size is as expected and act accordingly if not.
The "callback" and "return this" bits are interesting, they are what allow NightWatch to "chain" commands, look at the example, everything between "client" and the semi-colon is essentially one command-list of chained instructions.

So a successful test (this is for a profile edit session) might look something like this, from the command line perspective (you can see the custom-command output listed as 'checking file size');

$ ./run

[02 Profile] Test Suite
===========================

Running:  NAC Profile Test
 ✔ Element <body> was visible after 86 milliseconds.
 ✔ Element <#start-button> was visible after 602 milliseconds.
 ✔ Element <input[name="username"]> was visible after 709 milliseconds.
 ✔ Expected element <#dialog2-save> to be enabled
 ✔ Expected element <#dialog2-cancel> to be enabled
 ✔ Expected element <#dialog2-help> to be enabled
 ✔ Element <#menu-dropdown> was visible after 556 milliseconds.
 ✔ Element <#mmenu-prefs> was visible after 33 milliseconds.
 ✔ Element <#prefs-Edit> was visible after 1487 milliseconds.
 ✔ Element <#prefs-block> was visible after 52 milliseconds.
 ✔ Element <#photo-button-input> was present after 31 milliseconds.
 ✔ Element <#popup-yes> was visible after 1005 milliseconds.
 ✔ Passed [equal]: checking file size
 ✔ Passed [equal]: checking file size
 ✔ Element <#prefs-block> was visible after 487 milliseconds.
 ✔ Expected element <input[name="prefs-name"]> to have value equal: "Gareth Bult"
 ✔ Expected element <input[name="prefs-company"]> to have value equal: "Nothing in particular"
 ✔ Expected element <input[name="prefs-title"]> to have value equal: "Tester"
 ✔ Expected element <input[name="prefs-email"]> to have value equal: "myEmail"
 ✔ Expected element <select[name="prefs-sector"]> to have value equal: "General"
 ✔ Expected element <input[name="prefs-location"]> to have value equal: "Wales"
 ✔ Expected element <select[name="prefs-sender"]> to have value equal: "Yes"
 ✔ Expected element <select[name="prefs-receiver"]> to have value equal: "Yes"
 ✔ Expected element <select[name="prefs-mailcandidates"]> to have value equal: "Yes"
 ✔ Expected element <select[name="prefs-mailclients"]> to have value equal: "Yes"
 ✔ Expected element <select[name="prefs-mailme"]> to have value equal: "Yes"
 ✔ Element <#photo-button-input> was present after 20 milliseconds.
 ✔ Element <#popup-yes> was visible after 733 milliseconds.
 ✔ Passed [equal]: checking file size
 ✔ Passed [equal]: checking file size
 ✔ Element <#prefs-block> was visible after 394 milliseconds.
 ✔ Expected element <input[name="prefs-name"]> to have value equal: "Gareth Bult1"
 ✔ Expected element <input[name="prefs-company"]> to have value equal: "Nothing in particular1"
 ✔ Expected element <input[name="prefs-title"]> to have value equal: "Tester1"
 ✔ Expected element <input[name="prefs-email"]> to have value equal: "myEmail1"
 ✔ Expected element <select[name="prefs-sector"]> to have value equal: "Pharmaceutical"
 ✔ Expected element <input[name="prefs-location"]> to have value equal: "Wales1"
 ✔ Expected element <select[name="prefs-sender"]> to have value equal: "No"
 ✔ Expected element <select[name="prefs-receiver"]> to have value equal: "No"
 ✔ Expected element <select[name="prefs-mailcandidates"]> to have value equal: "No"
 ✔ Expected element <select[name="prefs-mailclients"]> to have value equal: "No"
 ✔ Expected element <select[name="prefs-mailme"]> to have value equal: "No"

OK. 42 assertions passed. (39.913s)

So the only remaining question is; how many user test cases do I consider before I decide that mowing the grass becomes an attractive proposition ...