使用Selenium测试AngularJS

29

我有一个基于 ASP MVC + AngularJS 的 SPA 应用程序,我想测试其用户界面。 目前我正在尝试使用 Selenium 和 PhantomJS 以及 WebKit 驱动程序。

这是一个样例测试页面 - 只包含一个元素。列表项 <li> 是通过服务器动态加载并由 Angular 绑定的。

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

我正在尝试通过一项测试,但是这一行代码出现了错误:

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

这时还没有加载任何元素,而且_driver.PageSource中也不包含元素。

我该如何等待项目加载?请不要建议使用Thread.Sleep()

12个回答

41

这将等待页面加载/ jquery.ajax(如果存在)和 $http 调用以及任何伴随的 digest/render 循环,将其放入实用函数中并等待。

/* 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;
}
/*");
});*/

1
谢谢你!我必须更改"window.angular.element('body')"为"window.angular.element(document.body)",因为没有加载jQuery。 - Rich
经过一些重构,这个问题得到了解决。不知道谁在编辑时把 C# 部分搞砸了。 - Rick Glos
在Javascript中,window.qa是什么? - BludShot
由于某些原因,angular.element(document.body).injector() 对我来说始终是未定义的。 - Richard Watts

28
创建一个新类,使您能够确定您的网站是否使用AngularJS完成了AJAX调用,方法如下:
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());
            }
        };
    }
}

您可以通过以下代码在代码的任何位置使用它:

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

1
我需要处理现代Web应用程序设计的完美解决方案。 - shadowcharly
当我执行时,我收到以下消息:“未知错误:未定义angular”。 - Big Al
这对我来说非常完美 - 做得好!我将此C#版本转换为TypeScript以适应我的用例 - SliverNinja - MSFT
谢谢Shahzaib。有人可以在代码片段上添加评论吗? - Manav Sharma
非常感谢,@Shahzaib Salim,这解决了我的动态处理页面加载问题。 - Bhuvanesh Mani
我不得不调整脚本字符串并将“document”替换为“document.body”,因为我的最外层的angular作用域在body上,否则我会得到未定义。@ManavSharma:WebDriverWait创建一个对象,您可以在其上调用until以等待条件。条件对象具有apply()函数,当满足条件时返回true。该脚本在浏览器中作为JavaScript运行,angular.element使用JQuery包装传递的obj,并添加了更多内容。其中之一是注入器函数,您可以使用它来获取$http angular服务,以查看是否存在挂起的请求。至少这是我的理解。 - Integrator

6
我们曾遇到过类似的问题,我们的in-house框架被用于测试多个站点,其中一些使用JQuery,一些使用AngularJS(甚至有一个混合使用)。我们的框架是用C#编写的,因此执行任何JScript时都很重要,以最小的代码块为单位进行(出于调试目的)。实际上,我们将以上许多答案结合起来(感谢@npjohns),下面是我们所做的解释:
以下代码可以检查HTML DOM是否已加载并返回true/false:
        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;
    }

然后我们检查是否正在使用JQuery:

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

如果正在使用JQuery,我们会检查它是否已加载:
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;
        }

然后我们为AngularJS做同样的操作:
    public bool AngularIsBeingUsed(IJavaScriptExecutor jsExecutor)
    {
        string UsingAngular = @"if (window.angular){
        return true;
        }";            
        var isTheSiteUsingAngular = jsExecutor.ExecuteScript(UsingAngular);
        return (bool) isTheSiteUsingAngular;
    }

如果正在使用,则我们检查它是否已加载:

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;
        }

在我们检查DOM成功加载之后,您可以使用这些布尔值进行自定义等待:

    var jquery = !IsJqueryBeingUsed(javascript) || wait.Until(x => JQueryHasLoaded(javascript));
    var angular = !AngularIsBeingUsed(javascript) || wait.Until(x => AngularHasLoaded(javascript));

3

您可以从protractor中挖掘有用的代码片段。该函数会阻塞,直到Angular完成页面渲染。这是Shahzaib Salim答案的一个变体,不同之处在于他正在轮询,而我正在设置回调。

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

将'html'替换为您的ng-app所在的任何元素。

这来自于https://github.com/angular/protractor/blob/71532f055c720b533fbf9dab2b3100b657966da6/lib/clientsidescripts.js#L51


3
如果你正在使用AngularJS,那么使用Protractor是一个好主意。
如果您使用protractor,则可以使用它的waitForAngular()方法,该方法将等待http请求完成。在对元素进行操作之前等待其显示仍然是一个好习惯,根据您的语言和实现方式,它可能看起来像同步语言中的这样。
WebDriverWait wait = new WebDriverWait(webDriver, timeoutInSeconds);
wait.until(ExpectedConditions.visibilityOfElementLocated(By.id<locator>));

在JS中,您可以使用wait方法,该方法会执行一个函数,直到它返回true

browser.wait(function () {
    return browser.driver.isElementPresent(elementToFind);
});

2
如果你的Web应用确实是使用Angular创建的,那么最好的端到端测试方式是使用Protractor
在内部,Protractor使用自己的waitForAngular方法,以确保Protractor等待自动直到Angular完成修改DOM。
因此,在正常情况下,您永远不需要在测试用例中编写显式的wait:Protractor会为您执行该操作。
您可以查看Angular Phonecat教程,了解如何设置Protractor。
如果您想认真使用Protractor,则需要采用。如果您想要一个示例,请查看我为Angular Phonecat编写的页面对象测试套件
使用 Protractor,你可以用 Javascript 编写测试用例(实际上 Protractor 是基于 Node 开发的),而不是 C#。但是作为回报,Protractor 会自动处理所有需要等待的操作。

2

我写了以下代码,它帮助我解决了异步竞争条件失败的问题。

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

现在在Selenium的PageObject模型中,您可以等待页面元素的出现。
Object result = ((RemoteWebDriver) driver).executeScript("return _docReady();");
                    return result == null ? false : (Boolean) result;

0
对于我在包含iframes并使用AnglularJS开发的HTML页面中遇到的问题,以下技巧为我节省了很多时间: 在DOM中,我清楚地看到有一个iframe包装了所有内容。 因此,以下代码应该可以解决问题:
driver.switchTo().frame(0);
waitUntilVisibleByXPath("//h2[contains(text(), 'Creative chooser')]");

但它没有起作用,告诉我类似于“无法切换到框架。窗口已关闭”的内容。 然后我修改了代码为:

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

之后一切都很顺利。 显然,Angular在处理iframe时出了点问题,就在加载页面后,当您期望驱动程序聚焦于默认内容时,它被一些已经被Angular删除的框架所聚焦。 希望这可以帮助到你们中的一些人。


0

如果您不想完全切换到Protractor,但是您想等待Angular,我建议使用Paul Hammants ngWebDriver(Java)。它基于protractor,但您不必进行切换。

我通过编写一个操作类来解决问题,在其中我等待Angular(使用ngWebDriver的waitForAngularRequestsToFinish())然后执行操作(单击、填充、检查等)。

有关代码片段,请参见我对这个问题的回答


0

我已经根据D Sayar的答案实现了使用方法,这可能对某些人有帮助。您只需将那里提到的所有布尔函数复制到单个类中,然后添加下面的PageCallingUtility()方法。此方法调用内部依赖项。

在正常使用中,您需要直接调用PageCallingUtility()方法。

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

        if (AngularIsBeingUsed() == true)
        {
            AngularHasLoaded();
        }
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接