Organise javascript webdriver promises code idiomatically - javascript

I am asserting on the contents of a particular row from an html table using webdriver via protractor. I have the following code which works correctly, but looks horrible. I want advice on how to better organise this code idiomatically with promises; in particular, I'd like to make it more obvious that there are 3 parts to this code:
Find the rows with a td containing the specified matchText on the page
Check that only one row matched, and handle the error cases with useful debug info
Check that the text content of the tds in this matched row is as expected
Is there a way I can organise this better to make this more readable, perhaps by chaining the promises or something?
browser.findElements(by.xpath("//td[text() = '" + matchText + "']/..")).then(function(trs) {
if (trs.length == 0) {
throw 'Unable to find td element equal to ' + matchText
} else if (trs.size > 1) {
protractor.promise.all(trs.map(function(tr){return tr.getInnerHtml()})).then(function(trsInnerHtml) {
throw 'Matched multiple td elements for ' + matchText + ':' + trsInnerHtml;
})
} else {
trs[0].findElements(by.tagName('td')).then(function(tds) {
protractor.promise.all(tds.map(function(td) {return td.getText()})).then(function(tdContent){
expect(tdContent).toEqual(expectedContent);
})
});
}
});

Yes, you can unwrap the promise callbacks from the non-throw case to a chained version:
browser.findElements(by.xpath("//td[text()='" + matchText + "']/..")).then(function(trs) {
if (trs.length == 0) {
throw 'Unable to find td element equal to ' + matchText
} else if (trs.size > 1) {
return protractor.promise.all(trs.map(function(tr) {
return tr.getInnerHtml()
})).then(function(trsInnerHtml) {
throw 'Matched multiple td elements for ' + matchText + ':' + trsInnerHtml;
});
} else {
return trs[0].findElements(by.tagName('td'));
}
}).then(function(tds) {
return protractor.promise.all(tds.map(function(td) {return td.getText()}));
}).then(function(tdContent){
expect(tdContent).toEqual(expectedContent);
});

How about using element, element.all() and letting expect() resolve the promises for us and get some help from jasmine-matchers package that introduces a handy toBeArrayOfSize() matcher:
element.all(by.xpath("//td[text() = '" + matchText + "']/..")).then(function(trs) {
expect(trs).toBeArrayOfSize(1);
expect(trs[0].element(by.tagName('td')).getText()).toEqual(expectedContent);
});

In the end I took chaining promises from #Bergi and the element api from #alexce (thanks both!) and came up with this:
it('Matches tds', function() {
browser.get('index.html');
var textToMatch = 'TDs';
expect(
element.all(trsContainingTd(textToMatch)).then(extractTds)
).toEqual(
[['Expected', 'TDs', 'content']]
);
});
function trsContainingTd(matchText) {
return by.xpath("//td[text() = '" + matchText + "']/..");
}
function extractTds(trs) {
return protractor.promise.all(trs.map(function(tr) {
return tr.all(by.tagName('td')).getText();
}));
}
This has a few advantages to my eye:
It's a single expectation
It will print out the helpful debug if there are more / fewer matching rows than expected
The trsContainingTd and extractTds functions are general enough to be used elsewhere

Related

Array gives errors after JSON function

I'm trying to check if the twitch stream is online or offline and if so change a background colour. If i check without the array and just put in the name it works, but with the array it doesn't (I don't have a lot of knowledge of JSON).
function test() {
var twitchChannels = ["imaqtpie", "summit1g", "tyler1", "greekgodx"];
for (var i = 0; i < twitchChannels.length; i++) {
console.log(i + " " + twitchChannels[i]);
$.getJSON('https://api.twitch.tv/kraken/streams/' + twitchChannels[i] + '?client_id=XXXX', function(channel) {
console.log(i + " " + twitchChannels[i]);
if (channel["stream"] == null) {
console.log("Offline: " + twitchChannels[i])
document.getElementById(twitchChannels[i]).style.backgroundColor = "red";
} else {
console.log("Online: " + twitchChannels[i])
document.getElementById(twitchChannels[i]).style.backgroundColor = "green";
}
});
}
}
Error: http://prntscr.com/i6qj51 inside the red part is what happens inside of json fuction
Your code is quite weak since you didn't manage the callback of every get you make.
Also you didn't check if:
document.getElementById(twitchChannels[i])
is null, since the exception clearly stated that you can't get :
.style.backgroundColor
from nothing.
Basic check VanillaJS:
if(!document.getElementById("s"))
console.log('id ' + twitchChannels[i] + ' not found in dom')
else
console.log('id ' + twitchChannels[i] + ' found in dom')
Also consider mixing JQuery with VanillaJS extremely bad practice; use proper JQuery method to access dom element by ID .
You should pass twitchChannel to the function because the var i is changing, this is an issue like others already mentioned: Preserving variables inside async calls called in a loop.
The problem is that you made some ajax call in a cicle, but the ajax calls are async.
Whey you get the first response, the cicle is already completed, and i==4, that is outside the twitchChannels size: that's why you get "4 undefined" on your console.
You can change your code in such way:
function test() {
var twitchChannels = ["imaqtpie", "summit1g", "tyler1", "greekgodx"];
for (var i = 0; i < twitchChannels.length; i++) {
executeAjaxCall(twitchChannels[i]);
}
}
function executeAjaxCall(twitchChannel){
$.getJSON('https://api.twitch.tv/kraken/streams/' + twitchChannel + '?client_id=XXXX', function(channel) {
console.log(twitchChannel);
if (channel["stream"] == null) {
console.log("Offline: " + twitchChannel)
$('#'+twitchChannel).css("background-color", "red");
} else {
console.log("Online: " + twitchChannel)
$('#'+twitchChannel).css("background-color", "green");
}
});
}
}
When console.log(i + " " + twitchChannels[i]); is called inside the callback function, the variable i has already been set to 4, and accessing the 4th element of array twitchChannels gives undefined since the array has only 4 elements.
This is because $.getJSON is a shorthand Ajax function which, as the name suggests, executes your requests asynchronously. So what actually happened is, based on the output you provided,
The loop is executed 4 times, and four Ajax requests have been sent.
The loop exits; i is already set to 4 now.
The ajax requests return; the callbacks are called, but the i value they see is now 4.
You can change the console.log inside your callback to something like console.log(i + " " + twitchChannels[i] + " (inside callback)"); to see this more clearly.
The correct result can be obtained by binding the current value of i to the closure.
function test() {
var twitchChannels = ["imaqtpie", "summit1g", "tyler1", "greekgodx"];
function make_callback(index) {
return function (channel) {
console.log(index + " " + twitchChannels[index]);
if (channel["stream"] == null) {
console.log("Offline: " + twitchChannels[index])
document.getElementById(twitchChannels[index]).style.backgroundColor = "red";
} else {
console.log("Online: " + twitchChannels[index])
document.getElementById(twitchChannels[index]).style.backgroundColor = "green";
}
}
}
for (var i = 0; i < twitchChannels.length; i++) {
console.log(i + " " + twitchChannels[i]);
$.getJSON('https://api.twitch.tv/kraken/streams/' + twitchChannels[i] + '?client_id=XXXX', make_callback(i));
}
}

Finding Sub Spans within Element XPath using Protractor Framework

I have the following lines of code:
.then(function click(services) {
var tryItElement;
if (services.length < 1) {
return;
}
buttonElement = element(by.xpath('//div[#class="my-class" and contains(., \'' + services[0].title + '\')]//span[contains(., \'Button Text\')]'));
return browser.wait(protractor.ExpectedConditions.elementToBeClickable(buttonElement), getWaitTime())
.then(function() {
return buttonElement.getWebElement();
})
.then(function(buttonElement) {
var protocol = url.parse(services[0].actionUrl).protocol;
if (protocol === null) {
throw new Error('expected ' + protocol + ' not to be null');
}
})
.then(function() {
return buttonElement.click();
})
.then(function() {
return browser.wait(helper.ExpectedConditions.responseCompleted(proxy, services[0].actionUrl), getWaitTime());
})
.then(function() {
return browser.get(browser.baseUrl);
})
.then(function() {
return click(services.slice(1));
})
})
.catch(fail)
.then(done);
I expect the first to find an element on my page with the class that contains some text as specified from services[0].title. It does and the following line finds a span within that element that contains the text Button Text.
I have this in a loop and for some reason, the buttonElement stays the same even though performing the action manually in the Chrome dev tools gives me the expected results.
Any other ways of trying this or suggestions? I can clarify some more if needed.

Issues with protractor test when finding element array using repeatable

I've created a protractor test for the following html:
<div class="well well-sm" data-ng-repeat="feedback in f.feedbackList">
Rating: {{feedback.rating}}
<blockquote class="small">{{feedback.comment}}</blockquote>
</div>
In the page.js file I have:
"use strict";
module.exports = (function () {
function AdminFeedbackPage() {
this.comments = element.all(by.repeater('feedback in f.feedbackList').column('feedback.comment')).map(function (comments) {
return comments.getText();
});
this.ratings = element.all(by.repeater('feedback in f.feedbackList').column('feedback.rating')).map(function (ratings) {
return ratings.getText();
});
}
return AdminFeedbackPage; })();
and then in the test in my step.js file:
var feedbackFound = false;
var feedbackIndex;
adminFeedbackPage.comments.then(function (commments) {
for (var i = 0; i < commments.length; i++) {
console.log("Comments " + i + ": " + commments[i]);
if (commments[i] == "TestApplicationFeedback") {
feedbackIndex = i;
console.log("FEEDBACK INDEX - " + feedbackIndex)
feedbackFound = true;
break;
}
}
}).then(function () {
expect(feedbackFound).to.be.true;
}).then(function() {
adminFeedbackPage.ratings.then(function (ratings) {
console.log(ratings);
console.log("RATINGS length " + ratings.length + " and rating is " + ratings[feedbackIndex]);
expect(ratings[feedbackIndex]).to.equal(3);
})
});
And I get the following logs:
Comments 0: Decent App
Comments 1: Really like it
Comments 2: TestApplicationFeedback
FEEDBACK INDEX - 2
[]
RATINGS length 0 and rating is undefined
AssertionError: expected undefined to equal 3
This is really confusing my since the comments are being found without any issue, but the ratings are just an empty array and as far as I can tell I have done the same thing for both.
Does anybody have any suggestions/reasons why the ratings aren't being found? I suspect it's something to do with what is in the page.js file, but I have no idea what could be wrong?
Thanks!
Solved this in the comments above, posting as an answer:
It was just a guess/suggestion based on the HTML, one was a child element and the other was directly inside the repeater (this one was failing to be captured). So my suggestion was to try using .evaluate() source, which acts as if on scope of the given element. So replacing .column() seems to work:
element.all(by.repeater('feedback in f.feedbackList')).evaluate('feedback.rating').then(function(val) {
console.log(val) // should return an array of values (feedback.rating)
})

How to work with the results of api.php using javascript?

I have searched around and can't seem to find what I'm looking for.
How can I use the results of api.php?action=query&list=allusers&augroup=sysop&aulimit=max&format=json in a javascript?
What I'm trying to do is create a script to simply change the color of usernames on the wiki if they are in certain groups, like sysop, bureaucrat, etc.
Although I'm usually pretty good at figuring these things out, I've been working on this all day and I've gotten nowhere with it. Can anyone help me out with maybe some examples or something? If it can be done with mostly jQuery that would be preferable.
Thanks in advance.
Edit: (in response to comment by ahren):
Well I started out trying to clean up and modify a script written by someone else to add more functionality/make it work as expected, but I had trouble making sense out of it:
/* HighlightUsers by Bobogoobo
* Changes color of links to specified groups and users
* TODO: redo but much better (recursive would be easier - I've learned a lot since I wrote this thing)
*/
function highlightUsers () {
"use strict";
var highlight = window.highlight || {}, selector = '', that, userstr,
indices = [],
i = 0,
user,
ns,
x,
y;
for (ns in mw.config.get('wgNamespaceIds')) {
if (i === 4) {
userstr = ns;
}
i++;
}
userstr = userstr.charAt(0).toUpperCase() + userstr.substring(1);
if (highlight['selectAll']) {
selector = 'a[href$=":';
} else {
selector = 'a[href="/wiki/' + userstr + ':';
}
for (y in highlight) {
indices.push(y);
}
for (x in highlight) {
that = highlight[x];
if (x === 'selectAll') {
continue;
} else if (x === 'users') {
for (user in that) {
$(selector + user.replace(/ /g, '_') + '"]').css({
'color': that[user],
'font-weight': 'bold'
}).attr('data-highlight-index',
$.inArray('users', indices));
}
} else {
(function (userColor, userGroup) { //JavaScript doesn't like to cooperate with me
$.getJSON('/api.php?action=query&list=allusers&augroup=' + userGroup +
'&aulimit=max&format=json', function (data) {
var stuff = data.query.allusers, //, select = '';
user;
for (user in stuff) {
//select += selector + stuff[user].name.replace(/ /g, '_') + '"], ';
$(selector + stuff[user].name.replace(/ /g, '_') + '"]').each(function () {
if (($(this).attr('data-highlight-index') || -1) < $.inArray(userGroup, indices)) {
$(this).attr('data-highlight-index', $.inArray(userGroup, indices));
$(this).css({
'color': userColor,
'font-weight': 'bold'
});
}
});
}
//select = select.substring(0, select.length - 2);
//$(select).css('color', userColor);
});
}(that, x));
}
}
}
That is my latest draft of it, I managed to accomplish a few things, like making the names bold, and correcting syntax mishaps, but I've decided I may be better off starting from scratch than trying to understand someone else's code.
i would prefer using jQuery AJAX functionality.
Usage is simple :
$.ajax({
url : 'api.php',
type : 'post',
datatype : 'json',
data : {
'list' : allusers,
'augroup' : 'sysop'
},
success : function(success_record) {
//here you can do Js dom related modifications like changing color etc.
// after php(server side) completes
}
});
I tried the AJAX solution described by Markrand, but unfortunately I wasn't able to get it to work. First off I was getting "allusers is not defined", so I wrapped it in quotes so that it wasn't treated as a var, then I had to add change 'api.php' to '/api.php' because it was becoming '/wiki/api.php' which doesn't exist, and adding the slash got it to use the base URL. It would then execute and return an object, however there was nothing useful in that object that I could use (such as an array of usernames), all it gave me was the API documentation... So I ended up doing this instead:
function highlightAdmins() {
$.getJSON('/api.php?action=query&list=allusers&augroup=sysop&aulimit=max&format=json',
function(data) {
for (var i = 0; i < data.query.allusers.length; i++) {
$('a[href$="User:' + data.query.allusers[i].name + '"]').css('color', '#FF6347');
}
});
}
This gave me an object containing the results of the query, in this case an array of sysop usernames (data.query.allusers[i].name) which I could iterate though and perform actions with.

JavaScript: SyntaxError: missing ) after argument list

I am getting the error:
SyntaxError: missing ) after argument list
With this javascript:
var nav = document.getElementsByClassName('nav-coll');
for (var i = 0; i < button.length; i++) {
nav[i].addEventListener('click',function(){
console.log('haha');
}
}, false);
};
What does this error mean?
You have an extra closing } in your function.
var nav = document.getElementsByClassName('nav-coll');
for (var i = 0; i < button.length; i++) {
nav[i].addEventListener('click',function(){
console.log('haha');
} // <== remove this brace
}, false);
};
You really should be using something like JSHint or JSLint to help find these things. These tools integrate with many editors and IDEs, or you can just paste a code fragment into the above web sites and ask for an analysis.
You got an extra } to many as seen below:
var nav = document.getElementsByClassName('nav-coll');
for (var i = 0; i < button.length; i++) {
nav[i].addEventListener('click',function(){
console.log('haha');
} // <-- REMOVE THIS :)
}, false);
};
A very good tool for those things is jsFiddle. I have created a fiddle with your invalid code and when clicking the TidyUp button it formats your code which makes it clearer if there are any possible mistakes with missing braces.
DEMO - Your code in a fiddle, have a play :)
just posting in case anyone else has the same error...
I was using 'await' outside of an 'async' function and for whatever reason that results in a 'missing ) after argument list' error.
The solution was to make the function asynchronous
function functionName(args) {}
becomes
async function functionName(args) {}
Similar to Josh McGee, I was trying to use await in the global scope, which, since it isn't an async function, will throw an error. See here for solutions.
This error maybe is the version of your Android (especially if you are using Web Views), try to change your code like an old version of the JavaScript Engine, like:
.then(function(canvas){
//canvas object can gained here
});
This error can also occur if you are missing a (tick) '
Failed:
if (!string.IsNullOrWhiteSpace(clientSideMethod))
{
return "function(e){" + clientSideMethod + " OnOptionChangedDxGrid(e," + hasConditionalFormatting.ToString().ToLower() + "','" + "false".ToString().ToLower() + "', '" + string.Empty + "'); }";
}
else
{
return "function(e){ OnOptionChangedDxGrid(e," + hasConditionalFormatting.ToString().ToLower() + "','" + "false".ToString().ToLower() + "', '" + string.Empty + "'); }";
}
Worked:
public static string GetOnOptionChangedMethodCall(string clientSideMethod, bool hasConditionalFormatting)
{
if (!string.IsNullOrWhiteSpace(clientSideMethod))
{
return "function(e){" + clientSideMethod + " OnOptionChangedDxGrid(e,'" + hasConditionalFormatting.ToString().ToLower() + "','" + "false".ToString().ToLower() + "', '" + string.Empty + "'); }";
}
else
{
return "function(e){ OnOptionChangedDxGrid(e,'" + hasConditionalFormatting.ToString().ToLower() + "','" + "false".ToString().ToLower() + "', '" + string.Empty + "'); }";
}
}
Notice there is a missing (tick) ` before the first double quote: (e," + hasConditionalFormatting.ToString()
I got the same error and I figured with the increased use of ES6 and string interpolation, that more and more people would start making the same mistake I did with Template Literals:
Initially, I logged a statement like so:
console.log(`Metadata: ${data}`);
But then changed it and forgot to remove the ${}:
console.log('Metadata: ' + JSON.stringify(${data}));

Categories

Resources