Nightwatch: Async result not received after 0 seconds / callback executed immediately

Created on 21 Apr 2016  路  5Comments  路  Source: nightwatchjs/nightwatch

Hi hi hi,

I'm trying to get all my tests to wait for a given element to be not visible (i.e. the loading animation) in a dynamic way, since initial load time for this app is anywhere between 3 and 20 seconds.

To this end, I created a custom command which injects a timer onto the page, checks for the animation element every second, and eventually either times out (returns false after 20 seconds) or proceeds (returns true, if the element is no longer there).

The problem is that if executing normally (non-async) the script evaluates immediately, passing control back to the test suite and defeating the purpose of the command. If I execute with executeAsync I get a different problem: "asynchronous script timeout: result was not received in 0 seconds".

The command:

exports.command = function(callback) {
    'use strict';
    var self = this;
    var timeout = this.globals.init;

    this.executeAsync(function(element, timeout, done) {
        let timer = 0;
        let checkGap = setInterval(function() {
            let results = document.querySelectorAll(element);

            if (timer >= timeout) {
                console.log('TIMED OUT');
                done(false);
            }

            if (results.length === 0) {
                clearInterval(checkGap);
                console.log('Gap Initialized');
                done(true);
            } else {
                console.log('Waiting ' + timer + 'ms');
                timer += 1000;
            }
        }, 1000);
    }, ['.gap-loader__init-text', timeout], function(response) {
        if (typeof callback === 'function') {
            callback.call(self, response);
        }
    });

    // allows command to be chained
    return this;
};

The test:

            .maximizeWindow()
            .url(browser.globals.gapUiUrl)
            .initializeGapX(function(response) {
                if (response === true) {
                    console.dir(response);
                    browser
                        .clickElementX('dashboard .gap-sidebar__customize-btn')
                        .waitForElementVisible('dashboard .gap-modal__body', 'Field %s visible after %d ms')
                        .end();
                } else {
                    console.error('Gap FAILED to initialize in time!');
                    console.dir(response);
                    browser.end();
                }
            })

The problem when executing with execute is that I immediately get this in the console (almost as soon as the test starts):

{ state: 'success',
  sessionId: '23849866-7bc6-4842-970b-5271e502cb16',
  hCode: 1819798191,
  value: null,
  class: 'org.openqa.selenium.remote.Response',
  status: 0 }

If I change it to async, I get the error code mentioned above with a status of -1.

Not sure if it relates to 121 or 898.

Cheers

Most helpful comment

Ahh.... finally fixed this. After a full day of... ahem, learning ... it ended up being a single line of code that fixed it.

At the start of the function I added:

this.timeoutsAsyncScript(timeout);

And now it works as expected.

Finished, working custom command:

exports.command = function(callback) {
    'use strict';
    var self = this;
    var timeout = this.globals.init;

    this.timeoutsAsyncScript(timeout);

    this.executeAsync(function(element, timeout, done) {
        let timer = 0;
        let checkGapLoaded = setInterval(function () {
            if (timer >= timeout) {
                console.log('TIMED OUT');
                done(false);
                clearInterval(checkGapLoaded);
            }
            if (document.querySelectorAll(element).length === 0) {
                console.log('Gap Initialized');
                done(true);
                clearInterval(checkGapLoaded);
            } else {
                console.log('Waiting...');
                timer += 1000;
            }
        }, 1000);

    }, ['.gap-loader__init-text', timeout], function(result) {
        if (typeof callback === 'function') {
            callback.call(self, result);
        }
    });

    // allows command to be chained
    return this;
};

All 5 comments

PS I can confirm the script works as expected on the actual page, since by adding a 10 second pause to the passed callback I was able to inspect the page and confirm that the script does indeed poll each second, eventually finishing once the page has loaded and outputting the expected output to the console (executing non-aysnc).

Also... I just realized. I _should_ be getting a return of either true or false... not the result of the querySelectorAll() method. Strange.

I think the problem is essentially that the execute command always gets its passed result from the let results = document.querySelectorAll when, if using executeAsync, it should be passed the result given with the done() callback.

Ahh.... finally fixed this. After a full day of... ahem, learning ... it ended up being a single line of code that fixed it.

At the start of the function I added:

this.timeoutsAsyncScript(timeout);

And now it works as expected.

Finished, working custom command:

exports.command = function(callback) {
    'use strict';
    var self = this;
    var timeout = this.globals.init;

    this.timeoutsAsyncScript(timeout);

    this.executeAsync(function(element, timeout, done) {
        let timer = 0;
        let checkGapLoaded = setInterval(function () {
            if (timer >= timeout) {
                console.log('TIMED OUT');
                done(false);
                clearInterval(checkGapLoaded);
            }
            if (document.querySelectorAll(element).length === 0) {
                console.log('Gap Initialized');
                done(true);
                clearInterval(checkGapLoaded);
            } else {
                console.log('Waiting...');
                timer += 1000;
            }
        }, 1000);

    }, ['.gap-loader__init-text', timeout], function(result) {
        if (typeof callback === 'function') {
            callback.call(self, result);
        }
    });

    // allows command to be chained
    return this;
};

I don't think this is a problem with Nightwatch. execute result is whatever you return in the function which is executed.

You are right @beatfactor. However this is only provided the script is actually allowed the time it needs to get to the done() callback.

Otherwise, the result is from the last part of whatever script was executed on the page which actually returns something. I imagine if no part of the script executed returned anything I would of been seeing undefined.

Thus, if document.querySelectorAll(cssSelector) is the last thing from the executed script which actually returns something from the page, it will be the result of that call (in this case an array) which is returned.

Interesting behaviour to note :+1:

Was this page helpful?
0 / 5 - 0 ratings