I am getting the error below as a result of using the each() method in protractor. It has worked fine in the past but is now consistently failing with this error.
Failed: stale element reference: element is not attached to the page document
element.all(bars).each((element) => element.getCssValue('width'))
Is there an alternative or a reason why this might be?
(For clarity, all I want to do is to get the width of each element in a set of web elements called bars.)
Thanks!
In a nutshell, it happens because each()
just fires commands simultaneously against all elements. In your case you probably need to go this way element.all(bars).getCssValue('width')).then(array => {/*do what you want with returned array here*/})
* Edited *
What you want to do is
element.all(bars).getCssValue('width')).then(array => {
// tests is at least one element will not be "0px"
let elementHasWidth = array.some(elem => elem !== "0px");
expect(elementHasWidth).toBe(true);
})
New problems like this with existing code are likely due to Protractor and Selenium evolving from Web Driver Control Flow and WebDriverJS Promise Manager to native promises. You used to be able to write code that looked like synchronous code and under the hood the toolkits would convert it to asynchronous code that waited for the DOM to load and JavaScript on the page to run. Going forward you need to convert your code to explicitly use async/await
or promises. (See the reasoning behind the Selenium change here.)
Unfortunately a lot of old (pre 2019) examples, even in the Protractor documentation, are written in synchronous style, so you should be careful about following them. Even more unfortunately, you cannot mix async
code with Control Flow code, and if you try, all the Control Flow will be disabled, and probably some of your tests that relied on it will start failing.
By the way, what is the value of bars
? A Protractor by
object works differently than a native WebDriver locator and I'm not sure they are reusable. Try using by.name('bars')
or whatever instead of bars
.
Your case is tricky because of all the promises involved. element.getCssValue
returns a promise. Since you are trying to get a true
or false
value out of this, I suggest using a reducer.
let nonZero = element.all(by.name('bars')).reduce((acc, elem) => {
return acc || elem.getCssValue('width').then( (width) => width > 0 );
}, false);
In a more complicated situation, you could use all().each()
but you have to be careful to ensure that nothing you do inside each
affects the DOM, because once it does, it potentially invalidates the rest of the array.
If you are potentially modifying the page with your ultimate action, then, as ugly as it may seem, you need to loop over finding the elements:
for (var i = 0; true; i++) {
let list = element.all(by.css('.items li'));
if (i >= await list.count();)
break;
list.get(i).click();
};
Try using this code before your error statement:
browser.wait(function() {
return element.all(bars).isPresent().then(function(result) {
return result;
});
}, 5000);
Thanks to everyone who helped. The solution was retry again if it resulted in error. Due to the realtime nature of the element, the element reference was changing before it had got the css width.