I'm writing automation tests for a website using Mocha + SeleniumServer + wd.js + chai-as-promised.
The website uses JavaScript for the front-end which seems to refresh the elements on the page when certain action is performed. i.e. Upon selecting an element in a grid, the "next" button is enabled to allow user to move on to the next page. It seems that this changes the reference to the button element resulting in the StaleElementReference error.
describe('1st step', function () {
it('should select an element is grid', function () {
return browser
.waitForElementByCss('#grid', wd.asserters.isDisplayed, 20000)
.elementByCss('#grid .elementToBeSelected')
.click()
.sleep(1000)
.hasElementByCss('#grid elementToBeSelected.active')
.should.eventually.be.true;
});
it('should proceed next step', function () {
return browser
.waitForElementByCss('.btnGrid .btn.nextBtn:not(.disabled)', wd.asserters.isDisplayed, 20000)
.elementByCss('.btnGrid .btn.nextBtn:not(.disabled)')
.click()//Error thrown here
.sleep(2000)
.url()
.should.eventually.become('http://www.somewebsite.com/nextpage');
});
});
With my limited experience with JavaScript, I have tried all that i could think off, but to no avail. So is there anyway I can avoid this StaleElementReference error? Also, the error is only sometimes thrown during execution.
You might want to read some more on the Stale Element Reference exception. From what you are describing, it sounds like you get a reference to an element, do something on the page which then changes/removes the referenced element. When you do something with the variable reference you get this error. The solution really depends on the code you are using to do your tests and your framework for accessing elements. In general, you need to be aware of when you perform an action that changes the page and refetch the element before you access it. You could always refetch an element before you access it, you could refetch all elements that are affected by a page change, and so on...
You code probably looks something like this
WebElement e = driver.findElement(...); // get the element
// do something that changes the page which, in turn, changes e above
e.click(); // throws the StaleElementReference exception
What you probably want is something more like one of these...
Don't fetch the element until you need it
// do something that changes the page which, in turn, changes e above
WebElement e = driver.findElement(...); // get the element
e.click(); // throws the StaleElementReference exception
...or fetch it again right before you need it...
WebElement e = driver.findElement(...); // get the element
// do something that changes the page which, in turn, changes e above
e = driver.findElement(...); // get the element
e.click(); // throws the StaleElementReference exception
I would prefer the first fix... just fetch what you need when you need it. That should be the most efficient way to solve this problem. The second fix might have performance issues because you might be refetching a bunch of elements over and over and either never using them or refetching them 10 times only to reference the element once at the end.
Related
So usually there are really easy ways to avoid mutating your DOM directly from React but unfortunately I'm not in one of those situations right now. I'm using an API that then embeds a lot of content on to my page into a div with an id of dashboard.
I need to manipulate the content on the page and move certain elements around and things. Currently I add an event listener to an element that exists before I call the API, with useEffect() like so:
useEffect(() => {
/*
Some code here...
*/
const setupDashboard = (dashboard) => {
document.querySelector('#run').addEventListener('click', () => {
dashboard.send('dashboard:run')
console.log("Run clicked")
})
console.log(document.querySelector("[data-title='States']"))
}
LookerEmbedSDK.createDashboardWithId(114)
.appendTo('#dashboard')
.on('dashboard:run:start',
/*() => updateState('#dashboard-state', 'Running')*/
)
.on('dashboard:run:complete',
/*() => updateState('#dashboard-state', 'Done')*/
)
.build()
.connect()
.then(setupDashboard)
.catch((error) => {
console.error('An unexpected error occurred', error)
})
});
The piece of code that adds the event handler to the button works, but the next console statement returns null. So it seems like there should be a better way to manipulate the DOM especially since I'm going to completely change it with all my modifications. I'll need to use all sorts of query selectors, deletions, appending and more. What's the best practice for all that.
Edit: to be clear, after the page loads if I run that line in the console it gets the element
I found the answer to my particular situation. In my case, the second query selector doesn't work because the embedded element is in an iframe that represents a second document and is therefore inaccessible.
However, if you're working with react and you need a way to mutate the DOM might I suggest using
ReactDOM.shouldComponentUpdate = () => false
found in this article https://www.reddit.com/r/reactjs/comments/2riuwa/mutating_reacts_dom/
I'm using Cypress 7.7.0 (also tested on 8.0.0), and I'm running into an interesting race condition. I'm testing a page where one of the first interactions that Cypress does is click a button to open a modal. To keep bundle sizes small, I split the modal into its own prefetched webpack chunk. My Cypress test starts with cy.get('#modal-button').click() but this doesn't load the modal because the modal hasn't finished downloading/loading. It does nothing instead (doesn't even throw any errors to the console). In other words, Cypress interacts with the page too quickly. This was also reproduced with manual testing (I clicked on the button super fast after page load). I have tried setting the modal to be preloaded instead, but that didn't work either.
I am able to solve the problem by introducing more delay between page load and button interaction. For example, inserting any Cypress command (even a cy.wait(0)) before I click on the button fixes the solution. Cypress, however, is known for not needing to insert these brittle solutions. Is there a good way to get around this? I'd like to keep the modal in its own chunk.
FYI: I'm using Vue as my front end library and am using a simple defineAsyncComponent(() => import(/* webpackPrefetch: true */ './my-modal.vue')) to load the modal component. I figure that this problem is general to Cypress though.
There's nothing wrong with cy.wait(0).
All you are doing is handing control from the test to the next process in the JS queue, in this case it's the app's startup script which is presumably waiting to add the click handler to the button.
I recently found that this is also needed in a React hooks app to allow the hook to complete it's process. You will likely also come across that in Vue 3, since they have introduced a hook-like feature.
If you want to empirically test that the event handler has arrived, you can use the method given here (modified for click()) - When Can The Test Start?
let appHasStarted
function spyOnAddEventListener (win) {
const addListener = win.EventTarget.prototype.addEventListener
win.EventTarget.prototype.addEventListener = function (name) {
if (name === 'click') {
appHasStarted = true
win.EventTarget.prototype.addEventListener = addListener // restore original listener
}
return addListener.apply(this, arguments)
}
}
function waitForAppStart() {
return new Cypress.Promise((resolve, reject) => {
const isReady = () => {
if (appHasStarted) {
return resolve()
}
setTimeout(isReady, 0) // recheck "appHasStarted" variable
}
isReady()
})
}
it('greets', () => {
cy.visit('app.html', {
onBeforeLoad: spyOnAddEventListener
}).then(waitForAppStart)
cy.get('#modal-button').click()
})
But note setTimeout(isReady, 0) will probably just achieve the same as cy.wait(0) in your app, i.e you don't really need to poll for the event handler, you just need the app to take a breath.
It seems like your problem is that you're already rendering a button before the code backing it is loaded. As you noticed, this isn't only an issue for fast automated bots, but even a "regular" user.
In short, the solution is to not display the button early, but show a loading dialog instead. Cypress allows waiting for a DOM element to be visible with even a timeout option. This is more robust than a brittle random wait.
I ended up going with waiting for the network to be idle, although there were several options available to me.
The cypress function I used to do this was the following which was heavily influenced by this solution for waiting on the network:
Cypress.Commands.add('waitForIdleNetwork', () => {
const idleTimesInit = 3
let idleTimes = idleTimesInit
let resourcesLengthPrevious
cy.window().then(win =>
cy.waitUntil(() => {
const resourcesLoaded = win.performance.getEntriesByType('resource')
if (resourcesLoaded.length === resourcesLengthPrevious) {
idleTimes--
} else {
idleTimes = idleTimesInit
resourcesLengthPrevious = resourcesLoaded.length
}
return !idleTimes
})
)
})
Here are the pros and cons of the solution I went with:
pros: no need to increase bundle size or modify client code when the user will likely never run into this problem
cons: technically still possible to have a race condition where the click event happens after the assets were downloaded, but before they could all execute and render their contents, but very unlikely, not as efficient as waiting on the UI itself for indication of when it is ready
This was the way I chose solve it but the following solutions would have also worked:
creating lightweight placeholder components to take the place of asychronous components while they download and having cypress wait for the actual component to render (e.g. a default modal that just has a spinner being shown while the actual modal is downloaded in the background)
pros: don't have to wait on network resources, avoids all race conditions if implemented properly
cons: have to create a component the user may never see, increases bundle size
"sleeping" an arbitrary amount (although this is brittle) with cy.wait(...)
pros: easy to implement
cons: brittle, not recommended to use this directly by Cypress, will cause linter problems if using eslint-plugin-cypress (you can disable eslint on the line that you use this on, but it "feels ugly" to me (no hate on anyone who programs that way)
This is the first time I get my hands on with automation instruments in xcode The script works well for all button taps but the one making server connection. I don't know the reason
Here is the script I tried so far
var target = UIATarget.localTarget();
target.pushTimeout(4);
target.popTimeout();
var window=target.frontMostApp().mainWindow()
var appScroll=window.scrollViews()[0];
appScroll.logElementTree();
UIATarget.localTarget().delay(2);
appScroll.buttons()[1].tap();
The above script works up to showing the UIActivityIndicator instead of moving to next controller after success
I know There must be a very simple point I am missing. So help me out
UIAutomation attempts to make things "easy" for the developer, but in doing so it can make things very confusing. It sounds like you're getting a reference to window, waiting for a button to appear, then executing .tap() on that button.
I see that you've already considered messing with target.pushTimeout(), which is related to your issue. The timeout system lets you do something that would be impossible in any sane system: get a reference to an element before it exists. I suspect that behind-the-scenes, UIAutomation repeatedly attempts to get the reference you want -- as long as the timeout will allow.
So, in the example you've posted, it's possible for this "feature" to actually hurt you.
var window=target.frontMostApp().mainWindow()
var appScroll=window.scrollViews()[0];
UIATarget.localTarget().delay(2);
appScroll.buttons()[1].tap();
What if the view changes during the 2-second delay? Your reference to target.frontMostApp().mainWindow.scrollViews()[0] may be invalid, or it may not point to the object you think you're pointing at.
We got around this in our Illuminator framework by forgetting about the timeout system altogether, and just manually re-evaluating a given reference until it actually returns something. We called it waitForChildExistence, but the functionality is basically as follows:
var myTimeout = 3; // how long we want to wait
// this function selects an element
// relative to a parent element (target) that we will pass in
var selectorFn = function (myTarget) {
var ret = myTarget.frontMostApp().mainWindow.scrollViews()[0];
// assert that ret exists, is visible, etc
return ret;
}
// re-evaluate our selector until we get something
var element = null;
var later = get_current_time() + myTimeout;
while (element === null && get_current_time() < later) {
try {
element = selectorFn(target);
} catch (e) {
// must not have worked
}
}
// check whether element is still null
// do something with element
For cases where there is a temporary progress dialog, this code will simply wait for it to disappear before successfully returning the element you want.
I'm on my third day working with Protractor and I'm constantly hitting bric walls in regards to waiting around for pages to load and elements to appear. This test case in particular has grown ugly and I would like to solve the issues without having to rely on sleeps.
I am currently "outside of the land of AngularJS"
it('it should reflect in both the field and the title when the personnel name is changed', function() {
var inputField, personnelHeader, personnelName;
personnelName = element(By.css(".overlay.editnameoverlay")).click();
personnelHeader = element(By.id("personnel_name_header"));
inputField = element(By.css("input[name='newvalue']"));
inputField.clear();
inputField.sendKeys("Test 123");
element(By.css("input[name='ok_button']")).click();
// browser.driver.sleep(2000); This test only works with this sleep added
browser.wait(function() {
console.log("Waiting for header to change...");
return personnelHeader.getText().then(function(text) {
return text === "Test 123";
});
}, 5000);
return expect(personnelHeader.getText()).toBe(personnelName.getText());
});
So the test here changes the name in an input field. submits it and waits for the changes to become reflected in the header of the modal. The problem is that without the browser.driver.sleep(2000) I get an error saying
Stacktrace:
StaleElementReferenceError: stale element reference: element is not attached to the page document
How do I go about solving this in this particular case?
From the documentation for Expect Conditions:
var EC = protractor.ExpectedConditions;
// Waits for the element with id 'abc' to contain the text 'foo'.
browser.wait(EC.textToBePresentInElement($('#abc'), 'foo'), 5000);
When you use Protractor to test for non-angular pages you're on your own regarding waiting for elements to be ready for interaction.
StaleElementReferenceError is probably the most useless selenium error, it happens when the element got removed from the DOM but is still cached somehow, I also suffered this problem when started with Protractor and even tried to convince it should be automatically retried Protractor-side.
The solution for me is to always explicitly wait for an element to appear on the page using a custom function waitReady() that browser.wait for elements ready, i.e: waits for the element to be ready for interaction:
expect($('#login_field').waitReady()).toBeTruthy();
First integrate this snippet in your code: https://gist.github.com/elgalu/2939aad2b2e31418c1bb
Not only the custom waitReady() waits for the element but it also swallows any unrelated useless webdriver error like StaleElementReferenceError and will simply retry up until finding the element or it will timeout.
So waitReady() each element before interacting, i.e. before clear() or sendKeys() or click() ...
// TODO: Move to Page Objects module
var personnelNameElm = $(".overlay.editnameoverlay");
var personnelHeaderElm = $("#personnel_name_header");
var inputFieldElm = $("input[name='newvalue']");
var okBtnElm = $("input[name='ok_button']");
it('it should reflect in both the field and the title when the ' +
'personnel name is changed', function() {
expect(personnelNameElm.waitReady()).toBeTruthy();
personnelNameElm.click();
expect(inputFieldElm.waitReady()).toBeTruthy();
inputFieldElm.clear().sendKeys("Test 123");
expect(okBtnElm.waitReady()).toBeTruthy();
okBtnElm.click();
browser.wait(function() {
console.log("Waiting for header to change...");
// Using waitReady() before getText() avoids Stale element errors
return personnelHeaderElm.waitReady().then(function() {
return personnelHeaderElm.getText().then(function(text) {
return text === "Test 123";
});
});
}, 5000);
expect(personnelHeaderElm.getText()).toEqual(personnelNameElm.getText());
});
I'm having a rather mind-boggling problem with some JS I'm working on for a web app. Unfortunately, I can't post the full code for this, as it's part of a not-yet-released project. This issue has both me and some colleagues I asked stumped - from what I can tell, it should work.
Now, to the actual problem: a user enters some info into some form fields and clicks a "confirm" image, whereupon an AJAX request is sent back to the server. The server does some processing, then sends back a response with a status and some attached data. A status message is displayed in the modal dialogue window the user was using, and an icon with a link is displayed. Here's the "onComplete" Handler for the Mootools Request.JSON object, with some error condition handling removed:
onComplete: function(response) {
if (response) {
if (response.status == 0) {
// this means the request was successful
licenses = response.licenses;
updateControls();
licenseList();
// here I add the status message...
$("createform_result").innerHTML = "<img src=\'/media/com_supportarea/images/db_success.png\' /> License created. Download:<br /><br />";
// ...and the download "link"
if (response.tlsid) {
$("createform_result").innerHTML += "<img src=\'/media/com_supportarea/images/download_small.png\' /> <em>TLS</em>";
// this line is here for debugging only, to make sure this
// block of code is run (it is) and the element is found (it is)
$("newtlslic-"+response.tlsid).style.border = "1px solid red";
$("newtlslic-"+response.tlsid).addEvent("click", function(e) {
// I've stripped out all other code, also for debugging
e.stop();
});
}
}
}
}
The message and icon with link appears, the style is applied (red border appears) and no error message appears in either Firefox or Chrome. However, clicking the icon results in a # being appended to the URL (the e.stop()) does nothing). According to the EventBug plugin, no click event is attached to the link. It seems like .addEvent() simply does nothing here.
Now, and here's they prize question: why is this and how can I fix it? Help!
strings in javascript are immutable. when you do stuff like:
$("createform_result").innerHTML += "<img src=\'/media/com_supportarea/images/download_small.png\' /> <em>TLS</em>";
you are referencing the innerHTML as a string. what this does is, it fetches the property into a string, concatenates it to the other strings you pass on and then returns a new string in the end, which gets set as the innerHTML.
in doing so, you are OVERWRITING the contents of the element every time, for every iteration.
events attached to elements are not done by a generic ID handler - they rely on the element being in the DOM, then the element UID (an internal property mootools assigns to all passed elements) is being read and the event callback is added into the element storage behind that UID.
you can see this work by doing console.log(element.retrieve("events"));
if you rewrite the innerHTML, the inner element is re-created and gets a NEW UID, which means the callback now points to an empty pointer as the UID is the key in element storage.
I may be wrong about what you are doing here as I don't actually see the bit where you rewrite it again, but there probably is one in the code you stripped, especially if you are running a loop.
the best way to deal with this is something else - use Event Delegation.
it can allow you to add the click event to the parent element instead via some selector. this will work for ANY element added in any way at any time that matches.
eg.
// add this once, outside the loop
$("createform_result").addEvent("click:relay(a.editLink)", function(event, element) {
console.log(this === element);
console.log(this === event.target);
console.log(this.get("data-id"));
});
// then as you loop the results, just inject the els or use innerHTML or whatever...
new Element("a.editLink", {
html: '<img src=\'/media/com_supportarea/images/download_small.png\' /></a> <em>TLS</em>',
"data-id": response.tlsid
}).inject($("createform_result"));
Event delegation is now a part of mootools core in 1.4.0 or is in mootools-more in previous versions.
have fun!