Випадковий "Елемент більше не приєднаний до DOM" StaleElementReferenceException


143

Я сподіваюся, що це тільки я, але Селен Вебдрайвер здається повним кошмаром. Зараз веб-диспетчер 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.0b3. Я бачив, як це відбувається з драйверами FF та IE. Єдиний спосіб я можу запобігти цьому - додати фактичний виклик до Thread.sleepтого, як станеться виняток. Це поганий спосіб вирішення, тому я сподіваюся, що хтось може вказати на мою помилку, яка зробить це все краще.


26
Сподіваємось, 17-ти перегляд свідчить про те, що це не тільки ти;) Це повинно бути найвиразнішим винятком Селену там.
Марк Майо

4
48k зараз! У мене така ж проблема ...
Гал,

3
Я вважаю, що Селен чистий і повний сміття ....
C Johnson

4
60k, все ще випуск :)
Пітер Де Бі

у моєму випадку це було через те, що робитиfrom selenium.common.exceptions import NoSuchElementException
Cpt. Senkfuss

Відповіді:


119

Так, якщо у вас проблеми зі 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();

Метод присутностіOfElementLocated () виглядатиме приблизно так:

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)

На моєму досвіді, явно очікування завжди надійніше.


2
Я маю рацію, кажучи, що більше неможливо читати елементи в змінні та використовувати їх повторно? Оскільки у мене величезний сухий та динамічний WATiR DSL, який покладається на елементи, що передаються, і я намагаюся перенести веб-привід, але у мене є та сама проблема. По суті, мені доведеться додати код, щоб перечитати всі елементи в модулі для кожного тестового кроку, який змінює DOM ...
kinofrost

Привіт. Чи можу я запитати, який тип функції у цьому прикладі? Я не можу його знайти .... ДЯКУЮ!
Ганнібал

1
@Hannibal: com.google.common.base.Function<F, T>надає Гуава .
Stephan202

@jarib, я зіткнувся з цим самим питанням один рік з моменту вирішення. проблема полягає в тому, що я пишу свої сценарії в рубіні, і немає функції за назвою "присутностіOfElementLocated" або нічого подібного. ЯКІ-небудь рекомендації?
Амей

56
@jarib Я не погоджуюся, що це все викликано погано розробленим тестом. Тому що навіть після появи елемента після виклику AJAX, все ще може бути код jQuery, який може викликати StaleElementReferenceException. І ви нічого не можете зробити, крім додавання явного очікування, що не здається дуже приємним. Я скоріше думаю , що це конструктивний недолік в WebDriver
жують

10

Мені вдалося з таким успіхом використовувати такий метод:

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 вважає несвіжим.


7
У вас є помилка в цьому коді, ви не повинні тримати рекурсивно виклику методу без якогось обмеження, інакше ви підірвете свій стек.
Гаррі

2
Я думаю, що краще додати лічильник чи щось таке, тому коли ми отримуємо помилку неодноразово, ми можемо насправді викинути помилку. В іншому випадку, якщо насправді з’явиться помилка, ви опинитесь у циклі
Sudara

Я згоден, що це не результат погано написаних тестів. Селен має тенденцію робити це на сучасних веб-сайтах, навіть для найкращих написаних тестів - можливо, тому, що веб-сайти постійно оновлюють свої елементи за допомогою двосторонніх прив’язок, які є загальними в реактивних структурах веб-додатків, навіть коли не змінюються ці елементи потрібно зробити. Такий метод повинен бути частиною кожної програми Selenium, яка перевіряє сучасний веб-додаток.
наждак

10

Я отримую цю помилку іноді, коли оновлення AJAX знаходиться посередині. Капібара виглядає досить розумним щодо очікування змін DOM (див. Чому wait_until був видалений з Capybara ), але часу очікування за замовчуванням 2 секунди у моєму випадку просто не вистачало. Змінено в _spec_helper.rb_ з напр

Capybara.default_max_wait_time = 5

2
Це також вирішило мою проблему: я отримував StaleElementReferenceError і збільшуючи проблему Capybara.default_max_wait_time.
бренда

1

Я зіткнувся з тією ж проблемою і сьогодні, і склав клас обгортки, який перевіряє перед кожним методом, чи посилання на елемент все-таки дійсна. Моє рішення відновити елемент досить просте, тому я подумав, що просто поділюся ним.

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 та за необхідності отримую елемент. Якщо сторінка буде перезавантажена, ця посилання більше не працюватиме. Але до тих пір, поки будуть внесені лише зміни до приреченості. І це повинно зробити роботу в більшості випадків.

Також це дозволяє уникнути повторного пошуку елемента.

Джон


1

У мене була така ж проблема, і мою викликала стара версія селену. Я не можу оновити до нової версії через середовище розробки. Проблема викликана HTMLUnitWebElement.switchFocusToThisIfNeeded (). Під час переходу на нову сторінку може статися, що елемент, який ви натиснули на стару сторінку, є oldActiveElement(див. Нижче). Селен намагається отримати контекст зі старого елемента і не вдається. Ось чому вони побудували спробу лову в майбутніх випусках.

Код з версії драйвера 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 &&
        !oldActiveEqualsCurrent &&
        !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 &&
            !oldActiveEqualsCurrent &&
            !isBody) {
        oldActiveElement.element.blur();
        }
    } catch (StaleElementReferenceException ex) {
      // old element has gone, do nothing
    }
    element.focus();
}

Не оновлюючи до 2.23.0 або новіших, ви можете просто надати будь-який елемент на сторінці. Я просто використовував, element.click()наприклад.


1
Нічого собі ... Це було справді незрозуміло, приємна робота .. Мені зараз цікаво, чи не мають інших драйверів (наприклад, Chromeedriver) подібні проблеми
kevlarr

0

Щойно зі мною трапилося, коли я намагався надіслати_кейси до вікна пошуку, який має автоматичне оновлення, залежно від того, що ви вводите. Як зазначає Eero, це може статися, якщо ваш елемент зробить якийсь Ajax оновлений, поки ви набираєте текст всередині вхідного елемента . Рішення полягає в тому, щоб надсилати по одному символу одночасно і знову шукати вхідний елемент . (Наприклад, у рубіні показано нижче)

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

0

Щоб додати відповідь @ 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. Другий параметр - це тайм-аут у секундах (код для тайм-ауту був скопійований з ID ID Selenium для FireFox). Код можна використовувати, щоб уникнути виключення застарілого елемента таким чином:

MyTestDriver.WaitForDisplayed(driver.SpecificElementToGet,5);

Вищевказаний код буде дзвонити driver.SpecificElementToGet().Displayedдо тих пір, поки driver.SpecificElementToGet()не буде виняток і не .Displayedбуде проведено true5 секунд. Через 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);
        }
}

0

Я думаю, що я знайшов зручний підхід для обробки 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;
    }
}

Щоб увімкнути цей аспект, створіть файл src\main\resources\META-INF\aop-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>

І це все. Сподіваюся, це допомагає.


0

Ви можете вирішити це за допомогою явного очікування, щоб не довелося використовувати жорстке очікування.

Якщо ви отримуєте всі елементи з одним властивістю і повторюєте через нього, використовуючи для кожного циклу, ви можете використовувати зачекати всередині циклу, як це,

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 

-1

У Java 8 для цього можна використовувати дуже простий метод :

private Object retryUntilAttached(Supplier<Object> callable) {
    try {
        return callable.get();
    } catch (StaleElementReferenceException e) {
        log.warn("\tTrying once again");
        return retryUntilAttached(callable);
    }
}

-5
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 = !result;
    }
    catch (Exception) { }
} while (result != true); // this will continue to look for the element until
                          // it ends throwing exception.

Я додав його тільки зараз після з'ясування. Вибачте за формат, який я вперше публікую. Просто намагаюся допомогти. Якщо вам це здається корисним, будь ласка, поділіться ним іншим :)
Alvin Vera

Ласкаво просимо в stackoverflow! Завжди краще надати короткий опис зразкового коду для підвищення точності публікації :)
Програмне забезпечення Picrofo

Запустивши код вище, ви можете назавжди застрягти в циклі, якщо, наприклад, на цій сторінці є помилка сервера.
жують
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.