Java – Use Selenium to wait for JavaScript events such as onchange to complete
A set of logically related input
fields has an onchange
event. After traversing the fields and modifying their values, some fields are updated correctly and others are not updated correctly because of what the onchange
event does.
The moment the onchange
event fires on a field, it starts some processing (involving other related fields), stores the values somewhere and clears other related fields (if they haven’t been handled by itself before) with the onchange
event.
I can let threads hibernate any time, but that doesn’t look good. It simply guesses how much time processing will take and then chooses whether to waste time in plenty of sleep or have a script that can abort the timeout.
Is there a way to know when the JavaScript code (invoked by the onchange
event) has done its job?
Original code
Wait<WebDriver> wait = new WebDriverWait(driver, 25, 500);
for(int i = 1; i <= fieldCount; i++) {
elementId = "field$" + i;
wait.until(ExpectedConditions.elementToBeClickable(By.id(elementId)));
driver.findElementById(elementId).sendKeys(data);
The mess happens if I don't sleep
Thread.sleep(3000);
}
Output
Sleep time: Field1
: _w_
… Field2
:_x_
… Field3
:_y_
… FieldN
:_z_
No sleep :Field1
:_w_
… Field2
:___
… Field3
:_y_
… FieldN
:___
Notes:
I
ran into some issues, so I think it’s worth mentioning the lessons learned in the short note:
WARNING: Do not mix implicit and explicit waits.
Use
WebDriverWait
(specialization ofFluentWait
) instead ofFluentWait
, unless you have a very specific requirement. E.g.,WebDriverWait
ignoresNotFoundException
(NoSuchElementException
‘s superclass) by default. See recomendation.
Solution
After a serious refactoring and some research, I finally did it. The onchange
event fires when the value of the input
field changes and the element loses focus. Manipulating fields using WebElements
(e.g. sendKeys())
is not an option because you have no control over background processing, so using JavascriptExecutor
is a choice. First, I used JavaScript (which doesn’t fire an event) to update the value of the field, and then I fired the onchange
event, also using JavaScript:
//Setting implicit wait to 0 to avoid mess with explicit waits
driver.manage().timeouts().implicitlyWait(0, TimeUnit.SECONDS);
Use WebDriverWait instead of FluentWait (it's superclass)
Wait<WebDriver> wait = new WebDriverWait(driver, 25, 500);
for(int i = 1; i <= fieldCount; i++) {
String elementId = "field$" + i;
String javaScript = String.format("document.getElementById('%s').value='%s';" , elementId , myValue);
Object jsResult = wait.until(ExpectedConditions.javaScriptThrowsNoExceptions(javaScript));
javaScript = String.format("return document.getElementById('%s').dispatchEvent(new Event('change'));" , elementId);
jsResult = wait.until(ExpectedConditions.jsReturnsValue(javaScript));
}
Here are some key aspects.
- Don’t mix implicit and explicit waiting (which I’ve learned from the hard lesson), which can lead to unexpected results.
- Use
WebDriverWait
(a specialization ofFluentWait
) instead of its parent class (super class), unless you have very specific requirements. If you useFluentWait
, make sure to ignore the appropriate exceptions; Otherwise, you will start receivingNoSuchElementException
. The onchange
event fires when the value of theinput
field changes and the element loses focus.-
dispatchEvent()
in the specified EventTarget ( synchronously ) dispatch an event, Call the affected EventListeners in the appropriate order. This applies to custom events as well. This is very nice post for activity.
I use the following code to get a better grasp of ExpectedConditions.javaScriptThrowsNoExceptionsand
ExpectedConditions.jsReturnsValue
This is a JavaScript function call that only keeps the engine busy for a few seconds. This allows you to see the explicit wait for interaction with JavaScript and examine the return value. Note that the JS code for each ExpectedCondition
is slightly different:
//ExpectedConditions.jsReturnsValue
String javaScript = "(function watcher(ms){var start=new Date().getTime(); var end = start; while(end<start+ms){end=new Date().getTime(); }; return 'complete'; })(5000); return 'success'; ";
log.trace("javaScript={}", javaScript);
Object jsResult = wait.until(ExpectedConditions.jsReturnsValue(javaScript));
log.trace("jsResult={}", jsResult);
ExpectedConditions.javaScriptThrowsNoExceptions
javaScript = "(function watcher(ms){var start=new Date().getTime(); var end = start; while(end<start+ms){end=new Date().getTime(); }; return 'complete'; })(5000); ";
log.trace("javaScript={}", javaScript);
jsResult = wait.until(ExpectedConditions.javaScriptThrowsNoExceptions(javaScript));
log.trace("jsResult={}", jsResult);