nightwatchjs / nightwatch

Integrated end-to-end testing framework written in Node.js and using W3C Webdriver API. Developed at @browserstack
https://nightwatchjs.org
MIT License
11.79k stars 1.31k forks source link

Waiting for element text to equal a specific value #246

Closed dkoo761 closed 9 years ago

dkoo761 commented 10 years ago

I need to do something like this:

client.waitForText('.total-price', '$15.00')

This is a pattern I have used all over my tests in the past but doesn't seem to be available in Nightwatch.

The reason I can't just use waitForElementVisible() and then getText() is because the element is initially visible with a value of $0.00 (ie. empty shopping cart) and then the value changes to $15.00 when an AJAX request finishes processing that adds an item to the cart.

I can see a similar usefulness in AJAX applications for waiting on: form element values, attribute values, CSS class presence, etc.

In the past, I've used Geb which does a great job of this by allowing you to wrap any content lookup in a waitFor block: http://www.gebish.org/async

Any thoughts? How are other people handling this today?

beatfactor commented 10 years ago

There isn't a generic waitFor in nightwatch but it's relatively easy to build a custom command which will perform what you need.

Here's an example (not tested, you may need to adapt it slightly):

var util = require('util');
var events = require('events');

function WaitFor() {
  events.EventEmitter.call(this);
  this.startTime = null;
}

util.inherits(WaitFor, events.EventEmitter);

WaitFor.prototype.command = function (element, text, ms) {
  this.startTime = new Date().getTime();
  var self = this;
  var message;

  if (typeof arguments[0] !== 'number') {
    ms = 500;
  }

  this.check(text, function (element, result, loadedMs) {
    if (result) {
      message = 'Text was present after ' + (loadedMs - self.startTime) + ' ms.';
    } else {
      message = 'Text wasn\'t present in ' + ms + ' ms.';
    }
    self.client.assertion(result, 'contains text: ' + text , 'not found', message, true);
    self.emit('complete');
  }, ms);

  return this;
};

WaitFor.prototype.check = function (element, text, cb, maxTime) {
  var self = this;

  this.api.getText(element, text, function (result) {
    var now = new Date().getTime();
    if (result.status === 0 && result.value.indexOf(text) > -1) {
      cb(true, now);
    } else if (now - self.startTime < maxTime) {
      setTimeout(function () {
        self.check(cb, maxTime);
      }, 100);
    } else {
      cb(false);
    }
  });
};

module.exports = WaitFor;
davidlinse commented 10 years ago

I've been also working on this recently for a PR but its not polished/pushed yet.. So i think i can trash it now.. :bowtie:

dkoo761 commented 10 years ago

Thanks Andrei, that was very helpful!

There were a couple minor bugs, so I cleaned those up and also repurposed it as a generic waitForExpr() command:

var util = require('util');
var events = require('events');

function WaitForExpr() {
    events.EventEmitter.call(this);
    this.startTime = null;
}

util.inherits(WaitForExpr, events.EventEmitter);

WaitForExpr.prototype.command = function (element, getter, checker, ms) {
    this.startTime = new Date().getTime();
    var self = this;
    var message;

    if (typeof ms !== 'number') {
        ms = 500;
    }

    this.check(element, getter, checker, function (result, loadedMs) {
        if (result) {
            message = 'Expression was true after ' + (loadedMs - self.startTime) + ' ms.';
        } else {
            message = 'Expression wasn\'t true in ' + ms + ' ms.';
        }
        self.client.assertion(result, 'expression false', 'expression true', message, true);
        self.emit('complete');
    }, ms);

    return this;
};

WaitForExpr.prototype.check = function (element, getter, checker, cb, maxTime) {
    var self = this;

    getter(element, function (result) {
        var now = new Date().getTime();
        if (result.status === 0 && checker(result.value)) {
            cb(true, now);
        } else if (now - self.startTime < maxTime) {
            setTimeout(function () {
                self.check(element, getter, checker, cb, maxTime);
            }, 100);
        } else {
            cb(false);
        }
    });
};

module.exports = WaitForExpr;

It can be used like this:

client.waitForExpr(cssSelector, client.getText, function (text) {
    return text !== '';
}, 5000)

The above example will call getText() on the cssSelector and ensure that the resulting element's text is !== ''. This can easily be reused for element attributes, form element values, etc simply by changing the getter param to client.getXXXXXX and the checker param to contain an appropriate expression to evaluate.

dkoo761 commented 10 years ago

Actually, the generic waitForExpr() won't work the way I wrote it above since I forgot that some getters require extra parameters such as getAttribute. I could pass the extra params as an array, but it's probably better to have separate functions for each getter as that would provide a cleaner interface. So I have reverted it back to waitForText() only for the time being.

aaronbriel commented 9 years ago

@dkoo761 , I tried implementing the custom command you entered as follows, but it seems to always return true. I'm trying to pass in the LOCATORS_CSS.page_title element and verify that its text is equal to validation. Any input would be greatly appreciated!!

verifyPage : function(role) {
    var validation = "lksadjflkasdjf";//STRINGS.page_title;
    if (this.browser.waitForElementVisible(LOCATORS_CSS.page_title, data.TIMEOUT, false,
        function() {}, "Page title found.") == 'visible') {
        this.browser.waitForText(LOCATORS_CSS.page_title, this.browser.getText, function (text) {
            return text !== validation; }, data.TIMEOUT);
    }

} 

Another question - have you been able to figure out a waitForExpr() that allows for one to wait for a certain property of an element to be a specific value?

dkoo761 commented 9 years ago

@aaronbriel for page title, you need to use the getTitle() Nightwatch command as getText() won't work on page titles even if you pass it a css selector for the page title element. So we created a waitForTitle() custom command which is very similar to the waitForText() command. We also have a waitForAttribute() custom command if we need to wait for an HTML element's attribute to equal some expression.

I'll paste all 3 of our custom commands here for anyone who would like to use them. @beatfactor it would be great if this got into the Nightwatch project as I'm guessing many would benefit from it.

waitForText()

var util = require('util');
var events = require('events');
var TestConstants = require('../pageObjects/testConstants.js');

/*
 * This custom command allows us to locate an HTML element on the page and then wait until the value of the element's
 * inner text (the text between the opening and closing tags) matches the provided expression (aka. the 'checker' function).
 * It retries executing the checker function every 100ms until either it evaluates to true or it reaches
 * maxTimeInMilliseconds (which fails the test).
 * Nightwatch uses the Node.js EventEmitter pattern to handle asynchronous code so this command is also an EventEmitter.
 */

function WaitForText() {
    events.EventEmitter.call(this);
    this.startTimeInMilliseconds = null;
}

util.inherits(WaitForText, events.EventEmitter);

WaitForText.prototype.command = function (element, checker, timeoutInMilliseconds) {
    this.startTimeInMilliseconds = new Date().getTime();
    var self = this;
    var message;

    if (typeof timeoutInMilliseconds !== 'number') {
        timeoutInMilliseconds = this.api.globals.waitForConditionTimeout;
    }

    this.check(element, checker, function (result, loadedTimeInMilliseconds) {
        if (result) {
            message = 'waitForText: ' + element + '. Expression was true after ' + (loadedTimeInMilliseconds - self.startTimeInMilliseconds) + ' ms.';
        } else {
            message = 'waitForText: ' + element + '. Expression wasn\'t true in ' + timeoutInMilliseconds + ' ms.';
        }
        self.client.assertion(result, 'expression false', 'expression true', message, true);
        self.emit('complete');
    }, timeoutInMilliseconds);

    return this;
};

WaitForText.prototype.check = function (element, checker, callback, maxTimeInMilliseconds) {
    var self = this;

    this.api.getText(element, function (result) {
        var now = new Date().getTime();
        if (result.status === 0 && checker(result.value)) {
            callback(true, now);
        } else if (now - self.startTimeInMilliseconds < maxTimeInMilliseconds) {
            setTimeout(function () {
                self.check(element, checker, callback, maxTimeInMilliseconds);
            }, TestConstants.TIMEOUT_RETRY_INTERVAL);
        } else {
            callback(false);
        }
    });
};

module.exports = WaitForText;

Be sure to set a value for this.api.globals.waitForConditionTimeout. We do this in a before() function which gets called before each test run like this (where client is the browser object):

client.globals.waitForConditionTimeout = TestConstants.MAX_WAIT_TIMEOUT;

Also, notice the dependency on an external TestConstants file. You can remove this and replace the use of TestConstants.TIMEOUT_RETRY_INTERVAL and TestConstants.MAX_WAIT_TIMEOUT with local variables. They are just Number literals that store a millisecond value.

You can then call it like so:

client
    .waitForText(someCssLocator, function (text) {
        return text === someValue; // this can be any expression
    });

waitForTitle()

var util = require('util');
var events = require('events');
var TestConstants = require('../pageObjects/testConstants.js');

/*
 * This custom command allows us to wait until the value of the page title matches the provided expression 
 * (aka. the 'checker' function).
 * It retries executing the checker function every 100ms until either it evaluates to true or it reaches
 * maxTimeInMilliseconds (which fails the test).
 * Nightwatch uses the Node.js EventEmitter pattern to handle asynchronous code so this command is also an EventEmitter.
 */

function WaitForTitle() {
    events.EventEmitter.call(this);
    this.startTimeInMilliseconds = null;
}

util.inherits(WaitForTitle, events.EventEmitter);

WaitForTitle.prototype.command = function (checker, timeoutInMilliseconds) {
    this.startTimeInMilliseconds = new Date().getTime();
    var self = this;
    var message;

    if (typeof timeoutInMilliseconds !== 'number') {
        timeoutInMilliseconds = this.api.globals.waitForConditionTimeout;
    }

    this.check(checker, function (result, loadedTimeInMilliseconds) {
        if (result) {
            message = 'waitForTitle: Expression was true after ' + (loadedTimeInMilliseconds - self.startTimeInMilliseconds) + ' ms.';
        } else {
            message = 'waitForTitle: Expression wasn\'t true in ' + timeoutInMilliseconds + ' ms.';
        }
        self.client.assertion(result, 'expression false', 'expression true', message, true);
        self.emit('complete');
    }, timeoutInMilliseconds);

    return this;
};

WaitForTitle.prototype.check = function (checker, callback, maxTimeInMilliseconds) {
    var self = this;

    this.api.getTitle(function (title) {
        var now = new Date().getTime();
        if (checker(title)) {
            callback(true, now);
        } else if (now - self.startTimeInMilliseconds < maxTimeInMilliseconds) {
            setTimeout(function () {
                self.check(checker, callback, maxTimeInMilliseconds);
            }, TestConstants.TIMEOUT_RETRY_INTERVAL);
        } else {
            callback(false);
        }
    });
};

module.exports = WaitForTitle;

Used like this:

client
    .waitForTitle(function (text) {
        return text === title;
    });

waitForAttribute()

var util = require('util');
var events = require('events');
var TestConstants = require('../pageObjects/testConstants.js');

/*
 * This custom command allows us to locate an HTML element on the page and then wait until the value of a specified
 * attribute matches the provided expression (aka. the 'checker' function). It retries executing the checker function
 * every 100ms until either it evaluates to true or it reaches maxTimeInMilliseconds (which fails the test).
 * Nightwatch uses the Node.js EventEmitter pattern to handle asynchronous code so this command is also an EventEmitter.
 */

function WaitForAttribute() {
    events.EventEmitter.call(this);
    this.startTimeInMilliseconds = null;
}

util.inherits(WaitForAttribute, events.EventEmitter);

WaitForAttribute.prototype.command = function (element, attribute, checker, timeoutInMilliseconds) {
    this.startTimeInMilliseconds = new Date().getTime();
    var self = this;
    var message;

    if (typeof timeoutInMilliseconds !== 'number') {
        timeoutInMilliseconds = this.api.globals.waitForConditionTimeout;
    }

    this.check(element, attribute, checker, function (result, loadedTimeInMilliseconds) {
        if (result) {
            message = 'waitForAttribute: ' + element + '@' + attribute + '. Expression was true after ' + (loadedTimeInMilliseconds - self.startTimeInMilliseconds) + ' ms.';
        } else {
            message = 'waitForAttribute: ' + element + '@' + attribute + '. Expression wasn\'t true in ' + timeoutInMilliseconds + ' ms.';
        }
        self.client.assertion(result, 'expression false', 'expression true', message, true);
        self.emit('complete');
    }, timeoutInMilliseconds);

    return this;
};

WaitForAttribute.prototype.check = function (element, attribute, checker, callback, maxTimeInMilliseconds) {
    var self = this;

    this.api.getAttribute(element, attribute, function (result) {
        var now = new Date().getTime();
        if (result.status === 0 && checker(result.value)) {
            callback(true, now);
        } else if (now - self.startTimeInMilliseconds < maxTimeInMilliseconds) {
            setTimeout(function () {
                self.check(element, attribute, checker, callback, maxTimeInMilliseconds);
            }, TestConstants.TIMEOUT_RETRY_INTERVAL);
        } else {
            callback(false);
        }
    });
};

module.exports = WaitForAttribute;

Used like this:

client
    .waitForAttribute(someCssElementLocator, 'src', function (imageSrc) {
        return imageSrc === pathToSomeImage;
    });
aaronbriel commented 9 years ago

Excellent! Thank you!!

benkeen commented 9 years ago

This is invaluable, thanks @dkoo761!

benkeen commented 9 years ago

Has this been put in a PR yet? It'd be a great addition to the project.

dkoo761 commented 9 years ago

Thanks @benkeen I haven't created a PR for it yet as I didn't hear back from @beatfactor whether he was interested in adding it to the project. But you're welcome to do that on my behalf if you like :) And yes, any code I've offered above would be available under the MIT license, same as Nightwatch.

benkeen commented 9 years ago

Thanks @dkoo761! Much appreciated. :D

maxgalbu commented 9 years ago

@dkoo761 can I include those commands as custom commands in my repository? I'm creating and collecting some commands and assertion that could be useful, here it is: https://github.com/maxgalbu/nightwatch-custom-commands-assertions

dkoo761 commented 9 years ago

@maxgalbu Go for it.

maxgalbu commented 9 years ago

Thanks, included. I'm not sure whether to add a common WaitFor command or leave as it is. I'll decide when the WaitFor* functions increase in number

carlara75 commented 9 years ago

Thanks for this code, it helped me a lot with some AJAX related test I'm writing. Currently I'm evaluating some testing frameworks and so far I'm very impressed how many things you can test with nightwatch.js with a relative compact code. However, AJAX support seems to be not as straight forward, specially when JQuery is in the middle.

Using waitForText() I'm able to wait for my JQuery UI Autosuggest field to validate the available options when typing something in my input element, but I cannot find the way to navigate through them.

My AJAX action search for surnames and returns a JSON object like below if typing "do":

[{"value":"100", "label":"Doe, Jane"}, {"value":"200", "label":"Doe, John"}, {"value":"300", "label":"Doyle, Peter"}]

Now, my test code look like this:

    browser
        .setValue('input[id="surname"]', 'do')
        .waitForText('ul[class*="ui-autocomplete"]', function (text) {
            return text.indexOf("Doe, John") > -1;
        })
    ;

So far so good. However If I inspect text I see the following:

    Doe, Jane
    Doe, John
    Doyle, Peter

And, I'm expecting a list of

  • elements (I maybe be expecting the wrong thing). Furthermore, adding the line to my code:

        .getText('ul[class*="ui-autocomplete"]', function(r) {
                    console.dir(r);

    reveals:

    { status: 0,
      sessionId: '2705b4b6-5845-48f5-baf7-a845f2f7e9dc',
      state: 'success',
      value: 'Doe, Jane\nDoe, John\nDoyle, Peter\n',
      class: 'org.openqa.selenium.remote.Response',
      hCode: 338684 }

    Now, I'm very confused. Could some one gave my a tip about what could be wrong? I think this scenario will be valuable for anyone in the community that is looking to use nightwatch.js specially when JQuery is not exactly alien to web development.

  • carlara75 commented 9 years ago

    Sorry everybody, specially dkoo761. My intention was not to steal your post. I still think my question somehow is valid under this topic.

    After a little more playing, learning I wrote the following custom command to select one item from the suggestion field from JQuery UI-autocomplte. It is working for what I require:

    File Name:

    selectValueOnActiveAutocomplete.js

    My code(far from perfect):

    exports.command = function(targetLabel, callback) {
        var self = this;
        this
                .getText('ul[class*="ui-autocomplete"]', function(r) {
            //console.dir(r);
                var values = (r.value !== "") ? r.value.split("\n") : [];
                for (var i = 0; i < values.length; i++) {
                    this.keys(this.Keys.DOWN_ARROW);
                    var selected = (values[i] === targetLabel);
                    console.log(i + ":" + values[i] + (selected ? " - SELECTED" : ""));
                    if (selected) {
                        this.keys(this.Keys.ENTER);
                    break;
                    }
                }
                 });
    
        if (typeof callback === "function") {
                callback.call(self);
            }
            return this;
    }

    Usage example:

    "Testing JQuery-UI Autocomplete" : function (browser) {
        browser
            .setValue('input[id="_decisionMakerFields__ajaxSelectOrderMaker"]', testData.decisionMaker.searchBy)
            .pause(1000)
            .waitForText('ul[class*="ui-autocomplete"]', function (text) {
                    return text.indexOf('Doe, John') > -1;
            })
        .selectValueOnActiveAutocomplete('Doe, John')
        ;

    Hope this could help someone.

    dkoo761 commented 9 years ago

    @carlara75 waitForText() calls getText() internally which returns the visible text, not an HTML element. In your case, you probably want to use waitForElementPresent('ul[class*="ui-autocomplete"] li') to make sure the list has some elements and then iterate through the elements and call waitForText() on each li item like this:

    browser
        .waitForElementPresent('ul[class*="ui-autocomplete"] li')
        .elements('css selector', 'ul[class*="ui-autocomplete"] li', function (elements) {
            _.times(elements.value.length, function (index) {
                var elementCss = 'ul[class*="ui-autocomplete"] li:nth-child(' + (index + 1) + ')';
                browser.waitForText(elementCss, function (text) {
                    return text.indexOf('Doe, John') > -1;
                });
            })
        });

    If you're not using the underscore library, you can convert _.times() into an equivalent standard JS loop.

    Another approach would be to use execute() to execute some arbitrary JS in the browser which selects your list items using jQuery and returns them as an array to the callback which can loop through the array and call browser.waitForText() on each element. More details on execute(): http://nightwatchjs.org/api#execute

    carlara75 commented 9 years ago

    Hi dkoo761, thanks for your response and clarification. Also thanks for pointing me out to the underscore.js library. I didn't know this one, I will add it to my bag of tricks.

    I tried your code, and it didn't work because the returned condition value is used on the assertion of waitForText(), hence, if the item to select is not the first on the list it will fail. Below, is the "error":

    ?  waitForText: ul[class*="ui-autocomplete"] li:nth-child(1). Expression wasn't true in undefined ms.  - expected "expression true" but got: expression false
    
    FAILED:  1 assertions failed and 2 passed (2.561s)

    Now, I played with Nighwatch/Selenium API and I managed to get what I needed with an small issue that I'm not completely happy. Code below:

    browser
        .waitForElementPresent('ul[class*="ui-autocomplete"] li', 2000)
        .elements('css selector', 'ul[class*="ui-autocomplete"] li', function (elements) {
            for (var i = 0; i < elements.value.length; i++) {
                var elementCss = 'ul[class*="ui-autocomplete"] li:nth-child(' + (i + 1) + ')';
                browser.getText(elementCss, function(li) {
                    browser.keys(this.Keys.DOWN_ARROW);
                    var selected = (li.value === 'Doe, John');
                    //console.log(':' + li.value + (selected ? ' - SELECTED' : ''));
                    if (selected) {
                        browser.keys(browser.Keys.ENTER);
                    }
                });
            }
        });

    And with underscore.js:

    browser
        .waitForElementPresent('ul[class*="ui-autocomplete"] li', 2000)
        .elements('css selector', 'ul[class*="ui-autocomplete"] li', function (elements) {
            _(elements.value.length).times(function(index) {
                var elementCss = 'ul[class*="ui-autocomplete"] li:nth-child(' + (index + 1) + ')';
                browser.getText('css selector', elementCss, function(li) {
                    browser.keys(this.Keys.DOWN_ARROW);
                    var selected = (li.value === testData.decisionMaker.displayName);
                    //console.log(':' + li.value + (selected ? ' - SELECTED' : ''));
                    if (selected) {
                        browser.keys(browser.Keys.ENTER);
                    }
                });
            });
        });

    The problem: when an item is selected it keep iterating because I do not have any way to break the loop from the most internal callback. This is not a major issue when the list is small, but on larger list could be a performance issue. The second problem is when an item is selected, the

    sknopf commented 9 years ago

    @dkoo761 et al you might be interested in a feature just added in 0.6.5 where you can retry failed assertions. It's an assertion rather than command, but serves the same general theme of waiting before failing for AJAX-heavy pages.

    For example in the initial example: client.waitForText('.total-price', '$15.00')

    As an assertion this would be: client.assert.containsText('.total-price', '$15.00')

    So if you set the global retryAssertionTimeout to some non-zero timeout (ms), it will retry the assertion until it reaches the timeout and then fails.

    dkoo761 commented 9 years ago

    @sknopf Good to know, thanks.

    easternbloc commented 9 years ago

    @sknopf thanks for that it works a treat. You can also adjust it per test which is nice...

    beatfactor commented 9 years ago

    @easternbloc there isn't a built-in way to adjust this setting per test. Have you found some kind of workaround?

    easternbloc commented 9 years ago

    @beatfactor Mmm, I have in one of my test files:

    
    'some test case': function (client) {
      client.globals.retryAssertionTimeout = 10;
      client.assert.visible(...)
    },
    
    'some other test case': function (client) {
      client.globals.retryAssertionTimeout = 10000;
      client.assert.visible(...)
    }
    

    Is that not how it was intended to be used...?

    beatfactor commented 9 years ago

    That just happens to be working because the globals is passed to the api as shallow object copy. I haven't given this too much thought but it's not something that is completely finalized, so the behaviour might change in the future.

    The proper way to do this on a per test-suite basis would be to define a property @retryAssertionTimeout (similarly to @endSessionOnFail) which will take precedence over the global value but that will be added in a future version.

    easternbloc commented 9 years ago

    @beatfactor thanks for letting me know. I do think it would be nice to have a more granular control but maybe that's just me!

    Puneeth14 commented 9 years ago

    HI I am new to nightwatch. I want to iterate list of

    aakashchavan-qa commented 9 years ago

    @Puneeth14

    first you need to create custom command http://nightwatchjs.org/guide#extending for iterate some set of validation then call it in your main sample.js class, hope it will work.

    Puneeth14 commented 9 years ago

    thanks @Akash--

    Puneeth14 commented 9 years ago

    I need to log the html element in console, can any one help me do this I had checked all the docs, gives the information of logging the static texts.

    mkrouwer commented 9 years ago

    Hey is there any word on adding in a "waitForTextToEqual" command? it's a huge issue for me in nightwatch testing

    beatfactor commented 9 years ago

    There's a new assertion library coming in 0.7 that will support this, among other things.

    mkrouwer commented 9 years ago

    does the expect library provide the equivalent of a "waitForTextToEqual" method? In other words, will it actually wait for the .keys or .setInput method to finish typing in a given text before moving on to the next test?

    beatfactor commented 9 years ago

    No, but you might be able to use .value for your use-case.

    mkrouwer commented 9 years ago

    Didn't you say this would be included in the 0.7 release? This is a major blocker for testing.

    mkrouwer commented 28 days ago Hey is there any word on adding in a "waitForTextToEqual" command? it's a huge issue for me in nightwatch testing @beatfactor Owner beatfactor commented 27 days ago There's a new assertion library coming in 0.7 that will support this, among other things.

    sknopf commented 9 years ago

    @mkrouwer depending on your use case you have several options:

    http://nightwatchjs.org/api#expect-value http://nightwatchjs.org/api#expect-text

    For example: browser.expect.element('#main').text.to.equal('The Night Watch').before(100);

    Or use the old library with retryAssertionTimeout: http://nightwatchjs.org/api#assert-containsText http://nightwatchjs.org/api#assert-value

    himanshu-teotia commented 8 years ago

    where i can get this file ? var TestConstants = require('../pageObjects/testConstants.js');

    yannbertaud commented 7 years ago

    Would love to see waitForTextVisible and waitForTextPresent functions be part of nightwatch. Please consider adding this.

    rsshilli commented 7 years ago

    It's basically there, just not as obvious as the nice function names you had. You can do:

    browser.expect.element("body").text.to.contain('Success').before(60*1000);

    That will search for the text "Success" somewhere in the body for 60 seconds.

    proclaim commented 7 years ago

    here's another solution that you can just copy and paste into commands folder https://gist.github.com/proclaim/7a242f7d4d7aa7cb0ec352beb3a3429c

    zwbetz-gh commented 4 years ago

    It's basically there, just not as obvious as the nice function names you had. You can do:

    browser.expect.element("body").text.to.contain('Success').before(60*1000);

    That will search for the text "Success" somewhere in the body for 60 seconds.

    Works great. Thanks @rsshilli