I have this site: http://embed.plnkr.co/Bs5iDqtXSSnvye2ORI6k/preview
Code:
var app = angular.module('plunker', []);
var a = new Array(1000);
for (var i = 0; i< 1000; i++) {
a[i] = 'Name' + i;
}
app.controller('MainCtrl', function($scope, $interval) {
$scope.names = a;
$scope.start = function () {
$interval(function () {
$scope.names.pop();
}, 50);
}
});
And the following spec:
'use strict';
describe('Name list', function () {
it('should get the text of the last name', function () {
browser.driver.get('http://embed.plnkr.co/Bs5iDqtXSSnvye2ORI6k/preview');
browser.switchTo().frame(browser.driver.findElement(protractor.By.tagName('iframe')));
element(by.buttonText('start')).click();
expect(element.all(by.tagName('span)).last().getText()).toBe('Name999');
});
});
And this config:
'use strict';
// An example configuration file.
exports.config = {
baseUrl: 'http://localhost:3000',
seleniumAddress: 'http://localhost:4444/wd/hub',
specs: ['stale.spec.js']
};
And when I run Protractor I get the following error:
StaleElementReferenceError: stale element reference: element is not attached to the page document (Session info: chrome=43.0.2357.81)
(Driver info: chromedriver=2.15.322455 (ae8db840dac8d0c453355d3d922c91adfb61df8f),platform=Mac OS X 10.10.3 x86_64) (WARNING: The server did not provide any stacktrace information) Command duration or timeout: 9 milliseconds For documentation on this error, please visit: http://seleniumhq.org/exceptions/stale_element_reference.html Build info: version: '2.45.0', revision: '5017cb8', time: '2015-02-26 23:59:50' System info: host: 'ITs-MacBook-Pro.local', ip: '129.192.20.150', os.name: 'Mac OS X', os.arch: 'x86_64', os.version: '10.10.3', java.version: '1.8.0_31' Driver info: org.openqa.selenium.chrome.ChromeDriver Capabilities [{applicationCacheEnabled=false, rotatable=false, mobileEmulationEnabled=false, chrome={userDataDir=/var/folders/rr/63848xd90yscgwpkfn8srbyh0000gq/T/.org.chromium.Chromium.rarNyX}, takesHeapSnapshot=true, databaseEnabled=false, handlesAlerts=true, version=43.0.2357.81, platform=MAC, browserConnectionEnabled=false, nativeEvents=true, acceptSslCerts=true, locationContextEnabled=true, webStorageEnabled=true, browserName=chrome, takesScreenshot=true, javascriptEnabled=true, cssSelectorsEnabled=true}] Session ID: 235ec977a69d98c7f5b75a329e8111b2
This means that the element I try to interact with (getting the text of the element), isn't attached to the DOM anymore. This example is really my spec simplyfied. What really happens in my real spec is I try to get the text of the last element of a list of elements (generated by ng-repeat). What also happens is that the model updates, by removing the last element of the array representing the list of element. This example above is just something to reproduce the error (every time).
If I comment out this line: element(by.buttonText('start')).click(); the spec is successful.
I struggled a lot with this and tried to figure out why this would happen. I first thought that the element finder which points to the last element of the list was created long before the interaction was done, so there was no surprise to me that the element could be detached from the DOM in that period of time between the creation of the element finder and the interaction.
What I've later found out is that the element is found just before the interaction is done, every time you interact with something. So pointing to the last element should actually point to the last element of the time interacting with the element.
Using browser.pause() I was able to see what WebDriver really does, and this is two tasks where in between the error is thrown:
(pending) Task::414<WebDriver.findElements(By.tagName("span"))>
| | | | | | Task: WebDriver.findElements(By.tagName("span"))
| | | | | | at Array.forEach (native)
Here in between, the DOM is updated according to the model, and the last element of the list is detached.
(pending) Task::1170<WebElement.getText()>
| | | | | | Task: WebElement.getText()
| | | | | | at Array.forEach (native)
The DOM is updated in this small hole of execution. Currently the model updates every 50 ms, and this is sure to throw a Stale Element Reference error. But if I increase the interval to, say 1000 ms, then the chance to get the error is much less. So it depends on how fast your computer runs if you get this error.
The fix is up to you, but with this information it should be a bit clearer what to do, I hope.
The browser is running asynchronously from your protractor tests. This example really highlights that nicely (its a problem for many protractor tests, but not usually so obvious). This is compounded by what looks like a single line:
expect(element.all(by.tagName('span')).last().getText()).toBe('Name999');
actually requires several round-trips to the browser (there are a lot of promises being returned and resolved: element.all, last, getText). For most web pages that are "passive" once they've stabilized, this isn't a problem, but for any web page that dynamically changes without user inputs, testing with protractor can be painful.
To make a lookup and test "atomic" in the browser, thus side-steping this issue, you can define a function to run in the browser (my CSS DOM-fu is weak, so hopefully someone can tweak this to make it less terrible):
expect(browser.executeScript(function() {
var spans = document.getElementsByTagName('span');
var lastSpan = spans[spans.length - 1]; // XXX handle empty spans
return lastSpan.innerText;
}).toBe('Name999');
Beware that the "function" is serialized and executed on the browser, so no variables from an enclosing scope will work. Also, this approach loses any browser-compatibility magic hiding in protractor or webdriver (e.g., I wouldn't be surprised if getText() wasn't simply a innerText accessor).
Also, note that you still have a race condition. Between the 'click' to get things started and this code to actually inspect the DOM, it may or may not have been mutated (and "Name999" might be gone).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With