Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Detect from selenium if angular is doing ... stuff

Why does driver.findElement(<some static element ng-if=simplebool>static text</some>).getText() ever return ""?

I have an angular app that I am testing with selenium via chromedriver via java on osx.

I have some markup that looks like:

<h1 id="my-unique-id" ng-if="model.shouldDisplayThisAttribute">static text</h1>

Very frequently I get:

assert(driver.findElement(By.id("my-unique-id").getText().contains("static text");

yielding:

java.lang.AssertionError: Not true that <""> contains <"static text">

Like, 30% of the time.

I don't understand how that element's .getText() could evaluate to "", so I'm assuming that angular is either $digesting or $compiling the page. That's fine. It's fine. It's fine.

I want to know when angular has finished $compiling and $digesting and $watching, so I can look at the page and see what is.

If I add:

(function() 
   function angularAppearsToBeIdle(callback) {
     var untilNextDigest,
        isIdle = angular.element(document.body).scope().$watch(function () {
           clearTimeout(untilNextDigest);
           untilNextDigest = setTimeout(function () {
           isIdle();
           callback('done');
         }, 100);
      });
    }
 angularAppearsToBeIdle(console.log.bind(console));
}());

to my page, I see the console.log messages at the time that I expect them.

If I paste this into the console:

(function() {
   function angularAppearsToBeIdle(callback) {
       var untilNextDigest,
       isIdle = angular.element(document.body).scope().$watch(function () {
           clearTimeout(untilNextDigest);
           untilNextDigest = setTimeout(function () {
               isIdle();
               callback('done');
           }, 100);
       });
   }
   angularAppearsToBeIdle(console.log.bind(console));
}());

I get 'undefined'.

Ultimately, what I'd like to do is from Java:

@Test
public void clickOnSomethingThatIsProbablyNotGoingToChange(driver.findElement(By.id("some-id"));

private WebElement idleElement() {

    new WebDriverWait(driver, 30).until(new Predicate<WebDriver>() {
        @Override
        public boolean apply(WebDriver input) {
            Object result = ((JavascriptExecutor) driver).executeScript("return window.angularAppearsToBeIdle;");
            return result.equals("idle");
        }
   }

I have tried the following, but there are no examples of Selenium doing anything like this:

public void waitForAngular() {

    ((JavascriptExecutor) driver).executeScript(
            " window.angularAppearsToBeIdle = 'not idle'; ");

    ((JavascriptExecutor) driver).executeScript(
            " window.signalThatAngularAppearsToBeIdle = function(signal) { " +
            " var untilNextDigest, " +
            " isIdle = angular.element(document.body).scope().$watch(function () { " +
            "    clearTimeout(untilNextDigest); " +
            "    untilNextDigest = setTimeout(function () { " +
            "       isIdle(); " +
            "       signal = 'idle'; " +
            "    }, 100); " +
            "  }); " +
            " } ");

    driver.manage().timeouts().setScriptTimeout(10, TimeUnit.SECONDS);

    ((JavascriptExecutor) driver).executeAsyncScript(
            " var callback = arguments[arguments.length - 1]; " +
                    " signalThatAngularAppearsToBeIdle(callback) "
            ,
            " window.angularAppearsToBeIdle ");

    new WebDriverWait(driver, 30).until(new Predicate<WebDriver>() {
        @Override
        public boolean apply(WebDriver input) {
            Object result = ((JavascriptExecutor) driver).executeScript("return window.angularAppearsToBeIdle;");
            return result.equals("idle");

        }
    });

How can I detect from Selenium if angular is busy doing stuff?

like image 860
Neil Cronin Avatar asked Sep 02 '15 05:09

Neil Cronin


3 Answers

You will want to write a custom ExpectedCondition that queries angular to see if it is done. Here's one I prepared earlier:

    public static ExpectedCondition angularHasFinishedProcessing() {
        return new ExpectedCondition<Boolean>() {
            @Override
            public Boolean apply(WebDriver driver) {
                String hasAngularFinishedScript = "var callback = arguments[arguments.length - 1];\n" +
                        "var el = document.querySelector('html');\n" +
                        "if (!window.angular) {\n" +
                        "    callback('false')\n" +
                        "}\n" +
                        "if (angular.getTestability) {\n" +
                        "    angular.getTestability(el).whenStable(function(){callback('true')});\n" +
                        "} else {\n" +
                        "    if (!angular.element(el).injector()) {\n" +
                        "        callback('false')\n" +
                        "    }\n" +
                        "    var browser = angular.element(el).injector().get('$browser');\n" +
                        "    browser.notifyWhenNoOutstandingRequests(function(){callback('true')});\n" +
                        "}";

                JavascriptExecutor javascriptExecutor = (JavascriptExecutor) driver;
                String isProcessingFinished = javascriptExecutor.executeAsyncScript(hasAngularFinishedScript).toString();

                return Boolean.valueOf(isProcessingFinished);
            }
        };
    }

This is assuming that your have defined ng-app on your html element. If you define it elsewhere in the DOM you will need to update the code above. This is pretty much stolen from protractor, it's checking to see if there are any outstanding ajax requests and checking to see if angular thinks it's in a testable state.

The important part is to execute this JavaScript snippet as an async script because angular is using promises to trigger a callback when it is ready. If you don't run this as an async script it will not work.

You will also need to set a script timeout on the driver object, this is the amount of time selenium will wait for the callback to complete before throwing a timeout exception. You can set it as follows:

driver.manage().timeouts().setScriptTimeout(15, TimeUnit.SECONDS);

It's then as simple as plugging this ExpectedCondition into a wait like so:

WebDriverWait wait = new WebDriverWait(driver, 15, 100);
wait.until(angularHasFinishedProcessing());

Hope this helps.

like image 74
Ardesco Avatar answered Sep 22 '22 02:09

Ardesco


I'm writing in C#.Net, and so I had to translate Ardesco's answer:

static class HelperFunctions
{

    public static bool AngularHasFinishedProcessing(IWebDriver driver)
    {
        string HasAngularFinishedScript = 
            @"var callback = arguments[arguments.length - 1];
            var el = document.querySelector('html');
            if (!window.angular) {
                callback('False')
            }
            if (angular.getTestability) {
                angular.getTestability(el).whenStable(function(){callback('True')});
            } else {
                if (!angular.element(el).injector()) {
                    callback('False')
                }
                var browser = angular.element(el).injector().get('$browser');
                browser.notifyWhenNoOutstandingRequests(function(){callback('True')});
            }";
        IJavaScriptExecutor javascriptExecutor = (IJavaScriptExecutor)driver;
        return Convert.ToBoolean(javascriptExecutor.ExecuteAsyncScript(HasAngularFinishedScript));
    }
}

To call it, I used:

WebDriverWait wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10));
wait.Until((d) => { return HelperFunctions.AngularHasFinishedProcessing(driver); });
like image 35
Roger Cook Avatar answered Sep 18 '22 02:09

Roger Cook


Tossing Python 2.7 into the mix:

class angular_is_ready(object):
    script = """var callback = arguments[arguments.length - 1];
    var el = document.querySelector('html');
    if (!window.angular) {
        callback(false)
    }
    if (angular.getTestability) {
        angular.getTestability(el).whenStable(function(){callback(true)});
    } else {
        if (!angular.element(el).injector()) {
            callback(false)
        }
        var browser = angular.element(el).injector().get('$browser');
        browser.notifyWhenNoOutstandingRequests(function(){callback(true)});
    };"""

    def __call__(self, driver):
        try:
            return driver.execute_async_script(self.script)
        except:
            return False

# Define driver and timeout,
# then use the condition like so:
WebDriverWait(driver, timeout).until(angular_is_ready)
like image 40
Monkpit Avatar answered Sep 22 '22 02:09

Monkpit