29

I have a SPA application on stack ASP MVC + AngularJS and I'd like to test the UI. For now I'm trying Selenium with PhantomJS and WebKit drivers.

This is a sample testing page - view with single element. The list items <li> load dynamically from server and are bounded by Angular.

<div id="items">
    <li>text</li>
    <li>text2</li>
</div>

I'm trying to pass a test and there is an error in this line:

_driver.FindElements(By.TagName('li'))

At this point there are no loaded elements and _driver.PageSource doesn't contain elements.

How can I wait for the items to load? Please do not suggest Thread.Sleep()

derloopkat
  • 6,232
  • 16
  • 38
  • 45
dr11
  • 5,166
  • 11
  • 35
  • 77

12 Answers12

41

This will wait for page loads / jquery.ajax (if present) and $http calls, and any accompanying digest/render cycle, throw it in a utility function and wait away.

/* C# Example
 var pageLoadWait = new WebDriverWait(WebDriver, TimeSpan.FromSeconds(timeout));
            pageLoadWait.Until<bool>(
                (driver) =>
                {
                    return (bool)JS.ExecuteScript(
@"*/
try {
  if (document.readyState !== 'complete') {
    return false; // Page not loaded yet
  }
  if (window.jQuery) {
    if (window.jQuery.active) {
      return false;
    } else if (window.jQuery.ajax && window.jQuery.ajax.active) {
      return false;
    }
  }
  if (window.angular) {
    if (!window.qa) {
      // Used to track the render cycle finish after loading is complete
      window.qa = {
        doneRendering: false
      };
    }
    // Get the angular injector for this app (change element if necessary)
    var injector = window.angular.element('body').injector();
    // Store providers to use for these checks
    var $rootScope = injector.get('$rootScope');
    var $http = injector.get('$http');
    var $timeout = injector.get('$timeout');
    // Check if digest
    if ($rootScope.$$phase === '$apply' || $rootScope.$$phase === '$digest' || $http.pendingRequests.length !== 0) {
      window.qa.doneRendering = false;
      return false; // Angular digesting or loading data
    }
    if (!window.qa.doneRendering) {
      // Set timeout to mark angular rendering as finished
      $timeout(function() {
        window.qa.doneRendering = true;
      }, 0);
      return false;
    }
  }
  return true;
} catch (ex) {
  return false;
}
/*");
});*/
Artjom B.
  • 61,146
  • 24
  • 125
  • 222
npjohns
  • 2,218
  • 1
  • 17
  • 16
28

Create a new class that lets you figure out whether your website using AngularJS has finished making AJAX calls, as follows:

import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.support.ui.ExpectedCondition;

public class AdditionalConditions {
    public static ExpectedCondition<Boolean> angularHasFinishedProcessing() {
        return new ExpectedCondition<Boolean>() {
            @Override
            public Boolean apply(WebDriver driver) {
                return Boolean.valueOf(((JavascriptExecutor) driver).executeScript("return (window.angular !== undefined) && (angular.element(document).injector() !== undefined) && (angular.element(document).injector().get('$http').pendingRequests.length === 0)").toString());
            }
        };
    }
}

You can use it anywhere in the your code by using the following code:

WebDriverWait wait = new WebDriverWait(getDriver(), 15, 100);
wait.until(AdditionalConditions.angularHasFinishedProcessing()));
Shaz
  • 597
  • 4
  • 5
  • 1
    Perfect solution to the modern web app designs i have to work with. – shadowcharly Dec 22 '16 at 11:23
  • I get the following message when I execute: "unknown error: angular is not defined" – Big Al May 17 '17 at 19:30
  • This worked perfectly for me - awesome job! I [converted this c# version to typescript for my use case](https://stackoverflow.com/a/44206851/175679) – SliverNinja - MSFT May 26 '17 at 17:08
  • Thanks Shahzaib. Can someone add a commentary on code snippet. – Manav Sharma Sep 19 '17 at 05:50
  • Thank you so much, @Shahzaib Salim, this fixed my problem to handle page loads dynamically. – Bhuvanesh Mani Dec 15 '17 at 06:59
  • I had to adjust the script string and replace "document" with "document.body" because my outermost angular scope was on the body, otherwise I got undefined. @ManavSharma: WebDriverWait creates an object that you can call until on to wait for a condition. The condition object has apply() that returns true when condition is meet. The script runs as JavaScript in browser, angular.element wraps passed obj with JQuery plus more. One it adds is injector function which you can use to get the $http angular service to see if there are pending requests or not. At least that is my understanding. – Integrator May 03 '20 at 20:45
6

We have had a similar issue where our in house framework is being used to test multiple sites, some of these are using JQuery and some are using AngularJS (and 1 even has a mixture!). Our framework is written in C# so it was important that any JScript being executed was done in minimal chunks (for debugging purposes). It actually took a lot of the above answers and mashed them together (so credit where credit is due @npjohns). Below is an explanation of what we did:

The following returns a true / false if the HTML DOM has loaded:

        public bool DomHasLoaded(IJavaScriptExecutor jsExecutor, int timeout = 5)
    {

        var hasThePageLoaded = jsExecutor.ExecuteScript("return document.readyState");
        while (hasThePageLoaded == null || ((string)hasThePageLoaded != "complete" && timeout > 0))
        {
            Thread.Sleep(100);
            timeout--;
            hasThePageLoaded = jsExecutor.ExecuteScript("return document.readyState");
            if (timeout != 0) continue;
            Console.WriteLine("The page has not loaded successfully in the time provided.");
            return false;
        }
        return true;
    }

Then we check whether JQuery is being used:

public bool IsJqueryBeingUsed(IJavaScriptExecutor jsExecutor)
    {
        var isTheSiteUsingJQuery = jsExecutor.ExecuteScript("return window.jQuery != undefined");
        return (bool)isTheSiteUsingJQuery;
    }

If JQuery is being used we then check that it's loaded:

public bool JqueryHasLoaded(IJavaScriptExecutor jsExecutor, int timeout = 5)
        {
                var hasTheJQueryLoaded = jsExecutor.ExecuteScript("jQuery.active === 0");
                while (hasTheJQueryLoaded == null || (!(bool) hasTheJQueryLoaded && timeout > 0))
                {
                    Thread.Sleep(100);
                timeout--;
                    hasTheJQueryLoaded = jsExecutor.ExecuteScript("jQuery.active === 0");
                    if (timeout != 0) continue;
                    Console.WriteLine(
                        "JQuery is being used by the site but has failed to successfully load.");
                    return false;
                }
                return (bool) hasTheJQueryLoaded;
        }

We then do the same for AngularJS:

    public bool AngularIsBeingUsed(IJavaScriptExecutor jsExecutor)
    {
        string UsingAngular = @"if (window.angular){
        return true;
        }";            
        var isTheSiteUsingAngular = jsExecutor.ExecuteScript(UsingAngular);
        return (bool) isTheSiteUsingAngular;
    }

If it is being used then we check that it has loaded:

public bool AngularHasLoaded(IJavaScriptExecutor jsExecutor, int timeout = 5)
        {
    string HasAngularLoaded =
        @"return (window.angular !== undefined) && (angular.element(document.body).injector() !== undefined) && (angular.element(document.body).injector().get('$http').pendingRequests.length === 0)";            
    var hasTheAngularLoaded = jsExecutor.ExecuteScript(HasAngularLoaded);
                while (hasTheAngularLoaded == null || (!(bool)hasTheAngularLoaded && timeout > 0))
                {
                    Thread.Sleep(100);
                    timeout--;
                    hasTheAngularLoaded = jsExecutor.ExecuteScript(HasAngularLoaded);
                    if (timeout != 0) continue;
                    Console.WriteLine(
                        "Angular is being used by the site but has failed to successfully load.");
                    return false;

                }
                return (bool)hasTheAngularLoaded;
        }

After we check that the DOM has successfully loaded, you can then use these bool values to do custom waits:

    var jquery = !IsJqueryBeingUsed(javascript) || wait.Until(x => JQueryHasLoaded(javascript));
    var angular = !AngularIsBeingUsed(javascript) || wait.Until(x => AngularHasLoaded(javascript));
D Sayer
  • 73
  • 1
  • 9
3

If you're using AngularJS then using Protractor is a good idea.

If you use protractor you can use it's waitForAngular() method which will wait for http requests to complete. It's still good practise to wait for elements to be displayed before acting on them, depending on your language and implementation it might look this in a synchronous language

WebDriverWait wait = new WebDriverWait(webDriver, timeoutInSeconds);
wait.until(ExpectedConditions.visibilityOfElementLocated(By.id<locator>));

Or in JS you can use wait method which executes a function until it returns true

browser.wait(function () {
    return browser.driver.isElementPresent(elementToFind);
});
eddiec
  • 7,608
  • 5
  • 34
  • 36
3

You may just mine protractor for useful code snippets. This function blocks until Angular is done rendering the page. It is a variant of Shahzaib Salim's answer, except that he is polling for it and I am setting a callback.

def wait_for_angular(self, selenium):
    self.selenium.set_script_timeout(10)
    self.selenium.execute_async_script("""
        callback = arguments[arguments.length - 1];
        angular.element('html').injector().get('$browser').notifyWhenNoOutstandingRequests(callback);""")

Replace 'html' for whatever element is your ng-app.

It comes from https://github.com/angular/protractor/blob/71532f055c720b533fbf9dab2b3100b657966da6/lib/clientsidescripts.js#L51

user7610
  • 25,267
  • 15
  • 124
  • 150
2

I did the following code and it helped me for the async race condition failures.

$window._docReady = function () {
        var phase = $scope.$root.$$phase;
        return $http.pendingRequests.length === 0 && phase !== '$apply' && phase !== '$digest';
    }

Now in selenium PageObject model, you can wait for

Object result = ((RemoteWebDriver) driver).executeScript("return _docReady();");
                    return result == null ? false : (Boolean) result;
Sanjay Bharwani
  • 3,317
  • 34
  • 31
2

If your web app is indeed created with Angular as you say, the best way to do end-to-end testing is with Protractor.

Internally, Protractor uses its own waitForAngular method, to ensure Protractor waits automatically until Angular has finished modifying the DOM.

Thus, in the normal case, you would never need to write an explicit wait in your test cases: Protractor does that for you.

You can look at the Angular Phonecat tutorial to learn how to set up Protractor.

If you want to use Protractor seriously, you will want to adopt . If you want an example of that have a look at my page object test suite for the Angular Phonecat.

With Protractor you write your tests in Javascript (Protractor is indeed based on Node), and not in C# -- but in return Protractor handles all waiting for you.

avandeursen
  • 8,458
  • 3
  • 41
  • 51
0

For my particular problem with the HTML page containing iframes and developed with AnglularJS the following trick saved me a lot of time: In the DOM I clearly saw that there is an iframe which wraps all the content. So following code supposed to work:

driver.switchTo().frame(0);
waitUntilVisibleByXPath("//h2[contains(text(), 'Creative chooser')]");

But it was not working and told me something like "Cannot switch to frame. Window was closed". Then I modified the code to:

driver.switchTo().defaultContent();
driver.switchTo().frame(0);
waitUntilVisibleByXPath("//h2[contains(text(), 'Creative chooser')]");

After this everything went smoothly. So evidently Angular was mangling something with iframes and just after loading the page when you expect that driver is focused on default content it was focused by some already removed by Angular frame. Hope this may help some of you.

Alexander Arendar
  • 3,365
  • 2
  • 26
  • 38
0

If you don't want to make the entire switch to Protractor but you do want to wait for Angular I recommend using Paul Hammants ngWebDriver (Java). It's based on protractor but you don't have to make the switch.

I fixed the problem by writing an actions class in which I waited for Angular (using ngWebDriver's waitForAngularRequestsToFinish()) before carrying out the actions (click, fill, check etc.).

For a code snippet see my answer to this question

Community
  • 1
  • 1
74nine
  • 838
  • 7
  • 13
0

Here is an example for how to wait on Angular if you're using WebDriverJS. Originally I thought you had to create a custom condition, but wait accepts any function.

// Wait for Angular to Finish
function angularReady(): any  {
  return $browser.executeScript("return (window.angular !== undefined) && (angular.element(document).injector() !== undefined) && (angular.element(document).injector().get('$http').pendingRequests.length === 0)")
     .then(function(angularIsReady) {                        
                    return angularIsReady === true;
                  });
}

$browser.wait(angularReady, 5000).then(...);

Sadly this doesn't work with PhantomJS because of CSP (content-security-policy) and unsafe-eval. Can't wait for headless Chrome 59 on Windows.

SliverNinja - MSFT
  • 31,051
  • 11
  • 110
  • 173
0

I have implemented usage based on D Sayar's answer And it might helpful for someone. You just have to copy all boolean functions mention over there in to single class, And then add below PageCallingUtility() method. This method is calling internal dependency.

In your normal usage you need to directly call PageCallingUtility() method.

public void PageCallingUtility()
{
    if (DomHasLoaded() == true)
    {
        if (IsJqueryBeingUsed() == true)
        {
            JqueryHasLoaded();
        }

        if (AngularIsBeingUsed() == true)
        {
            AngularHasLoaded();
        }
    }
}
Ishita Shah
  • 3,955
  • 2
  • 27
  • 51
-1

Beside eddiec's suggest. If you test an AngularJS app, I strongly suggest you to think about protractor

Protractor will help you solve the waiting matter (sync, async). However, there are some notes

1 - You need to develop your test in javascript

2 - There are some different mechanism in handling flow

Nguyen Vu Hoang
  • 1,570
  • 14
  • 19
  • and what about E2E testing? i'd like to get "happy path". For unit tests i'm using Jasmine. Is it possible to test UI with Jasmine? – dr11 Aug 01 '14 at 08:36
  • protractor is used for e2e. You can use it with Jasmine, Cucumber and Mocha test framework. However, Mocha has limited beta support. You will need to include your own assertion framework if working with mocha. – Nguyen Vu Hoang Aug 01 '14 at 09:00
  • 1
    I'm using ASP MVC, AngularJS, Jasmine. Can i use 'protractor' without Node.JS? – dr11 Aug 01 '14 at 10:05