【问题标题】:How do I avoid StaleElementException after WebElement.click() for an ajax button?ajax 按钮的 WebElement.click() 之后如何避免 StaleElementException?
【发布时间】:2012-09-27 10:25:14
【问题描述】:

我有一个页面,它有一个带有一个输入字段和一个提交按钮的小表单。 提交按钮是 AJAX! 当我单击它时,它会将输入值提交给服务器,服务器要么验证它是否可接受并加载新页面,要么发现问题并添加反馈错误标签。

我编写了以下 selenium 代码来测试此功能:

WebDriverWait waiter = new WebDriverWait(driver, 20);
WebElement inputField = waiter.until(ExpectedConditions.presenceOfElementLocated(By.id("inputField")));
inputField.sendKeys("Test input");

WebElement submitInput = waiter.until(ExpectedConditions.presenceOfElementLocated(By.id("submitInput")));
submitInput.click();

WebElement feedback = waiter.until(ExpectedConditions.presenceOfElementLocated(By.id("feedback")));
Assert.assertTrue("Feedback not was not empty, but was: " + feedback.getText(), feedback.getText().isEmpty());

问题是submitInput.click() 完成并立即返回,如果新页面是通过事件加载的:the selenium docs,因此通过“反馈”进行检索的调用工作正常,我可以通过调试看到它正确地获取了元素,但是当我调用feedback.getText() 时我得到了StaleElementException,因为页面已经在后台刷新了。

如果没有:我怎样才能通过测试:

  1. 添加 Thread.sleep()。
  2. 添加 StaleElementException 捕获以重新获取元素。

注意:上述测试在输入无效时有效,因为页面没有刷新,但在输入有效时无效。

任何帮助将不胜感激。

编辑:请注意,在元素出现之前轮询页面不是问题,而是检测submitInput.click() 是否加载新页面或向现有标签添加文本时存在问题。

编辑 2:为了说明问题,我编译了一个时间记录的贯穿测试和服务器日志:

16:34:48,589 - TEST   - Navigating to page.
16:34:48,605 - SERVER - Page request started.
16:34:48,631 - SERVER - Page request ended.
16:34:49,050 - TEST   - Typing test input.
16:34:49,456 - TEST   - Clicking button.
16:34:49,519 - SERVER - Received click event.
16:34:49,529 - SERVER - Responding with redirect.
16:34:49,556 - TEST   - Finding feedback by id.
16:34:49,560 - SERVER - Page request started.
16:34:49,581 - SERVER - Page request ended.
16:34:49,775 - TEST   - Test threw exception.

您可以看到服务器在测试发送后仅半秒多一点就收到了点击事件。它立即响应(在这种情况下)重定向命令。由于单击是 AJAX 事件,测试同时有continued in the background as per the docs,这导致(如您所见)测试在服务器收到新页面请求之前 0.004 秒检索反馈。当 feedback.getText() 被解析并发送到 ChromeDriver 时,服务器已经响应了一个新页面,因此该函数返回过时异常。

真正的关键是我可以使用Thread.sleep(1000) 解决这个问题,但这远非理想;首先是因为在某些情况下,服务器解析 AJAX 并返回结果的时间可能超过半秒,这使得测试具有不确定性,其次因为这些时间很快就会堆积起来并浪费时间,通常是不必要的,当只有一个需要几毫秒。

【问题讨论】:

    标签: java selenium junit selenium-chromedriver


    【解决方案1】:

    我实际上会采取其他方式来解决这个问题。 我会使用一些 js 来克服 StaleElementException +++ fluent wait +++ css 选择器而不是 xpaths。 1) 流畅的等待方法。它会返回您实际的(定位的)网络元素:

    public WebElement fluentWait(final By locator){
        Wait<WebDriver> wait = new FluentWait<WebDriver>(driver)
                .withTimeout(30, TimeUnit.SECONDS)
                .pollingEvery(5, TimeUnit.SECONDS)
                .ignoring(NoSuchElementException.class);
    
        WebElement foo = wait.until(
            new Function<WebDriver, WebElement>() {
                public WebElement apply(WebDriver driver) {
                    return driver.findElement(locator);
                }
            }
        );
        return  foo;
    };
    

    关于流利的等待你可以得到herehere

    2) 字符串 cssSelectorInputField = "[id='inputField']" 字符串 cssSelectorSubmitInput = "[id='submitInput']" 字符串 cssSelectorFeedback = "[id='feedback']"

    3) 现在我提供一些 jsMethods 模板(我用于自动化的 prolly 可能对您有用)

    public String jsGetText(String cssSelector){
    
      JavascriptExecutor js = (JavascriptExecutor) driver;
            StringBuilder stringBuilder = new StringBuilder();
    
    stringBuilder.append("var x = $(\""+cssSelector+"\");");
            stringBuilder.append("return x.text().toString();")       ;
    
    
           String res= (String) js.executeScript(stringBuilder.toString());
    return res;
    }
    
    public String jsGetColor(String css){
    
            JavascriptExecutor js = (JavascriptExecutor) driver;
            StringBuilder stringBuilder = new StringBuilder();
            stringBuilder.append("var x=$(\'"+css+"\');");
            stringBuilder.append("return x.css('color')");
            String res= (String) js.executeScript(stringBuilder.toString());
            return res;
    
        }
    
    public void jsClickOnElement(String css){
    
    JavascriptExecutor js = (JavascriptExecutor) driver;
            StringBuilder stringBuilder = new StringBuilder();
            stringBuilder.append("var x = $(\'"+css+"\');");
            stringBuilder.append("x.click();");
            js.executeScript(stringBuilder.toString());
    
    }
    

    4) 所以最后的步骤是这样的:

    driver.findElement(By.cssSelector(cssSelectorInputField)).sendKeys("Test input");
    driver.findElement(By.cssSelector(cssSelectorSubmitInput)).click();
    //or jsClickOnElement(cssSelectorSubmitInput);
    WebElement feedBack =fluentWait(cssSelectorFeedback);
    String textForVerifying=feedBack.getText().trim();
    //or String textForVerifying= jsGetText(cssSelectorFeedback);
    Assert.assertTrue(textForVerifying.equals("..blablablab..."));
    

    希望现在对你有用)

    【讨论】:

    • 您好,感谢您的回答。我正在使用扩展 FluentWait 的 WebDriverWait,并且基本上为我完成了您在第 1 点中展示的所有初始化。真正的问题是反馈标签已经存在于页面上,但被隐藏了。因此,它在 ajax 事件往返服务器之前捕获此元素,但在页面更改后使用它。我将修改问题以使其更清楚。
    • 尽量坚持元素(例如按钮,但不是流利等待的反馈)。然后按照我的描述使用 jsMehtod 获取文本。 js 方法应该总是有效的。
    • 我已经尝试了 javascript 方法,但它仍然没有阻止,所以我在一个页面上获取了一个元素,但是当我使用它时,页面已经在后台刷新,我'还尝试将 FluentWaits 放在不同的 findElement() 方法上,包括所有这些方法,但无济于事,我正在重新编辑我的问题,希望能更好地显示问题所在。
    • 我明白了。谈到流畅的等待用法,最好使用它而不是 driver.findElement(blablabla) 我的意思是调用 driver.findElement(By.cssSelector(..blabla..)) 并调用 fluendWait(By.cssSelector(..blablabla) ...)) 是完全等价的。
    • @Jamey - 我看不出使用 FluentWait 可能会失败。根据我的经验,它总是有效的。只要您处理所有可能抛出的错误条件,它们就可以通过循环来处理。当你说“我尝试过”时,我会说更加努力。只需查看您收到的异常并使用 .ignoring 方法处理它。
    【解决方案2】:

    别忘了这样的方法

    input.findElements(By.xpath("//xpath")).size() > 0
    
    
    driver.findElement(By.xpath("//xpath")).isDisplayed()
    
    public bool IsElementPresent(By selector)
    {
        return driver.FindElements(selector).Any();
    }
    

    用于验证页面上的元素是否存在。它们也很有帮助。但请注意您找到的定位器(在 firebug 中验证您正确定位的所有元素)

    【讨论】:

      【解决方案3】:

      我不确定如何在 Java 中做到这一点,也不确定我是否正确理解了这个场景,但我会尝试一下。顺便说一句,它在 Ruby 中......

       @wait.until { @driver.find_element(:id,"feedback") }
       assert "true","Feedback was not empty but was #{@driver.find_element(:id,\"feedback\").text}"
      

      我基本上想要做的不是使用在断言上方的行中找到/存在的网络元素“反馈”。相反,我让 webdriver 再次找到该元素,然后对其执行一些操作(获取文本)。

      编辑


      @wait.until { alert_text = @driver.find_element(:id,"feedback").text }
      assert_match "expected text","#{alert_text}","Feedback is not empty and expected"
      

      【讨论】:

      • 感谢您的建议,这是一个改进,但是对 find_elementtext 的调用是不同的请求,即 find_element 执行 webdriver 请求以获取元素,但 text 执行从该元素获取文本的单独请求(文本不是从find_element 的结果中返回的)。这意味着页面可能仍会在请求之间的后台更新。我刚刚试了一下,它确实......
      • 仍然有同样的问题,(无论如何在 Java 中)。不过谢谢,我找到了一个足够好的解决方案。
      【解决方案4】:

      对于遇到类似问题的任何人(这是两个不同的问题,1 - 检测 AJAX 控制的页面更改和 2 - 检测 AJAX 控制的元素更改),我的最终解决方案是

      第一步:创建几个函数通过jQuery跟踪页面变化(所有页面都使用jQuery,所以脚本执行良好):

      private void updateLastChange()
      {
          driver.executeScript("$('html').data('CHANGED_PAGE', false)");
      }
      
      public boolean pageChanged()
      {
          Object script = driver.executeScript("return $('html').data('CHANGED_PAGE');");
          Boolean changed = (Boolean)script;
          if(changed == null)
          {
              updateLastChange();
              return true;
          }
          return false;
      }
      

      然后我可以简单地调用pageChanged() 来检测页面更改。

      第 2 步:创建一个实现 ExpectedCondition&lt;Boolean&gt; 的匿名类。

      使用上面的示例,问题在于反馈何时准备就绪是不确定的,我将问题中代码的底部两行更改为:

      WebDriverWait waiter = new WebDriverWait(driver, 5, 200);
      waiter.ignoring(AssertionFailedError.class);
      waiter.ignoring(StaleElementReferenceException.class);
      waiter.until(new ExpectedCondition<Boolean>()
      {
          public Boolean apply(WebDriver input)
          {
              WebElement feedback = input.findElement(By.id("feedback"));
              Assert.assertTrue("Feedback not was not empty, but was: " + feedback.getText(), feedback.getText().isEmpty());            
              return true;
          }
      });
      

      这样,它每 200 毫秒轮询一次页面,持续 5 秒,忽略断言异常和过时元素异常。它通过的唯一方法是在 5 秒用完之前到达 return true; 行。我目前正在考虑如何使它更整洁,但它工作得很好,比简单地睡觉更快,并且由于轮询而提前(如果可能)成功。

      【讨论】:

      • 这对于无法编辑源代码的测试人员来说不是一个好的解决方案。此外,即使页面发生了变化,它也不能确保该元素存在于页面上。我认为下面的尤金斯回答要可靠得多。
      • @djangofan,你说的是哪个源代码?当然,测试人员可以编辑他们正在编写的测试的源代码,这是唯一的编辑......我同意这不是一个好的解决方案,但在使用 Selenium 时,我们应该始终努力从用户的角度进行测试,并且用户不关心页面是否改变,他们只关心显示给他们的数据。我已经删除了 pageChanged 方法,因为它不是一种有价值的测试方法,并且使测试更容易变得脆弱。
      • 当我说“源代码”时,我的意思是 QA 和 DEV 通常是分开的,DEV 为 QA 无权访问的应用程序编写代码。
      • 如果你删除了pageChanged(),你有替代解决方案吗?
      • 是的@frederik-de-mets,我们现在将PageObject pattern 与FindBy 注释和PageFactory 辅助类结合使用,提供AjaxElementLocatorFactory。现在,selenium 负责轮询我的元素,直到提供的超时。我可以使用预制的 ExpectedConditions 类来帮助轮询特定条件,甚至可以为特殊情况创建自己的。我强烈推荐它。 :)
      猜你喜欢
      • 2017-05-23
      • 1970-01-01
      • 1970-01-01
      • 2010-10-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-10-07
      相关资源
      最近更新 更多