Thanks Driven Life

日々是感謝

Capybara で SeleniumWebdriver の Stale Element Reference Exception 対策

前略

Selenium::WebDriver::Error::StaleElementReferenceError になると悲しい

Stale Element Reference Error とは

see http://docs.seleniumhq.org/exceptions/stale_element_reference.jsp

まあ私もよくわかってないんですが、感覚としては

  1. ノード取得
  2. ページが切り替わる
  3. 1 で取得したノードにアクセスする
  4. raise Selenium::WebDriver::Error::StaleElementReferenceError

かしら。コードにするとこんな感じ

require 'sinatra'
require 'erb'
 
get '/' do
  erb :index
end
 
get '/foo' do
  erb :foo
end
 
post '/bar' do
  sleep 2
  'ok'
end
 
__END__
 
@@ layout
<!DOCTYPE html>
  <body>
  <%= yield %>
  </body>
</html>
 
@@ index
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script>
$(function() {
    $('#foo').click(function() {
        $.post(
            '/bar',
            { 'baz':'hoge' },
            function(data) {
                location.href = '/foo';
            }
        )
    });
});
</script>
<h1>Hello, World!</h1>
<input type="button" id="foo" value="push" />
 
@@ foo
<h1>FooBarBaz</h1>

テストコードはこんな感じ

class HelloWorldTest < Test::Unit::TestCase
  include Capybara::DSL
  Capybara.default_driver = :selenium
 
  def setup
    Capybara.app = Sinatra::Application.new
  end
 
  def test_button_and_goto
    visit '/'
    page.click_button('push')
    assert page.find(:css, 'h1').has_content?('FooBarBaz')
  end
end

ボタンクリックして <h1> 取得して has_content? (内部では have_content が走って synchronize) になる。 その間にページが切り替わって h1 があたらしくなる。って動きです。

実はこれでも再現性は100%ではなく、3回に1回ぐらいです。

やりたいこと

perhaps the page has changed since it was looked up って言われて悲しい思いするのはもうコリゴリなので

Stale な Exception が来たら node を再取得する

という動きにしました。

module Capybara
  module Node
    class Base
      old_sync = instance_method(:synchronize)
 
      define_method :synchronize do |*args, &block|
        retry_flag = true
        begin
          old_sync.bind(self).(*args, &block)
        rescue ::Selenium::WebDriver::Error::StaleElementReferenceError => e
          raise e unless retry_flag
          retry_flag = false
          retry
        end
      end
    end
  end
end
  1. リトライは1回だけ
    • 連続で Stale になるの今のところ想像つかないですが、なんかもうそこは諦めたいですね
  2. method.bind(self).() にした理由
    • super() とかやると Capybara::Node::Documentsynchronize()なんてねーよっていわれてめんどかったので
    • あまりよく挙動わかってない

今のところこれで安定しています。

終わり

とりあえず今回のやつはここに https://gist.github.com/gongo/6509722

$ bundle install --path vendor/bundle
$ bundle exec ruby test_web.rb

とかやるとテスト走ると思います。多分