I have a page which has a small form with one input field and a submit button. The submit button is AJAX! When I click it, it submits the input value to the server which either validates it as acceptable and loads a new page, or else finds a problem and adds a feedback error label.
I have written the following selenium code to test this functionality:
WebDriverWait waiter = new WebDriverWait(driver, 20);
WebElement inputField = waiter.until(ExpectedConditions.presenceOfElementLocated(By.id("inputField")));
inputField.sendKeys("Test input");
WebElement submitInput = waiter.until(ExpectedConditions.presenceOfElementLocated(By.id("submitInput")));
submitInput.click();
WebElement feedback = waiter.until(ExpectedConditions.presenceOfElementLocated(By.id("feedback")));
Assert.assertTrue("Feedback not was not empty, but was: " + feedback.getText(), feedback.getText().isEmpty());
The problem is that submitInput.click() completes and returns straight away if the new page is loaded via an event as per: the selenium docs, so the call to retrieve by “feedback” works fine, and I can see through debugging that it gets the element correctly, however I get a StaleElementException when I call feedback.getText(), because the page has since refreshed in the background.
How can I get the test to pass without:
- Adding Thread.sleep().
- Adding a
StaleElementExceptioncatch to refetch the element.
Note: The above test works when the input is invalid because the page is not refreshed, but not when the input is valid.
Any help would be much appreciated.
EDIT: Please note this is not a problem with polling the page until an element appears, but is a problem with detecting whether submitInput.click() loads a new page or adds text to an existing label.
EDIT 2: To illustrate the problem I have compiled a time logged run-through interleaving the test and server logs:
16:34:48,589 - TEST - Navigating to page.
16:34:48,605 - SERVER - Page request started.
16:34:48,631 - SERVER - Page request ended.
16:34:49,050 - TEST - Typing test input.
16:34:49,456 - TEST - Clicking button.
16:34:49,519 - SERVER - Received click event.
16:34:49,529 - SERVER - Responding with redirect.
16:34:49,556 - TEST - Finding feedback by id.
16:34:49,560 - SERVER - Page request started.
16:34:49,581 - SERVER - Page request ended.
16:34:49,775 - TEST - Test threw exception.
You can see that the server receives the click event just over half a second after it’s sent from the test. It immediately responds with (in this case) a redirect command. The test has meanwhile continued in the background as per the docs due to the click being an AJAX event, and this results in (as you can see) the test retrieving the feedback 0.004 seconds before the server receives a new page request. By the time feedback.getText() has been parsed and sent to ChromeDriver, the server has already responded with a new page, and therefore this function returns the stale exception.
The key point really is that I can fix this fine using Thread.sleep(1000), but this is far from ideal; firstly because there may be occasions where the server takes longer than half a second to parse the AJAX and return a result, and this makes the tests non-deterministic, and secondly because these times soon stack up and waste time, usually unnecessarily when only a few milliseconds was required.
My solution in the end for anyone who has a similar problem (which was two different problems, 1 – detecting an AJAX controlled page change, and 2 – detecting an AJAX controlled element change), was
step 1: Create a few functions to track page changes through jQuery (jQuery is used on all pages, so the script executes fine):
I can then simply call
pageChanged()to detect a page change.step 2: to create an anonymous class which implements
ExpectedCondition<Boolean>.Using my example from above where the problem was that feedback is non-deterministic regarding when it will be ready, I changed the bottom two lines from the code in the question to:
This way, it polls the page every 200 milliseconds for 5 seconds ignoring assertion exceptions and stale element exceptions. The only way it passes is if the
return true;line is reached before the 5 seconds has run out. I’m currently thinking about how to make this neater but it works great, is faster than simply sleeping, and succeeds early (when possible) due to the polling.