Wdrażam wiele testów Selenium w Javie. Czasami moje testy kończą się niepowodzeniem z powodu pliku StaleElementReferenceException
. Czy mógłbyś zasugerować jakieś podejście do uczynienia testów bardziej stabilnymi?
Wdrażam wiele testów Selenium w Javie. Czasami moje testy kończą się niepowodzeniem z powodu pliku StaleElementReferenceException
. Czy mógłbyś zasugerować jakieś podejście do uczynienia testów bardziej stabilnymi?
Odpowiedzi:
Może się tak zdarzyć, jeśli operacja DOM na stronie tymczasowo powoduje niedostępność elementu. Aby pozwolić na takie przypadki, możesz spróbować uzyskać dostęp do elementu kilka razy w pętli, zanim ostatecznie wyrzucisz wyjątek.
Wypróbuj to doskonałe rozwiązanie ze strony darrelgrainger.blogspot.com :
public boolean retryingFindClick(By by) {
boolean result = false;
int attempts = 0;
while(attempts < 2) {
try {
driver.findElement(by).click();
result = true;
break;
} catch(StaleElementException e) {
}
attempts++;
}
return result;
}
Miałem ten problem sporadycznie. Bez mojej wiedzy BackboneJS działał na stronie i zastępował element, który próbowałem kliknąć. Mój kod wyglądał tak.
driver.findElement(By.id("checkoutLink")).click();
Co oczywiście jest funkcjonalnie takie samo.
WebElement checkoutLink = driver.findElement(By.id("checkoutLink"));
checkoutLink.click();
Czasami zdarzało się, że javascript zastępował element checkoutLink między znalezieniem a kliknięciem, tj.
WebElement checkoutLink = driver.findElement(By.id("checkoutLink"));
// javascript replaces checkoutLink
checkoutLink.click();
Co słusznie doprowadziło do wyjątku StaleElementReferenceException podczas próby kliknięcia łącza. Nie mogłem znaleźć żadnego niezawodnego sposobu, aby powiedzieć WebDriverowi, aby zaczekał, aż JavaScript się zakończy, więc oto, jak ostatecznie to rozwiązałem.
new WebDriverWait(driver, timeout)
.ignoring(StaleElementReferenceException.class)
.until(new Predicate<WebDriver>() {
@Override
public boolean apply(@Nullable WebDriver driver) {
driver.findElement(By.id("checkoutLink")).click();
return true;
}
});
Ten kod będzie nieustannie próbował kliknąć łącze, ignorując StaleElementReferenceExceptions, dopóki kliknięcie się nie powiedzie lub nie zostanie osiągnięty limit czasu. Podoba mi się to rozwiązanie, ponieważ oszczędza konieczności pisania logiki ponawiania i wykorzystuje tylko wbudowane konstrukcje WebDriver.
Zwykle jest to spowodowane tym, że DOM jest aktualizowany i próbujesz uzyskać dostęp do zaktualizowanego / nowego elementu - ale DOM został odświeżony, więc jest to nieprawidłowe odniesienie, które masz ...
Aby obejść ten problem, najpierw użyj jawnego czekania na element, aby upewnić się, że aktualizacja jest zakończona, a następnie ponownie pobierz nowe odwołanie do elementu.
Oto kod pseudo do zilustrowania (zaadaptowany z kodu C #, którego używam DOKŁADNIE w tym problemie):
WebDriverWait wait = new WebDriverWait(browser, TimeSpan.FromSeconds(10));
IWebElement aRow = browser.FindElement(By.XPath(SOME XPATH HERE);
IWebElement editLink = aRow.FindElement(By.LinkText("Edit"));
//this Click causes an AJAX call
editLink.Click();
//must first wait for the call to complete
wait.Until(ExpectedConditions.ElementExists(By.XPath(SOME XPATH HERE));
//you've lost the reference to the row; you must grab it again.
aRow = browser.FindElement(By.XPath(SOME XPATH HERE);
//now proceed with asserts or other actions.
Mam nadzieję że to pomoże!
Rozwiązanie Kenny'ego jest dobre, ale można je zapisać w bardziej elegancki sposób
new WebDriverWait(driver, timeout)
.ignoring(StaleElementReferenceException.class)
.until((WebDriver d) -> {
d.findElement(By.id("checkoutLink")).click();
return true;
});
Lub też:
new WebDriverWait(driver, timeout).ignoring(StaleElementReferenceException.class).until(ExpectedConditions.elementToBeClickable(By.id("checkoutLink")));
driver.findElement(By.id("checkoutLink")).click();
W każdym razie najlepszym rozwiązaniem jest skorzystanie z biblioteki Selenide, która obsługuje tego typu rzeczy i nie tylko. (zamiast odwołań do elementów obsługuje serwery proxy, więc nigdy nie musisz zajmować się przestarzałymi elementami, co może być dość trudne). Selenid
Powód, dla którego StaleElementReferenceException
występuje, został już określony: aktualizacje DOM-u pomiędzy znalezieniem a zrobieniem czegoś z elementem.
W przypadku problemu z kliknięciem niedawno zastosowałem takie rozwiązanie:
public void clickOn(By locator, WebDriver driver, int timeout)
{
final WebDriverWait wait = new WebDriverWait(driver, timeout);
wait.until(ExpectedConditions.refreshed(
ExpectedConditions.elementToBeClickable(locator)));
driver.findElement(locator).click();
}
Kluczową częścią jest „powiązanie” własnego Selenu ExpectedConditions
za pomocą ExpectedConditions.refreshed()
. To faktycznie czeka i sprawdza, czy dany element został odświeżony w określonym czasie i dodatkowo czeka, aż element stanie się klikalny.
Zajrzyj do dokumentacji odświeżonej metody .
W moim projekcie wprowadziłem pojęcie StableWebElement. Jest to opakowanie dla WebElement, które jest w stanie wykryć, czy element jest nieaktualny i znaleźć nowe odniesienie do oryginalnego elementu. Dodałem metody pomocnika do lokalizowania elementów, które zwracają StableWebElement zamiast WebElement i problem ze StaleElementReference zniknął.
public static IStableWebElement FindStableElement(this ISearchContext context, By by)
{
var element = context.FindElement(by);
return new StableWebElement(context, element, by, SearchApproachType.First);
}
Kod w C # jest dostępny na stronie mojego projektu, ale można go łatwo przenieść do java https://github.com/cezarypiatek/Tellurium/blob/master/Src/MvcPages/SeleniumUtils/StableWebElement.cs
Rozwiązaniem w C # byłoby:
Klasa pomocnika:
internal class DriverHelper
{
private IWebDriver Driver { get; set; }
private WebDriverWait Wait { get; set; }
public DriverHelper(string driverUrl, int timeoutInSeconds)
{
Driver = new ChromeDriver();
Driver.Url = driverUrl;
Wait = new WebDriverWait(Driver, TimeSpan.FromSeconds(timeoutInSeconds));
}
internal bool ClickElement(string cssSelector)
{
//Find the element
IWebElement element = Wait.Until(d=>ExpectedConditions.ElementIsVisible(By.CssSelector(cssSelector)))(Driver);
return Wait.Until(c => ClickElement(element, cssSelector));
}
private bool ClickElement(IWebElement element, string cssSelector)
{
try
{
//Check if element is still included in the dom
//If the element has changed a the OpenQA.Selenium.StaleElementReferenceException is thrown.
bool isDisplayed = element.Displayed;
element.Click();
return true;
}
catch (StaleElementReferenceException)
{
//wait until the element is visible again
element = Wait.Until(d => ExpectedConditions.ElementIsVisible(By.CssSelector(cssSelector)))(Driver);
return ClickElement(element, cssSelector);
}
catch (Exception)
{
return false;
}
}
}
Wezwanie:
DriverHelper driverHelper = new DriverHelper("http://www.seleniumhq.org/docs/04_webdriver_advanced.jsp", 10);
driverHelper.ClickElement("input[value='csharp']:first-child");
Podobnie może być używane w przypadku języka Java.
Rozwiązanie Kenny'ego jest przestarzałe. Użyj tego, używam klasy akcji do dwukrotnego klikania, ale możesz zrobić wszystko.
new FluentWait<>(driver).withTimeout(30, TimeUnit.SECONDS).pollingEvery(5, TimeUnit.SECONDS)
.ignoring(StaleElementReferenceException.class)
.until(new Function() {
@Override
public Object apply(Object arg0) {
WebElement e = driver.findelement(By.xpath(locatorKey));
Actions action = new Actions(driver);
action.moveToElement(e).doubleClick().perform();
return true;
}
});
findByAndroidId
metoda, która z wdziękiem radzi sobie StaleElementReference
.Jest to w dużej mierze oparte na odpowiedzi jspcal, ale musiałem zmodyfikować tę odpowiedź, aby działała bezproblemowo z naszą konfiguracją, więc chciałem dodać ją tutaj, na wypadek, gdyby była pomocna dla innych. Jeśli ta odpowiedź ci pomogła, przejdź do odpowiedzi upvote jspcal .
// This loops gracefully handles StateElementReference errors and retries up to 10 times. These can occur when an element, like a modal or notification, is no longer available.
export async function findByAndroidId( id, { assert = wd.asserters.isDisplayed, timeout = 10000, interval = 100 } = {} ) {
MAX_ATTEMPTS = 10;
let attempt = 0;
while( attempt < MAX_ATTEMPTS ) {
try {
return await this.waitForElementById( `android:id/${ id }`, assert, timeout, interval );
}
catch ( error ) {
if ( error.message.includes( "StaleElementReference" ) )
attempt++;
else
throw error; // Re-throws the error so the test fails as normal if the assertion fails.
}
}
}
To działa dla mnie (100% działa) przy użyciu C #
public Boolean RetryingFindClick(IWebElement webElement)
{
Boolean result = false;
int attempts = 0;
while (attempts < 2)
{
try
{
webElement.Click();
result = true;
break;
}
catch (StaleElementReferenceException e)
{
Logging.Text(e.Message);
}
attempts++;
}
return result;
}
Problem polega na tym, że zanim przekażesz element z JavaScript do Java z powrotem do Javascript, może on opuścić DOM.
Spróbuj zrobić wszystko w Javascript:
driver.executeScript("document.querySelector('#my_id').click()")
Spróbuj tego
while (true) { // loops forever until break
try { // checks code for exceptions
WebElement ele=
(WebElement)wait.until(ExpectedConditions.elementToBeClickable((By.xpath(Xpath))));
break; // if no exceptions breaks out of loop
}
catch (org.openqa.selenium.StaleElementReferenceException e1) {
Thread.sleep(3000); // you can set your value here maybe 2 secs
continue; // continues to loop if exception is found
}
}
Znalazłem tutaj rozwiązanie . W moim przypadku element staje się niedostępny w przypadku opuszczenia bieżącego okna, zakładki lub strony i ponownego powrotu.
.ignoring (StaleElement ...), .refreshed (...) i elementToBeClicable (...) nie pomogły i otrzymałem wyjątek na act.doubleClick(element).build().perform();
łańcuchu.
Używanie funkcji w mojej głównej klasie testowej:
openForm(someXpath);
Moja funkcja BaseTest:
int defaultTime = 15;
boolean openForm(String myXpath) throws Exception {
int count = 0;
boolean clicked = false;
while (count < 4 || !clicked) {
try {
WebElement element = getWebElClickable(myXpath,defaultTime);
act.doubleClick(element).build().perform();
clicked = true;
print("Element have been clicked!");
break;
} catch (StaleElementReferenceException sere) {
sere.toString();
print("Trying to recover from: "+sere.getMessage());
count=count+1;
}
}
Funkcja My BaseClass:
protected WebElement getWebElClickable(String xpath, int waitSeconds) {
wait = new WebDriverWait(driver, waitSeconds);
return wait.ignoring(StaleElementReferenceException.class).until(
ExpectedConditions.refreshed(ExpectedConditions.elementToBeClickable(By.xpath(xpath))));
}
Może istnieć potencjalny problem prowadzący do wyjątku StaleElementReferenceException, o którym nikt do tej pory nie wspomniał (w odniesieniu do działań).
Wyjaśniam to w Javascript, ale tak samo jest w Javie.
To nie zadziała:
let actions = driver.actions({ bridge: true })
let a = await driver.findElement(By.css('#a'))
await actions.click(a).perform() // this leads to a DOM change, #b will be removed and added again to the DOM.
let b = await driver.findElement(By.css('#b'))
await actions.click(b).perform()
Ale ponowne utworzenie instancji akcji rozwiąże problem:
let actions = driver.actions({ bridge: true })
let a = await driver.findElement(By.css('#a'))
await actions.click(a).perform() // this leads to a DOM change, #b will be removed and added again to the DOM.
actions = driver.actions({ bridge: true }) // new
let b = await driver.findElement(By.css('#b'))
await actions.click(b).perform()
Zwykle StaleElementReferenceException pojawia się, gdy element, do którego próbujemy uzyskać dostęp, ale inne elementy mogą wpływać na pozycję elementu, który nas interesuje, dlatego gdy próbujemy kliknąć lub pobraćText lub spróbować wykonać jakąś akcję na WebElement, otrzymujemy wyjątek, który zwykle mówi, że element nie jest dołączony do DOM .
Rozwiązanie, które wypróbowałem, jest następujące:
protected void clickOnElement(By by) {
try {
waitForElementToBeClickableBy(by).click();
} catch (StaleElementReferenceException e) {
for (int attempts = 1; attempts < 100; attempts++) {
try {
waitFor(500);
logger.info("Stale element found retrying:" + attempts);
waitForElementToBeClickableBy(by).click();
break;
} catch (StaleElementReferenceException e1) {
logger.info("Stale element found retrying:" + attempts);
}
}
}
protected WebElement waitForElementToBeClickableBy(By by) {
WebDriverWait wait = new WebDriverWait(getDriver(), 10);
return wait.until(ExpectedConditions.elementToBeClickable(by));
}
W powyższym kodzie najpierw próbuję poczekać, a następnie klikam element, jeśli wystąpi wyjątek, a następnie łapię go i próbuję zapętlić, ponieważ istnieje możliwość, że nadal wszystkie elementy mogą nie zostać załadowane i ponownie może wystąpić wyjątek.
Być może został dodany niedawno, ale inne odpowiedzi nie wspominają o ukrytej funkcji oczekiwania Selenium, która wykonuje wszystkie powyższe czynności za Ciebie i jest wbudowana w Selenium.
driver.manage().timeouts().implicitlyWait(10,TimeUnit.SECONDS);
Spowoduje to ponowienie findElement()
wywołań do momentu znalezienia elementu lub przez 10 sekund.
Źródło - http://www.seleniumhq.org/docs/04_webdriver_advanced.jsp