はじめに
capybaraでは find
メソッドを使うとある一定の時間が経過するまでリトライし続けます。
今回は find
メソッドをターゲットに、どうやってリトライし続ける動作を実現しているのかを見ていこうと思います。
ちなみに私は最初それを知らずに 「ページのロードが終わってないからかな?(適当)」なんてアタリをつけて sleep 1
をテストコードに埋めてたりしていました。
しかしそんなことをするくらいだったら Capybara.default_max_wait_time
の時間を伸ばしたほうがいいです。
Capybara.default_max_wait_time
とは find
や all
メソッドを使用したときに探し続けるのに使用する最大の待ち時間です。
ちなみに調べた時点での 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メソッドを呼び出しているので結局要素が現れるまで待ってくれてるようです