nightwatch: Page objects. Elements cannot have dynamic selectors.

I believe it would be great to have the possibility to have dynamic selectors in a page object, in the same way as we already have dynamic urls. I am sure this might be needed by many people, as some css selectors are set dynamically.

module.exports = {
    url:  function() {
        return 'https://' + this.api.globals.host + '/#/products/';
    },
    elements: {
         // What I can do currently
        // productToSelect: {
        //     selector: ".product[data-product-name='T-Shirt']"
        // }
         // What I would like to do
        // productToSelect: {
        //     selector: function() {
        //      return ".product[data-product-name='" + this.api.globals.productName + "']";
        // }
    }
};

About this issue

  • Original URL
  • State: open
  • Created 8 years ago
  • Reactions: 27
  • Comments: 27 (5 by maintainers)

Most upvoted comments

Similar to what @ilyapalkin mentioned, you can create a page command to perform the selector mutation. The command doesn’t have to call other commands; it would basically just parameterize the element selector and give you back what you need.

command:

module.exports = {
    url:  function() {
        return 'https://' + this.api.globals.host + '/#/products/';
    },
    elements: {
        product: ".product[data-product-name='%s']"
    },
    comands: [{
        el: function(elementName, data) {
            var element = this.elements[elementName.slice(1)];
            return util.format(element.selector, data);
        }
    }]
};

test:

'my test': function (browser) {
  var page = browser.page.myPage();
  page.click(page.el('@product', 'milk')); // .product[data-product-name='milk']
}

Might not be the exact right code, you get the idea.

#1464 looks promising. Given it’s not likely it will get merged soon, we ended up implementing the same thing as a helper function and calling it from a section command. Most of the work was done by @federico-pellegatta, I thought it might be useful to post it here.

Helper function:

const util = require('util');
const Section = require('nightwatch/lib/page-object/section');

const dynamicSection = (section, ...selector) => new Section(
  Object.assign({}, section, {
    selector: util.format(section.selector, selector),
    name: `${section.name}:${selector.join('-')}`
  })
);

Page object:

sections: {
  rowWithId: {
    selector: '.row[data-id="%s"]'
  }
}

commands: [{
  rowWithId(id) {
    return dynamicSection(sections.rowWithId, id);
  }
}]

Test:

const row5 = pageObject.rowWithId(5);

@drptbl, @oubre I thought I’d give this a shot, and this is what I came up with. I don’t know how robust it is, but it seems to work for this simple case:

var util = require('util');

module.exports = {

  sections: {

    firstSec: {

      commands: [
        {
          formatEl: function (modifier) {

            // create new, temporary selector
            var options = Object.create(this);
            options.sections = {};
            options.elements = {};
            var name;
            for (name in this.section)
              options.sections[name] = Object.create(this.section[name]);
            for (name in this.elements)
              options.elements[name] = Object.create(this.elements[name]);
            var Section = this.constructor;
            var sec = new Section(options);

            // make the selector change
            sec.selector =  util.format(this.props.selector, modifier); // uses props version
            return sec;
          }
        }
      ],

      props: {
        selector: '.product[data-product-name="%s"]' // format string for dynamic selector
      },

      selector: '.product', // default, unformatted selector

      elements: {

        testEl: {
          selector: '.something',
        },
      },
    },
  },
};

Usage:

var firstSec = browser.page.po().section.firstSec;
firstSec.click('@testEl');
// vs
firstSec.formatEl('milk').click('@testEl');

So here we have a section command, formatEl which is dynamically “changing” the selector for the section. And by “changing” I mean creating a new, temporary section based off of the original that has a different selector. That selector is based off of a string in the original section’s props object so the original selector of that original section is able to function on its own sans-formatEl() by being just a normal selector unaffected by any changes.

Edit: I guess “formatEl” isn’t the best name since we’re not working on an element, rather a section selector ;P

How are people working around it in the mean time?

I’ve been using the selector member associated with the elements, like this:

module.exports = {
  'Testing locators': function(browser) {
    let myPg = new browser.page.MyPage();
    let myElem = `${myPg.section.mySec.elements.myElem.selector}${i} `;

@drptbl @senocular Another solution to this would be (inside PageObject):

var productArray = ['Milk', 'TShirt', 'Apple'];
productArray.forEach(
    function(product){
        module.exports.sections.productSection.elements[product.toLowerCase() + 'Element'] =
        {
            selector: '.product[data-product-name="' + product +'"]'
        }
    }
);

And the test:

var productSection= browser.page.myPage().section.productSection;
productSection.click('@milkElement');

Thank you, also removed the unnecessary slice (which removed my class . shorthand selector!) - silly - thought it was necessary for formatting. Also using %d was a mistake, I think.

Finished function in case people keen:

    chiefImageVisible(child) {
      let element = this.elements.chiefImages.selector
      element = util.format(element, child)
      this.api
        .assert.visible(element, 2000, `Testing if founder image ${child} visible`)
      return this

Now, before test execution, I can generate a random number to represent the length of the returned node list to randomly select an element.

Future custom command will attempt to do some async stuff before test execution and actually get the length from the page (instead of hard coding) using Selenium’s elements() - which will make the test more resistant to simple page changes (like adding an image, etc). Thanks again

@GrayedFox your problem is that you’re using an arrow function for el which is breaking its context. Use a different function syntax instead.

@drptbl my particular example wouldn’t work for your case because what it does is transforms the ‘@<name>’ selector used in the final call (e.g. click()) into a string selector. The path leading up to that is obscured, handled internally.

What you would need to do is go through and parse the section selector before making the call, something like:

var page = browser.page.myPage();
page.customFunc('firstSec', 'milk');

Where customFunc would be some page command that went through and found a section with the name ‘firstSec’ and modified its selector for format in the second argument of ‘milk’. The problem with this approach is that after doing this, you’ve baked in the resulting value, replacing the format string. So you can only do it once until you reinstantiate another page object. Now, you could get around that by using the sections props to store the format string, but you’d still have to be careful of tracking the use of the selector since its value would persist past that initial call.

I’m not entirely sure if there’s a clean way to do this now. Even adding some kind of intermediary section command would still require changing the underlying section for a persistent change

browser.page.myPage().section.firstSec.customFunc('milk').click('@testEl');

… unless that method completely rebuilt a temporary section from scratch in the background, which could be possible I suppose. The path up to ‘@testEl’ doesn’t resolve until click() is called, and that would be from whatever customFunc() returned rather than the original section. Could be a little messy to implement I would guess.