Not able to click on elements in loop in protractor - loops

I have a ecommerce website, which has list of products similar to amazon. i am trying to look for a element and click it. i have tried by filter and map and both not working for me. Please help.
Below is the code for each product:
<b class="productNameHover pcursor word-break">product1</b>
<b class="productNameHover pcursor word-break">product2</b>
<b class="productNameHover pcursor word-break">product3</b>
<b class="productNameHover pcursor word-break">product4</b>
say i have 4 elements in my page. so, i have tried to get the count and it working for me.
var products = element.all(by.className('productNameHover')) ;
expect(products.count()).toBe(4);
when i tried filter,
Solution A) not working, no error message but did nothing
var products = element.all(by.className('productNameHover'));
products.filter(function(elem) {
return products.getText().then(function(text) {
return text === 'product4';
});
}).click();
browser.sleep(5000);
Solution B) not working; index out of bound; Trying to access element at index: 0, but there are only 0 elements that match locator
var products = element.all(by.className('productNameHover'));
products.filter(function(elem) {
return products.getText().then(function(text) {
return text === 'product4';
});
}).first().click();
browser.sleep(5000);
Soluction C) no error message but did nothing
var products = element.all(by.className('productNameHover'));
products.filter(function(elem) {
return products
.element(by.xpath(
"//div[#class='item text-center slide-image-content active']/img"
))
.getText()
.then(function(text) {
expect(text).toContain(product4);
})
.then(function(filteredElements) {
filteredElements.first().click();
});
});
browser.sleep(5000);
Solution D) This is working and giving me all the products; but i need to either click on a single product or loop through
var products = element.all(by.className('productNameHover'));
products.map(function(item) {
return item.getText();
}).then(function(txt) {
console.log(txt);
//expect(txt).toContain("product 4")
});
Solution E) not working and no error message
products.map(function(item) {
return item.getText();
}).then(function(txt) {
console.log(txt);
if( txt== 'product4') {
console.log(item);
item.click();
browser.sleep(5000);
}
});
Solution F) I tried to click on all the elements in loop but it is clicking on the first element and not clicking on the second one; it is giving Failed: stale element reference: element is not attached to the page document.
products.map(function(item) {
browser.sleep(5000);
item.click();
browser.sleep(5000);
var backButton = element.all(by.className('btn btn-outline btn-big mt-3 ml-0 ml-sm-2')).first() ;
backButton.click();
browser.sleep(5000);
})

You have wrong understand the Protractor API: each() / map() / filter(). Recommend you to learn JavaScript Array's forEach() / map() / filter() to have a better understanding before you use Protractor API.
Solution A)
return products.getText() should be return elem.getText().
filter() return an element array, thus you can't call click() on array.
Solution B)
return products.getText() should be return elem.getText().
Solution E) map() return array too.
products.map(function(item) {
return item.getText();
}).then(function(txt) {
// txt = [ 'product1', 'product2', 'product3', 'product4']
console.log(txt);
if( txt == 'product4') { // this condition never be true
console.log(item);
item.click();
browser.sleep(5000);
}
});
// correct code example
products.map(function(item) {
return item.getText();
}).then(function(txts) {
// txts = [ 'product1', 'product2', 'product3', 'product4']
console.log(txts);
let index = txts.indexOf('product4');
if( index > -1) {
products.get(index).click();
browser.sleep(5000);
}
});

Have you tried using each() function of the protractor API ?
var products = element.all(by.className('productNameHover'));
products.each(function(element) {
return element.getText().then(function (text) {
if(text === 'product4') {
return element.click();
}
});
})
I would suggest you to switch to new async/await syntax of writing js code.
const products = element.all(by.className('productNameHover'));
products.each(async function(element) {
let text = await element.getText();
if(text === 'product4') {
await element.click();
}
});
You can also use map() function -
element.all(by.className('productNameHover')).map(function(element) {
return element.getText(function(text) {
return text === 'product4'
});
}).then(function(elements) {
for (var i = 0; i < elements.length; i++) {
elements.get(i).click();
}
});

Sounds like you got it working and I may be misunderstanding, but if you're just trying to click on a specific product in the list, you can use
element(by.cssContainingText('.productNameHover', 'product4')).click();

Related

Protractor: More specific feedback to User?

Say I have a page of 10 images and the 3rd and 5th don't load. How can I say something like:
2/5 images didn't load.
Or "image2.jpeg" and image5.jpeg" hasn't loaded.
browser.waitForAngularEnabled(false);
it('should find all images', function () {
browser.get('https://popeyes.com/');
var imagesBrokenCount = browser.executeScript(`
var elms = document.querySelectorAll("img");
return [].filter.call(elms, e => e.offsetHeight > 1 && e.naturalHeight <= 1).length;
`);
browser.sleep(1000);
expect(imagesBrokenCount).toEqual(0);
});
I would switch to Protractor-specific functionality here - using filter() to filter out the broken images, then, using map() to get an array of src values and then using Jasmine's fail() to fail with a desired error message:
it('should find all images', function () {
browser.get('https://www.popeyes.com');
var brokenImages = $$("img").filter(function (img) {
return img.getAttribute("offsetHeight").then(function (offsetHeight) {
return img.getAttribute("naturalHeight").then(function (naturalHeight) {
return offsetHeight > 1 && naturalHeight <= 1;
});
});
});
brokenImages.count().then(function (countBrokenImages) {
if (countBrokenImages > 0) {
console.log(countBrokenImages + " images loaded successfully");
brokenImages.map(function (img) {
return img.getAttribute("src");
}).then(function (sources) {
fail("Failed to load the following images: " + sources.join(","));
})
}
});
});
You could simply return the source from the broken images and then assert that the returned array is empty:
browser.get('https://popeyes.com/');
var brokenImages = browser.executeScript(`
return [].slice.call(document.querySelectorAll("img"))
.filter(e => e.offsetHeight > 1 && e.naturalHeight < 1)
.map(e => e.src);
`);
browser.sleep(1000);
expect(brokenImages).toBeEmptyArray();

Angular template won't load. Even with $loaded. Data resolves after Load

Using AngularFire, Angular, Firebase.
I load a list of users from a Firebase Database. I use $loaded to ensure it waits until data loads.
I take this list, compare it against another firebase database of groups and push the results into two arrays.
Based on the console.logs the data sorts correctly. However, inside my template I get a blank page (I think this is because the page loads before the data is sorted).
Thoughts?
let userLoggedIn = AuthFactory.getUser();
var allUsersArray = $firebaseArray(ConnectFactory.fbUserDb);
var x = firebase.database().ref('groups');
var friendArr = [];
var notFriendArr = [];
allUsersArray.$loaded().then(function(){
angular.forEach(allUsersArray, function(user, i) {
var haveIAdded = x.child(userLoggedIn).child(allUsersArray[i].uid).once('value').then(function (snap) {
if (snap.val() !== null) {
return true;
} else {
return false;
}
});
var haveTheyAdded = x.child(allUsersArray[i].uid).child(userLoggedIn).once('value').then(function (snap) {
if (snap.val() !== null) {
return true;
} else {
return false;
}
});
Promise.all([haveIAdded, haveTheyAdded]).then(function([you, they]) {
if (you && they) {
console.log('We Are Friends', allUsersArray[i]);
friendArr.push(allUsersArray[i]);
} else {
console.log('not a friend ', allUsersArray[i]);
notFriendArr.push(allUsersArray[i]);
}
});
});
$scope.friendList = friendArr;
$scope.notFriendList = notFriendArr;
});
Alright, this time I tried to actually read the question before attempting to answer. ;-)
When you set your $scope.friendList and $scope.notFriendList within the $loaded promise, your Promise.all may (and most likely) havn't resolved yet when those are called, since angular.forEach doesn't wait for the promises to finish before moving on to the next statement in the function. So you'll have to build an array of promises and wait for them all to resolve outside of the loop before attempting to set your $scope variables.
allUsersArray.$loaded().then(function(){
var promises = [];
var friendArr = [];
var notFriendArr = [];
angular.forEach(allUsersArray, function(user, i) {
... // Same as before
promises.push(
Promise.all([haveIAdded, haveTheyAdded]).then(function([you, they]) {
if (you && they) {
console.log('We Are Friends', allUsersArray[i]);
friendArr.push(allUsersArray[i]);
} else {
console.log('not a friend ', allUsersArray[i]);
notFriendArr.push(allUsersArray[i]);
}
})
);
});
Promise.all(promises).then(function(){
$scope.friendList = friendArr;
$scope.notFriendList = notFriendArr;
});
});

Multiple optional filters in angular

I am very new to angular and, I am not sure how to control the behavior of my filters.
In the app, I have two different single-select drop down controls that filter the results of my data set and fill a table. However, even though these filters work, the results are dependent of both controls and if both are not being used , the empty set is returned. So, my question is: How can I use these filters optionally? So, the app returns every result when the filters are not used or returns the filtered results by one of the controls or both?
Thank you
Here is the code:
AngularJS
The filters for each control. They look very similar:
.filter('byField', function () {
return function (results, options) {
var items = { options: options, out: [] };
angular.forEach(results, function (value, key) {
for (var i in this.options) {
if ((options[i].value === value.fieldId &&
options[i].name === "Field" &&
options[i].ticked === true)) {
this.out.push(value);
}
}
}, items);
return items.out;
};
})
.filter('byClass', function () {
return function (results, options) {
var items = { options: options, out: [] };
angular.forEach(results, function (value, key) {
for (var i in this.options) {
if ((options[i].value === value.documentClass &&
options[i].name === "Class" &&
options[i].ticked === true)) {
this.out.push(value);
}
}
}, items);
return items.out;
};
})
HTML
This is what I am doing to populate the rows of the table:
<tr ng-repeat="result in results | byField:outputFields | byClass:outputClasses">
<td>{{result.documentId}}</td>
...
</tr>
Dorado7.1 in all event listeners provides a view implicit variable pointing to the current event host's view, the variable can completely replace the use of this scenario.
Well, as I imagined the answer was more related to set theory than to angular.
I just made an union between the empty set and every result, and it worked.
.filter('byField', function () {
return function (results, options) {
var items = { options: options, out: [] };
angular.forEach(results, function (value, key) {
if (options.length) {
for (var i in this.options) {
if ((options[i].value === value.fieldId &&
options[i].name === "Field" &&
options[i].ticked === true)) {
this.out.push(value);
}
}
} else {
this.out = results.slice();
}
}, items);
return items.out;
};
})

Can any one tell me the css selector for this expression:

xpath = a[contains(text(),'dbfsbdj')]
I have tried with a:contains('dbfsbdj') - but this is invalid ?
You can't select an element by its text content with CSS.
If you want to use some JavaScript, you could do something like this (jsfiddle):
var paragraphs = document.getElementsByTagName('p');
[].slice.call(paragraphs).forEach(function(p) {
if(p.textContent.indexOf('World') > -1) {
console.log(p);
}
});
// => <p>World</p>
HTML:
<p>Hello</p>
<p>World</p>
You could then make a little function like this:
var paragraphs = document.getElementsByTagName('p');
console.log(contains(paragraphs, 'World')); // => [<p>World</p>]
function contains(_elements, text) {
var elements = [];
[].slice.call(_elements).forEach(function(p) {
if(p.textContent.indexOf('World') > -1) {
elements.push(p);
}
});
return elements;
}

Angular filter returning an array of objects causing infinite $digest loop

I have a custom filter which returns an array of matches to search field input and it works, but only after causing an infinite $digest loop. This also apparently only began happening after upgrading from Angular 1.0.6. This is the filter code:
angular.module("Directory.searches.filters", [])
.filter('highlightMatches', function() {
var ary = [];
return function (obj, matcher) {
if (matcher && matcher.length) {
var regex = new RegExp("(\\w*" + matcher + "\\w*)", 'ig');
ary.length = 0;
angular.forEach(obj, function (object) {
if (object.text.match(regex)) {
ary.push(angular.copy(object));
ary[ary.length-1].text = object.text.replace(regex, "<em>$1</em>");
}
});
return ary;
} else {
return obj;
}
}
});
I've seen elsewhere that this could be caused by having the filter inside of an ng-show, or that it's because the array being returned is interpreted as a new array every time it's checked, but I'm not sure how I could fix either problem. You can see a production example of this issue at https://www.popuparchive.com/collections/514/items/4859 and the open source project is available at https://github.com/PRX/pop-up-archive. Thank you!
This is happening because of angular.copy(object). Each time the digest cycle runs, the filter returns an array of new objects that angular has never seen before, so the the digest loop goes on forever.
One solution is return an array containing the original items that match the filter, with a highlightedText property added to each item...
angular.module("Directory.searches.filters", [])
.filter('highlightMatches', function() {
return function (items, matcher) {
if (matcher && matcher.length) {
var filteredItems = [];
var regex = new RegExp("(\\w*" + matcher + "\\w*)", 'ig');
angular.forEach(items, function (item) {
if (item.text.match(regex)) {
item.highlightedText = item.text.replace(regex, "<em>$1</em>");
filteredItems.push(item);
}
});
return filteredItems;
} else {
angular.forEach(items, function (item) {
item.highlightedText = item.text;
});
return items;
}
}
});
You can bind to the highlightedText property, something like...
<div>
Results
<ul>
<li ng-repeat="item in items | highlightMatches : matcher" ng-bind-html="item.highlightedText"></li>
</ul>
</div>

Resources