Acak "Elemen tidak lagi melekat pada DOM" StaleElementReferenceException

143

Saya berharap ini hanya saya, tetapi Selenium Webdriver sepertinya seperti mimpi buruk. Webdriver Chrome saat ini tidak dapat digunakan, dan driver lain cukup tidak dapat diandalkan, atau sepertinya begitu. Saya menghadapi banyak masalah, tetapi ini adalah salah satunya.

Secara acak, pengujian saya akan gagal dengan a

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

Saya menggunakan versi webdriver 2.0b3. Saya telah melihat ini terjadi dengan driver FF dan IE. Satu-satunya cara saya dapat mencegah ini adalah dengan menambahkan panggilan aktual Thread.sleepsebelum pengecualian terjadi. Itu adalah solusi yang buruk, jadi saya berharap seseorang dapat menunjukkan kesalahan pada bagian saya yang akan membuat ini semua lebih baik.

Ray Nicholus
sumber
26
Semoga tampilan 17k menunjukkan bahwa itu bukan hanya Anda;) Ini harus menjadi pengecualian Selenium yang paling membuat frustrasi di luar sana.
Mark Mayo
4
48k sekarang! Saya memiliki masalah yang sama ...
Gal
3
Saya menemukan bahwa Selenium adalah sampah murni dan lengkap ....
C Johnson
4
60k, masih menjadi masalah :)
Pieter De Bie
dalam kasus saya itu karena melakukanfrom selenium.common.exceptions import NoSuchElementException
Cpt. Senkfuss

Jawaban:

119

Ya, jika Anda mengalami masalah dengan StaleElementReferenceExceptions itu karena tes Anda ditulis dengan buruk. Ini kondisi balapan. Pertimbangkan skenario berikut:

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

Sekarang pada titik di mana Anda mengklik elemen, referensi elemen tidak lagi valid. Hampir tidak mungkin bagi WebDriver untuk membuat perkiraan yang baik tentang semua kasus di mana ini mungkin terjadi - jadi itu mengangkat tangannya dan memberikan kontrol kepada Anda, yang sebagai penulis tes / aplikasi harus tahu persis apa yang mungkin atau mungkin tidak terjadi. Yang ingin Anda lakukan adalah menunggu secara eksplisit hingga DOM berada dalam kondisi di mana Anda tahu segala sesuatu tidak akan berubah. Misalnya, menggunakan WebDriverWait untuk menunggu elemen tertentu ada:

// 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();

Metode presenceOfElementLocated () akan terlihat seperti ini:

private static Function<WebDriver,WebElement> presenceOfElementLocated(final By locator) {
    return new Function<WebDriver, WebElement>() {
        @Override
        public WebElement apply(WebDriver driver) {
            return driver.findElement(locator);
        }
    };
}

Anda cukup benar tentang driver Chrome saat ini yang cukup tidak stabil, dan Anda akan senang mendengar bahwa bagasi Selenium memiliki driver Chrome yang ditulis ulang, di mana sebagian besar implementasinya dilakukan oleh pengembang Chromium sebagai bagian dari pohon mereka.

PS. Sebagai alternatif, alih-alih menunggu secara eksplisit seperti pada contoh di atas, Anda dapat mengaktifkan menunggu secara implisit - dengan cara ini WebDriver akan selalu berulang hingga batas waktu yang ditentukan menunggu elemen menjadi ada:

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

Dalam pengalaman saya, menunggu secara eksplisit selalu lebih dapat diandalkan.

jarib
sumber
2
Apakah saya benar mengatakan bahwa tidak mungkin lagi untuk membaca elemen ke dalam variabel dan menggunakannya kembali? Karena saya memiliki DSL WATiR kering dan dinamis yang besar yang bergantung pada elemen yang lewat dan saya mencoba untuk port ke webdriver, tapi saya mengalami masalah yang sama. Pada dasarnya saya harus menambahkan kode untuk membaca kembali semua elemen dalam modul untuk setiap langkah tes yang mengubah DOM ...
kinofrost
Hai. Bolehkah saya bertanya apa fungsi Function dalam contoh ini? Sepertinya saya tidak dapat menemukannya .... Terima kasih!
Hannibal
1
@Hannibal:, com.google.common.base.Function<F, T>disediakan oleh Guava .
Stephan202
@jarib, saya menghadapi masalah yang sama ini satu tahun sejak solusi Anda. masalahnya adalah saya menulis skrip saya di ruby, dan tidak ada fungsi dengan nama 'presenceOfElementLocated' atau yang serupa. Rekomendasi saya?
Amey
56
@jarib Saya tidak setuju ini semua disebabkan oleh tes yang dirancang dengan buruk. Karena bahkan setelah elemen muncul setelah panggilan AJAX mungkin ada kode jQuery masih berjalan yang dapat menyebabkan StaleElementReferenceException. Dan tidak ada yang dapat Anda lakukan kecuali menambahkan tunggu eksplisit yang tampaknya tidak terlalu bagus. Saya agak berpikir ini adalah cacat desain di WebDriver
mengunyah
10

Saya telah dapat menggunakan metode seperti ini dengan beberapa keberhasilan:

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

Ya, itu hanya terus mengumpulkan elemen sampai tidak lagi dianggap basi (segar?). Tidak benar-benar sampai ke akar masalahnya, tetapi saya telah menemukan bahwa WebDriver bisa agak pilih-pilih untuk melempar pengecualian ini - kadang-kadang saya mendapatkannya, dan kadang-kadang tidak. Atau bisa jadi DOM benar-benar berubah.

Jadi saya tidak setuju dengan jawaban di atas bahwa ini tentu menunjukkan tes yang ditulis dengan buruk. Saya mendapatkannya di halaman baru yang saya belum berinteraksi dengan cara apa pun. Saya pikir ada beberapa kelemahan dalam bagaimana DOM diwakili, atau dalam apa yang dianggap WebDriver basi.

Aearon
sumber
7
Anda memiliki bug dalam kode ini, Anda seharusnya tidak terus memanggil metode ini tanpa semacam batasan atau Anda akan meledakkan tumpukan Anda.
Harry
2
Saya pikir lebih baik menambahkan penghitung atau sesuatu, jadi ketika kita mendapatkan kesalahan berulang kali, kita sebenarnya bisa melempar kesalahan. Kalau tidak, jika sebenarnya ada kesalahan, Anda akan berakhir dalam satu lingkaran
Sudara
Saya setuju bahwa itu bukan hasil tes yang ditulis dengan buruk. Ada kecenderungan bagi Selenium untuk melakukan ini di situs web modern, bahkan untuk tes tertulis terbaik - mungkin karena situs web terus menyegarkan elemen-elemen mereka melalui ikatan dua arah yang umum dalam kerangka kerja aplikasi web reaktif, bahkan ketika tidak ada perubahan pada elemen-elemen itu perlu dibuat. Metode seperti ini harus menjadi bagian dari setiap kerangka kerja Selenium yang menguji aplikasi web modern.
Amril
10

Saya mendapatkan kesalahan ini kadang-kadang ketika pembaruan AJAX di tengah jalan. Capybara tampaknya cukup pintar menunggu perubahan DOM (lihat Mengapa wait_until dihapus dari Capybara ), tetapi waktu tunggu default 2 detik tidak cukup dalam kasus saya. Berubah _spec_helper.rb_ dengan misalnya

Capybara.default_max_wait_time = 5
Eero
sumber
2
Ini juga memperbaiki masalah saya: Saya mendapatkan StaleElementReferenceError dan meningkatkan Capybara.default_max_wait_time memecahkan masalah.
brendan
1

Saya menghadapi masalah yang sama hari ini dan membuat kelas pembungkus, yang memeriksa sebelum setiap metode jika referensi elemen masih valid. Solusi saya untuk retrive elemen cukup sederhana jadi saya pikir saya baru saja membagikannya.

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

Anda melihat saya "mencari" atau lebih tepatnya menyimpan elemen dalam variabel global js dan mengambil elemen jika diperlukan. Jika halaman dimuat ulang referensi ini tidak akan berfungsi lagi. Tetapi selama hanya perubahan yang dilakukan untuk menghancurkan referensi tetap. Dan itu harusnya berhasil dalam banyak kasus.

Juga menghindari pencarian ulang elemen.

John

Iwan1993
sumber
1

Saya memiliki masalah yang sama dan masalah saya disebabkan oleh versi selenium lama. Saya tidak dapat memperbarui ke versi yang lebih baru karena lingkungan pengembangan. Masalahnya disebabkan oleh HTMLUnitWebElement.switchFocusToThisIfNeeded (). Ketika Anda menavigasi ke halaman baru, mungkin terjadi bahwa elemen yang Anda klik pada halaman lama adalah oldActiveElement(lihat di bawah). Selenium mencoba untuk mendapatkan konteks dari elemen yang lama dan gagal. Itu sebabnya mereka membuat try catch di rilis mendatang.

Kode dari versi 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();
    }
}

Kode dari versi selenium-htmlunit-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();
}

Tanpa memperbarui ke 2.23.0 atau yang lebih baru, Anda bisa memberikan elemen apa saja pada fokus halaman. Saya hanya menggunakan element.click()misalnya.

preman-gamer
sumber
1
Wow ... Ini adalah penemuan yang benar-benar tidak jelas, kerja bagus .. Saya sekarang bertanya-tanya apakah driver lain (misalnya chromedriver) memiliki masalah serupa juga
kevlarr
0

Kebetulan saya ketika mencoba mengirim_keys ke kotak input pencarian - yang memiliki pembaruan otomatis tergantung pada apa yang Anda ketik. Seperti yang disebutkan oleh Eero, ini dapat terjadi jika elemen Anda melakukan beberapa pembaruan Ajax saat Anda mengetik teks di dalam elemen input . Solusinya adalah mengirim satu karakter sekaligus dan mencari lagi elemen input . (Mis. Dalam rubi yang ditunjukkan di bawah)

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
ibaralf
sumber
0

Untuk menambah jawaban @ jarib, saya telah membuat beberapa metode ekstensi yang membantu menghilangkan kondisi balapan.

Ini pengaturan saya:

Saya memiliki kelas yang disebut "Driver.cs". Ini berisi kelas statis penuh metode ekstensi untuk driver dan fungsi statis lain yang berguna.

Untuk elemen yang biasanya perlu saya ambil, saya membuat metode ekstensi seperti berikut:

public static IWebElement SpecificElementToGet(this IWebDriver driver) {
    return driver.FindElement(By.SomeSelector("SelectorText"));
}

Ini memungkinkan Anda untuk mengambil elemen itu dari kelas tes apa pun dengan kode:

driver.SpecificElementToGet();

Sekarang, jika ini menghasilkan a StaleElementReferenceException, saya memiliki metode statis berikut di kelas driver saya:

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

Parameter pertama fungsi ini adalah fungsi apa pun yang mengembalikan objek IWebElement. Parameter kedua adalah batas waktu dalam detik (kode untuk batas waktu itu disalin dari Selenium IDE untuk FireFox). Kode dapat digunakan untuk menghindari pengecualian elemen basi dengan cara berikut:

MyTestDriver.WaitForDisplayed(driver.SpecificElementToGet,5);

Kode di atas akan memanggil driver.SpecificElementToGet().Displayedhingga driver.SpecificElementToGet()tidak ada pengecualian dan .Displayedmengevaluasi hingga true5 detik belum berlalu. Setelah 5 detik, tes akan gagal.

Di sisi lain, untuk menunggu elemen tidak ada, Anda dapat menggunakan fungsi berikut dengan cara yang sama:

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);
        }
}
Pantai Jared
sumber
0

Saya rasa saya menemukan pendekatan yang mudah untuk menangani StaleElementReferenceException. Biasanya Anda harus menulis pembungkus untuk setiap metode WebElement untuk mencoba lagi tindakan, yang membuat frustrasi dan menghabiskan banyak waktu.

Menambahkan kode ini

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

sebelum setiap tindakan WebElement dapat meningkatkan stabilitas tes Anda, tetapi Anda masih bisa mendapatkan StaleElementReferenceException dari waktu ke waktu.

Jadi ini yang saya buat (menggunakan 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;
    }
}

Untuk mengaktifkan aspek ini buat file src\main\resources\META-INF\aop-ajc.xml dan tulis

<aspectj>
    <aspects>
        <aspect name="path.to.your.aspects.WebElementAspect"/>
    </aspects>
</aspectj>

Tambahkan ini ke 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>

Dan itu saja. Semoga ini bisa membantu.

Alexander Oreshin
sumber
0

Anda dapat menyelesaikan ini dengan menggunakan tunggu eksplisit sehingga Anda tidak perlu menggunakan menunggu keras.

Jika Anda mengambil semua elemen dengan satu properti dan mengulanginya menggunakan setiap loop yang bisa Anda gunakan tunggu di dalam loop seperti ini,

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
}

atau untuk elemen tunggal Anda dapat menggunakan kode di bawah ini,

new WebDriverWait(driver,10).until(ExpectedConditions.presenceOfAllElementsLocatedBy("Your object property"));
driver.findElement("Your object property").click();//or anyother action 
Prajeeth Anand
sumber
-1

Di Java 8 Anda dapat menggunakan metode yang sangat sederhana untuk itu:

private Object retryUntilAttached(Supplier<Object> callable) {
    try {
        return callable.get();
    } catch (StaleElementReferenceException e) {
        log.warn("\tTrying once again");
        return retryUntilAttached(callable);
    }
}
Łukasz Jasiński
sumber
-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
sumber
Saya menambahkannya sekarang setelah mencari tahu. maaf untuk format ini pertama kali saya memposting. Hanya berusaha membantu. Jika Anda merasa itu berguna, silakan bagikan kepada orang lain :)
Alvin Vera
Selamat datang di stackoverflow! Itu selalu lebih baik untuk memberikan deskripsi singkat untuk kode sampel untuk meningkatkan akurasi posting :)
Perangkat Lunak Picrofo
Menjalankan kode di atas Anda mungkin terjebak dalam loop selamanya, jika misalnya ada kesalahan server pada halaman itu.
Mengunyah