DEV Community

Cover image for A Beginner’s Guide To Test Automation With Javascript(Nightwatch.js). Part 3.
Loadero
Loadero

Posted on • Originally published at blog.loadero.com

A Beginner’s Guide To Test Automation With Javascript(Nightwatch.js). Part 3.

Callback functions and command queue in Nightwatch.js

Welcome to the “A beginners guide to test automation with Javascript(Nightwatch.js)” blog series part 3! If you have missed out on the previous parts, make sure to read part 1: Introduction to Nightwatch.js and part 2: The most useful Nightwatch.js commands in our blog.

In this article we will look at callback functions and command queue in Nightwatch.js, and as always – feel free to skip to any part you are the most interested in.

Code used in this article can be found in Loadero’s public GitHub examples repository here.

Prerequisites

  • It is really recommended that you have read part 1 and part 2 of our “Beginner’s guide to test automation with Javascript(Nightwatch.js).”

  • Text editor of your choice (in Loadero we prefer Visual Studio Code).

  • Node.js (the latest version is preferable, in this example v14.15.0 will be used).

  • Google Chrome and Firefox browsers.

Callbacks

General

Nightwatch.js is a framework that utilizes the concept of a callback function (or shortly “callback”) heavily. But what is a callback function exactly?

A callback function is a function passed into another function as an argument, which is then invoked inside the outer function to complete some kind of routine or action.

(From MDN Web Docs)

For example, a callback can be as simple as the following code.

const outerFunction = callback => {
    const callbackResult = callback();
    console.log(callbackResult);
};
const callbackFunction = () => {
    return "Callbacks are cool!";
};
outerFunction(callbackFunction); // => "Callbacks are cool!"
Enter fullscreen mode Exit fullscreen mode

outerFunction() has one parameter defined, namely, callback. callbackFunction definition is passed as an argument to outerFunction() which is then called inside the outer function body. Then the callback’s result is saved and logged in the console.

Important: Notice that callbackFunction is passed as an argument without function call parenthesis, i.e., (). That is done because a function definition is being passed rather the result of the function call.

Nightwatch.js callbacks

Since Nightwatch.js commands are chained one after another, they can’t return any value that can be assigned to a variable. To solve this problem, Nightwatch.js offers to utilize the “returned” value in a callback function, where it can be processed. This callback should accept either no arguments if you don’t want to process the returned value or accepts 1 parameter of type Object.

Let’s take a closer look at callbacks on .getText() Nightwatch.js function (check its documentation here).

Callback's of  raw `.getText()` endraw  function
To see the contents of the response object, let’s write the following code.

module.exports = {
    test: client => {
        client
            .url('http://example.com/')
            .waitForElementVisible('div', 10 * 1000)
            .getText('h1', response => console.log(response));
    }
};
Enter fullscreen mode Exit fullscreen mode
// Logs out the following object
{
     sessionId: '8f3d666f316e6aa063b6ed70cfeaafff',
     status: 0,
     value: 'Example Domain'
}
Enter fullscreen mode Exit fullscreen mode

As you can see in the callback function response object was logged out with its 3 properties:

  • sessionID- ID of the particular Nightwatch.js session. Generally, it is not that useful but can be helpful when you are running tests locally in a concurrent manner.
  • status– indicates the status of the request. Simply put: 0 is a successful request and anything else indicates that an error has occurred.
  • value– returned value of the command, in this case, the text of HTML element is returned. In most cases, you will be interested in this field since it can be used later in the callback. For example, you can assert whether the text matches your expectations or just save it in some variable for future processing.

Tip: You can name callback function parameter as you like but response or result are used the most often.

Now let’s take a look at what happens if the command fails to execute properly.

module.exports = {
    test: client => {
        client
            .url('http://example.com/')
            .waitForElementVisible('div', 10 * 1000)
            .getText('non.existing.element', response => console.log(response));
    }
};
Enter fullscreen mode Exit fullscreen mode
// Logs out the following object
{
    status: -1,
    value: {
        error: 'An error occurred while running .getText() command on <non.existing.element>: ',
        message: 'An error occurred while running .getText() command on <non.existing.element>: ',
        stack: 'NoSuchElementError: An error occurred while running .getText() command on <non.existing.element>: \n' +
        '    at GetText.noSuchElementError (/Users/loadero/loadero/test/test-scripts/scripts/javascript/node_modules/nightwatch/lib/element/command.js:399:12)\n' +
        '    at Object.errorHandler (/Users/loadero/loadero/test/test-scripts/scripts/javascript/node_modules/nightwatch/lib/api/element-commands/_baseElementCommand.js:42:30)\n' +
        '    at PeriodicPromise.runAction (/Users/loadero/loadero/test/test-scripts/scripts/javascript/node_modules/nightwatch/lib/utils/periodic-promise.js:64:29)\n' +
        '    at processTicksAndRejections (internal/process/task_queues.js:93:5)'
    }
}
Enter fullscreen mode Exit fullscreen mode

Oh, that looks differently, doesn’t it? The 'response' object fields look changed, for example, 'sessionID' is not present anymore, so let’s see what fields are present:

  • status– this field doesn’t have 0 value since this request is not successful anymore.
  • value– this field now doesn’t have string value as in the previous example but rather a whole object.
  • value.error– the error that was raised when running the previous command.
  • value.message– a more detailed explanation for the value.error. In this case, they are the same but in different scenarios, this field may host a more detailed explanation.
  • value.stack– a stacktrace where this error occurred (notice that Nightwatch.js logs specific line and column of the file where the specific error was triggered to help you debug).

Now, when you know what are callbacks and how they are used in Nightwatch.js, let’s look at the command queue!

Command queue

Now buckle up because this is the most challenging concept of Nightwatch.js to grasp but don’t be scared away by this – after reading this section, you will understand it well!

Before we get started, let’s take a look at the following example

module.exports = {
    test: client => {
        let text;
        client
            .url('https://duckduckgo.com/')
            .perform(() => {
                text = 'first'; // assigns 'first' to text
            })
            .setValue('#search_form_input_homepage', text)
            .getValue('#search_form_input_homepage', ({ value }) => console.log(value))
            .clearValue('#search_form_input_homepage') // clears input field
            .perform(() => client.setValue('#search_form_input_homepage', text))
            .getValue('#search_form_input_homepage', ({ value }) => console.log(value));
        console.log('second');
    }
};
Enter fullscreen mode Exit fullscreen mode

And when this script is run, then the following is logged in the console

second
undefined
first
Enter fullscreen mode Exit fullscreen mode

Some of you may get confused, to say the least, by this point. Why Nightwatch.js doesn’t log values like this as they should be read – from top to bottom like this?

first
first 
second
Enter fullscreen mode Exit fullscreen mode

The reason for this behavior is thoroughly explained in Understanding the Command Queue Nightwatch.js GitHub wiki (it is a long read but it’s worth it). In this section, we will not look in such detail, this is going to be a summary of sorts. By the way, this command queue example can be found in Loadero’s public GitHub examples repository here.

TL; DR
Nightwatch.js firstly schedules commands in the queue, before executing them. All variables used in scheduled commands are set to their initial values.

Javascript is always executed first, before executing scheduled Nightwatch.js commands.

Then the commands are executed and run in a FIFO (first-in, first-out) manner.

If the command has a callback, then the callback is scheduled right after the outer function.

Once the outer function finishes its execution, the callback is called.

Command queue creation
Nightwatch.js utilizes the queue concept for executing various functions. Since that is a queue it follows FIFO rule, so the first command you call is run the first, then the second and so on. This is a key point to remember because Nightwatch.js doesn’t execute the functions as they are called, instead it schedules them in a queue (which essentially is the reason why commands are not executed as you would expect them to be run).

Let’s take a look at the following example

module.exports = {
    test: client => {
        client
            .url('http://example.com/')
            .click('a');
    }
  };
Enter fullscreen mode Exit fullscreen mode

When Nightwatch.js runs the test, it calls test() function which then calls each of these 3 commands. Since they are synchronous, each of these commands is added to the queue one after another. This results in the following queue.

[
  {command: 'url', args: ['http://example.com/']},
  {command: 'click', args: ['a']}
]

Enter fullscreen mode Exit fullscreen mode

Only then Nightwatch.js traverses the queue and calls each command separately.

Callbacks in the command queue
Let’s add a callback to .click() function and see how it changes the queue.

// test with 'click button' test case
module.exports = {
    test: client => {
        client
            .url('http://example.com/')
            .click('a', () => client.pause(5 * 1000))
    }
};
Enter fullscreen mode Exit fullscreen mode

Initially, the command queue looks just like before

[
  {command: 'url', args: ['http://example.com/']},
  {command: 'click', args: ['a', callback]}
]
Enter fullscreen mode Exit fullscreen mode

When Nightwatch.js begins to traverse the queue, it executes .url() command, waits for it to finish, only then executes .click() with the provided arguments, and then calls the callback. When the callback is called, it is added to the queue right after .click() so it can be executed as the next function. This results in the following queue:

[
  {command: 'url', args: ['http://example.com/']}, // executed
  {command: 'click', args: [a, callback]}, // executed
  {command: 'pause', args: [5000]}, // <-- just added
]
Enter fullscreen mode Exit fullscreen mode

Important: One of the most important aspects of the Nightwatch.js command queue is that commands are not executed at the moment they are scheduled. But rather commands get scheduled, then JavaScript code is executed and only then Nightwatch.js scheduled commands get executed one-by-one in queue’s order. This is the reason why console.log('second') in the example before was executed first and only afterward console logs in Nightwatch commands.

Variable resolution in the queue

When it comes to variables in Nightwatch.js, you always have to take into consideration the command queue. If you don’t, then variables might have uninitialized values or just have them incorrectly set.

Initial variable values
Take a look at the following code snippet from the already mentioned example.

module.exports = {
    test: client => {
        let text;
        client
            .url('https://duckduckgo.com/')
            .perform(() => {
                text = 'first'; // assigns 'first' to text
            })
            .setValue('#search_form_input_homepage', text)
            .getValue('#search_form_input_homepage', ({ value }) => console.log(value))
        // ...
    }
};
Enter fullscreen mode Exit fullscreen mode

In this example, console.log(value) logs undefined. The reason for this is simply that when Nightwatch.js commands queued, their arguments are assigned with the values of that moment. In this case, JavaScript assigns undefined to text because the variable is not initialized (only declared) and, when .setValue() is called, Nightwatch.js uses initial text value instead of the one assigned in the .perform() callback.

Variables inside callbacks
Now, let’s take a look at why variables inside callbacks have proper values, like in this example.

module.exports = {
    test: client => {
        let text;
        client
            .url('https://duckduckgo.com/')
            .perform(() => {
                text = 'first'; // assigns 'first' to text
            })            
            .perform(() => client.setValue('#search_form_input_homepage', text))
            .getValue('#search_form_input_homepage', ({ value }) => console.log(value));
        // ...
    }
};
Enter fullscreen mode Exit fullscreen mode

Here console.log(value) logs out 'first'. The reason for such behavior is that when Nightwatch.js adds callbacks to the queue, it adds them with values of that current moment, so when they are executed, they have “the latest” values. In this case, when .perform() callback is added to the queue, it has text with a value of 'first'.

The magic of .perform()
At the beginning of this blog post, it was mentioned that .perform() with no parameters is mainly used for dealing with command queue execution order. The reason for that is simply because it allows wrapping some other functions inside a callback, hence making them use the latest variable values. Like so

client.perform(() => client.setValue('#search_form_input_homepage', text));
Enter fullscreen mode Exit fullscreen mode

Tip: It is really recommended to use arrow functions for callbacks, mainly because of their conciseness. The recommended code style will be looked at in more detail in the next part of this series.

Another important reason why .perform() is often used, is to get rid of the callback hell where inside a callback you have a callback that has a callback inside that…, like this

module.exports = {
    test: client => {
        let text, text2, text3; // ...
        client.getValue('#input', response => {
            text = response.value;
            client.getValue('#' + text, response => {
                text2 = response.value;
                client.getValue('#' + text2, response => {
                    text3 = response.value;
                    client.getValue('#' + text3, response => {
                        // ...
                    });
                });
            });
        });
    }
};

Enter fullscreen mode Exit fullscreen mode

To avoid such situations, wrap these functions inside multiple .perform() calls, like this to make your code easier to follow and increase its readability.

module.exports = {
    test: client => {
        let text, text2, text3; // ...
        client
            .getValue('#input', response => {
                text = response.value;
            })
            .perform(() =>
                client.getValue('#' + text, response => {
                    text2 = response.value;
                })
            )
            .perform(() =>
                client.getValue('#' + text2, response => {
                    text3 = response.value;
                })
            )

            // ...
    }
};
Enter fullscreen mode Exit fullscreen mode

Summary

Today you’ve learned what is a callback and how to handle Nightwatch.js responses inside the callback itself. You also learned the challenging Nightwatch.js command queue and its unusual execution order of commands. If you read previous parts of these series, you should be able to create quite complex test scripts and run them locally testing or launching them at a scale in Loadero.

See you in the next and final part of “A beginner’s guide to test automation with Javascript(Nightwatch.js)” series that will contain some of the best practices used by our engineers to write better scripts, so don’t miss it. Hope this helps you. Thank you for reading, and let's test together!

Top comments (0)