Let's write β

プログラミング中にできたことか、思ったこととか

React UIをCapybaraで30文字以上の入力のテストしている時には、fill_inにrapid: falseを設定する事も検討してみよう

フロントエンドの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を設定しようしている

のどちらかの条件にひっかかった時に実施されるようです。

そして

  1. 先頭の4文字をキー入力として送信する
  2. 中間の文字をなにやらJSの処理の流している
  3. 末尾の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から注入する事で処理しようとしているようです。

推測

私のコードでも動いている時はうごいていて、フォームのコードを書きかえると発生するようになったりという事をしていた事もあり

また、フォームには

final-form.org

こちらのライブラリを利用していました。

そのため、おそらくこの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文字以下にする事でも要件が検証できるならばそちらを採用する方がのぞましいかとおもいます。