Capybara の attach_file で、非表示の input 要素にファイルを添付する

TL; DR

  • Capybara の finder methods は非表示のものを find できません。その対象は dispaly: none になっている要素、だけでなく、 opacity: 0 になっている要素も対象になるので注意しましょう。
  • 非表示になっている <input type="file"> 要素に対して attach_file を使う際には、 make_visible: true を渡すことで、非表示になっている要素も一時的に hidden でない状態にして find してくれます。

ストーリー

非表示という落とし穴

Bootswatch という、Bootstrap をカスタマイズしたテーマ集の中から、 Litera というテーマを採用してデザインをあてているアプリケーションがありまして。

そのアプリケーションのとあるページに、CSV ファイルをアップロードするためのこんな感じ↓の HTML がありました。

<div class="form-group">
  <div class="input-group mb-3 input-user-file">
    <div class="custom-file">
      <input type="file" name="csv" id="inputFile" accept="text/csv" required="required" class="custom-file-input">
      <label class="custom-file-label" for="inputFile">ファイルを選択</label>
    </div>
  </div>
</div>

このファイルアップロード機能を確かめるための E2E テストを書きたくて、Capybara を使ってテストを書いておりました。ちなみにわたし Gherkin が大好きです。Turnip が特に好き。

Capybara で <input type="file"> な要素にファイルを読み込ませるためには attach_file メソッドを使います。ですから

attach_file("inputFile", "/path/to/file")

みたいなコードを書いていたんです...が、なぜかその input 要素が見つからず、以下のエラーが出ました。。

Failure/Error: attach_file("inputFile", Rails.root.join("spec", "fixtures", file_name))

Capybara::ElementNotFound:
  Unable to find visible file field "inputFile"

rdoc とにらめっこしながら、 attach_file に渡す locator が間違っているのかなーとか、もしかしたら別の指定の仕方があるのかなーとかいろいろ試したんだけどどれも結果は同じ。

Method: Capybara::Node::Actions#attach_file — Documentation for jnicklas/capybara (master)

save_and_open_page すると確かに要素は存在しているので、もしやこれは非表示(CSSdisplay: none が指定されている)になっている...?と思って確認してみたけど、そんなようすもなく...。。

...と、調べていると気になるキーワードを発見。

ruby - Capybara Unable To Find Input Field - Stack Overflow

Since the login field is invisible (opacity: 0 to allow the emptyText to show through from below) Capybara won't find it by default.

Opacity!それか!?と思って CSS を見てみたところ...果たして。

.custom-file-input {
    position: relative;
    z-index: 2;
    width: 100%;
    height: calc(2.25rem + 2px);
    margin: 0;
    opacity: 0;
}

BINGO でした。(ちなみにこれは Bootstrap で定義されている CSS です)

なるほどなあ、確かに透明になってちゃ見えないもんね...。

attach_file には make_visible というオプションが用意されている

Capybara のコードとドキュメントを詳しく見ていきます。

Method: Capybara::Node::Actions#attach_file — Documentation for jnicklas/capybara (master)

In the case of the file field being hidden for styling reasons the make_visible option can be used to temporarily change the CSS of the file field, attach the file, and then revert the CSS back to original.

(スタイル上の理由で file フィールドが非表示になっている場合は、 make_visible オプションを使って一時的に file フィールドの CSS を変更し、ファイルを添付してから CSS を元の状態に戻すことができます。)

なんと。まさに今回のわたしのパターンにおあつらえ向きじゃないですか。「そんなこともあろうかと!」という開発者のかたがたの声が聞こえてきそうです。

GitHub のコードを見てみても、

capybara/actions.rb at 10e12bf0645b010bf50bfa99d0f22b3621a7cd1f · teamcapybara/capybara · GitHub

def attach_file(locator = nil, paths, make_visible: nil, **options) # rubocop:disable Style/OptionalArguments
  Array(paths).each do |path|
    raise Capybara::FileNotFound, "cannot attach file, #{path} does not exist" unless File.exist?(path.to_s)
  end
  options[:allow_self] = true if locator.nil?
  # Allow user to update the CSS style of the file input since they are so often hidden on a page
  if make_visible
    ff = find(:file_field, locator, options.merge(visible: :all))
    while_visible(ff, make_visible) { |el| el.set(paths) }
  else
    find(:file_field, locator, options).set(paths)
  end
end

となっていて、 visible: :all (表示・非表示関係なく、すべての要素を find の対象にする)の状態にしていることがわかります。

おわりに

今回はこのふたつのことを学べました。

  • Capybara の finder method は要素の opacity も考慮していること
  • attach_file には非表示状態であることを考慮したオプションがあること

結論にたどり着くまでに、ちゃんと save_and_open_page で要素が表示されているかを確認したことが今回の解決につながるキーだったかな。

成長すると検証のしかたも変わってくるね。

おつかれさまでした!