Is there a way to iterate and compare dom elements in nodejs? - javascript

I'm trying to get two values from a site I'm automating. I iterate through an array of elements and try to compare the value of the dropdown to what is on the page to make sure they equal one another. One of the values can only be accessed by .getValue(). The other is accessed by .getText(). I would like to save the result.value of these callback functions and compare the results.
I've tried to console.log both of these values and I get them back, but I can't return anything from this callback function. I also can't seem to save it's value in a variable and return that either. I've tried looking to do it in plain javascript with document.getElementById() but that works for client-side javascript, not serverside like nodejs. Just trying to compare two values together
for (let i = 1; i <= 20; i++) {
browser
.element('css selector', `mat-nav-list > a:nth-child(${i})`,
function(result) {
if (result.value && result.value.ELEMENT) {
browser.isVisible(`mat-nav-list > a:nth-child(${i})`,
function(result) {
if (result.value === true) {
browser.click(`mat-nav-list > a:nth-child(${i})`)
let chunkView = '#mat-input-0';
let sideBar = `body > gps-app-root > div > div.sidebar-desktop > gps-app-sidebar-menu > div > div.product-list-wrap > mat-nav-list > a:nth-child(${i}) > div`
browser.getValue(chunkView, function(result) {
chunkView = result.value
console.log(chunkView)
})
browser.getText(sideBar, function(result) {
console.log(result.value);
})
}
})
}
})
//.pause(2000)
//.pause(10000)
}
When I loop through I would expect to get the two values sideBar result.value to equal chunkView result.value. Current output can only log the two separate values.

I haven't used Nightwatch.js before, so I'm basing my answer on the assumption that browser.click, browser.getValue, and browser.getText run asynchronously, as that's fairly common with UI and UI test frameworks, and, if they did run synchronously, there would be no point in using callbacks.
You're probably going to want to get used to working with the JavaScript Promise. Since JavaScript engines are single-threaded, there is no way to spinlock/sleep while another thread handles some change (such as updating the UI after a click event). A Promise allows you to get around this by working with callbacks and handling events behind the scenes.
You can then chain promises using promise.then() which pass the returned value to the next callback.
In your case, though, I would wrap the two functions that retrieve values in promises and then use Promise.all(). This allows them to complete in any order which could improve performance.
browser.isVisible(`mat-nav-list > a:nth-child(${i})`,
function(result) {
if (result.value === true) {
browser.click(`mat-nav-list > a:nth-child(${i})`);
let chunkView = '#mat-input-0';
let sideBar = `body > gps-app-root > div > div.sidebar-desktop > gps-app-sidebar-menu > div > div.product-list-wrap > mat-nav-list > a:nth-child(${i}) > div`;
let valPromise = new Promise(resolve => {
browser.getValue(chunkView, resolve);
});
let textPromise = new Promise(resolve => {
browser.getText(sideBar, resolve);
});
Promise.all([valPromise, textPromise]).then(([valueResult, textResult]) => {
browser.assert.strictEqual(valueResult.value, textResult.value,
`Server-side value '${value.result}' does not match client-side value '${text.result}'`);
});
}
});

Use the perform method so that callbacks are ensured to have completed before the next command. (see https://github.com/nightwatchjs/nightwatch/wiki/Understanding-the-Command-Queue#the-perform-command)
Something like this
browser.getValue(chunkView, function(result) {
chunkView = result.value
console.log(chunkView)
}).perform(function() {
// here you have access to chunkView so you can compare it
browser.getText(sideBar, function(result) {
console.log(result.value);
if (chunkView === result.value) {
console.log('They are the same!');
}
})
});
or you could chain perform commands so that you can do the comparison at the end regarding the number of intermediate steps.
let chunkView = '#mat-input-0',
chunkViewResult;
let sideBar = `body > gps-app-root > div > div.sidebar-desktop > gps-app-sidebar-menu > div > div.product-list-wrap > mat-nav-list > a:nth-child(${i}) > div`,
sideBarResult;
browser.getValue(chunkView, function(result) {
chunkViewResult = result.value
console.log(chunkView)
}).getText(sideBar, function(result) {
sideBarResult = result.value
console.log(sideBarResult);
}).perform(function() {
if (chunkViewResult === sideBarResult) {
console.log('They are the same!')
}
})

I found if I nested .getValue() and .getText and assigned variables I was able to compare the two.

You can always use .execute() from nightwatch and run any javascript you want in the page.
client.execute(
function (data) {
// this is runned in browser
},
[what_you_pass_to_the_function_above_as_data], // multiple parameters = multiple elements in array
function (result) {
// result.status == -1 if error happend
// here you cand compare what you want... result.value is the returned things from the browser executed function.
}
);

Related

Trouble with Addition Assignment in Array from Firebase

I have a scenario where i need to query multiple collections at once and retrieve the values based on the collection name. I use Promise.all to do so and it works accordingly like so
var dbPromises = [];
dbPromises.push(
admin.firestore().collection("collection1").where("user_id", "==", uid).get(),
admin.firestore().collection("collection2").where("user_id", "==", uid).get(),
admin.firestore().collection("collection3").where("user_id", "==", uid).get(),
);
const promiseConst = await Promise.all(dbPromises);
promiseConst.forEach((qs) => {
if (qs.size > 0) {
if (qs.query._queryOptions.collectionId == "collection1") {
qs.docs.map((doc) => {
valuesArr1.push(doc.data().arr);
});
} else if (qs.query._queryOptions.collectionId == "Collection2") {
qs.docs.map((doc) => {
valuesArr2.push(doc.data());
});
} else if (qs.query._queryOptions.collectionId == "collection3") {
qs.docs.map((doc) => {
valuesArr3.push(doc.data());
});
}
} else {
return
}
});
for (var i=0; i < valuesArr1.length; i++) {
if (valuesArr1[i].desiredData) {
console.log('datas from for loop on datas array', valuesArr1[i].desiredData)
globalVariable += `<img src="${valuesArr1[i].desiredData}">`;
}
}
Once I do this I map the query snapshot I get and am able to retrieve the values up to this point like so
From the first collection I retrieve an array from a firestore document and then the following collections i just retrieve all documents from the collections. This all 'works' in that when I console.log into the functions console the data shows up exactly as expected. It's only when I want to iterate over the data and assign the results to a global variable to use elsewhere that strange behavior occurs.
The console.log shows the desired data in the functions console with no issues, but the output when I interpolate that data into the html and send it off in nodemailer I get the following result
undefined is always the first in the response when i use the += addition assignment operator, but if i just use the = assignment operator there's no undefined but I obviously don't get all the data I'm expecting.
There are no undefined values or documents in the collections that I'm retrieving, I've checked thoroughly and even deleted documents to make sure of it. After days of researching I've come to the conclusion it has to do with the asynchronous nature of the promise I'm working with and the data not being immediately ready when I iterate it.
Can someone help me understand what I'm doing wrong and how to fix it in node?
I figured out a solution to my problem and would like to share it in hopes it saves a future viewer some time.
Before, I was storing the results of the array from Firebase inside a global variable. To save some head scratching I'll post the code again below.
var globalVariableArray = []
var globalVariable
var dbPromises = [];
dbPromises.push(
admin.firestore().collection("DataCollection").where("user_id", "==", uid).get()
);
const promiseConst = await Promise.all(dbPromises);
promiseConst.forEach((qs) => {
if (qs.size > 0) {
if (qs.query._queryOptions.collectionId == "DataCollection") {
Promise.all(
qs.docs.map(doc => {
globalVariableArray = doc.data().arrayWithDesiredData;
})
);
}
else {
return
}
});
globalVariableArray.map(gv => {
globalVariable += `<p>gv.desiredData</p>` // <--- Right here is where the problem area was
})
var mailOptions = {
from: foo#blurdybloop.com,
to: 'bar#blurdybloop.com
subject: 'Almost but not quite',
html: `${globalVariable}`
};
The above code give the expected output, but the output would always have undefined first before the data showed. This happened no matter how the array from Firebase was iterated over.
After strengthening my Google-Fu, I worked out the following solution
var globalVariableArray = []
var globalVariable
var dbPromises = [];
dbPromises.push(
admin.firestore().collection("DataCollection").where("user_id", "==", uid).get()
);
const promiseConst = await Promise.all(dbPromises);
promiseConst.forEach((qs) => {
if (qs.size > 0) {
if (qs.query._queryOptions.collectionId == "DataCollection") {
Promise.all(
qs.docs.map(doc => {
globalVariableArray = doc.data().arrayWithDesiredData;
})
);
}
else {
return
}
});
var mailOptions = {
from: foo#blurdybloop.com,
to: 'bar#blurdybloop.com
subject: 'It works!!',
html: `${globalVariableArray.map(dataIWantedAllAlong => <p>dataIWantedAllAlong.desiredData</p> )}` <--- Here I simply loop through the array inside the interpolation blocks and voila! no more undefined showing up in the results
};
I perform the loop inside the brackets where I interpolate the dynamic data and am no longer getting that pesky undefined showing up in my emails.
Safe travels and happy coding to you all!

Break For Each Loop in a Custom Cypress Command

I have a custom Cypress command to scroll through a list until you reach the item passed into the command. In my command I have $.each so I can compare the name of the item to the item name passed into the function. If they match then I send a click command which is "ENTER" in my environment.
I am able to successful scroll through the list and find the the item I am looking for and click on it but the loop continues to execute. I added return false which is what Cypress says should break the loop but it is now working for me. Any ideas why that is the case?
listItems.getChildren('OneLineTextElement', 'type').each(($elm) => {
let labelAndValue = cy.wrap($elm).getChildren('LabelAndValue')
let label = labelAndValue.getChild('Label')
label.getProperty('texture-text').then(val => {
if (val == subject) {
cy.action('ENTER')
return false
}
else {
cy.action('DOWN')
}
})
})
You can try adding a control variable in the the scope of the .each() command.
listItems.getChildren('OneLineTextElement', 'type').each(($elm) => {
let done = false;
let labelAndValue = cy.wrap($elm).getChildren('LabelAndValue')
let label = labelAndValue.getChild('Label')
label.getProperty('texture-text').then(val => {
if (val == subject) {
cy.action('ENTER').then(() => { // is cy.action a custom command?
// Likely you may need to wait
done = true;
})
} else {
cy.action('DOWN')
}
})
if (done) {
return false;
}
})
However there are some method calls that look like custom commands inside .each() so you may not get the flow of execution you expect (Cypress commands and test javascript can run asynchronously).
It looks like the code may be refactored to avoid "scrolling through the list". The only thing this does not do is cy.action('DOWN') on the non-subject list items.
listItems.getChildren('OneLineTextElement', 'type')
.getChildren('LabelAndValue')
.getChild('Label')
.getProperty('texture-text')
.should(val => {
expect(val).to.equal(subject);
cy.action('ENTER');
})
You have to use promise after finding your match within the loop, and then apply the assertion. Below an example searching value '23' within the column 8 which corresponds to age field.
cy.get('.column-8')
.each(ele => {
if (ele.text() === '23') {
isValuePresent = true
if (isValuePresent) return false
}
}).then(() => {
expect(isValuePresent).to.be.true
})

How to break for loop inside Cypress's then() function?

I want to break for loop if .contacts element's style doen't equal display: none, but I can't find a way to do this inside then() fuction.
Use case: I want to click through table elements until contacts panel appears and then fill contacts. But if I don't stop the loop after panel appears it will disappear and I will get an error.
cy.get('#count').then($count => {
for (let i = 1; i <= $count - 1; i++) {
if (i == 1) {
cy.get(`:nth-child(${i}) > .room-type`).eq(1).click()
}
else {
cy.get(`:nth-child(${i}) > .room-type`).click()
}
cy.get('.spinner').should('not.exist')
cy.get('.contacts').then($contacts => {
if ($contacts.attr('style') != 'display: none;') {
//I want to break the loop here if condition is met
}
})
}
})
I believe the best way to go about about is the recursive approach. Using recursive approach your problem of synchronous and Asynchronous code will be resolved automatically.
So you can approach this problem with something like this below. The below code might not work as it is, you may have to do some changes accordingly. The main Idea is try going recursive approach if you want to avoid the sync-async code problem
cy.get("#count").then(($count) => {
test_recusive($count);
});
export const test_recusive = (count) => {
if (i == 1) {
cy.get(`:nth-child(${i}) > .room-type`).eq(1).click();
} else {
cy.get(`:nth-child(${i}) > .room-type`).click();
}
cy.get(".spinner").should("not.exist");
cy.get(".contacts").then(($contacts) => {
if ($contacts.attr("style") != "display: none;") {
//I want to break the loop here if condition is met
return;
} else {
return test_recusive(count - 1);
}
});
Now all the calls in your code will be synchronous ones.

How to use a value received from one service in another service within the same Angular 6 component?

The following component's code is from an Angular 6 web application that I am creating. The app displays a table with CRUD functionality. I have an Angular service called GetDBValuesService that is connected to a database and uses DBValues() to retrieve an array of arrays (each inner array contains the values of a given row in the database). My code then collects rows whose 'Number' attribute is equal to 10. These rows are then used by my EventEmitter dataItems, which allows them to be displayed in my web page's CRUD table.
I have created another Angular service called DataService that receives an integer value from another component and sends that value to the shown component (after being subscribed to). I subscribed to this service in the following code and let an instance of gotdata (a public var declared in this component) receive the service's value. However, when I try to use this instance outside of that subscription (to replace the hardcoded 10 described above), this.gotdata is undefined.
How can I modify my code so that I can use the value given by the DataService service in the GetDBValuesService service? Currently, the below code does work due to the hardcoded 10, but does not if I remove that line. Thank you for taking the time to read this.
This is the portion of my CRUD component:
refresh = () => {
this.DataService.DataID$.subscribe((data) => {
this.gotdata = data;
console.log(this.gotdata); //10 (value from console)
});
console.log(this.gotdata); //undefined (value from console)
this.gotdata = 10; //hardcoded value allows further functionality, will be removed when this.gotdata retains its value from the above subscription
if (this.gotdata != null) {
this.GetDBValuesService.DBValues().subscribe((result) => {
var a = 0;
for (var i = 0; i < result.length; i++) {
if (result[i].Number == this.gotdata) {
this.info[a] = result[i];
a = a + 1;
}
}
this.dataItems.next(this.info); //sets rows to be displayed in the web page's table (used by component's HTML file)
});
}}
this.gotdata is undefined because the data is not resolved yet.
refresh = () => {
this.DataService.DataID$.subscribe((data) => {
this.gotdata = data;
console.log(this.gotdata); //10 (value from console)
if (this.gotdata != null) {
this.GetDBValuesService.DBValues().subscribe((result) => {
var a = 0;
for (var i = 0; i < result.length; i++) {
if (result[i].Number == this.gotdata) {
this.info[a] = result[i];
a = a + 1;
}
}
this.dataItems.next(this.info); //sets rows to be displayed in the web page's table (used by component's HTML file)
});
}}
});
Or you can put it inside subscription on complete callback:
this.service.subscribe((data) => {
// code here
},
(error) => console.error(error),
() => {
// do stuff.
});
The problem is that, at the time when you are calling the console.log(...) and the code below, data from the dataID$observable are still on way to you. ( Why do u need to work with observables?)
The best approach for this would to be use the RXJS switchMap operator (what is switchMap?). Because as I see you want to subscribe to the first observable and after that subscribe to another observable. So it can be done this way:
refresh = () => {
this.DataService.DataID$.pipe(switchMap(data: any) => {
if (data) {
this.gotdata = data;
return this.GetDBValuesService.DBValues();
} else {
return of(null); // if 'data' are null, return "empty" observable
}
})).subscribe((result: any) => {
if (!result) {
return; // return if 'result' is null (so the code bellow won't be executed)
}
var a = 0;
for (var i = 0; i < result.length; i++) {
if (result[i].Number == this.gotdata) {
this.info[a] = result[i];
a = a + 1;
}
}
this.dataItems.next(this.info);
});

IE8 long running script error when using DataTables

I have an application that uses the DataTables jQuery library to render content in my target browser IE8. The problem is when I push a big array to be rendered, IE8 sometimes throws up the infamous long running script error.
After profiling the app it seems that the call to __fnAddData in the following code is causing the problem:
if (bUsePassedData) {
for (var i = 0, len = oInit.aaData.length; i < len; i++) {
_fnAddData(oSettings, oInit.aaData[i]);
}
} else if (oSettings.bDeferLoading ||
(oSettings.sAjaxSource === null && oSettings.ajax === null)) {
_fnAddTr(oSettings, $(oSettings.nTBody).children('tr'));
}
I was looking around for solutions and saw Nicholas Zakas' write up here and tons of other solutions that would work if the for loop wasn't inside of an if else if "block". When I tried, on my 1st attempt of many, to wrap it in a setTimeout function it of course didn't work because the 2nd part of the if else if resolves to true.
(oSettings.sAjaxSource === null && oSettings.ajax === null) // true
What is a good solution for this? Thanks in advance.
I think you might split up your function in 3 functions:
Before the if statement.
Processing the oInit.aaData
After the if statement
Here is the code split up in 3 functions:
function beforeIf(){
if (bUsePassedData) {
procesData(oSettings,oInit.aaData.concat());
} else if (oSettings.bDeferLoading ||
(oSettings.sAjaxSource === null && oSettings.ajax === null)) {
_fnAddTr(oSettings, $(oSettings.nTBody).children('tr'));
}
afterIF();
}
function processData(oSettings,arr){
//process in chuncks of 50;
// setTimeout takes a long time in IE
// it'll noticibly slow donw your script when
// only processing one item at the time
var tmp=arr.splice(0,50);
for (var i = 0, len = tmp.length; i < len; i++) {
_fnAddData(oSettings, tmp[i]);
}
if(arr.length!==0){
setTimeout(function(){
processData(oSettings,arr);
},0);
return;
}
afterIf();
}
function afterIf(){
//continue processing
}
Thanks #HMR. You helped to bring me closer to my goal. To solve the problem I worked my code down to this IIFE:
(function processData(oSettings, arr) {
var tmp = arr.splice(0, 50);
tickApp.$orders.dataTable().fnAddData(tmp);
if (arr.length !== 0) {
setTimeout(function () {
processData(oSettings, arr);
}, 0);
}
}(oSettings, oInit.aaData.concat()));
Instead of using the private _fnAddData function I opted for the DataTables public fnAddData (http://datatables.net/ref#fnAddData) function. This way I am able to push 50 rows at a time into the table which is stored in the tickApp.$orders object which I just a reference to my jQuery object that stores the table in memory:
tickApp.$orders = $('#orders');
In another part of my code. They way you had it it was still pushing 1 row at a time instead of the whole 50.
Thanks again.
If you are using ajax to fetch your data, you can override "fnServerData" in your datatables config object. This will allow you to fetch the data to be loaded and then process it however you want.
In my case, I have a generic datatables config object that I use for all my datatables. I override the default fnServerData function with one that passes rows to the datatable in sets of 200 using fnAddData and setTimeout to call the function again until all the data has been processed, finally I call fnDraw to draw the table.
var DEFAULT_CHUNK_SIZE = 200;
function feedDataToDataTableInChunks(startIndex, data, oSettings) {
var chunk = data.slice(startIndex, DEFAULT_CHUNK_SIZE);
oSettings.oInstance.fnAddData(chunk, false);
if((startIndex += DEFAULT_CHUNK_SIZE) < data.length) {
setTimeout(function () {
feedDataToDataTableInChunks(startIndex, data, oSettings);
});
} else {
oSettings.oApi._fnInitComplete(oSettings, data);
oSettings.oInstance.fnDraw();
}
}
var config = {fnServerData: function(){
oSettings.jqXHR = $.getJSON(sSource, aoData)
.done(function (result) {
feedDataToDataTableInChunks(0, result || [], oSettings);
});
}}
I am using datatables version 1.9.4

Categories

Resources