Selenium WebDriver如何解决陈旧元素引用异常



我在Selenium 2 Web Driver测试中有以下代码,该代码在我调试时工作,但大多数时间在构建中运行时失败。我知道这一定是与页面没有被刷新的方式,但不知道如何解决它,所以任何指针,我做错了赞赏。我使用JSF primefaces作为我的web应用程序框架。当我点击添加新链接,弹出对话框出现一个输入框,我可以输入一个日期,然后点击保存。它是在获得输入元素输入文本,我得到一个过时的元素ref异常。

import static org.junit.Assert.assertEquals;
 import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.StaleElementReferenceException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.support.ui.ExpectedCondition;
import org.openqa.selenium.support.ui.WebDriverWait;

public class EnterActiveSubmissionIntegrationTest {
Map<String, Map<String, String>> tableData = new HashMap<String, Map<String, String>>();
@Test
public void testEnterActiveSubmission() throws Exception {
    // Create a new instance of the Firefox driver
    // Notice that the remainder of the code relies on the interface, 
    // not the implementation.
    System.setProperty("webdriver.chrome.driver", "C:/apps/chromedriver.exe");
    WebDriver driver = new ChromeDriver();
    // And now use this to visit Google
    driver.get("http://localhost:8080/strfingerprinting");
    // Alternatively the same thing can be done like this
    // driver.navigate().to("http://www.google.com");
    // Find the text input element by its name
    WebElement element = driver.findElement(By.linkText("Manage Submissions"));
    element.click();
    parseTableData(driver, "form:submissionDataTable_data", 1);
    assertEquals(tableData.get("form:submissionDataTable_data").get("12"), "Archived");
    
    WebElement newElement = driver.findElement(By.linkText("Add new"));
    newElement.click();
    
    WebDriverWait wait = new WebDriverWait(driver,10);
    wait.until(new ExpectedCondition<Boolean>() {
        public Boolean apply(WebDriver driver) {
            WebElement button = driver.findElement(By
                    .name("createForm:dateInput_input"));
            if (button.isDisplayed())
                return true;
            else
                return false;
        }
    });
    
    WebElement textElement = driver.findElement(By.name("createForm:dateInput_input"));
    textElement.sendKeys("24/04/2013");
    WebElement saveElement = driver.findElement(By.name("createForm:saveButton"));
    saveElement.click();
    
    driver.navigate().refresh();
    
    parseTableData(driver, "form:submissionDataTable_data", 2);
    
    //Close the browser
    driver.quit();
}

private void parseTableData(WebDriver driver, String id, int expectedRows) {
    // Check the title of the page or expected element on page
    WebElement subTableElement = driver.findElement(By.id(id));
    List<WebElement> tr_collection=subTableElement.findElements(By.xpath("id('"+ id + "')/tr"));
    assertEquals("incorrect number of rows returned", expectedRows, tr_collection.size());
    int row_num,col_num;
    row_num=1;
    
    if(tableData.get(id) == null) {
        tableData.put(id, new HashMap<String, String>());
    }
    Map<String, String> subTable = tableData.get(id);
    for(WebElement trElement : tr_collection)
    {
        List<WebElement> td_collection=trElement.findElements(By.xpath("td"));
        col_num=1;
        for(WebElement tdElement : td_collection)
        {
            subTable.put(row_num + "" + col_num, tdElement.getText());
            col_num++;
        }
        row_num++;
    }
}
}

当我运行这个时,我得到以下异常,但它可能发生在

WebElement textElement = driver.findElement(By.name("createForm:dateInput_input")); 

if (button.isDisplayed())

异常跟踪

org.openqa.selenium.StaleElementReferenceException: stale element reference: element is not attached to the page document
(Session info: chrome=26.0.1410.64)
  (Driver info: chromedriver=0.8,platform=Windows NT 6.0 SP2 x86) (WARNING: The server did not provide any stacktrace information)
Command duration or timeout: 56 milliseconds
For documentation on this error, please visit:        http://seleniumhq.org/exceptions/stale_element_reference.html
Build info: version: '2.32.0', revision: '6c40c187d01409a5dc3b7f8251859150c8af0bcb', time: '2013-04-09 10:39:28'
System info: os.name: 'Windows Vista', os.arch: 'x86', os.version: '6.0', java.version: '1.6.0_10'
Session ID: 784c53b99ad83c44d089fd04e9a42904
Driver info: org.openqa.selenium.chrome.ChromeDriver
Capabilities [{platform=XP, acceptSslCerts=true, javascriptEnabled=true,   browserName=chrome, rotatable=false, driverVersion=0.8, locationContextEnabled=true,  version=26.0.1410.64, cssSelectorsEnabled=true, databaseEnabled=true, handlesAlerts=true,  browserConnectionEnabled=false, nativeEvents=true, webStorageEnabled=true,   applicationCacheEnabled=false, takesScreenshot=true}]
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at  sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:39)
at  sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:27)
at java.lang.reflect.Constructor.newInstance(Constructor.java:513)
at org.openqa.selenium.remote.ErrorHandler.createThrowable(ErrorHandler.java:187)
at org.openqa.selenium.remote.ErrorHandler.throwIfResponseFailed(ErrorHandler.java:145)
at org.openqa.selenium.remote.RemoteWebDriver.execute(RemoteWebDriver.java:554)
at org.openqa.selenium.remote.RemoteWebElement.execute(RemoteWebElement.java:268)
at org.openqa.selenium.remote.RemoteWebElement.isDisplayed(RemoteWebElement.java:320)
at com.integration.web.EnterActiveSubmissionIntegrationTest$1.apply(EnterActiveSubmissionIntegrationTest.java:58)
at com.integration.web.EnterActiveSubmissionIntegrationTest$1.apply(EnterActiveSubmissionIntegrationTest.java:1)
at org.openqa.selenium.support.ui.FluentWait.until(FluentWait.java:208)
at com.integration.web.EnterActiveSubmissionIntegrationTest.testEnterActiveSubmission(EnterActiveSubmissionIntegrationTest.java:53)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

首先让我们弄清楚什么是WebElement

WebElement是对DOM中一个元素的引用。

当你正在交互的元素被销毁然后重新创建时抛出一个StaleElementException。如今,大多数复杂的网页都会在用户与之交互时动态地移动内容,这就需要销毁并重新创建DOM中的元素。

当这种情况发生时,对DOM中先前拥有的元素的引用变得过时,并且您不再能够使用此引用与DOM中的元素进行交互。当发生这种情况时,您需要刷新引用,或者在现实世界中再次查找元素。

这不是问题。如果你在try-catch块中封装了. findelement调用,并捕获了StaleElementReferenceException,那么你就可以循环并重试尽可能多的次数,直到成功。

下面是我写的一些例子。

来自Selenide项目的另一个例子:

public static final Condition hidden = new Condition("hidden", true) {
    @Override
    public boolean apply(WebElement element) {
      try {
        return !element.isDisplayed();
      } catch (StaleElementReferenceException elementHasDisappeared) {
        return true;
      }
    }
  };

发生在我身上的是webdriver会找到对DOM元素的引用,然后在获得该引用后的某个时刻,javascript会删除该元素并重新添加它(因为页面正在重新绘制,基本上)。

试试这个。找出导致从dom中删除dom元素的操作。在我的例子中,这是一个异步ajax调用,当ajax调用完成时,元素将从DOM中删除。在该操作之后,等待元素过期:

... do a thing, possibly async, that should remove the element from the DOM ...
wait.until(ExpectedConditions.stalenessOf(theElement));
此时,您可以确定元素现在已经过期了。因此,下次引用该元素时,再次等待,这次等待它被重新添加到DOM:
wait.until(ExpectedConditions.presenceOfElementLocated(By.id("whatever")))

元素过时的两个原因

  1. 一个在WebDriver中被引用为WebElement的网页元素,然后DOM改变(可能是由于JavaScript函数),WebElement失效

  2. 元素已完全删除

当你尝试与停滞的WebElement交互时[以上任何情况],会抛出StaleElementException。

如何避免/解决陈旧异常?

  1. 将定位符存储到元素中而不是引用
driver = webdriver.Firefox();
driver.get("http://www.github.com");
search_input = lambda: driver.find_element_by_name('q');
search_input().send_keys('hello worldn'); 
time.sleep(5);

search_input().send_keys('hello frankn') // no stale element exception
  • 使用JS库中的钩子
  •    # Using Jquery queue to get animation queue length.
        animationQueueIs = """
        return $.queue( $("#%s")[0], "fx").length;
        """ % element_id
        wait_until(lambda: self.driver.execute_script(animationQueueIs)==0)
    
  •  self.driver.execute_script("$("li:contains('Narendra')").click()");
    
  • 主动等待元素失效
  •   # Wait till the element goes stale, this means the list has updated
      wait_until(lambda: is_element_stale(old_link_reference))
    

    这个解决方案,这对我有用,我在这里提到,如果你有任何额外的场景,这对你有用,然后在下面评论

    尝试等待这样的元素:

    // Waiting 30 seconds for an element to be present on the page, checking
    // for its presence once every 5 seconds.
    Wait<WebDriver> stubbornWait = new FluentWait<WebDriver>(driver)
        .withTimeout(30, SECONDS)
        .pollingEvery(5, SECONDS)
        .ignoring(NoSuchElementException.class)
        .ignoring(StaleElementReferenceException.class);
    WebElement foo = stubbornWait.until(new Function<WebDriver, WebElement>() {
        public WebElement apply(WebDriver driver) {
            return driver.findElement(By.id("foo"));
        }
    });
    

    StaleElementReferenceException是由于findelement方法访问的元素不可用。

    在对元素执行任何操作之前(如果您对该元素的可用性有疑问)

    等待一个元素的可见性

    (new WebDriverWait(driver, 10)).until(new ExpectedCondition()
        {
               public Boolean apply(WebDriver d) {
                  return d.findElement(By.name("createForm:dateInput_input")).isDisplayed();
         }});
    
    使用这个逻辑来验证元素是否存在。

    使用Selenium提供的预期条件来等待web元素。

    当你调试时,客户端并不像你只运行单元测试或maven构建那么快。这意味着在调试模式下,客户端有更多的时间准备元素,但如果构建运行相同的代码,他会快得多,你寻找的web元素可能在页面的DOM中不可见。

    相信我,我也遇到过同样的问题。

    例如:

    inClient.waitUntil(ExpectedConditions.visibilityOf(YourElement,2000))
    

    这个简单的方法调用后等待2秒对你的WebElement在DOM上的可见性。

    我建议不要将@CachelookUp用于StaleElementReferenceException的Selenium WebDriver。

    如果您正在使用@FindBy注释并且有@CacheLookUp,请将其注释出来并检查。

    我用下面的代码解决了这个问题

    public WebElement waitForElement(final By findBy, final int waitTime) {
        Wait<AppiumDriver> wait = new FluentWait<>((AppiumDriver) driver)
                .withTimeout(waitTime, TimeUnit.SECONDS)
                .pollingEvery(POLL_TIME, TimeUnit.SECONDS)
                .ignoring(NoSuchElementException.class,StaleElementReferenceException.class);
        WebElement webElement = wait.until(new Function<AppiumDriver, WebElement>() {
            @Override
            public WebElement apply(AppiumDriver driver) {
                System.out.println("Trying to find element " + findBy.toString());                
                WebElement element = driver.findElement(findBy);
                return element;
            }
        });
        return webElement;
    }
    

    当Stale Element异常发生时!!

    当支持这些文本框/按钮/链接的库发生变化时,可能会发生陈旧元素异常,这意味着元素是相同的,但参考现在在网站中发生了变化,而不影响定位器。因此,我们存储在缓存中的引用(包括库引用)现在已经变旧或过时,因为页面已经被更新的库刷新了。
    for(int j=0; j<5;j++)
    try {
        WebElement elementName=driver.findElement(By.name(“createForm:dateInput_input”));
        break;
    } catch(StaleElementReferenceException e){
    e.toString();
    System.out.println(“Stale element error, trying ::  ” + e.getMessage());
    }
    elementName.sendKeys(“20/06/2018”);
    

    这对我有效(来源在这里):

     /**
         * Attempts to click on an element multiple times (to avoid stale element
         * exceptions caused by rapid DOM refreshes)
         *
         * @param d
         *            The WebDriver
         * @param by
         *            By element locator
         */
        public static void dependableClick(WebDriver d, By by)
        {
            final int MAXIMUM_WAIT_TIME = 10;
            final int MAX_STALE_ELEMENT_RETRIES = 5;
            WebDriverWait wait = new WebDriverWait(d, MAXIMUM_WAIT_TIME);
            int retries = 0;
            while (true)
            {
                try
                {
                    wait.until(ExpectedConditions.elementToBeClickable(by)).click();
                    return;
                }
                catch (StaleElementReferenceException e)
                {
                    if (retries < MAX_STALE_ELEMENT_RETRIES)
                    {
                        retries++;
                        continue;
                    }
                    else
                    {
                        throw e;
                    }
                }
            }
        }
    

    WebDriver必须等待,直到元素被定位,超时时间为10秒后。

    WebElement myDynamicElement1 = new WebDriverWait(driver, 10).until(
        ExpectedConditions.presenceOfElementLocated(
            By.name("createForm:dateInput_input")
        )
    );
    

    在带有for循环的try catch块中使用webdriverwait和ExpectedConditionEX: for python

    for i in range(4):
        try:
            element = WebDriverWait(driver, 120).until( 
                    EC.presence_of_element_located((By.XPATH, 'xpath')))
            element.click()    
            break
        except StaleElementReferenceException:
            print "exception "
    

    这个解决方案对我来说很好:

    添加错误处理功能并重试

    var pollLoop = function () {
          element(by.id('spinnerElem')).getAttribute('class').then(function (cls) {
                    if (cls.indexOf('spinner-active') > -1) {
                        // wait for the spinner
                    } else {
                        //do your logic
                        promise.defer().fulfill();
                    }
                }, function () {
                    // This err handling function is to handle the {StaleElementReferenceError} and makes sure we find the element always.
                    pollLoop();
                });
            };
    

    经过深入调查,我发现选择仅为Bootstrap添加的div元素时会出现错误。Chrome浏览器删除了这样的DIVS,出现错误。为了修复错误,退一步选择实元素就足够了。例如,我的模态对话框的结构是:

    <div class="modal-content" uib-modal-transclude="">
        <div class="modal-header">
            ...
        </div>
        <div class="modal-body">
            <form class="form-horizontal ...">
                ...
            </form>
        <div>
    <div>
    

    选择div class="modal-body"会产生错误,选择form…正常工作。

    在我的例子中,这个错误是由我在

    之外定义ActionChains元素引起的
    def parse(self, response):
    

    方法,当同时使用Selenium和Scrapy时,例如:

    行不通:

    class MySpider(scrapy.Spider):
         action_chains = ActionChains(self.driver)
    

    action_chains = ActionChains(self.driver)移到def parse(self, response):中解决了这个问题,例如:

    :

    def parse(self, response):
         self.driver.get(response.url)
         action_chains = ActionChains(self.driver)
    

    我发现避免陈旧元素引用的最佳方法是不使用PageFactory,而是存储定位器(即按元素)。

    public class WebDriverFactory {
        // if you want to multithread tests, use a ThreadLocal<WebDriver> 
        // instead.
        // This also makes it so you don't have to pass around WebDriver objects
        // when instantiating new Page classes
        private static WebDriver driver = null;
        public static WebDriver getDriver() {
           return driver;
        }
        public static void setDriver(WebDriver browser)  {
           driver = browser;
        }       
    }
    // class to let me avoid typing out the lengthy driver.findElement(s) so 
    // much
    public Abstract class PageBase {
        private WebDriver driver = WebDriverFactory.getDriver();
        // using var args to let you easily chain locators
        protected By getBy(By... locator) {
          return new ByChained(locator);
        }
        protected WebElement find(By... locators) {
          return driver.findElement(getBy(locators));
        }
        protected List<WebElement> findEm(By... locators) {
          return driver.findElements(getBy(locators));
        }
        protected Select select(By... locators) {
          return new Select(getBy(locators));
        }
    }
    public class somePage extends PageBase {
      private static WebDriver driver = WebDriverFactory.getDriver();
      private static final By buttonBy = By.cssSelector(".btn-primary");
      public void clickButton() {
         WebDriverWait wait = new WebDriverWait(driver, 10);
         wait.until(ExpectedConditions.elementToBeClickable(buttonBy));
         find(buttonBy).click();
      }
    }
    

    我有一个类充满静态WebDriverWait方法,我使用。我不记得上面使用的WebDriver wait是否会处理StaleElement异常。如果没有,你可以使用一个流畅的等待,就像DjangoFan的答案一样。但是,我所显示的原则将工作(即使特定行与WebDriverWait爆炸。

    所以tldr;

    1. 使用定位器,并结合WebDriverWait/Fluent wait/重新定位元素,所以如果你的元素过时了,你可以重新定位它,而不必在@FindBy中复制定位器(对于pagefactory初始化的元素),因为没有WebElement.relocate()方法。
    2. 为了简化生活,使用一个抽象的BasePage类来方便地定位元素/元素列表。

    我尝试了上面的许多建议,但最简单的一个有效。在我的例子中,是对web元素使用@CachelookUp导致了陈旧元素异常。我猜刷新页面后,元素引用没有重新加载,未能找到元素。禁用元素的@ cacheookup行有效。

        //Search button
        @FindBy(how=How.XPATH, using =".//input[@value='Search']")  
        //@CachelookUp
        WebElement BtnSearch;
    

    在Selenium 3中,您可以使用ExpectedConditions。刷新(你的情况)。

    它将处理StaleElementReferenceException并重试该条件。

    如果我们不确定答案,请不要混淆我们之间的其他人。这对终端用户来说是相当令人沮丧的。最简单的答案是在webdriver中使用@CacheLookup注释。请参考下面的链接。如何@ cachellookup工作在WebDriver?

    参考@djangofan给出的答案,看起来可行的解决方案是将您的代码保留在可能发生过时的try catch块内。当我使用下面的代码时,我没有遇到任何问题。

    public void inputName(String name)
    {
        try {
            waitForVisibilityElement(name);//My own visibility function
            findElement(By.name("customerName")).sendKeys(name);
        }
        catch (StaleElementReferenceException e)
        {
            e.getMessage();
        }
    }
    

    我已经尝试使用ExpectedConditions.presenceOfElementLocated(By),但过期异常仍然间歇性抛出。

    希望这个解决方案有帮助。

    只要下载新的chrome扩展并使用selenium server 3就可以了

    最新更新