问题
您可能面临的问题是该方法返回正确(且有效!)的元素,但是当您尝试在一秒钟后访问它时,它会过时并抛出。
这通常出现在以下情况下:
- 单击异步加载新页面或至少更改新页面的内容。
- 您立即(在页面加载完成之前)搜索元素...你找到了!
- 页面最终卸载,新页面加载。
- 您尝试访问以前找到的元素,但现在它已过时,即使新页面也包含它。
解决方案
我知道有四种方法可以解决它:
-
使用适当的等待
面对异步页面时,请在每次预期的页面加载后使用适当的等待时间。在初始单击后插入显式等待,并等待新页面/新内容加载。只有在此之后,您才能尝试搜索所需的元素。这应该是您要做的第一件事。它将大大提高测试的鲁棒性。
-
您的工作方式
我已经使用您的方法的变体两年了(以及解决方案1中的上述技术),它在大多数情况下绝对有效,并且仅在奇怪的WebDriver错误上失败。尝试在找到找到的元素后立即(从方法返回之前)通过方法或其他方式访问它。如果它抛出,您已经知道如何再次搜索。如果它通过,你还有一个(错误的)保证。.isDisplayed()
-
使用在过时时重新定位自身的 WebElement
编写一个装饰器,记住它是如何被发现的,并在访问和投掷时重新找到它。这显然会迫使您使用自定义方法,这些方法将返回装饰器的实例(或者,更好的是,装饰方法将返回通常的实例和方法)。像这样做:WebElement
findElement()
WebDriver
findElement()
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开发人员认为尝试这样的事情是错误的,并且选择不公开此信息。重新查找过时的元素可以说是一种不好的做法,因为您正在隐式地重新查找元素,而没有任何机制来检查它是否合理。重新查找机制可能会找到一个完全不同的元素,而不是再次找到相同的元素。此外,当有许多找到的元素时,它会失败得很可怕(您要么需要禁止重新查找找到的元素,要么记住您的元素来自返回的元素的数量)。foundBy
findElements()
findElements()
List
我认为这有时会很有用,但确实没有人会使用选项1和2,这显然是测试稳健性的更好解决方案。使用它们,只有在您确定需要它之后,才去做。
-
使用任务队列(可以重新运行过去的任务)
以全新方式实施您的整个工作流程!
- 创建要运行的作业的中央队列。使此队列记住过去的作业。
- 通过命令模式方式实现每个需要的任务(“找到一个元素并单击它”,“找到一个元素并向其发送密钥”等)。调用时,将任务添加到中心队列,然后该队列将(同步或异步,无关紧要)运行它。
- 根据需要使用 等注释每个任务。
@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);
}
}
这样,您可以大大减少需要维护的代码量。