如何解决陈旧元素异常?如果元素不再附加到 DOM?

我有一个关于“元素不再附加到DOM”的问题。

我尝试了不同的解决方案,但它们断断续续地工作。请提出一个可能是永久性的解决方案。

WebElement getStaleElemById(String id, WebDriver driver) {
    try {
        return driver.findElement(By.id(id));
    } catch (StaleElementReferenceException e) {
        System.out.println("Attempting to recover from StaleElementReferenceException ...");
        return getStaleElemById(id, driver);
    }
}

WebElement getStaleElemByCss(String css, WebDriver driver) {
    try {
        return driver.findElement(By.cssSelector(css));
    } catch (StaleElementReferenceException e) {
        System.out.println("Attempting to recover from StaleElementReferenceException ...");
        return getStaleElemByCss(css, driver);
    } catch (NoSuchElementException ele) {
         System.out.println("Attempting to recover from NoSuchElementException ...");
         return getStaleElemByCss(css, driver);
    }
}

谢谢 阿努


答案 1

问题

您可能面临的问题是该方法返回正确(且有效!)的元素,但是当您尝试在一秒钟后访问它时,它会过时并抛出。

这通常出现在以下情况下:

  1. 单击异步加载新页面或至少更改新页面的内容。
  2. 您立即(在页面加载完成之前)搜索元素...你找到了!
  3. 页面最终卸载,新页面加载。
  4. 您尝试访问以前找到的元素,但现在它已过时,即使新页面也包含它。

解决方案

我知道有四种方法可以解决它:

  1. 使用适当的等待

    面对异步页面时,请在每次预期的页面加载后使用适当的等待时间。在初始单击后插入显式等待,并等待新页面/新内容加载。只有在此之后,您才能尝试搜索所需的元素。这应该是您要做的第一件事。它将大大提高测试的鲁棒性。

  2. 您的工作方式

    我已经使用您的方法的变体两年了(以及解决方案1中的上述技术),它在大多数情况下绝对有效,并且仅在奇怪的WebDriver错误上失败。尝试在找到找到的元素后立即(从方法返回之前)通过方法或其他方式访问它。如果它抛出,您已经知道如何再次搜索。如果它通过,你还有一个(错误的)保证。.isDisplayed()

  3. 使用在过时时重新定位自身的 WebElement

    编写一个装饰器,记住它是如何被发现的,并在访问和投掷时重新找到它。这显然会迫使您使用自定义方法,这些方法将返回装饰器的实例(或者,更好的是,装饰方法将返回通常的实例和方法)。像这样做:WebElementfindElement()WebDriverfindElement()findElemens()

    public class NeverStaleWebElement implements WebElement {
        private WebElement element;
        private final WebDriver driver;
        private final By foundBy;
    
        public NeverStaleWebElement(WebElement element, WebDriver driver, By foundBy) {
            this.element = element;
            this.driver = driver;
            this.foundBy = foundBy;
        }
    
        @Override
        public void click() {
            try {
                element.click();
            } catch (StaleElementReferenceException e) {
                // log exception
    
                // assumes implicit wait, use custom findElement() methods for custom behaviour
                element = driver.findElement(foundBy);
    
                // recursion, consider a conditioned loop instead
                click();
            }
        }
    
        // ... similar for other methods, too
    
    }
    

    请注意,虽然我认为应该从通用的WebElements访问这些信息以使其更容易,但Selenium开发人员认为尝试这样的事情是错误的,并且选择不公开此信息。重新查找过时的元素可以说是一种不好的做法,因为您正在隐式地重新查找元素,而没有任何机制来检查它是否合理。重新查找机制可能会找到一个完全不同的元素,而不是再次找到相同的元素。此外,当有许多找到的元素时,它会失败得很可怕(您要么需要禁止重新查找找到的元素,要么记住您的元素来自返回的元素的数量)。foundByfindElements()findElements()List

    我认为这有时会很有用,但确实没有人会使用选项1和2,这显然是测试稳健性的更好解决方案。使用它们,只有在您确定需要它之后,才去做。

  4. 使用任务队列(可以重新运行过去的任务)

    以全新方式实施您的整个工作流程!

    • 创建要运行的作业的中央队列。使此队列记住过去的作业。
    • 通过命令模式方式实现每个需要的任务(“找到一个元素并单击它”,“找到一个元素并向其发送密钥”等)。调用时,将任务添加到中心队列,然后该队列将(同步或异步,无关紧要)运行它。
    • 根据需要使用 等注释每个任务。@LoadsNewPage@Reversible
    • 您的大多数任务将自行处理其异常,它们应该是独立的。
    • 当队列遇到过时的元素异常时,它将从任务历史记录中获取最后一个任务,然后重新运行它以重试。

    这显然需要付出很多努力,如果不仔细考虑,很快就会适得其反。我使用了一个(更复杂和更强大的)变体,以便在我手动修复它们所在的页面后恢复失败的测试。在某些情况下(例如,在 a 上),失败不会立即结束测试,但会等待(在 15 秒后最终超时之前),弹出一个信息窗口,并为用户提供手动刷新页面/单击右键/修复表单/任何内容的选项。然后,它将重新运行失败的任务,甚至有可能在历史记录中退后一些步骤(例如,到最后一个作业)。StaleElementException@LoadsNewPage


最后的挑剔

总而言之,您的原始解决方案可以使用一些抛光。您可以将这两个方法组合成一个更通用的方法(或者至少使它们委托给这个方法以减少代码重复):

WebElement getStaleElem(By by, WebDriver driver) {
    try {
        return driver.findElement(by);
    } catch (StaleElementReferenceException e) {
        System.out.println("Attempting to recover from StaleElementReferenceException ...");
        return getStaleElem(by, driver);
    } catch (NoSuchElementException ele) {
        System.out.println("Attempting to recover from NoSuchElementException ...");
        return getStaleElem(by, driver);
    }
}

使用Java 7,即使是单个多块即可:

WebElement getStaleElem(By by, WebDriver driver) {
    try {
        return driver.findElement(by);
    } catch (StaleElementReferenceException | NoSuchElementException e) {
        System.out.println("Attempting to recover from " + e.getClass().getSimpleName() + "...");
        return getStaleElem(by, driver);
    }
}

这样,您可以大大减少需要维护的代码量。


答案 2

我通过1解决这个问题。保留过时的元素并轮询它,直到它引发异常,然后是2。等到元素再次可见。

    boolean isStillOnOldPage = true;
    while (isStillOnOldPage) {
        try {
            theElement.getAttribute("whatever");
        } catch (StaleElementReferenceException e) {
            isStillOnOldPage = false;
        }
    }
    WebDriverWait wait = new WebDriverWait(driver, 15);
    wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("theElementId")));

推荐