Open jesusreal opened 8 years ago
:+1: would like to see this feature too.
:+1:
It looks like something along these lines got a burst of attention last year, but I can't find anything more recent. It seems pretty crucial.
How are people working around it in the mean time?
:+1:
👍
How are people working around it in the mean time?
If you do not assert anything on productToSelect
then you can put a sequence of operations into command. E.g:
module.exports = {
url: function() {
return 'https://' + this.api.globals.host + '/#/products/';
},
comands: [{
selectProduct: function(productName) {
return this.click(".product[data-product-name='" + productName + "']");
}
}]
};
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} `;
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.
@senocular this is a great example, thank you.
Any ideas how to make it work with sections? I'm really struggling with that.
module.exports = {
sections: {
firstSec: {
selector: '.product[data-product-name='%s']',
elements: {
testEl: '.something',
},
},
},
commands: [{
sec: (sectionName, data) => {
const section = this.sections[sectionName.slice(1)];
return util.format(section.selector, data);
},
}],
};
'my test': function (browser) {
var page = browser.page.myPage().section.firstSec(?).click('@testEl'); // not sure how to apply command to section
}
@drptbl my particular example wouldn't work for your case because what it does is transforms the '@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.
Would
el: function(sectionName, elementName, data) {
var section = this.sections[sectionName.slice(1)];
var element = section.elements[elementName.slice(1)];
return util.format(element.selector, data);
}
work?
@oubre the problem is altering section.selector
which is used as a path into what will ultimately help resolve section.elements[elementName].selector
. The element is what the click()
command is getting a reference to, and we can easily swap that out with something else like a non-element string selector (which is what el()
was being used for). But section selectors are handled in an internal recursive element query chain that precedes the ultimate command query. So before even the click()
selector is ran, the section selector is run providing a base element. Then from that base element the click()
selector is run using section result as its base.
@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
Hi there,
Am trying to implement your solution @senocular using Nightwatch 0.9.8 and getting undefined for the passed element. Example:
// page_objects/about.js
module.exports = {
url: function() {
const globals = this.api.globals
return globals.domain + globals.pages.about
},
elements: {
chiefImages: { selector: '.chief-positions div:nth-child(%d) .round-image' }
},
commands: [{
el: (elementName, child) => {
const element = this.elements[elementName.slice(1)]
return util.format(element.selector, child)
}
}]
}
Calling line:
// tests/aboutPages.js
...
const about = client.page.about()
about.navigate()
.assert.visible(about.el('@chiefImages', chiefNumber))
Error output:
TypeError: Cannot read property 'chiefImages' of undefined
I've tried changing the offending line, for example to be 'this.elements[elementName].selector.slice(1)'
But then strangely, I get the literal passed string in the output:
TypeError: Cannot read property '@chiefImages' of undefined
Thought this was the most appropriate place to ask, but happy to create an issue if this is something outside of me missing the obvious?
Thanks for your time
@GrayedFox your problem is that you're using an arrow function for el
which is breaking its context. Use a different function syntax instead.
@GrayedFox this
inside an arrow (lambda) function "does not bind its own this, arguments, super, or new.target". Try:
commands: [{
el: function(elementName, child) {
const element = this.elements[elementName.slice(1)]
return util.format(element.selector, child)
}
}]
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
@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');
Just a friendly FYI, missing shorthand selectors and CSS selectors in pageobjects is currently broken. If you're getting allElements.shift()
is not a function during test retries, it's probably because of a pageObject function which looks something like the ones found here, i.e.
ourPageObjectFunction(option) {
return this
.waitForElementVisible('@shortHandSelector', 'Where are you baby?')
.click('@shortHandSelector')
.waitForElementVisible(`.some .css [selector="${option}"]`)
.somethingElse()
}
Note, this is not about using dynamic selectors (as in the example above) - it's about a bug in the Nightwatch framework forgetting the selector strategy (CSS vs XPATH). Workaround (untested) might be to ensure your pageObject functions that mix shorthand and CSS selectors always pass the selector strategy as the first argument (and ensure to pass 3 args), i.e.
.getText('css', '.some .element', 'Custom message')
Is there any work planned on this? Could I help anyone if they've started work on something similar?
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);
@dhinus thanks for this great code - be aware: (at least I) couldn't get commands to work on the dynamic Section - so you need to create your commands on the parent and simply get the dynamic section there.
It works like a charm, thanks <3
Hi everyone, is there any news on this issue? I also tried out @dhinus solution but it appears not to be working on the dynamic section. Any status updates or tips are appreciated!
I want to know how to new a section on the Nightwatch 1.0.19,Can somebody tell me?
Any news on this issue? Having dynamic selectors in page objects would be so helpful!
working for v 0.9
module.exports = {
sections: {
search_result: {
selector: '[class=list]',
elements: {
record: {
selector: '//*[@class="record" and normalize-space()="{name}"]',
locateStrategy: 'xpath'
}
}
}
},
commands: [
{
isRecordPresent: function (name) {
let search_result = this.section.search_result
search_result.elements.record.selector = search_result.elements.record.selector.replace('{name}', name)
return search_result.waitForElementVisible('@record')
}
}
]
}
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.
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.