フロントエンドのReact UIのテストをしている時に、ローカルでUIをいじっていても正常に挙動しているように見えるUIが Capybaraを利用したSystem Specでは動かない事があり、中々に奇妙な動作で困惑していたのですが ソースコードを追って理解と対処ができたのでまとめておきます。
環境
- capybara (3.33.0)
- webdrivers (4.4.1)
- selenium-webdriver (3.142.7)
発生していた事象
System SpecでChromeブラウザを利用したフォーム送信のテストをしていたときに、 なぜか、指定した文字の「先頭4文字、末尾3文字」だけが入力された事になって正常にフォームからのデータ保存が確認できない。
という挙動が発生していました。
たとえば、
fill_in 'name', with: expected_name
のように、fill_in
をつかってフォームに入力をして、その後submitしてもexpected_nameがきちんと保存されていないという事がありました。
具体例をあげると、
expected: "ebyl30gzjd2ygy24zcpwdo2cxw6fvsb7" got: "ebylsb7"
であったり、
expected: "8dp6hsd8c6jlsxxc6tocfupc7ovmm9dc" got: "8dp69dc"
といった感じにこのエラーが発生する時には決まって、「先頭4文字、末尾3文字」だけ送信されてくるというエラーになっていました。
TL;DR
- Capybaraでは30文字以上の文字をfill_inしようとするとJSからvalueを設定する事で文字の途中をまとめて入力しようとする。
- fill_inに
fill_options: { rapid: false }
を指定する事で一文字ずつキーイベントを送信するよう強制できる
調査
最初は、ブラウザのドライバー側のバグで長文の入力がまにあわないとかあるのかなぁとなんとなくで考えて、 文字数を短くすると解決するので対処していたりしたのですが、
にしても、なんで毎回「先頭4文字、末尾3文字」なんだ?
という疑問があり、
普通の競合バグであったり、処理がまにあわないといった内容なら:
- 「先頭4文字、末尾3文字」というのがランダムに生成される文字列にたいして常に同じ形式で発生したり、
- 再実行して負荷が違う時にも発生箇所はちがっても文字列のパターンは一緒
といった事はこのエラーが発生する回数がつれて法則が見えてきてからさすがにありえないだろうと思うようになりました。
テストが不安定なまま原因も不明というまま放置するのも怖くなったので せっかくのオープンソースなのでCapybaraを確認しにいきました。
fill_in
def fill_in(locator = nil, with:, currently_with: nil, fill_options: {}, **find_options) find_options[:with] = currently_with if currently_with find_options[:allow_self] = true if locator.nil? find(:fillable_field, locator, **find_options).set(with, **fill_options) end
capybara/actions.rb at fe5940c6afbfe32152df936ce03ad1371ae05354 · teamcapybara/capybara · GitHub
fill_inは、こんな感じで指定されたフィールドを検索してsetを読んでいるだけのようです。 という事でsetを見にいきます。
set
抜粋してみると
def set(value, **options) # .... tag_name, type = attrs(:tagName, :type).map { |val| val&.downcase } @tag_name ||= tag_name case tag_name when 'input' case type # .... else set_text(value, **options) end #.... end end
capybara/node.rb at c20a798d5f674669b6c6d0afd1741e7d440075f0 · teamcapybara/capybara · GitHub
という感じで、どうもset_textに処理を投げているようですね。
set_text
def set_text(value, clear: nil, rapid: nil, **_unused) value = value.to_s if value.empty? && clear.nil? native.clear elsif clear == :backspace # Clear field by sending the correct number of backspace keys. backspaces = [:backspace] * self.value.to_s.length send_keys(*([:end] + backspaces + [value])) elsif clear.is_a? Array send_keys(*clear, value) else driver.execute_script 'arguments[0].select()', self unless clear == :none if rapid == true || ((value.length > auto_rapid_set_length) && rapid != false) send_keys(value[0..3]) driver.execute_script RAPID_APPEND_TEXT, self, value[4...-3] send_keys(value[-3..-1]) else send_keys(value) end end end
capybara/node.rb at c20a798d5f674669b6c6d0afd1741e7d440075f0 · teamcapybara/capybara · GitHub
この箇所を見つけたときはおどろきました。
send_keys(value[0..3]) driver.execute_script RAPID_APPEND_TEXT, self, value[4...-3] send_keys(value[-3..-1])
この箇所をみると、どうもなにやら「先頭4文字、末尾3文字」の痕跡が見えます。 ここに何かあるとおもってコードを読んでみました。
どうも、この箇所では:
- 明示的にrapidというオプションが指定されている
- rapidがfalseにされていない時に一定の長さ
auto_rapid_set_length
(30のようです) よりも長いvalueを設定しようしている
のどちらかの条件にひっかかった時に実施されるようです。
そして
- 先頭の4文字をキー入力として送信する
- 中間の文字をなにやらJSの処理の流している
- 末尾の3文字をキー入力として送信する
という事は、私のケースでは 2 の処理が動いていないのかもしれません
RAPID_APPEND_TEXT
では、この実行しているjsのスクリプトを確認してみると
(function(el, value) { value = el.value + value; if (el.maxLength && el.maxLength != -1){ value = value.slice(0, el.maxLength); } el.value = value; })(arguments[0], arguments[1])
capybara/node.rb at c20a798d5f674669b6c6d0afd1741e7d440075f0 · teamcapybara/capybara · GitHub
というコードのようで、ようするにエレメントのvalueに直接値をJSから注入する事で処理しようとしているようです。
推測
私のコードでも動いている時はうごいていて、フォームのコードを書きかえると発生するようになったりという事をしていた事もあり
また、フォームには
こちらのライブラリを利用していました。
そのため、おそらくこのjsレイヤーからvalueを設定しようとする処理とタイミングによってはなんらか ライブラリ側の処理と競合してvalue設定が実施されないようになってしまっているのではないかとおもいます。
対策
どうやらこのrapid処理をしようとするとダメなようなので
- このrapidを明示的にfalseにする
- 入力文字数を30文字以下にする
のどちらかで対応できそうです。
30文字以下にするのはfillのwithに指定する文字数を調整すれば達成できます
rapidをfalseにする方は、fillのfill_optionが伝播してset_textに渡されているので
fill_in 'name', with: expected_name, fill_options: { rapid: false }
のようにする事で対応できます。 実際に私の手元で、これを実施する事でたしかに値が飛ぶ事なく入力できるようになった事が確認できました。
ただし、rapidをしなくなるとパフォーマンスが低下するとおもうので、 30文字以下にする事でも要件が検証できるならばそちらを採用する方がのぞましいかとおもいます。