Nightwatch: Page - chaining commands breaks page api

Created on 2 Mar 2016  路  8Comments  路  Source: nightwatchjs/nightwatch

Here's a sample page object that chains commands:

const myCommands = {
    someCommand: function() {
        return this.navigate('/some-page')
            .waitForElementPresent('@body', 10000)  // works
            .api.execute(function() { })
            .waitForElementPresent('@body', 10000) // does not work, it's looking for the selector @body, not resolving it to 'body'
    }
}

module.exports = {
    elements: {
        body: {
            selector: 'body'
        }
    },
    commands: [myCommands]
}

I'm pretty sure the issue is that api.execute does not return the proper wrapped object, and returns the standard client, for example I don't have to use api for subsequent calls:

            .api.execute(function() {})
            .execute(function() {}) // Works, I didn't have to use .api
bug stale

Most helpful comment

Another way we found to get around having to insert .api.thing and .page.thing was to add a custom pause method for the page object.

I've got usefulFunctions.js with...

module.exports = {
        pause: function (time) {
            this.api.pause(time);

            return this;
        }
    };

Then my page object looks like...

var usefulFunctions = require('./extras/usefulFunctions.js')

var loginFormChecks =
    {
        testErroneousLogin: function() {
            return this.click('@loginButton')
                .pause(2000)
                .assert.elementPresent('@errorMessage')
                .assert.elementNotPresent('@successMessage');
        },
        testCorrectLogin: function() {
            return this.setValue('@username', 'tomsmith')
                .setValue('@password', 'SuperSecretPassword!')
                .click('@loginButton')
                .pause(2000)
                .assert.elementPresent('@successMessage')
                .assert.elementNotPresent('@errorMessage');
        }
    };

module.exports = {
    url: 'http://the-internet.herokuapp.com/login',
    elements: {
        loginForm: '#login',
        username: '#username',
        password: '#password',
        loginButton: '#login > button',
        errorMessage: '#flash.flash.error',
        successMessage: '#flash.flash.success'
    },
    commands: [usefulFunctions, loginFormChecks]
}

All 8 comments

I also have the same problem and therefore I cannot properly chain commands when I am using page objects. When you do "api.XXX", the returned value is the api object, not the page object. So if you want to make this work, you have to split the commands. I would love to be able to chain something like the following:

myPageObj.acceptCookies()
    .waitForElementVisible(tabToSelect)
    .click(tabToSelect)
    .waitForElementVisible(productToSelect);   
    // Wait for transition to finish
    myPageObj.api.pause(2000);
    myPageObj.click(productToSelect);

Another problem is that you cannot use page object elements with client commands (using myPageObj.api.XXX). For example:

this.api.element('css selector', '@myPageObjElement', function(result) {});

Is it not possible to have client commands (like pause) directly accessible via page object, instead of api object inside page object? I.e., to have client commands in the same context as page objects, and not one level deeper?

I have a custom page object command which grabs and copies api methods into the page object and returns the page after called allowing this to work for me.

As far as out of the box, right now, page objects don't get the client commands added to them. I don't know if this was an oversight or on purpose, but they could be added easily enough.

There's a couple of issues in doing this. The big one is that by adding these new commands into the page interface means that you're increasing the possibility for command collisions. There's currently a check that logs an error if you attempt to add a command that already exists. For example, if you created a custom page command called pause today, thats perfectly fine. But by adding the client commands into the mix for pages, pause would then be a pre-existing command, and your previously-functioning page object would break with a collision error because of your custom pause function.

// currently:
module.exports = {
    elements: {},
    commands: [ {
        click: function () { ... } // collision error!
    }]
};

Note: The example above uses click (not pause) to show a collision error for pages today since, being a valid page command (pause is not), it will cause an error if you try to make a custom page command with that same name.

For this I think we can simply omit the collision check. Custom commands would simply overwrite the loaded command. This is fine because if someone actually needs the original command, they can go through the old, roundabout way using the api reference. They'd just need to remember the context changes at that point and you're in the realm of the browser/api object and not the page where all your page commands and loverly @-element references live.

// proposed:
browser.page.myPageObj()
    .click() // uses overriding custom page command
    .api.click() // uses original api command (careful about chaining from here)

Additionally, for this to work well, I think callbacks for page commands should be called in the context of the page. This way you can easily and conveniently work with the page without relying on a closure variable or binding the callback etc.

// proposed:
browser.page.myPageObj()
    .customPageCommand()
    .perform(function(){
        this.customPageCommand() // page context, so this works!
    })

Currently, callbacks are in the context of the browser/api object. If we were to _just_ update callback contexts to reference the page object instead of the api today, we'd have some breakages because people might be calling something like this.pause(1000) in callbacks now, and that's not a valid page command right now. However, including the client commands into the page interface would address this and allow that code to continue to function. Going with the full set of proposed changes, the only case that I know of that would break is if someone is calling an api command by a name shared with a custom page command. In the present-day case, that would call the api command, but with what I'm suggesting here, it would call into the page version of that command.

browser.page.myPageObj()
    .perform(function(){
        this.customCommand();
        // ^ Now: api.customCommand(), w/change: myPageObj.customCommand()
        // assuming there's both a customCommand custom command and a
        // customCommand page command
    })

I have changes for this locally and it _seems_ to all be working fine. Its about a 25 line change, so not that bad. I just ran the tests, and only one fails - the test that seems to test for the omission of some client commands like end(). Well, I guess that answers the question of whether or not the behavior was intended :stuck_out_tongue_winking_eye:

Edit: Speaking of collisions, I just realized a nasty one: url. This is a property used by Page objects as well as a selenium protocol command. Not really sure how that would be resolved. Edit2: I guess the natural solution there is to use the page url property and ignore the url call in favor of navigate(). Edit3: though I guess any internal commands depending on the url() api would suffer from the page url version being there... I think if this did happen, page.url would have to be renamed.

For anyone following this, I added a PR #1099 to play with. This gives page access to all commands to allow chaining to work without having to go back and forth between the page and api.

// before
browser.page.myPage()
  .click('@element')
  .api.pause(1000) // changes chain context to api
  .click('@element2') // FAIL: api.click doesn't support elements, only page.click

// now
browser.page.myPage()
  .click('@element')
  .pause(1000) // no api, pause in page
  .click('@element2') // OK: still page.click, no change to api

Note that element strings aren't supported in all commands - as @jesusreal mentioned earlier. They're pretty much limited to what they were before since there's a specific format the command needs to adhere to for it to work (basically the element commands wrapping the selenium variations which don't include their own using/locateStrategy). For example:

browser.page.myPage()
  .element('css selector', '@element', function () {...}); // FAIL: no @element support

Basically most of the previous comment in this issue is in place.

The two big collisions were:

  • Page.url (vs. url() selenium command)
  • Page.elements (vs. elements() selenium command)

I moved these to a settings object within the page object (and same for elements within a section object). This is a little awkward, but keeps them out of the way of the commands.

Otherwise, for the most part, things _seem_ to work. I haven't done a whole lot of testing yet, but it does make working with page objects a lot easier, and, given this approach, lessens the documentation impact of having to explain the limited command surface for pages.

Open concerns:

  • Where do url and elements for page/section objects really go? Should all internal properties be namespaced out like this to help prevent collisions with custom additions?
  • How far should @-element support go? (likely a difficult task to go further than what it is now)
  • Error conditions for name collisions? I have some logs in there for collisions now, but nothing is fatal if, for example, you create custom settings command which is what now houses url and elements.

So I found a workaround taking in account the following considerations:

  1. All api methods return the same api object.
  2. All page object methods return the same page object
  3. Page objects have a reference to the api object.
  4. Api object have a reference to the page objects.

So:

  .somePageObjectMethod()
  .someOtherPageObjectMethod()
  .api.someApiMethod()
  .someOtherApiMethod()
  .page.myPageObject()
  .anotherPageObjectMethod()

works.

Another way we found to get around having to insert .api.thing and .page.thing was to add a custom pause method for the page object.

I've got usefulFunctions.js with...

module.exports = {
        pause: function (time) {
            this.api.pause(time);

            return this;
        }
    };

Then my page object looks like...

var usefulFunctions = require('./extras/usefulFunctions.js')

var loginFormChecks =
    {
        testErroneousLogin: function() {
            return this.click('@loginButton')
                .pause(2000)
                .assert.elementPresent('@errorMessage')
                .assert.elementNotPresent('@successMessage');
        },
        testCorrectLogin: function() {
            return this.setValue('@username', 'tomsmith')
                .setValue('@password', 'SuperSecretPassword!')
                .click('@loginButton')
                .pause(2000)
                .assert.elementPresent('@successMessage')
                .assert.elementNotPresent('@errorMessage');
        }
    };

module.exports = {
    url: 'http://the-internet.herokuapp.com/login',
    elements: {
        loginForm: '#login',
        username: '#username',
        password: '#password',
        loginButton: '#login > button',
        errorMessage: '#flash.flash.error',
        successMessage: '#flash.flash.success'
    },
    commands: [usefulFunctions, loginFormChecks]
}

This issue has been automatically marked as stale because it has not had any recent activity.
If possible, please retry using the latest Nightwatch version and update the issue with any relevant details. If no further activity occurs, it will be closed. Thank you for your contribution.

Would maintainers be open to some of the solutions here making it into the docs?

This issue has been automatically marked as stale because it has not had any recent activity.
If possible, please retry using the latest Nightwatch version and update the issue with any relevant details. If no further activity occurs, it will be closed. Thank you for your contribution.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

lgaticaq picture lgaticaq  路  3Comments

bushev picture bushev  路  4Comments

antogyn picture antogyn  路  4Comments

davidlinse picture davidlinse  路  4Comments

Pieras2 picture Pieras2  路  3Comments