source

랜덤 "Element are not attached to the DOM" StaleElementReferenceException

gigabyte 2022. 7. 23. 13:58
반응형

랜덤 "Element are not attached to the DOM" StaleElementReferenceException

나만 그랬으면 좋겠는데, 셀레늄 웹드라이버는 완전 악몽 같아.Chrome Web 드라이버는 현재 사용할 수 없고, 다른 드라이버는 매우 신뢰할 수 없는 것 같습니다.저는 많은 문제와 싸우고 있지만, 여기 한가지 문제가 있습니다.

랜덤으로 테스트에 불합격하고

"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'"

2.0b3로 하다FF 드라이버와 IE 드라이버에서 이러한 현상이 발생하는 것을 본 적이 있습니다.할 수 있는 을 에 입니다.Thread.sleep예외가 발생하기 전에.하지만 그것은 서투른 대처법이기 때문에, 누군가 제 쪽에서 모든 것을 개선시킬 수 있는 오류를 지적하고 개선해 주었으면 합니다.

네, StaleElementReferenceExceptions에 문제가 있는 경우 레이스 조건이 있기 때문입니다.다음 시나리오를 고려합니다.

WebElement element = driver.findElement(By.id("foo"));
// DOM changes - page is refreshed, or element is removed and re-added
element.click();

요소를 클릭하면 요소 참조가 더 이상 유효하지 않습니다.WebDriver가 이러한 상황이 발생할 수 있는 모든 경우를 정확하게 추측하는 것은 불가능에 가깝습니다.따라서 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();

presence Of Element Located() 메서드는 다음과 같습니다.

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 드라이버가 재작성되어 대부분의 구현이 Chrome 개발자에 의해 트리의 일부로 이루어졌다는 것을 들으면 기쁘실 것입니다.

PS. 또는 위의 예시와 같이 명시적으로 대기하는 대신 암묵적인 대기를 활성화 할 수 있습니다.이렇게 하면 WebDriver는 지정된 타임아웃까지 항상 루프업하여 요소가 존재하기를 기다립니다.

driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS)

하지만 내 경험상, 명시적으로 기다리는 것이 항상 더 신뢰할 수 있다.

저는 다음과 같은 방법을 사용하여 어느 정도 성공을 거두고 있습니다.

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가 오래된 것으로 간주하고 있는지, 어느 쪽인가에 결함이 있다고 생각합니다.

AJAX 업데이트 도중 이 오류가 발생할 수 있습니다.Capybara는 DOM 변경을 기다리는 것이 매우 스마트한 것 같습니다('Why wait_until is removed from Capybara' 참조).하지만 기본 대기시간인 2초로는 충분하지 않았습니다._spec_helper.rb_에서 다음과 같이 변경되었습니다.

Capybara.default_max_wait_time = 5

오늘도 같은 문제에 직면하여 모든 메서드에 앞서 요소 참조가 유효한지 확인하는 래퍼 클래스를 만들었습니다.엘리먼트를 회수하는 방법은 매우 간단하기 때문에 공유하려고 합니다.

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 변수에 저장하고 필요에 따라 요소를 가져옵니다.페이지가 새로고침되면 이 참조는 더 이상 작동하지 않습니다.그러나 변경만 이루어진다면 참조는 그대로 유지됩니다.그리고 대부분의 경우 그렇게 할 수 있습니다.

또, 요소의 재검색도 회피합니다.

존.

나도 같은 문제가 있었는데 내 문제는 오래된 셀레늄 버전 때문에 생긴 거야.개발 환경상 최신 버전으로 업데이트할 수 없습니다.HTMLUnitWebElement.switchFocusToThisIfNeeded() 입니다.가 " " "일 수 .oldActiveElement(일부러)Selenium은 오래된 요소에서 콘텍스트를 가져오려고 하지만 실패합니다.그것이 그들이 향후 출시에서 트라이캐치를 만든 이유입니다.

selenium-htmlunit-driver 버전 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-self-unit-driver 버전 >= 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포커스 상의 할 수 .2.23.0 입니다. ★★★★★★★★★★★★★★★★★★★★★★★★★element.click()예를들면.

검색 입력 상자에 _keys를 전송하려고 할 때 발생했습니다.입력한 항목에 따라 자동 업로드 날짜가 지정됩니다.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

@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 개체를 반환하는 함수입니다.두 번째 파라미터는 타임아웃(초단위)입니다(타임아웃 코드는 FireFox의 Selenium IDE에서 복사되었습니다).코드는 다음과 같은 방법으로 오래된 요소의 예외를 회피하기 위해 사용할 수 있습니다.

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);
        }
}

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은 때때로 얻을 수 있습니다.

(Aspect J를 사용하여) 생각해 낸 것은 다음과 같습니다.

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>

그리고 그게 다예요.도움이 됐으면 좋겠다.

이 문제를 해결하려면 명시적 대기를 사용해야 합니다.

하나의 속성을 가진 모든 요소를 가져오고 각 루프에 대해 사용하여 반복하면 다음과 같이 루프 내에서 대기할 수 있습니다.

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 

Java 8에서는 매우 간단한 방법을 사용할 수 있습니다.

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

언급URL : https://stackoverflow.com/questions/5709204/random-element-is-no-longer-attached-to-the-dom-staleelementreferenceexception

반응형