Nightwatch How to use local variable from one function to another - nightwatch.js

I want to use local variable pass to another function or compare with any variable. Example:
browser
.waitForElementVisible("//div[#class='col-xs-7 alignR uppercase']//strong", 5000, function () {
browser
.pause(500)
.getText("//div[#class='col-xs-7 alignR uppercase']//strong", function(result){
console.log('++++++++++++++',result.value);
let numb1 = result.value;
numb = numb1.match(/\d/g);
numb = numb1.join("");
console.log("value=", numb1);
return numb1;
})
})
.element('xpath', "(//div[#class='row']//div[#class='col-xs-7 alignR'])[1]", function (present) {
console.log(present);
if (present.status == 0) {
//arrange
browser
.pause(500)
.getText("(//div[#class='row']//div[#class='col-xs-7 alignR'])[1]", function (result) {
console.log("string", result.value);
let numb = result.value;
numb = numb.match(/\d/g);
numb = numb.join("");
console.log("value=", numb);
return numb;
})
}
})
I want to compare numb/numb1 or get the total both of them.

Well, if you want to keep your test structure as is (pretty suspicious!), then why don't you declare some test-file global variables?
let numb, numb1;
browser
.waitForElementVisible("//div[#class='col-xs-7 alignR uppercase']//strong", 5000, function () {
browser.pause(500)
.getText("//div[#class='col-xs-7 alignR uppercase']//strong", function(result) {
numb1 = result.value;
// > do more stuff here if necessary <
});
})
.element('xpath', "(//div[#class='row']//div[#class='col-xs-7 alignR'])[1]", function (present) {
console.log(present);
if (present.status == 0) {
browser.pause(500)
.getText("(//div[#class='row']//div[#class='col-xs-7 alignR'])[1]", function (result) {
numb = result.value;
console.log(`numb & numb1 values: ${numb} & ${numb1}`);
// > do more stuff with num & numb1 if necessary <
});
}
});
That being said... a finer approach would be to break-down your big steps & extract the logic into single-purpose functions, so you would either use:
page-object commands;
or, custom commands
Taking into account your current setup, let's consider adding the following custom command for extracting your num1:
❒ getNumb1.js (should reside in test/custom_commands/):
exports.command = function(callback) {
this.perform((done) => {
this.api.waitForElementVisible("//div[#class='col-xs-7 alignR uppercase']//strong", 5000, () => {
this.api.pause(500);
this.api.getText("//div[#class='col-xs-7 alignR uppercase']//strong", (result) => {
let numb1 = result.value;
// > do some other stuff here if necessary <
callback(numb1);
});
});
});
};
Similarly, add a second custom-command file (getNumb.js), which will return the value of numb. Then, use the two in your test file and do the data manipulation, or data checking (assertions) as you please:
let numb = browser.getNumb();
let numb1 = browser.getNumb1();
// > do something with numb & numb1 here <
❖ !Note: The examples given above are purely didactical! I don't advise
creating custom_commands containing test-specific logic. Only general-purpose and/or utility methods (e.g: closeGDPR, addCookie, getHttpCode, etc.) should reside in the custom_commands folder.
Thus, in the current setup, it would be best if you would add commands such as getNumb & getNumb1 inside the commands section of your page-object.

Related

Does Jasmine have an after-advice spy?

When spying on a method, we can either callThrough (use original implementation) or callFake (use a custom implementation).
What I want is a behaviour similar to callThrough but inspect/modify its return value before returning it to the caller.
So I can do something like this:
spyOn(foo, "fetch").and.afterCall(function(result) {
expect(result).toBeDefined();
result.bar = "baz";
return result;
});
Right now the simplest way is doing something like this:
var original = foo.fetch;
foo.fetch = function() {
var result = original.apply(this, arguments);
expect(result).toBeDefined();
result.bar = "baz";
return result;
}
Which is somewhat annoying because now I have to manually restore the spy instead of having the framework automatically does it for me.
Does Jasmine have an after-advice spy?
Generally: no.
You could extend the SpyStrategy object with such a function though:
this.callThroughAndModify = function(resultModifier) {
var result;
plan = function() {
result = originalFn.apply(this, arguments);
return resultModifier(result);
};
return getSpy();
};
You've to clone the above SpyStrategy file and insert that method.
Usage:
var obj = {
fn: function(a) { return a * 2; }
};
spyOn(obj, "fn").and.callThroughAndModify(function(result) {
console.log("Original result: ", result);
return 1;
});
expect(obj.fn(2)).toBe(1);
Drawbacks:
You've to replace the whole SpyStrategy.js
You've to load that script before Jasmine initializes the original SpyStrategy at boot

How to retry failures with $q.all

I have some code that saves data using Breeze and reports progress over multiple saves that is working reasonably well.
However, sometimes a save will timeout, and I'd like to retry it once automatically. (Currently the user is shown an error and has to retry manually)
I am struggling to find an appropriate way to do this, but I am confused by promises, so I'd appreciate some help.
Here is my code:
//I'm using Breeze, but because the save takes so long, I
//want to break the changes down into chunks and report progress
//as each chunk is saved....
var surveys = EntityQuery
.from('PropertySurveys')
.using(manager)
.executeLocally();
var promises = [];
var fails = [];
var so = new SaveOptions({ allowConcurrentSaves: false});
var count = 0;
//...so I iterate through the surveys, creating a promise for each survey...
for (var i = 0, len = surveys.length; i < len; i++) {
var query = EntityQuery.from('AnsweredQuestions')
.where('PropertySurveyID', '==', surveys[i].ID)
.expand('ActualAnswers');
var graph = manager.getEntityGraph(query)
var changes = graph.filter(function (entity) {
return !entity.entityAspect.entityState.isUnchanged();
});
if (changes.length > 0) {
promises.push(manager
.saveChanges(changes, so)
.then(function () {
//reporting progress
count++;
logger.info('Uploaded ' + count + ' of ' + promises.length);
},
function () {
//could I retry the fail here?
fails.push(changes);
}
));
}
}
//....then I use $q.all to execute the promises
return $q.all(promises).then(function () {
if (fails.length > 0) {
//could I retry the fails here?
saveFail();
}
else {
saveSuccess();
}
});
Edit
To clarify why I have been attempting this:
I have an http interceptor that sets a timeout on all http requests. When a request times out, the timeout is adjusted upwards, the user is displayed an error message, telling them they can retry with a longer wait if they wish.
Sending all the changes in one http request is looking like it could take several minutes, so I decided to break the changes down into several http requests, reporting progress as each request succeeds.
Now, some requests in the batch might timeout and some might not.
Then I had the bright idea that I would set a low timeout for the http request to start with and automatically increase it. But the batch is sent asynchronously with the same timeout setting and the time is adjusted for each failure. That is no good.
To solve this I wanted to move the timeout adjustment after the batch completes, then also retry all requests.
To be honest I'm not so sure an automatic timeout adjustment and retry is such a great idea in the first place. And even if it was, it would probably be better in a situation where http requests were made one after another - which I've also been looking at: https://stackoverflow.com/a/25730751/150342
Orchestrating retries downstream of $q.all() is possible but would be very messy indeed. It's far simpler to perform retries before aggregating the promises.
You could exploit closures and retry-counters but it's cleaner to build a catch chain :
function retry(fn, n) {
/*
* Description: perform an arbitrary asynchronous function,
* and, on error, retry up to n times.
* Returns: promise
*/
var p = fn(); // first try
for(var i=0; i<n; i++) {
p = p.catch(function(error) {
// possibly log error here to make it observable
return fn(); // retry
});
}
return p;
}
Now, amend your for loop :
use Function.prototype.bind() to define each save as a function with bound-in parameters.
pass that function to retry().
push the promise returned by retry().then(...) onto the promises array.
var query, graph, changes, saveFn;
for (var i = 0, len = surveys.length; i < len; i++) {
query = ...; // as before
graph = ...; // as before
changes = ...; // as before
if (changes.length > 0) {
saveFn = manager.saveChanges.bind(manager, changes, so); // this is what needs to be tried/retried
promises.push(retry(saveFn, 1).then(function() {
// as before
}, function () {
// as before
}));
}
}
return $q.all(promises)... // as before
EDIT
It's not clear why you might want to retry downsteam of $q.all(). If it's a matter of introducing some delay before retrying, the simplest way would be to do within the pattern above.
However, if retrying downstream of $q.all() is a firm requirement, here's a cleanish recursive solution that allows any number of retries, with minimal need for outer vars :
var surveys = //as before
var limit = 2;
function save(changes) {
return manager.saveChanges(changes, so).then(function () {
return true; // true signifies success
}, function (error) {
logger.error('Save Failed');
return changes; // retry (subject to limit)
});
}
function saveChanges(changes_array, tries) {
tries = tries || 0;
if(tries >= limit) {
throw new Error('After ' + tries + ' tries, ' + changes_array.length + ' changes objects were still unsaved.');
}
if(changes_array.length > 0) {
logger.info('Starting try number ' + (tries+1) + ' comprising ' + changes_array.length + ' changes objects');
return $q.all(changes_array.map(save)).then(function(results) {
var successes = results.filter(function() { return item === true; };
var failures = results.filter(function() { return item !== true; }
logger.info('Uploaded ' + successes.length + ' of ' + changes_array.length);
return saveChanges(failures), tries + 1); // recursive call.
});
} else {
return $q(); // return a resolved promise
}
}
//using reduce to populate an array of changes
//the second parameter passed to the reduce method is the initial value
//for memo - in this case an empty array
var changes_array = surveys.reduce(function (memo, survey) {
//memo is the return value from the previous call to the function
var query = EntityQuery.from('AnsweredQuestions')
.where('PropertySurveyID', '==', survey.ID)
.expand('ActualAnswers');
var graph = manager.getEntityGraph(query)
var changes = graph.filter(function (entity) {
return !entity.entityAspect.entityState.isUnchanged();
});
if (changes.length > 0) {
memo.push(changes)
}
return memo;
}, []);
return saveChanges(changes_array).then(saveSuccess, saveFail);
Progress reporting is slightly different here. With a little more thought it could be made more like in your own answer.
This is a very rough idea of how to solve it.
var promises = [];
var LIMIT = 3 // 3 tris per promise.
data.forEach(function(chunk) {
promises.push(tryOrFail({
data: chunk,
retries: 0
}));
});
function tryOrFail(data) {
if (data.tries === LIMIT) return $q.reject();
++data.tries;
return processChunk(data.chunk)
.catch(function() {
//Some error handling here
++data.tries;
return tryOrFail(data);
});
}
$q.all(promises) //...
Two useful answers here, but having worked through this I have concluded that immediate retries is not really going to work for me.
I want to wait for the first batch to complete, then if the failures are because of timeouts, increase the timeout allowance, before retrying failures.
So I took Juan Stiza's example and modified it to do what I want. i.e. retry failures with $q.all
My code now looks like this:
var surveys = //as before
var successes = 0;
var retries = 0;
var failedChanges = [];
//The saveChanges also keeps a track of retries, successes and fails
//it resolves first time through, and rejects second time
//it might be better written as two functions - a save and a retry
function saveChanges(data) {
if (data.retrying) {
retries++;
logger.info('Retrying ' + retries + ' of ' + failedChanges.length);
}
return manager
.saveChanges(data.changes, so)
.then(function () {
successes++;
logger.info('Uploaded ' + successes + ' of ' + promises.length);
},
function (error) {
if (!data.retrying) {
//store the changes and resolve the promise
//so that saveChanges can be called again after the call to $q.all
failedChanges.push(data.changes);
return; //resolved
}
logger.error('Retry Failed');
return $q.reject();
});
}
//using map instead of a for loop to call saveChanges
//and store the returned promises in an array
var promises = surveys.map(function (survey) {
var changes = //as before
return saveChanges({ changes: changes, retrying: false });
});
logger.info('Starting data upload');
return $q.all(promises).then(function () {
if (failedChanges.length > 0) {
var retries = failedChanges.map(function (data) {
return saveChanges({ changes: data, retrying: true });
});
return $q.all(retries).then(saveSuccess, saveFail);
}
else {
saveSuccess();
}
});

Trouble w/ Meteor Sorting

I'm trying to add a simple drop down control above a list such that I can sort it by "created" or "title".
The list template is called posts_list.html. In it's helper .js file I have:
posts: function () {
var sortCriteria = Session.get("sortCriteria") || {};
return Posts.find({},{sort: {sortCriteria: 1}});
}
Then, I have abstracted the list into another template. From here I have the following click event tracker in the helper.js
"click": function () {
// console.log(document.activeElement.id);
Session.set("sortCriteria", document.activeElement.id);
// Router.go('history');
Router.render('profile');
}
Here I can confirm that the right Sort criteria is written to the session. However, I can't make the page refresh. The collection on the visible page never re-sorts.
Frustrating. Any thoughts?
Thanks!
You can't use variables as keys in an object literal. Give this a try:
posts: function() {
var sortCriteria = Session.get('sortCriteria');
var options = {};
if (sortCriteria) {
options.sort = {};
options.sort[sortCriteria] = 1;
}
return Posts.find({}, options);
}
Also see the "Variables as keys" section of common mistakes.
thanks so much for that. Note I've left commented out code below to show what I pulled out. If I required a truly dynamic option, versus the simply binary below, I would have stuck w/ the "var options" approach. What I ended up going with was:
Template.postList.helpers({
posts: function () {
//var options = {};
if (Session.get("post-list-sort")) {
/*options.sort = {};
if (Session.get("post-list-sort") == "Asc") {
options.sort['created'] = 1;
} else {
options.sort['created'] = -1;
}*/
//return hunts.find({}, options);}
console.log(Session.get("hunt-list-sort"));
if (Session.get("hunt-list-sort") == "Asc") {
return Hunts.find({}, {sort: {title: 1}});
}
else {
return Hunts.find({}, {sort: {title: -1}});
};
}
}
});

CasperJS: Iterating through URL's

I'm pretty new to CasperJS, but isn't there a way to open a URL and execute CasperJS commands in for loops? For example, this code doesn't work as I expected it to:
casper.then(function() {
var counter = 2013;
for (i = counter; i < 2014; i++) {
var file_name = "./Draws/wimbledon_draw_" + counter + ".json";
// getting some local json files
var json = require(file_name);
var first_round = json["1"];
for (var key in first_round) {
var name = first_round[key].player_1.replace(/\s+/g, '-');
var normal_url = "http://www.atpworldtour.com/Tennis/Players/" + name;
// the casper command below only executes AFTER the for loop is done
casper.thenOpen(normal_url, function() {
this.echo(normal_url);
});
}
}
});
Instead of Casper is calling thenOpen on each new URL per iteration, it gets only called AFTER the for loop executes. Casper thenOpen then gets called with the last value normal_url is set to. Is there no Casper command to have it work each iteration within the for loop?
Follow up: How do we make casper thenOpen return a value on the current iteration of the for loop?
Say for example, I needed a return value on that thenOpen (maybe if the HTTP status is 404 I need to evaluate another URL so I want to return false). Is this possible to do?
Editing casper.thenOpen call above:
var status;
// thenOpen() only executes after the console.log statement directly below
casper.thenOpen(normal_url, function() {
status = this.status(false)['currentHTTPStatus'];
if (status == 200) {
return true;
} else {
return false;
}
});
console.log(status); // This prints UNDEFINED the same number of times as iterations.
If you need to get context then use the example here:
https://groups.google.com/forum/#!topic/casperjs/n_zXlxiPMtk
I used the IIFE (immediately-invoked-function-expression) option.
Eg:
for(var i in links) {
var link = links[i];
(function(index) {
var link = links[index]
var filename = link.replace(/#/, '');
filename = filename.replace(/\//g, '-') + '.png';
casper.echo('Attempting to capture: '+link);
casper.thenOpen(vars.domain + link).waitForSelector('.title h1', function () {
this.capture(filename);
});
})(i);
}
links could be an array of objects and therefore your index is a reference to a group of properties if need be...
var links = [{'page':'some-page.html', 'filename':'page-page.png'}, {...}]
As Fanch and Darren Cook stated, you could use an IIFE to fix the url value inside of the thenOpen step.
An alternative would be to use getCurrentUrl to check the url. So change the line
this.echo(normal_url);
to
this.echo(this.getCurrentUrl());
The problem is that normal_url references the last value that was set but not the current value because it is executed later. This does not happen with casper.thenOpen(normal_url, function(){...});, because the current reference is passed to the function. You just see the wrong url, but the correct url is actually opened.
Regarding your updated question:
All then* and wait* functions in the casperjs API are step functions. The function that you pass into them will be scheduled and executed later (triggered by casper.run()). You shouldn't use variables outside of steps. Just add further steps inside of the thenOpen call. They will be scheduled in the correct order. Also you cannot return anything from thenOpen.
var somethingDone = false;
var status;
casper.thenOpen(normal_url, function() {
status = this.status(false)['currentHTTPStatus'];
if (status != 200) {
this.thenOpen(alternativeURL, function(){
// do something
somethingDone = true;
});
}
});
casper.then(function(){
console.log("status: " + status);
if (somethingDone) {
// something has been done
somethingDone = false;
}
});
In this example this.thenOpen will be scheduled after casper.thenOpen and somethingDone will be true inside casper.then because it comes after it.
There are some things that you need to fix:
You don't use your counter i: you probably mean "./Draws/wimbledon_draw_" + i + ".json" not "./Draws/wimbledon_draw_" + counter + ".json"
You cannot require a JSON string. Interestingly, you can require a JSON file. I still would use fs.read to read the file and parse the JSON inside it (JSON.parse).
Regarding your question...
You didn't schedule any commands. Just add steps (then* or wait*) behind or inside of thenOpen.

Magento Enterprise Tabs - How to select specific tab in link?

I am trying to link to a specific tab in Magento Enterprise. It seems that all of the answers I've found don't apply well to their method. I just need a link to the page to also pull up a specific tab. This is the code they use:
Enterprise.Tabs = Class.create();
Object.extend(Enterprise.Tabs.prototype, {
initialize: function (container) {
this.container = $(container);
this.container.addClassName('tab-list');
this.tabs = this.container.select('dt.tab');
this.activeTab = this.tabs.first();
this.tabs.first().addClassName('first');
this.tabs.last().addClassName('last');
this.onTabClick = this.handleTabClick.bindAsEventListener(this);
for (var i = 0, l = this.tabs.length; i < l; i ++) {
this.tabs[i].observe('click', this.onTabClick);
}
this.select();
},
handleTabClick: function (evt) {
this.activeTab = Event.findElement(evt, 'dt');
this.select();
},
select: function () {
for (var i = 0, l = this.tabs.length; i < l; i ++) {
if (this.tabs[i] == this.activeTab) {
this.tabs[i].addClassName('active');
this.tabs[i].style.zIndex = this.tabs.length + 2;
/*this.tabs[i].next('dd').show();*/
new Effect.Appear (this.tabs[i].next('dd'), { duration:0.5 });
this.tabs[i].parentNode.style.height=this.tabs[i].next('dd').getHeight() + 15 + 'px';
} else {
this.tabs[i].removeClassName('active');
this.tabs[i].style.zIndex = this.tabs.length + 1 - i;
this.tabs[i].next('dd').hide();
}
}
}
});
Anyone have an idea?
I would consider modifying how the class starts up.
initialize: function (container) {
this.container = $(container);
this.container.addClassName('tab-list');
this.tabs = this.container.select('dt.tab');
// change starts here //
var hashTab = $(window.location.hash.slice(1));
this.activeTab = ( this.tabs.include(hashTab) ? hashTab : this.tabs.first());
// change ends here //
this.tabs.first().addClassName('first');
this.tabs.last().addClassName('last');
this.onTabClick = this.handleTabClick.bindAsEventListener(this);
for (var i = 0, l = this.tabs.length; i < l; i ++) {
this.tabs[i].observe('click', this.onTabClick);
}
this.select();
}
Here, I have only changed how the initial tab is chosen. It checks for an URL fragment which is commonly known as a hash, if that identifies one of the tabs it is preselected. As a bonus the browser will also scroll to that element if possible.
Then you only need to append the tab's ID to the URL. For example you might generate the URL by;
$productUrl = Mage::getUrl('catalog/product/view', array(
'id' => $productId,
'_fragment' => 'tab_id',
));
If you've recently migrated from an earlier Magento release, e.g. from Enterprise 1.11 to Enterprise 1.12, make sure the javascript in /template/catalog/product/view.phtml
right after the foreach that generates the tabs gets updated to the 1.12 version:
<script type="text/javascript">
var collateralTabs = new Enterprise.Tabs('collateral-tabs');
Event.observe(window, 'load', function() {
collateralTabs.select();
});
</script>
surfimp's VERY helpful suggestions did not produce the desired opening of the closed tab otherwise. Once this updated javascript was added, clicking on a link to read Review or Add Your Review on the product page, jumped to the Reviews tab, even if the tab had been hidden.
Similar to Zifius' answer, you can modify the initialize function to just take another argument which will be the active tab.
Event.observe(window, 'load', function() {
new Enterprise.Tabs('collateral-tabs', $('tab_review'));
});
and then in the scripts.js (or wherever this class may exist for you)
initialize: function (container, el) {
...
this.activeTab = el;
...
}
Use whatever logic in the template you like to set 'el' to the desired value.
The reason I did it this way is because when I used Zifius' method, the desired tab would be the active tab, but the default tab's content was still displayed.
Had the same task yesterday and as I don't know about prototype much I solved it by adding another method:
selectTab: function (element) {
this.activeTab = element;
this.select();
},
Usage:
var Tabs = new Enterprise.Tabs('collateral-tabs');
Tabs.selectTab($('tabId'));
Would like to know if it's a correct approach

Resources