#java #selenium-webdriver #webdriver #automated-tests
#java #selenium-webdriver #webdriver #автоматизированные тесты
Вопрос:
Я надеюсь, что это только у меня, но Selenium Webdriver кажется полным кошмаром. Веб-драйвер Chrome в настоящее время непригоден для использования, а другие драйверы довольно ненадежны, или так кажется. Я борюсь со многими проблемами, но вот одна.
Случайно, мои тесты завершатся неудачей с
"org.openqa.selenium.StaleElementReferenceException: Element is no longer attached
to the DOM
System info: os.name: 'Windows 7', os.arch: 'amd64',
os.version: '6.1', java.version: '1.6.0_23'"
Я использую webdriver версии 2.0 b3. Я видел, как это происходило с драйверами FF и IE. Единственный способ, которым я могу предотвратить это, — добавить фактический вызов Thread.sleep
до возникновения исключения. Однако это плохое решение, поэтому я надеюсь, что кто-нибудь сможет указать на ошибку с моей стороны, которая сделает все это лучше.
Комментарии:
1. Надеюсь, 17 тысяч просмотров указывают на то, что дело не только в вас 😉 Это, должно быть, самое неприятное исключение Selenium из существующих.
2. сейчас 48 кб! У меня та же проблема…
3. Я обнаружил, что Selenium — это чистый и законченный мусор….
4. 60 кб, все еще проблема 🙂
5. 123 кб и все еще подсчитывается! Хотите верьте, хотите нет, исключение StaleElementReferenceException все еще остается проблемой в 2022 году…
Ответ №1:
Да, если у вас возникли проблемы со StaleElementReferenceExceptions, это потому, что существует условие гонки. Рассмотрим следующий сценарий:
WebElement element = driver.findElement(By.id("foo"));
// DOM changes - page is refreshed, or element is removed and re-added
element.click();
Теперь в точке, где вы нажимаете на элемент, ссылка на элемент больше не действительна. Для WebDriver практически невозможно правильно угадать все случаи, когда это может произойти, поэтому он разводит руками и передает управление вам, который как автор теста / приложения должен точно знать, что может произойти, а что нет. Что вы хотите сделать, это явно подождать, пока DOM не перейдет в состояние, в котором, как вы знаете, ничего не изменится. Например, использование WebDriverWait для ожидания существования определенного элемента:
// times out after 5 seconds
WebDriverWait wait = new WebDriverWait(driver, 5);
// while the following loop runs, the DOM changes -
// page is refreshed, or element is removed and re-added
wait.until(presenceOfElementLocated(By.id("container-element")));
// now we're good - let's click the element
driver.findElement(By.id("foo")).click();
Метод presenceOfElementLocated() будет выглядеть примерно так:
private static Function<WebDriver,WebElement> presenceOfElementLocated(final By locator) {
return new Function<WebDriver, WebElement>() {
@Override
public WebElement apply(WebDriver driver) {
return driver.findElement(locator);
}
};
}
Вы совершенно правы в том, что текущий драйвер Chrome довольно нестабилен, и вы будете рады услышать, что на магистрали Selenium есть переписанный драйвер Chrome, где большая часть реализации была выполнена разработчиками Chromium как часть их дерева.
PS. В качестве альтернативы, вместо явного ожидания, как в примере выше, вы можете включить неявное ожидание — таким образом, WebDriver всегда будет зацикливаться до указанного тайм-аута, ожидая появления элемента:
driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS)
Однако, по моему опыту, явное ожидание всегда надежнее.
Комментарии:
1. Прав ли я, говоря, что больше невозможно считывать элементы в переменные и повторно использовать их? Потому что у меня огромный сухой и динамичный WATiR DSL, который полагается на передаваемые элементы, и я пытаюсь портировать на webdriver, но у меня та же проблема. По сути, мне придется добавлять код для повторного чтения всех элементов в модуле для каждого шага тестирования, который изменяет DOM…
2. привет. Могу я спросить, какой тип функции в этом примере? Кажется, я не могу его найти…. Спасибо!
3. @Hannibal:
com.google.common.base.Function<F, T>
, предоставлено Guava .4. @jarib, я сталкиваюсь с этой же проблемой через год после вашего решения. проблема в том, что я пишу свои скрипты на ruby, и нет функции с именем ‘presenceOfElementLocated’ или чего-либо подобного. Какие-либо рекомендации?
5. @jarib Я не согласен, что все это вызвано плохо разработанным тестом. Поскольку даже после появления элемента после вызова AJAX может быть все еще запущен код jQuery, который может вызвать исключение StaleElementReferenceException. И вы ничего не можете сделать, кроме добавления явного ожидания, что кажется не очень приятным. Я скорее думаю, что это недостаток дизайна в WebDriver
Ответ №2:
Я смог использовать подобный метод с некоторым успехом:
WebElement getStaleElemById(String id) {
try {
return driver.findElement(By.id(id));
} catch (StaleElementReferenceException e) {
System.out.println("Attempting to recover from StaleElementReferenceException ...");
return getStaleElemById(id);
}
}
Да, он просто продолжает опрашивать элемент, пока он больше не будет считаться устаревшим (свежим?). На самом деле не доходит до корня проблемы, но я обнаружил, что WebDriver может быть довольно придирчивым к выбрасыванию этого исключения — иногда я получаю его, а иногда нет. Или может быть, что DOM действительно меняется.
Поэтому я не совсем согласен с ответом выше, что это обязательно указывает на плохо написанный тест. Я получил его на свежих страницах, с которыми я никак не взаимодействовал. Я думаю, что есть некоторая неточность либо в том, как представлен DOM, либо в том, что WebDriver считает устаревшим.
Комментарии:
1. В этом коде ошибка, вам не следует продолжать рекурсивный вызов метода без какого-либо ограничения, иначе вы взорвете свой стек.
2. Я думаю, что лучше добавить счетчик или что-то в этом роде, поэтому, когда мы получаем ошибку повторно, мы действительно можем выдать ошибку. В противном случае, если на самом деле произошла ошибка, вы окажетесь в цикле
3. Я согласен, что это не результат плохо написанных тестов. У Selenium есть тенденция делать это на современных веб-сайтах, даже для самых хорошо написанных тестов — вероятно, потому, что веб-сайты постоянно обновляют свои элементы с помощью двухсторонних привязок, которые распространены в реактивных фреймворках веб-приложений, даже когда никаких изменений в эти элементы вносить не требуется. Подобный метод должен быть частью каждого фреймворка Selenium, который тестирует современное веб-приложение.
Ответ №3:
Иногда я получаю эту ошибку, когда обновления AJAX находятся на полпути. Capybara, похоже, довольно умно относится к ожиданию изменений DOM (см. Почему wait_until был удален из Capybara ), но времени ожидания по умолчанию в 2 секунды было просто недостаточно в моем случае. Изменено в _spec_helper.rb_, например
Capybara.default_max_wait_time = 5
Комментарии:
1. Это также устранило мою проблему: я получал ошибку StaleElementReferenceError и увеличивал Capybara.default_max_wait_time решил проблему.
Ответ №4:
Сегодня я столкнулся с той же проблемой и создал класс-оболочку, который проверяет перед каждым методом, действительна ли ссылка на элемент. Мое решение для повторного поиска элемента довольно простое, поэтому я подумал, что просто поделюсь им.
private void setElementLocator()
{
this.locatorVariable = "selenium_" DateTimeMethods.GetTime().ToString();
((IJavaScriptExecutor)this.driver).ExecuteScript(locatorVariable " = arguments[0];", this.element);
}
private void RetrieveElement()
{
this.element = (IWebElement)((IJavaScriptExecutor)this.driver).ExecuteScript("return " locatorVariable);
}
Вы видите, что я «нахожу» или, скорее, сохраняю элемент в глобальной переменной js и при необходимости извлекаю элемент. Если страница будет перезагружена, эта ссылка больше не будет работать. Но до тех пор, пока вносятся изменения только для doom, ссылка остается. И это должно выполнить задание в большинстве случаев.
Также это позволяет избежать повторного поиска элемента.
Джон
Ответ №5:
У меня была такая же проблема, и моя была вызвана старой версией selenium. Я не могу обновить до более новой версии из-за среды разработки. Проблема вызвана HTMLUnitWebElement.switchFocusToThisIfNeeded(). При переходе на новую страницу может случиться так, что элемент, на который вы нажали на старой странице, является oldActiveElement
(см. Ниже). Selenium пытается получить контекст из старого элемента и терпит неудачу. Вот почему они создали try catch в будущих выпусках.
Код из selenium-htmlunit-версия драйвера < 2.23.0:
private void switchFocusToThisIfNeeded() {
HtmlUnitWebElement oldActiveElement =
((HtmlUnitWebElement)parent.switchTo().activeElement());
boolean jsEnabled = parent.isJavascriptEnabled();
boolean oldActiveEqualsCurrent = oldActiveElement.equals(this);
boolean isBody = oldActiveElement.getTagName().toLowerCase().equals("body");
if (jsEnabled amp;amp;
!oldActiveEqualsCurrent amp;amp;
!isBody) {
oldActiveElement.element.blur();
element.focus();
}
}
Код из selenium-htmlunit-версия драйвера >= 2.23.0:
private void switchFocusToThisIfNeeded() {
HtmlUnitWebElement oldActiveElement =
((HtmlUnitWebElement)parent.switchTo().activeElement());
boolean jsEnabled = parent.isJavascriptEnabled();
boolean oldActiveEqualsCurrent = oldActiveElement.equals(this);
try {
boolean isBody = oldActiveElement.getTagName().toLowerCase().equals("body");
if (jsEnabled amp;amp;
!oldActiveEqualsCurrent amp;amp;
!isBody) {
oldActiveElement.element.blur();
}
} catch (StaleElementReferenceException ex) {
// old element has gone, do nothing
}
element.focus();
}
Без обновления до 2.23.0 или новее вы можете просто указать любой элемент в фокусе страницы. Я просто использовал element.click()
для примера.
Комментарии:
1. Вау… Это была действительно непонятная находка, хорошая работа.. Теперь мне интересно, есть ли у других драйверов (например, chromedriver) похожие проблемы
Ответ №6:
Просто случилось со мной при попытке отправить ключи в поле ввода поиска, которое имеет автоматическое обновление в зависимости от того, что вы вводите. Как упоминалось Eero, это может произойти, если ваш элемент обновляет некоторый Ajax, пока вы вводите текст внутри элемента ввода. Решение состоит в том, чтобы отправлять по одному символу за раз и снова выполнять поиск элемента ввода. (Например, в ruby, показанном ниже)
def send_keys_eachchar(webdriver, elem_locator, text_to_send)
text_to_send.each_char do |char|
input_elem = webdriver.find_element(elem_locator)
input_elem.send_keys(char)
end
end
Ответ №7:
Чтобы добавить к ответу @jarib, я создал несколько методов расширения, которые помогают устранить условие гонки.
Вот моя настройка:
У меня есть класс под названием «Driver.cs». Он содержит статический класс, полный методов расширения для драйвера и других полезных статических функций.
Для элементов, которые мне обычно нужно извлекать, я создаю метод расширения, подобный следующему:
public static IWebElement SpecificElementToGet(this IWebDriver driver) {
return driver.FindElement(By.SomeSelector("SelectorText"));
}
Это позволяет вам извлекать этот элемент из любого тестового класса с помощью кода:
driver.SpecificElementToGet();
Теперь, если это приведет к StaleElementReferenceException
, у меня есть следующий статический метод в моем классе драйвера:
public static void WaitForDisplayed(Func<IWebElement> getWebElement, int timeOut)
{
for (int second = 0; ; second )
{
if (second >= timeOut) Assert.Fail("timeout");
try
{
if (getWebElement().Displayed) break;
}
catch (Exception)
{ }
Thread.Sleep(1000);
}
}
Первым параметром этой функции является любая функция, которая возвращает объект IWebElement. Второй параметр — это время ожидания в секундах (код для времени ожидания был скопирован из Selenium IDE для FireFox). Код можно использовать, чтобы избежать исключения устаревшего элемента следующим образом:
MyTestDriver.WaitForDisplayed(driver.SpecificElementToGet,5);
Приведенный выше код будет вызываться driver.SpecificElementToGet().Displayed
до driver.SpecificElementToGet()
тех пор, пока не будет выдано никаких исключений и .Displayed
вычислено true
, и не пройдет 5 секунд. Через 5 секунд тест завершится неудачей.
С другой стороны, чтобы дождаться отсутствия элемента, вы можете использовать следующую функцию таким же образом:
public static void WaitForNotPresent(Func<IWebElement> getWebElement, int timeOut) {
for (int second = 0;; second ) {
if (second >= timeOut) Assert.Fail("timeout");
try
{
if (!getWebElement().Displayed) break;
}
catch (ElementNotVisibleException) { break; }
catch (NoSuchElementException) { break; }
catch (StaleElementReferenceException) { break; }
catch (Exception)
{ }
Thread.Sleep(1000);
}
}
Ответ №8:
Я думаю, что нашел удобный подход для обработки исключения StaleElementReferenceException. Обычно вам приходится писать оболочки для каждого метода WebElement, чтобы повторить действия, что расстраивает и отнимает много времени.
Добавление этого кода
webDriverWait.until((webDriver1) -> (((JavascriptExecutor) webDriver).executeScript("return document.readyState").equals("complete")));
if ((Boolean) ((JavascriptExecutor) webDriver).executeScript("return window.jQuery != undefined")) {
webDriverWait.until((webDriver1) -> (((JavascriptExecutor) webDriver).executeScript("return jQuery.active == 0")));
}
перед каждым действием WebElement может повысить стабильность ваших тестов, но вы все равно можете время от времени получать исключение StaleElementReferenceException.
Итак, это то, что я придумал (используя AspectJ):
package path.to.your.aspects;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.StaleElementReferenceException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.remote.RemoteWebElement;
import org.openqa.selenium.support.pagefactory.DefaultElementLocator;
import org.openqa.selenium.support.pagefactory.internal.LocatingElementHandler;
import org.openqa.selenium.support.ui.WebDriverWait;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
@Aspect
public class WebElementAspect {
private static final Logger LOG = LogManager.getLogger(WebElementAspect.class);
/**
* Get your WebDriver instance from some kind of manager
*/
private WebDriver webDriver = DriverManager.getWebDriver();
private WebDriverWait webDriverWait = new WebDriverWait(webDriver, 10);
/**
* This will intercept execution of all methods from WebElement interface
*/
@Pointcut("execution(* org.openqa.selenium.WebElement.*(..))")
public void webElementMethods() {}
/**
* @Around annotation means that you can insert additional logic
* before and after execution of the method
*/
@Around("webElementMethods()")
public Object webElementHandler(ProceedingJoinPoint joinPoint) throws Throwable {
/**
* Waiting until JavaScript and jQuery complete their stuff
*/
waitUntilPageIsLoaded();
/**
* Getting WebElement instance, method, arguments
*/
WebElement webElement = (WebElement) joinPoint.getThis();
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
Object[] args = joinPoint.getArgs();
/**
* Do some logging if you feel like it
*/
String methodName = method.getName();
if (methodName.contains("click")) {
LOG.info("Clicking on " getBy(webElement));
} else if (methodName.contains("select")) {
LOG.info("Selecting from " getBy(webElement));
} else if (methodName.contains("sendKeys")) {
LOG.info("Entering " args[0].toString() " into " getBy(webElement));
}
try {
/**
* Executing WebElement method
*/
return joinPoint.proceed();
} catch (StaleElementReferenceException ex) {
LOG.debug("Intercepted StaleElementReferenceException");
/**
* Refreshing WebElement
* You can use implementation from this blog
* http://www.sahajamit.com/post/mystery-of-stale-element-reference-exception/
* but remove staleness check in the beginning (if(!isElementStale(elem))), because we already caught exception
* and it will result in an endless loop
*/
webElement = StaleElementUtil.refreshElement(webElement);
/**
* Executing method once again on the refreshed WebElement and returning result
*/
return method.invoke(webElement, args);
}
}
private void waitUntilPageIsLoaded() {
webDriverWait.until((webDriver1) -> (((JavascriptExecutor) webDriver).executeScript("return document.readyState").equals("complete")));
if ((Boolean) ((JavascriptExecutor) webDriver).executeScript("return window.jQuery != undefined")) {
webDriverWait.until((webDriver1) -> (((JavascriptExecutor) webDriver).executeScript("return jQuery.active == 0")));
}
}
private static String getBy(WebElement webElement) {
try {
if (webElement instanceof RemoteWebElement) {
try {
Field foundBy = webElement.getClass().getDeclaredField("foundBy");
foundBy.setAccessible(true);
return (String) foundBy.get(webElement);
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
} else {
LocatingElementHandler handler = (LocatingElementHandler) Proxy.getInvocationHandler(webElement);
Field locatorField = handler.getClass().getDeclaredField("locator");
locatorField.setAccessible(true);
DefaultElementLocator locator = (DefaultElementLocator) locatorField.get(handler);
Field byField = locator.getClass().getDeclaredField("by");
byField.setAccessible(true);
return byField.get(locator).toString();
}
} catch (IllegalAccessException | NoSuchFieldException e) {
e.printStackTrace();
}
return null;
}
}
Чтобы включить этот аспект, создайте файл
srcmainresourcesMETA-INFaop-ajc.xml
и напишите
<aspectj>
<aspects>
<aspect name="path.to.your.aspects.WebElementAspect"/>
</aspects>
</aspectj>
Добавьте это в свой pom.xml
<properties>
<aspectj.version>1.9.1</aspectj.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.0</version>
<configuration>
<argLine>
-javaagent:"${settings.localRepository}/org/aspectj/aspectjweaver/${aspectj.version}/aspectjweaver-${aspectj.version}.jar"
</argLine>
</configuration>
<dependencies>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>${aspectj.version}</version>
</dependency>
</dependencies>
</plugin>
</build>
И это все. Надеюсь, это поможет.
Ответ №9:
Вы можете решить это с помощью явного ожидания, чтобы вам не приходилось использовать жесткое ожидание.
Если вы извлекаете все элементы с одним свойством и повторяете его, используя для каждого цикла, вы можете использовать wait внутри цикла следующим образом,
List<WebElement> elements = driver.findElements("Object property");
for(WebElement element:elements)
{
new WebDriverWait(driver,10).until(ExpectedConditions.presenceOfAllElementsLocatedBy("Object property"));
element.click();//or any other action
}
или для отдельного элемента вы можете использовать приведенный ниже код,
new WebDriverWait(driver,10).until(ExpectedConditions.presenceOfAllElementsLocatedBy("Your object property"));
driver.findElement("Your object property").click();//or anyother action
Ответ №10:
В Java 8 для этого можно использовать очень простой метод:
private Object retryUntilAttached(Supplier<Object> callable) {
try {
return callable.get();
} catch (StaleElementReferenceException e) {
log.warn("tTrying once again");
return retryUntilAttached(callable);
}
}
Ответ №11:
FirefoxDriver _driver = new FirefoxDriver();
// create webdriverwait
WebDriverWait wait = new WebDriverWait(_driver, TimeSpan.FromSeconds(10));
// create flag/checker
bool result = false;
// wait for the element.
IWebElement elem = wait.Until(x => x.FindElement(By.Id("Element_ID")));
do
{
try
{
// let the driver look for the element again.
elem = _driver.FindElement(By.Id("Element_ID"));
// do your actions.
elem.SendKeys("text");
// it will throw an exception if the element is not in the dom or not
// found but if it didn't, our result will be changed to true.
result = !resu<
}
catch (Exception) { }
} while (result != true); // this will continue to look for the element until
// it ends throwing exception.
Комментарии:
1. Я добавил его только сейчас, после выяснения этого. извините за формат, который я публикую впервые. Просто пытаюсь помочь. Если вы найдете это полезным, пожалуйста, поделитесь им с другими 🙂
2. Добро пожаловать в stackoverflow! Всегда лучше предоставить краткое описание для примера кода, чтобы повысить точность post 🙂
3. Запустив приведенный выше код, вы можете навсегда застрять в цикле, если, например, на этой странице ошибка сервера.