afnf.net

SeleniumのWebElement.click()がページ読み込みの完了を待ってくれない問題への対処

Selenium Java 2015/09/03 20:56

現象

以前のエントリで、Google ChromeのみSelenium統合テストが失敗すると書きました。

具体的には、WebElement.click()の後の操作で以下のような例外が発生します。

org.openqa.selenium.StaleElementReferenceException: stale element reference: element is not attached to the page document

2015/07/30付近から発生し始めました。エラーが起きるのはGoogle Chromeだけで、FirefoxやPhantomJSでは発生しません。

原因

WebElementのJavadocには以下のようにあります。

https://seleniumhq.github.io/selenium/docs/api/java/org/openqa/selenium/WebElement.html#click%28%29

Click this element. If this causes a new page to load, you should discard all references to this element and any further operations performed on this element will throw a StaleElementReferenceException. Note that if click() is done by sending a native event (which is the default on most browsers/platforms) then the method will _not_ wait for the next page to load and the caller should verify that themselves. There are some preconditions for an element to be clicked. The element must be visible and it must have a height and width greater then 0.

自分でロード完了を検証しないとStaleElementReferenceExceptionが起きるよ、って書いてありますね。

blog-java2では、clickでAjaxが2回実行されたり、またjQueryの$(document).ready()をトリガーとした遅延実行があります。この問題に対処するため、Javascript側のafnfblog.loadingというフラグを使って、ローディングが完了したかどうかをチェックしていましたが、Chromeに限ってはこれでも足りなくなってしまったようです。

対処

WebElement.click()実行後に、Javascriptを使って状態のチェックを行います。一般的には、clickして待つというユーティリティーメソッドで置き換えるのが普通だと思うのですが、大量のテストケースを修正するのがあまりに面倒なので、透過的に扱う方法を考えました。

まずWebDriverのラッパーを作って、findElement/findElementsの戻り値を、自作のClickAndWaitRemoteWebElementに置き換えます。

@Override
public WebElement findElement(By by) {
  WebElement e = getInstance().findElement(by);
  if (e instanceof RemoteWebElement) {
    e = new ClickAndWaitRemoteWebElement(e, this);
  }
  return e;
}

@Override
public List<WebElement> findElements(By by) {
  List<WebElement> list = getInstance().findElements(by);
  List<WebElement> newList = new ArrayList<>(list.size());
  for (WebElement e : list) {
    if (e instanceof RemoteWebElement) {
      e = new ClickAndWaitRemoteWebElement(e, this);
    }
    newList.add(e);
  }
  return newList;
}


ClickAndWaitRemoteWebElementはclick()をオーバーライドしています。親クラスのclick()を呼んだ後でwd.waitForPageLoaded();を実行します。

@Override
public void click() {
  try {
    parent.click();
  }
  catch (TimeoutException e) {
    // phantomjsが偶発的にTimeoutExceptionを出してしまう
    if (wd.getInstance() instanceof PhantomJSDriver) {
      SeleniumTestBase.takeScreenShot(wd, "TimeoutException");
      logger.warn("TimeoutException", e);
    }
    else {
      throw e;
    }
  }
  wd.waitForPageLoaded();
}

PhantomJSについては、clickでTimeoutExceptionが偶発的に発生しました。これは誤検出で実際には成功しているため、例外を握りつぶしています。


waitForPageLoadedはWebDriverWrapperに定義されている通り、以下のJavascriptがtrueになるまで待ってくれます。

document.readyState == 'complete' && afnfblog.loading == false


以上の修正によって、問題は解決できたようです。ちなみに、readyStateとafnfblog.loadingの両方のチェックが必要でした。片方だとNG。

あまり一般的な方法ではないと思いますが、参考までってことで。

Selenium Java 2015/09/03 20:56
comments (0)

blog-java2 engine (build:2016-07-31 15:08 JST)