capybaraコードリーディング(find, synchronize)

はじめに

capybaraでは find メソッドを使うとある一定の時間が経過するまでリトライし続けます。

今回は find メソッドをターゲットに、どうやってリトライし続ける動作を実現しているのかを見ていこうと思います。

ちなみに私は最初それを知らずに 「ページのロードが終わってないからかな?(適当)」なんてアタリをつけて sleep 1 をテストコードに埋めてたりしていました。

しかしそんなことをするくらいだったら Capybara.default_max_wait_time の時間を伸ばしたほうがいいです。 Capybara.default_max_wait_time とは findall メソッドを使用したときに探し続けるのに使用する最大の待ち時間です。

ちなみに調べた時点での capybara のバージョンは2.14.3です

コードリーディング

まずはエントリーポイントとなるfindメソッドを見ます

31 def find(*args, &optional_filter_block)
32   query = Capybara::Queries::SelectorQuery.new(*args, &optional_filter_block)
33   synchronize(query.wait) do
34     if (query.match == :smart or query.match == :prefer_exact) and query.supports_exact?
35       result = query.resolve_for(self, true)
36       result = query.resolve_for(self, false) if result.empty? && !query.exact?
37     else
38       result = query.resolve_for(self)
39     end
40     if query.match == :one or query.match == :smart and result.size > 1
41       raise Capybara::Ambiguous.new("Ambiguous match, found #{result.size} elements matching #{query.description}")
42     end
43     if result.empty?
44       raise Capybara::ElementNotFound.new("Unable to find #{query.description}")
45     end
46     result.first
47   end.tap(&:allow_reload!)
48 end

https://github.com/teamcapybara/capybara/blob/2.14.3/lib/capybara/node/finders.rb#L31

(ドキュメント) http://www.rubydoc.info/github/teamcapybara/capybara/master/Capybara/Node/Finders#find-instance_method

要素が見つからないシチュエーションを考える

要素が見つからない場合、

query.resolve_for

したあとに、下記の行が実行されて Capybara::ElementNotFound が発生します

43     if result.empty?
44       raise Capybara::ElementNotFound.new("Unable to find #{query.description}")
45     end

じゃあどこでリトライの処理をしているかというと

        synchronize

にリトライの実装があります。

synchronizeをコードリーディング

 77 def synchronize(seconds=Capybara.default_max_wait_time, options = {})
 78   start_time = Capybara::Helpers.monotonic_time
 79
 80   if session.synchronized
 81     yield
 82   else
 83     session.synchronized = true
 84     begin
 85       yield
 86     rescue => e
 87       session.raise_server_error!
 88       raise e unless driver.wait?
 89       raise e unless catch_error?(e, options[:errors])
 90       raise e if (Capybara::Helpers.monotonic_time - start_time) >= seconds
 91       sleep(0.05)
 92       raise Capybara::FrozenInTime, "time appears to be frozen, Capybara does not work with libraries which freeze time, consider using time travelling instead" if Capybara::Helpers.monotonic_time == start_time
 93       reload if Capybara.automatic_reload
 94       retry
 95     ensure
 96       session.synchronized = false
 97     end
 98   end
 99 end

https://github.com/teamcapybara/capybara/blob/2.14.3/lib/capybara/node/base.rb#L77

(ドキュメント) http://www.rubydoc.info/github/teamcapybara/capybara/master/Capybara/Node/Base#synchronize-instance_methodhttp://www.rubydoc.info/github/teamcapybara/capybara/master/Capybara/Node/Base#synchronize-instance_method

Capybara::ElementNotFoundときには

 84     begin
 85       yield
 86     rescue => e

yield を実行中のはずなので、87 ~ 93行目の条件に引っかからない場合には

 94       retry

が実行され yield が再び実行されます。 これが Capybara::ElementNotFound が発生されなくなるまでループするという実装になっているということがわかりました。

おわりに

余談ですが、 click_link などのメソッドでも内部的には findメソッドを呼び出しているので結局要素が現れるまで待ってくれてるようです