Nightwatch how can I make element selector visible within a command? - javascript

I am writing a test in Nightwatch to test pagination for a grid. I am using Page Objects and element selectors to make maintenance of the test suite easier. It seems, however, that I have run into a limitation with using element selectors within commands. The following code executes without error:
pagination() {
var lastPageNum;
var currentPageNum;
var newPageNum
return this
.getText('#pageNum1', function(result) {
currentPageNum = parseInt(result.value);
console.log('Current Page Number = ' + currentPageNum);
})
.getText('#pageNum2', function(result) {
lastPageNum = parseInt(result.value);
console.log('last Page Number = ' + lastPageNum);
if (lastPageNum >= 2) {
this.useXpath()
.waitForElementVisible('//*[#id="borderLayout_eRootPanel"]/div[2]/div/div/span[2]/button[3]', 3000)
.click('//*[#id="borderLayout_eRootPanel"]/div[2]/div/div/span[2]/button[3]')
.getText('//*[#id="borderLayout_eRootPanel"]/div[2]/div/div/span[2]/span[1]', function(result) {
newPageNum = parseInt(result.value);
console.log('New Page = ' + newPageNum);
this.assert.equal(newPageNum, currentPageNum + 1, "Assert pagination to Next page passed.");
})
} else {
console.log('Not enough rows exist in grid to test pagination, pages = ' + lastPageNum)
}
})
},
Note that I am using element selectors for the .getText commands. Here are the selectors I am using:
pageNum1: {
selector: '//*[#id="borderLayout_eRootPanel"]/div[2]/div/div/span[2]/span[1]',
locateStrategy: 'xpath'
},
pageNum2: {
selector: '//*[#id="borderLayout_eRootPanel"]/div[2]/div/div/span[2]/span[2]',
locateStrategy: 'xpath'
},
So far, so good. The issue I am running into is when I attempt to replace the xpath elements inside the .getText command with element selectors as well. Here is the code I am attempting to replace the above with:
pagination() {
var lastPageNum;
var currentPageNum;
var newPageNum
return this
.getText('#pageNum1', function(result) {
currentPageNum = parseInt(result.value);
console.log('Current Page Number = ' + currentPageNum);
})
.getText('#pageNum2', function(result) {
lastPageNum = parseInt(result.value);
console.log('last Page Number = ' + lastPageNum);
if (lastPageNum >= 2) {
this.useXpath()
.waitForElementVisible('#nextPageButton', 3000)
.click('#nextPageButton')
.getText('#pageNum1', function(result) {
newPageNum = parseInt(result.value);
console.log('New Page = ' + newPageNum);
this.assert.equal(newPageNum, currentPageNum + 1, "Assert pagination to Next page passed.");
})
} else {
console.log('Not enough rows exist in grid to test pagination, pages = ' + lastPageNum)
}
})
},
And here is the additional element selector I am using:
nextPageButton: {
selector: '//*[#id="borderLayout_eRootPanel"]/div[2]/div/div/span[2]/button[3]',
locateStrategy: 'xpath'
},
When I try to run the test after replacing the xpaths in the .getText command with element selectors, the test fails with the following error:
Timed out while waiting for element <#NextPageButton> to be present for 3000 milliseconds. - expected "visible" but got: "not found"
Is there a way to get the element selector to be visible within the .getText command function?

Since there does not appear to be a way to actually use the selectors within a command, I will post the workaround I have been using recently. It is fairly simple: in the page object, I am declaring a constant that contains the web element I want to use as a selector. This needs to be at the top of the page object before declaring the constants or variables you are using to hold the page object methods. The original snippets I posted above only contain the method, not the entire page object, but the declaration needs to happen well before that. Here is an example from the page object that contains the above method:
const nPageButton = '//*[#id="borderLayout_eRootPanel"]/div[2]/div/div/span[2]/button[3]';
const gridComands = {
//page object methods go here
}
Once you have declared the web element as a constant, you can use that constant when constructing your element selector:
nextPageButton: {
selector: nPageButton,
locateStrategy: 'xpath'
},
This will allow for easier maintenance, while sacrificing a bit of the functionality that selectors provide. You can use the constant within the page object when the selector does not work, you will just lose the utility of having the locate strategy defined, so you will need to rely on .useXpath() in the method when using xpaths for your web elements.

Related

Puppeteer - why doesn't element reflect DOM changes?

I have a function that waits for a selector under the scope of an element. The ElementHandle I'm passing in doesn't seem to be getting updated as the DOM updates- I log the outerHTML of this element each time my waitForFunction call fails, and changes aren't there.
async function waitForSubElSelector(page, el, subElSelector) {
// offset/rects checks for visible check (from https://stackoverflow.com/a/33456469)
await page.waitForFunction((selector, scopeEl) => {
const subEl = scopeEl.querySelector(selector)
let found = !!(subEl && (subEl.offsetWidth || subEl.offsetHeight || subEl.getClientRects().length));
if (!found) {
console.log(`waitForSubElSelector failed EL: ${scopeEl.outerHTML} SELECTOR: "${selector}", ${!subEl ? 'not found' : subEl.offsetWidth + ', ' + subEl.offsetHeight + ', ' + subEl.getClientRects().length}`)
}
return found;
}, {}, subElSelector, el
).catch(e => {
console.log(`waitForSubElSelector failed w/ selector '${subElSelector}'`)
throw e;
})
return await el.$(subElSelector);
}
I also verified this in the browser Console - I'm able to find the element and then the selector from there.
One use case is waiting for a Next button to be available, e.g.:
await waitForSubElSelector(this.browserPage, pageEl, '#nextButton:not([disabled])')
How can I get this working?
One idea I had— maybe my web framework replaces part of the DOM so the ElementHandle I have isn't the current one; is there some 'in-DOM' check in Puppeteer?

parsley.js extending error message text

By design I'm using one div above form to show errors. For this, case I also need some prefix, before each error message, typically it's text from corresponding label tag.
I'd implemented this as extension, just as the new function, which should be executed manually... but in this case I've lost default ui behaviuor.
So, the question is it possible to implement such behaviour as part of parsley (by extending parsley)?
You can customize where parsley puts the error with the errorsContainer. Could be a function that creates the container within your summary area.
That errors container could also have some sort of other information that you populate.
There is a need for a better error message system, see among others this issue.
I'd implemented desired behavior by extending two internal functions. Not sure it was good idea, but it does exactly what I needed in some particular cases:
(function ($) {
function getFieldLabel(parsleyField, suffix) {
var result = '',
labels = $('label[for=' + parsleyField.$element.prop('id') + ']');
suffix = suffix || '';
if (labels.length) {
result = labels[0].innerText + suffix;
}
return result;
}
window.ParsleyExtend = window.ParsleyExtend || {};
window.ParsleyExtend = $.extend(window.ParsleyExtend, {
// NB! this method replaces parsley's default `_addError`
_addError: function _addError(name, _ref5) {
var message = _ref5.message;
var assert = _ref5.assert;
this._insertErrorWrapper();
this._ui.$errorsWrapper.addClass('filled').append($(this.options.errorTemplate).addClass('parsley-' + name).html(
getFieldLabel(this, ' - ') + (message || this._getErrorMessage(assert))
));
},
// NB! this method replaces parsley's default `_updateError`
_updateError: function _updateError(name, _ref6) {
var message = _ref6.message;
var assert = _ref6.assert;
this._ui.$errorsWrapper.addClass('filled').find('.parsley-' + name).html(
getFieldLabel(this, ' - ') + (message || this._getErrorMessage(assert))
);
}
});
})(jQuery);

waitForElementsToBePresent in Protractor

So I found this little bit of code online that works pretty well for waiting for a unique identifier to be loaded on the page before you interact with it..
this.waitForElementToBePresent = function(element) {
browser.wait(function() {
return element.isPresent();
}, 60000);
};
I am new to JS and Protractor.. I was wondering how this code could be changed to wait for the presence of an element where there are multiples. I know you use $$ for the identifier when there are multiples, but how can I change this method to recognize that so I would then do something like..
utility.waitForElementsToBePresent(myElement).get(0);
Look at Alecxe's answer on this question. I've been using it for awhile and it works perfectly. Here's my slightly modified version:
// wait for X number of elements
presenceOfAll = function (elem, num, timeout) {
var time = timeout || 5000;
console.log('Waiting for elements ' + elem.locator() + ' to have a count of ' + num);
return browser.wait(function () {
return elem.count().then(function (count) {
return count >= num;
});
}, time, 'Failed waiting for ' + elem.locator() + ' to have ' + num + ' total items');
};
Rather than making a new function I would probably just last element in a group then wait for it.
var els = elements.all(by.css("#id"));
waitForElementToBePresent(els.last());
As something to remember ther is "isPresent" and "isDisplayed", present mean that an element exists on the page, visible or not. If you want to wait for it to actually show on the page, first wait for it to be present then wait for it to be displayed.
http://www.protractortest.org/#/api?view=ElementArrayFinder.prototype.last

Twitch TV JSON API Issue

So,I am trying to use the twitch API:
https://codepen.io/sterg/pen/yJmzrN
If you check my codepen page you'll see that each time I refresh the page the status order changes and I can't figure out why is this happening.
Here is my javascript:
$(document).ready(function(){
var ur="";
var tw=["freecodecamp","nightblue3","imaqtpie","bunnyfufuu","mushisgosu","tsm_dyrus","esl_sc2"];
var j=0;
for(var i=0;i<tw.length;i++){
ur="https://api.twitch.tv/kraken/streams/"+tw[i];
$.getJSON(ur,function(json) {
$(".tst").append(JSON.stringify(json));
$(".name").append("<li> "+tw[j]+"<p>"+""+"</p></li>");
if(json.stream==null){
$(".stat").append("<li>"+"Offline"+"</li>");
}
else{
$(".stat").append("<li>"+json.stream.game+"</li>");
}
j++;
})
}
});
$.getJSON() works asynchronously. The JSON won't be returned until the results come back. The API can return in different orders than the requests were made, so you have to handle this.
One way to do this is use the promise API, along with $.when() to bundle up all requests as one big promise, which will succeed or fail as one whole block. This also ensures that the response data is returned to your code in the expected order.
Try this:
var channelIds = ['freecodecamp', 'nightblue3', 'imaqtpie', 'bunnyfufuu', 'mushisgosu', 'tsm_dyrus', 'esl_sc2'];
$(function () {
$.when.apply(
$,
$.map(channelIds, function (channelId) {
return $.getJSON(
'https://api.twitch.tv/kraken/streams/' + encodeURIComponent(channelId)
).then(function (res) {
return {
channelId: channelId,
stream: res.stream
}
});
})
).then(function () {
console.log(arguments);
var $playersBody = $('table.players tbody');
$.each(arguments, function (index, data) {
$playersBody.append(
$('<tr>').append([
$('<td>'),
$('<td>').append(
$('<a>')
.text(data.channelId)
.attr('href', 'https://www.twitch.tv/' + encodeURIComponent(data.channelId))
),
$('<td>').text(data.stream ? data.stream.game : 'Offline')
])
)
})
})
});
https://codepen.io/anon/pen/KrOxwo
Here, I'm using $.when.apply() to use $.when with an array, rather than list of parameters. Next, I'm using $.map() to convert the array of channel IDs into an array of promises for each ID. After that, I have a simple helper function with handles the normal response (res), pulls out the relevant stream data, while attaching the channelId for use later on. (Without this, we would have to go back to the original array to get the ID. You can do this, but in my opinion, that isn't the best practice. I'd much prefer to keep the data with the response so that later refactoring is less likely to break something. This is a matter of preference.)
Next, I have a .then() handler which takes all of the data and loops through them. This data is returned as arguments to the function, so I simply use $.each() to iterate over each argument rather than having to name them out.
I made some changes in how I'm handling the HTML as well. You'll note that I'm using $.text() and $.attr() to set the dynamic values. This ensures that your HTML is valid (as you're not really using HTML for the dynamic bit at all). Otherwise, someone might have the username of <script src="somethingEvil.js"></script> and it'd run on your page. This avoids that problem entirely.
It looks like you're appending the "Display Name" in the same order every time you refresh, by using the j counter variable.
However, you're appending the "Status" as each request returns. Since these HTTP requests are asynchronous, the order in which they are appended to the document will vary each time you reload the page.
If you want the statuses to remain in the same order (matching the order of the Display Names), you'll need to store the response data from each API call as they return, and order it yourself before appending it to the body.
At first, I changed the last else condition (the one that prints out the streamed game) as $(".stat").append("<li>"+jtw[j]+": "+json.stream.game+"</li>"); - it was identical in meaning to what you tried to achieve, yet produced the same error.
There's a discrepancy in the list you've created and the data you receive. They are not directly associated.
It is a preferred way to use $(".stat").append("<li>"+json.stream._links.self+": "+json.stream.game+"</li>");, you may even get the name of the user with regex or substr in the worst case.
As long as you don't run separate loops for uploading the columns "DisplayName" and "Status", you might even be able to separate them, in case you do not desire to write them into the same line, as my example does.
Whatever way you're choosing, in the end, the problem is that the "Status" column's order of uploading is not identical to the one you're doing in "Status Name".
This code will not preserve the order, but will preserve which array entry is being processed
$(document).ready(function() {
var ur = "";
var tw = ["freecodecamp", "nightblue3", "imaqtpie", "bunnyfufuu", "mushisgosu", "tsm_dyrus", "esl_sc2"];
for (var i = 0; i < tw.length; i++) {
ur = "https://api.twitch.tv/kraken/streams/" + tw[i];
(function(j) {
$.getJSON(ur, function(json) {
$(".tst").append(JSON.stringify(json));
$(".name").append("<li> " + tw[j] + "<p>" + "" + "</p></li>");
if (json.stream == null) {
$(".stat").append("<li>" + "Offline" + "</li>");
} else {
$(".stat").append("<li>" + json.stream.game + "</li>");
}
})
}(i));
}
});
This code will preserve the order fully - the layout needs tweaking though
$(document).ready(function() {
var ur = "";
var tw = ["freecodecamp", "nightblue3", "imaqtpie", "bunnyfufuu", "mushisgosu", "tsm_dyrus", "esl_sc2"];
for (var i = 0; i < tw.length; i++) {
ur = "https://api.twitch.tv/kraken/streams/" + tw[i];
(function(j) {
var name = $(".name").append("<li> " + tw[j] + "<p>" + "" + "</p></li>");
var stat = $(".stat").append("<li></li>")[0].lastElementChild;
console.log(stat);
$.getJSON(ur, function(json) {
$(".tst").append(JSON.stringify(json));
if (json.stream == null) {
$(stat).text("Offline");
} else {
$(stat).text(json.stream.game);
}
}).then(function(e) {
console.log(e);
}, function(e) {
console.error(e);
});
}(i));
}
});

Populating JSON In $window.onload AngularJS

I have the following code I am using to populate a JSON in one of my controllers:
$rootScope.commands.forEach(function(element, index, array){
$rootScope.voiceEvents[element] = function() {
$scope.$broadcast(element, element + ' from VoiceControlCtrl');
}
});
alert(JSON.stringify($rootScope.voiceEvents));
The problem with the above is that it breaks the app periodically since ,from time to time, $rootScope.commands is undefined, depending on (I assume) how fast some other controllers or views are loaded.
I have been attempting to implement the following fix:
$window.onload = function(commands){
//alert($rootScope.commands);
$rootScope.commands.forEach(function(element, index, array){
$rootScope.voiceEvents[element] = function() {
$scope.$broadcast(element, element + ' from VoiceControlCtrl');
}
});
alert(JSON.stringify($rootScope.voiceEvents));
};
The second version works, except for the fact that $rootScope.voiceEvents comes out empty. Why is that? What can I do to fix it?

Categories