I'm new to Javascript, Node.js, and Nightmare. I'm trying to add a feature to count the number of titles on a page and then wait until the total number of titles increases by 5.
Here is the snippet:
.evaluate(function () {
var elements = Array.from(document.getElementsByClassName('title'));
var firstCount = elements.length;
var higherCount = firstCount + 5;
var selector = '\'#price' + higherCount + '\'';
return selector;
})
.wait(selector)
When I run I get the error:
.wait(selector)
^ ReferenceError: selector is not defined at Object.
How do I get the variable selector to the .wait command?
According to the docs, the function evaluate passes the return value from the function on to the caller.
http://phantomjs.org/api/webpage/method/evaluate.html
so in your example, the call would be
var selector = page.evaluate(function () {
var elements = Array.from(document.getElementsByClassName('title'));
var firstCount = elements.length;
var higherCount = firstCount + 5;
var selector = '\'#price' + higherCount + '\'';
return selector;
});
If for some reason the contents of the evaluate call are asyncronous, you could return a promise;
page.evaluate(function () {
return new Promise( function(success, error) {
// whatever is the async thing
success(value);
// or if there is an error
error("details of error");
}
}).then( function (results) {
// we have our results
}).catch( function(err) {
// ... oops
});
This might then be compatible with async / await, depending on the context.
You can add something to your Selector html and target it with wait() assuming your Selector is an html element. Something like this.
.evaluate(function () {
var selector = document.getElementById("demo");
selector.className += " waitForThis";
})
.wait(".waitForThis")
I don't think you can use evaluate's return value for wait() though.
Related
I'm trying to dynamically add elements to a Table, kind of like a ledger. I have a few HTML buttons. The buttons are loaded to the DOM dynamically from a MongoDB collection. I am using NodeJS, using PUG/Jade as the view engine, and I'm putting the _id values from the DB as the id values of each button. This way I can query the DB for other properties with an onclick function.
Each time I click a button, I run a fetch call to my backend get the price property stored in the MongoDB:
function getPriceFromDatabase() {
var target = event.target;
var id = target.id;
var url = '/menus/' + id;
fetch(url, {
method: 'GET'
}).then(function(res){
if(!res.ok) console.log("Error");
else return res.json().then(function(result){
var returnedItem = JSON.parse(result)
console.log("Item Price: " + returnedItem[0].itemPrice);
return returnedItem[0].itemPrice;
}).catch(function(err){
console.log(err);
});
});
}
This works fine.
My problem is when I try to call this function to add new rows to a table.
function createTableElement () {
var newTableRow = document.createElement("tr");
var target = event.target;
var nameCell = document.createElement("td");
var nameText = document.createTextNode(target.textContent);
nameCell.appendChild(nameText);
newTableRow.appendChild(nameCell);
var priceOfItem = getPriceFromDatabase()
var priceCell = document.createElement("td");
var priceText = document.createTextNode(priceOfItem);
priceCell.appendChild(priceText);
newTableRow.appendChild(priceCell);
ledgerTable.appendChild(newTableRow);
}
The function runs without waiting for the result of getPriceFromDatabase()
This means that I get a table where the first column correctly states the name of the button which was clicked, but the price column is undefined. It simply takes some time for the database to be queried and return a value, and by the time it happens the function which calls the query has already completed.
What are my options? I'm still a novice to using promises and asynchronous functions. I understand that fetch returns a promise as a response object, which is why I can use .then()
I have tried changing my DOM-table function to something like:
getPriceFromDatabase().then( [construct the rest of the table elements] )
This is invalid since .then() is not a property of that function, since it doesn't return a promise. What can I do?
I have tried doing
async getPriceFromDatabase() {}, but it still gives me an error that I can't use .then()
How can I solve this? Do I have to use new Promise ()? I would appreciate if someone could point me into the right direction.
getPriceFromDatabase() is an asynchronous function. You need to return the promise from it and use .then() on that returned promise:
function getPriceFromDatabase() {
var target = event.target;
var id = target.id;
var url = '/menus/' + id;
return fetch(url, {
method: 'GET'
}).then(function(res){
if(!res.ok) console.log("Error");
else return res.json().then(function(result){
var returnedItem = JSON.parse(result)
console.log("Item Price: " + returnedItem[0].itemPrice);
return returnedItem[0].itemPrice;
}).catch(function(err){
console.log(err);
throw err;
});
});
}
function createTableElement () {
var newTableRow = document.createElement("tr");
var target = event.target;
var nameCell = document.createElement("td");
var nameText = document.createTextNode(target.textContent);
nameCell.appendChild(nameText);
newTableRow.appendChild(nameCell);
getPriceFromDatabase().then(function(priceOfItem) {
var priceCell = document.createElement("td");
var priceText = document.createTextNode(priceOfItem);
priceCell.appendChild(priceText);
newTableRow.appendChild(priceCell);
ledgerTable.appendChild(newTableRow);
});
}
Summary of changes:
fetch(url) was changed to return fetch(url), thus returning the promise.
var priceOfItem = getPriceFromDatabase() is changed to getPriceFromDatabase.then(...).
Also, you really should be passing the event into both functions rather than relying on a global.
I have tried changing my DOM-table function to something like: getPriceFromDatabase().then( [construct the rest of the table elements] ).
That didn't work for you because you did not return the promise from getPriceFromDatabase() so therefore there was no .then() handler on the return value.
I tried async getPriceFromDatabase() {}, but it still gives me an error that I can't use .then()
Same issue. You still have to return the promise.
function (type) getMember {
var that = this;
var url = "/XXXXX";
return axios.post(url)
}
axios.all().then(axios.spread(function () {
console.log('init finished')
}));
now I have to function GetMember of different type,so I choose
axios.all(),I hope it work,yeah, it can work.
axios.all([getMember(0),getMember(1),getMember(2)]).then(axios.spread(
function () {
console.log('init finished');
console.log(arguments.length)//3
}));
but I think it`s not graceful for coding. I want to write a circulation ,and push arguments into "all(" ")",like this,I try 'eval(str)',it can work and run the function that I want to run,but arguments.length only be one, I can get all the data from all requests.
I found the following method. It is not the best, but it does work:
var requestFun = '[';
for (var i = 0; i < operType.length; i++) {
requestFun += 'this.getMemberInfo(operType[' + i + ']),';
}
requestFun += ']'
axios.all(eval(requestFun)).then(axios.spread(function () {
})
I am trying to write a function which takes a list of Rich Text Content Controls and a single string as argument, and which replaces the content of all matching content controls with this string.
While this works with a smaller amount of content controls, it fails with documents with a huge amount of them. I have to work with documents with over 700 Content Controls with individual titles. In this case, the code just replaces the first 66X CCs and then aborts with a GeneralException. I assume this is just due to the huge amount of content controls. I am having similar problems, when I try to register bindings for all these CCs (GeneralException). But this is a different topic.
I tried to work around this problem, by limiting the amounts of changes per .sync() and looping through the CCs, performing as many loops as necessary. However, this is not that easy, due to the asynchronous nature of office-js. I am not very familiar with javascript-async-promise-programming so far. But this is what I have come up with:
function replaceCCtextWithSingleString (CCtitleList, string) {
var maxPerBatch = 100;
/*
* A first .then() block is executed to get proxy objects for all selected CCs
*
* Then we would replace all the text-contents in one single .then() block. BUT:
* Word throws a GeneralException if you try to replace the text in more then 6XX CCs in one .then() block.
* In consequence we only process maxPerBatch CCs per .then() block
*/
Word.run(function (context) {
var CCcList = [];
// load CCs
for(var i = 0; i < CCtitleList.length; i++) {
CCcList.push(context.document.contentControls.getByTitle(CCtitleList[i]).load('id'));
}
return context.sync().then(function () { // synchronous
var CClist = [];
// aggregate list of CCs
for(var i = 0; i < CCcList.length; i++) {
if(CCcList[i].items.length == 0) {
throw 'Could not find CC with title "'+CCtitleList[j]+'"';
}
else {
CClist = CClist.concat(CCcList[i].items);
}
}
$('#status').html('Found '+CClist.length+' CCs matching the criteria. Started replacing...');
console.log('Found '+CClist.length+' CCs matching the criteria. Started replacing...');
// start replacing
return context.sync().then((function loop (replaceCounter, CClist) {
// asynchronous recoursive loop
for(var i = 0; replaceCounter < CClist.length && i < maxPerBatch; i++) { // loop in loop (i does only appear in condition)
// do this maxPerBatch times and then .sync() as long as there are still unreplaced CCs
CClist[replaceCounter].insertText(string, 'Replace');
replaceCounter++;
}
if(replaceCounter < CClist.length) return context.sync() // continue loop
.then(function () {
$('#status').html('...replaced the content of '+replaceCounter+' CCs...');
return loop(replaceCounter, numCCs);
});
else return context.sync() // end loop
.then(function () {
$('#status').html('Replaced the content of all CCs');
});
})(0, CClist));
});
}).catch(function (error) {
$('#status').html('<pre>Error: ' + JSON.stringify(error, null, 4) + '</pre>');
console.log('Error: ' + JSON.stringify(error, null, 4));
if (error instanceof OfficeExtension.Error) {
console.log('Debug info: ' + JSON.stringify(error.debugInfo, null, 4));
}
throw error;
});
}
However... it is not working. It replaces the first 100 CCs and then stops. Without a failure, without an exception or anything. The return loop(replaceCounter, CClist); is just not executed and I don't know why. If I try to step into this line in the debugger it throws me somewhere in the office-js code.
Any suggestions?
Edit:
I updated my code based on the suggestions of Juan Balmori and it works as a charm:
function replaceCCtextWithSingleString_v1_1 (CCtitleList, string) {
Word.run(function (context) {
var time1 = Date.now();
// load the title of all content controls
var CCc = context.document.contentControls.load('title');
return context.sync().then(function () { // synchronous
// extract CC titles
var documentCCtitleList = [];
for(var i = 0; i < CCc.items.length; i++) { documentCCtitleList.push(CCc.items[i].title); }
// check for missing titles and replace
for(var i = 0; i < CCtitleList.length; i++) {
var index = documentCCtitleList.indexOf(CCtitleList[i]);
if(index == -1) { // title is missing
throw 'Could not find CC with title "'+CCtitleList[i]+'"';
}
else { // replace
CCc.items[index].insertText(string, 'Replace');
}
}
$('#status').html('...replacing...');
return context.sync().then(function () {
var time2 = Date.now();
var tdiff = time2-time1;
$('#status').html('Successfully replaced all selected CCs in '+tdiff+' ms');
});
});
}).catch(function (error) {
$('#status').html('<pre>Error: ' + JSON.stringify(error, null, 4) + '</pre>');
console.log('Error: ' + JSON.stringify(error, null, 4));
if (error instanceof OfficeExtension.Error) {
console.log('Debug info: ' + JSON.stringify(error.debugInfo, null, 4));
}
});
}
It still takes 13995 ms to complete, but at least it works :-)
Any ideas, what was provoking the GeneralException though?
I posted a new question concerning the speed issue: What is the fastest way of replacing the text of many content controls via office-js?
Good Question.. I did some perf test long time ago and I was able to change more than 10k content controls in a document. with 700 you should be ok.
Not sure why are you pre-filling a list, that is not needed, you are actually navigating 2 times the collection which is not good for perf. You can do the string comparison while traversing the collection!
Here is an example, I just did a quick test with a 700 content control document with a hypothetical tag of "test".
I was able to
1. Compare their text against whatever you want to compare it (its a string)
2. Change the value if the condition is true.
It took 5134 milliseconds to complete the operation and here is the code. which I think its quite acceptable.
Hope this helps!
function perfContentControls() {
var time1 = Date.now(); // lets see in how much time we complete the operation :)
var CCs =0
Word.run(function (context) {
var myCCs = context.document.body.contentControls.getByTag("test");
context.load(myCCs);
return context.sync()
.then(function () {
CCs = myCCs.items.length
for (var i = 0; i < CCs; i++) {
if (myCCs.items[i].text == "new text 3") // you can check the cc content and if needed replace it....
myCCs.items[i].insertText("new text 4", "replace");
}
return context.sync()
.then(function () {
var time2 = Date.now();
var diff = time2 - time1;
console.log("# of CCs:" + CCs + " time to change:" + diff + "ms");
})
})
.catch(function (er) {
console.log(er.message);
})
})
}
I'm trying to use deferred/promise in a loop, but I get strange behavior. My code is as follows:
var images = [];
var numImages = Blobs.length;
var image = {};
console.log("numImages: " + numImages);
function doAsyncOriginal(i) {
var defer = $.Deferred();
image.original = Blobs[i].key;
image.resized = '';
image.thumbnail = '';
images.push(image);
console.log("i: " + i + " image: " + image.original);
console.log("images[" + i + "]: " + images[i].original);
defer.resolve(i);
return defer.promise();
}
$(function(){
var currentImage = doAsyncOriginal(0);
for(var i = 1; i < numImages; i++){
currentImage = currentImage.pipe(function(j) {
return doAsyncOriginal(j+1);
});
}
$.when(currentImage).done(function() {
console.log(JSON.stringify(images));
});
});
The Blob used in the code is an array of objects that I get from remote webservice, which contains properties about the images (it comes from filepicker.io's pickandstore method to be precise).
When I run this, I get the following in console:
numImages: 2
i: 0 image: pictures_originals/3QnQVZd0RryCr8H2Q0Iq_picture1.jpg
images[0]: pictures_originals/3QnQVZd0RryCr8H2Q0Iq_picture1.jpg
i: 1 image: pictures_originals/MD3KO6GjT8SNFYoPcG8J_picture2.jpg
images[1]: pictures_originals/MD3KO6GjT8SNFYoPcG8J_picture2.jpg
[
{
"original":"pictures_originals/MD3KO6GjT8SNFYoPcG8J_picture2.jpg",
"resized":"",
"thumbnail":""
},
{
"original":"pictures_originals/MD3KO6GjT8SNFYoPcG8J_picture2.jpg",
"resized":"",
"thumbnail":""
}
]
Although it shows images[0] and images[1] correctly, when printing separately, the object array shows only twice images[1]!!!
Am I doing something wrong???
Thanks in advance for your time.
UPDATE: I corrected the code based on comment of #TrueBlueAussie
You are reusing the same image object in every call to doAsyncOriginal(), so every element of your images array is pointing to the same object.
You need to create the object inside your function:
var image = {}; // <-- delete this
function doAsyncOriginal(i) {
var image = {};
// ...
}
This problem is unrelated to promises/deferreds, and promises/deferreds really aren't serving any purpose in your code. You could just do this:
$(function(){
var images = Blobs.map(function (blob) {
return {
original: blob.key,
resized: '',
thumbnail: ''
};
});
console.log(JSON.stringify(images));
});
In doAsyncOriginal you resolve your deferred before returning it's promise or even before adding the done handler on it.
You should delay the defer.resolve(i) call, so the deferred will be resolved later and enter the done handler...
function doAsyncOriginal(i) {
var defer = $.Deferred();
// ...
// Function.bind equivalent to jQuery.proxy
window.setTimeOut(defer.resolve.bind(defer, i), 0);
return defer.promise();
}
I have been trying to write a protractor test that selects an item from a custom dropdown menu. The only problem is that when it tries to click an element other than the last one in the list it hangs and timesout. When I remove the click() method invocation it seems to work fine. Since all these calls are done asynchronously I also don't see a way of stopping the loop when it finds the element. My code looks like this:
var it = null;
for(var i = 1; i <= totalNumberOfAccounts; i++) {
var listItemLocator = '//div[#id="payment-accounts"]/div/ul/li[' + i + ']/label/div/div[2]/div[2]/span[2]';
var item = browser.driver.findElement(protractor.By.xpath(listItemLocator));
item.getText().then(function(value) {
if(value === accountNumber) {
it = item;
}
console.log(value);
})
.then(function clickOption() {
console.log('Clicking...');
if (it) {
console.log('Clicking desired item');
it.click();
console.log('Clicked..');
}
})
}
I also tried this approach:
this.selectRichSelectOption = function (selector, item) {
var selectList = browser.driver.findElement(selector);
selectList.click();
var desiredOption = '';
var i = 1;
selectList.findElements(protractor.By.tagName('li'))
.then(function findMatchingOption(options) {
console.log(options);
options.some(function (option) {
console.log('Option:');
console.log(option);
var listItemLocator = '//div[#id="payment-accounts"]/div/ul/li[' + i + ']/label/div/div[2]/div[2]/span[2]';
console.log(listItemLocator);
var element = option.findElement(protractor.By.xpath('//label/div/div[2]/div[2]/span[2]'));
console.log('Element:');
console.log(element);
i++;
element.getText().then(function (value) {
console.log('Value: ' + value);
console.log('Item:');
console.log(item);
if (item === value) {
console.log('Found option..');
desiredOption = option;
return true;
}
return false;
});
});
})
.then(function clickOption() {
console.log('Click option');
console.log(desiredOption);
if (desiredOption) {
console.log('About to click..');
desiredOption.click();
}
});
};
The result of this one is even more strange. Now all of a sudden the getText() method invocation returns an empty String. But when I try to retrieve the e.g. the class attribute I get the correct value back. Where did the Text value go?
Can somebody please help me out?
This seems to be an issue with page load. After you select, the page does not load completely.
Try using a browser.sleep(timeInMs);
try using node 8+'s async functions such as await. I went through this headache and it was solved by awaiting for certain things to appear or have certain attributes.
await browser.wait(EC.presenceOf(element(by.xpath('path leading to element based off attribute'))))
Good luck