かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

RSpec Capybara 非表示な要素のテストにハマる

RSpec でテストをしていて、非表示にしてある要素が取れずにハマった。

Capybara さんはデフォルトでは画面上に見える要素のみを検索するので display: nonehidden になっている要素を触るできないっぽい。

<div class="modal" style="display:none"></div>
<input type="hidden" id="secret_val" value="XXX">
scenario 'モーダルが存在する' do
  expect(page).to have_selector('.modal')
end

scenario '#secret_val の値は XXX' do
  expect( find('#secret_val').value ).to eq 'XXX'
end

画面に見えてない要素は触れないのでテストが通らない

visible: false オプションを使う

scenario 'モーダルが存在する' do
  expect(page).to have_selector('.modal', visible: false)
end

scenario '#secret_val の値は XXX' do
  expect( find('#secret_val', visible: false).value ).to eq 'XXX'
end

👉 success
オプションを付ければ非表示要素も検索してくれるのでテストが可能になる!

Options Hash (**options):
visible (Boolean, Symbol) — Only find elements with the specified visibility:

  • true - only finds visible elements.
  • false - finds invisible and visible elements.
  • :all - same as false; finds visible and invisible elements.
  • :hidden - only finds invisible elements.
  • :visible - same as true; only finds visible elements.

cf. Module: Capybara::Node::Finders — Documentation for jnicklas/capybara (master)

非表示になっている要素のコンテンツは have_content / have_text で取れない

例えばデフォルトで非表示になっているモーダルのタイトルの内容があっているかテストしたいような場合

<div class="modal" style="display:none">
  <p class="modal_title">アイカツを見ろ</p>
</div>
scenario '正しいモーダルが出力されていること' do
  expect( find('.modal', visible: false) ).to have_content 'アイカツを見ろ'
end

👇

Failure/Error: expect( find('.modal', visible: false) ).to have_content 'アイカツを見ろ'
   expected to find text "アイカツを見ろ" in "". (However, it was found 1 time including non-visible text.)

非表示の要素は取れていても have_contenthave_text は 空 ("") と判断されてテストが落ちてしまう

have_selectortext: オプションも使ってマッチさせることでコンテンツ内容のテストができる

但し、非表示要素内の要素は have_selector でも visible オプションが必要

非表示なコンテナ内にあって、直接 display: none などのスタイルを当てられててない要素でも visible オプションがないと要素を取得できずにテストが落ちてしまう

scenario '正しいモーダルが出力されていること' do
  expect( find('.modal', visible: false) ).to have_selector('.modal_title', text: 'アイカツを見ろ')
end

👇

Failure/Error: expect( find('.modal', visible: false) ).to have_selector('.modal_title', text: 'アイカツを見ろ')
   expected to find visible css ".modal_title" with text "アイカツを見ろ" within #<Capybara::Node::Element tag="div" path="/HTML/BODY[1]/DIV[2]/DIV[2]/DIV[2]/DIV[1]"> but there were no matches. Also found "", which matched the selector but not all filters.

have_selector にも visible オプションを付ける

scenario '正しいモーダルが出力されていること' do
  expect( find('.modal', visible: false) ).to have_selector('.modal_title', visible: false, text: 'アイカツを見ろ')
end

👉 success
これならテストが通る!
ただ感覚的に「◯◯のテキスト が 'XXXX'」となってる方が直感的なわかり易さもテストの構造としても良い気がするので微妙な感じ…

.text(:all) で非表示になっているテキストノードを取得できる

#text(type = nil, normalize_ws: false) ⇒ String
Retrieve the text of the element. If ignore_hidden_elements is true, which it is by default, then this will return only text which is visible. The exact semantics of this may differ between drivers, but generally any text within elements with display:none is ignored. This behaviour can be overridden by passing :all to this method.
cf. Method: Capybara::Node::Element#text — Documentation for jnicklas/capybara (master)

text(:all) でテキストを取得したほうが「◯◯のテキスト が 'XXXX'」と書けるので非表示になっている要素の内容のテストには良さそう

<div class="modal" style="display:none">
  <p class="modal_title">アイカツを見ろ</p>
  <div class="modal_body">
    <strong>アイカツオンパレード始まったから絶対見て!!!!</strong>
  </div>
</div>
scenario '正しいモーダルが出力されていること' do
  # 文字列に含まれるをテストするので include を使う
  expect( find('.modal', visible: false).text(:all) ).to include 'アイカツを見ろ'
end

👉 success
テストが通りました ٩(ˊᗜˋ*)و

非表示の要素内のテキストは find()visible オプション・ text():all 引数の両方がないとダメ

text:all がないと空("")になるのでテストが通らない

scenario '正しいモーダルが出力されていること' do
  expect( find('.modal', visible: false).text ).to include 'アイカツを見ろ'
end

👇

Failure/Error: expect( find('.modal', visible: false).text ).to include 'アイカツを見ろ'
   expected "" to include "アイカツを見ろ"

find()visible オプションがないとそもそも要素を取得できないのでテキストも取得できない

scenario '正しいモーダルが出力されていること' do
  expect( find('.modal').text(:all) ).to include 'アイカツを見ろ'
end

👇

Failure/Error: expect( find('.modal').text(:all) ).to include 'アイカツを見ろ'
  Capybara::ElementNotFound:
    Unable to find visible css ".modal"
text の引数はこんな感じになっているそうです

見えないテキスト(display:none)が text メソッドの戻り値に含まれるかどうかについて
2.1 では、text メソッドの引数によって挙動を切り替えることもできる

find("#thing").text           # Capybara.ignore_hidden_elements によって挙動が変わる
find("#thing").text(:all)     # 見えないテキストも含む
find("#thing").text(:visible) # 見えるテキストだけ
cf. capybara 2.1 を学ぶ - おもしろwebサービス開発日記

感想

  • 非表示になっている要素には visible: false オプションを使う
  • 非表示になっている要素の内容をテストするには find(selector, visible: false).text(:all) を使う

のが良さそうです。
 
RSpec も Capybara も何も分からん… (docker-mac でのテスト走らせるのが遅くて辛いからどうにかなってほしぃ…


[参考]

カピバラさん PCクッション

カピバラさん PCクッション