Detecting that Angular 2 is done running

2020-02-05 08:26发布

问题:

We use Selenium WebDriver to automate our UI based tests. One of our challenges is to detect when a page is truly done loading, and Angular 1 was a challenge in that regard as well. We ended up executing this piece of code specifically to detect if Angular 1 is done:

if(typeof window.angular !== \"undefined\")
{
  var injector = window.angular.element(\"*[ng-app]\").eq(0).injector();

  if(injector)
  {
    var $rootScope = injector.get(\"$rootScope\");
    var $http = injector.get(\"$http\");

    if($rootScope.$$phase === \"$apply\" || $rootScope.$$phase === \"$digest\" || $http.pendingRequests.length !== 0)
    {
      return false;
    }
  }
}

The app that we are testing recently switched over to use Angular 2. The code snippet above does not wait for Angular 2 to finish. Any suggestions?

回答1:

In case of Angular 2, you should wait for stableness of "testabilities" of all Angular 2 apps:

functions.waitForAllAngular2 = function(callback) {
  try {
    var testabilities = window.getAllAngularTestabilities();
    var count = testabilities.length;
    var decrement = function() {
      count--;
      if (count === 0) {
        callback();
      }
    };
    testabilities.forEach(function(testability) {
      testability.whenStable(decrement);
    });
  } catch (err) {
    callback(err.message);
  }
};

Taken from Protractor source code. Protractor is a wrapper around WebDriverJS javascript selenium bindings; designed to test AngularJS applications (not only, but best suited for).



回答2:

Using @alecxe answer i ended up with following javascript one liner to check if all Angular Testabilities are stable

window.getAllAngularTestabilities().findIndex(x=>!x.isStable()) === -1


回答3:

Building upon alexce's answer, this is the code we use to wait for angular in our selenium tests:

public static void waitForPageLoaded(WebDriver webDriver) {
    ExpectedCondition<Boolean> expectation = new
            ExpectedCondition<Boolean>() {
                public Boolean apply(WebDriver driver) {
                    return ((JavascriptExecutor) driver).executeAsyncScript(
                        "var callback = arguments[arguments.length - 1];" +
                        "if (document.readyState !== 'complete') {" +
                        "  callback('document not ready');" +
                        "} else {" +
                        "  try {" +
                        "    var testabilities = window.getAllAngularTestabilities();" +
                        "    var count = testabilities.length;" +
                        "    var decrement = function() {" +
                        "      count--;" +
                        "      if (count === 0) {" +
                        "        callback('complete');" +
                        "      }" +
                        "    };" +
                        "    testabilities.forEach(function(testability) {" +
                        "      testability.whenStable(decrement);" +
                        "    });" +
                        "  } catch (err) {" +
                        "    callback(err.message);" +
                        "  }" +
                        "}"
                    ).toString().equals("complete");
                }
            };
    try {
        WebDriverWait wait = new WebDriverWait(webDriver, waitSeconds);
        wait.until(expectation);
    } catch (Throwable error) {
        new Exception("Timeout waiting for Page Load Request to complete.");
    }
} 


回答4:

I fixed this by writing an actions class in which I waited for Angular before carrying out the actions (click, fill, check etc.) using Paul Hammants ngWebDriver:

import com.paulhammant.ngwebdriver.NgWebDriver;

public class ActionsWithWaits {

private NgWebDriver ngdriver;
private JavascriptExecutor js;

public ActionsWithWaits(){
    WebDriver driver = getDriver();
    js = (JavascriptExecutor) driver;
    driver.manage().timeouts().setScriptTimeout(9, TimeUnit.SECONDS);
    ngdriver = new NgWebDriver(js);
}


public void waitForAngular(){
    ngdriver.waitForAngularRequestsToFinish();
}

public void waitAndClick(WebElementFacade button){
    waitForAngular();
    button.click();
}

public void waitAndFillIn(String text, WebElementFacade field){
    waitForAngular();
    field.type(text);
}

etc..

Now you can just use those actions instead of the standard selenium actions and you don't have to worry about a thing (=



回答5:

I would emit an event from within ngAfterViewInit() in the root component

@Component({
  selector: 'my-app',
  ...
})
export class AppComponent {
  constructor(private renderer:Renderer, private elementRef:ElementRef){}
  ngAfterViewInit() {
    this.renderer.invokeElementMethod(this.elementRef.nativeElement, 
        'dispatchEvent', 
        [new CustomEvent('angular2-loaded', { bubbles: true })]);
  }
}

and then listen to this event using WebDriver.