简体   繁体   中英

Java - Wait for JavaScript event (e.g. onchange) to complete using Selenium

A set of logically related input fields have an onchange event. After iterating over the fields and modifying their values some get updated correctly and some don't because of the stuff done by onchange event.

The moment the onchange event triggers on a field, it starts some processing (involving other related fields), stores the value somewhere and clears other related fields if they weren't previously processed by their own onchange event.

I could put the thread to sleep for an arbitrary amount of time but that doesn't look good. It would be just guessing how much time is going to take the processing and choosing between wasting time in a generous sleep or having a script that can abort due timeouts.

Is there a way to know when the JavaScript code (which is called by the onchange event) has finished doing its work?

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

With sleep: Field1 : _w_ ... Field2 : _x_ ... Field3 : _y_ ... FieldN : _z_

Without sleep: Field1 : _w_ ... Field2 : ___ ... Field3 : _y_ ... FieldN : ___

Notes:

I experienced some issues in the way so I just think it's worth mentioning the lessons learned in brief notes:

WARNING : Do not mix implicit and explicit waits.

Use WebDriverWait (specialization of FluentWait ) instead of FluentWait , unless you have a very specific requirement. Eg, WebDriverWait ignores NotFoundException ( NoSuchElementException 's superclass) by default. See recomendation .

After serious refactoring and some research, I finally made it. The onchange event fires when the value of the input field is changed and the element loses focus. Manipulating the fields with WebElement (eg sendKeys() ) is not an option because you are not in control of the background processing, so using JavascriptExecutor is the choice. First, I updated the value of the field using JavaScript (which does NOT trigger the event), and after that, I triggered 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));
}

There are some key aspects here.

  • Do NOT mix implicit and explicit waits (I learnt it the hard way), it will cause unexpected results.
  • Use WebDriverWait (specialization of FluentWait ) instead of its superclass, unless you have a very specific requirement. If you use FluentWait make sure to ignore appropriate exceptions; otherwise you will start getting NoSuchElementException .
  • The onchange event fires when the value of the input field is changed and the element loses focus.
  • dispatchEvent() dispatches an Event at the specified EventTarget, ( synchronously ) invoking the affected EventListeners in the appropriate order. This applies for custom events as well. This is a very nice post for events.

I used the following code to get a better grasp of ExpectedConditions.javaScriptThrowsNoExceptions and ExpectedConditions.jsReturnsValue . It is a JavaScript function call that just keeps the engine busy for few seconds. That way you can see the interaction of the explicit wait with JavaScript and inspect the return values. Notice that the JS code is slightly different for each ExpectedCondition :

//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);

You might try this method which waits until JQuery is set to inactive:

/**
 * Wait until JQuery is inactive
 * @author Bill Hileman
 */
public void waitForJQueryToBeInactive() {

    Boolean isJqueryUsed = (Boolean) ((JavascriptExecutor) driver)
            .executeScript("return (typeof(jQuery) != 'undefined')");

    if (isJqueryUsed) {
        while (true) {
            // JavaScript test to verify jQuery is active or not
            Boolean ajaxIsComplete = (Boolean) (((JavascriptExecutor) driver)
                    .executeScript("return jQuery.active == 0"));
            if (ajaxIsComplete)
                break;
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
            }
        }
    }

}

To my best of knowledge, you can't wait for an asynchronous event using Selenium Web Driver. You may, however, be able to wait for effects of that event using WebDriverWait class. If events You mentioned, make some changes in the DOM, you can detect those changes by using selected ExpectedConditions.

   Wait<WebDriver> wait = new WebDriverWait(driver, 15, 500);

   // here you make some change to the input

   wait.until(ExpectedConditions.elementToBeClickable(By.xpath("//input")));

This simple example would wait until a button is active. If the expected conditions are not met in the next 15s exception will be thrown.

In case, that provided set of ExpectedConditions is insufficient you can always create your own by implementing ExpectedCondition interface. For more information visit the documentation.

https://seleniumhq.github.io/selenium/docs/api/java/org/openqa/selenium/package-summary.html https://seleniumhq.github.io/selenium/docs/api/java/org/openqa/selenium/support/ui/package-summary.html

The following routine checks the document ready state for "complete" and returns when the page is finished loading/changing.

/**
 * Wait for the web page to finish loading
 * @author Bill Hileman
 */
public void waitForPageToLoad() {

    WebDriverWait wait = new WebDriverWait(driver, 10);
    wait.until(new ExpectedCondition<Boolean>() {

        public Boolean apply(WebDriver wdriver) {
            return ((JavascriptExecutor) driver).executeScript("return document.readyState").equals("complete");
        }
    });
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM