CapybaraはWebアプリケーションをテストする際に便利なライブラリです。ところで、フォームに入力するメソッドとしてはfill_inがありますが、要素の指定にCSSセレクタが使えません。click_button, click_linkも同様にCSSセレクタが使えません。ではXPathは使えるかというと、XPathも使えません。指定できるのは単なる文字列で、例えばfill_inの場合、その文字列がid属性またはname属性または対応するlabel要素の文字列に合致するものを探すことになっています。他のfindなどの要素指定メソッドではCSSセレクタを使うので、他のメソッドとの統一性がなくて分かりにくいし、細かい指定もできないので、CSSセレクタを使いたいところです。
単純な解決策としては、findメソッドで要素を絞り込んでsetやclickすればいいわけですが、それだけだともとの機能と同一にはになりません。というのは、fill_in, click_button, click_linkの特長として、それぞれのタイプに合致する要素だけを探してくれるのです。例えばfill_inならば入力可能フィールドのみを検索対象としてくれます。
これは、
Capybaraのこのソースコードを見るとよくわかります。
例えばfill_inの場合、"fillable_field"という条件で探してsetしています。これと同じことをできればいいわけです。条件の詳細は
XPathライブラリのこのソースコードを見るとわかります。
これによるとfill_inに使われる"fillable_field"の条件は、
- input要素あるいはtextarea要素であり、
- type属性が次のいずれでも「ない」:'submit', 'image', 'radio', 'checkbox', 'hidden', ‘file'
click_buttonに使われる"button"の条件は、
- input要素かつ、type属性が次のいずれかである:'submit', 'reset', 'image', ‘button'
- または、button要素である
click_linkに使われる"link"の条件は、
となっています。これらの条件を満たすように、CSSセレクタに条件を付加してXPathに変換するメソッドを作ってみました。
# CSSセレクタをXPathに変換し、さらに入力可能フィールドの条件を付加する
# @param [String] css CSSセレクタ
# @return [String] 変換後のXPath
def convert_css_to_fillable_xpath(css)
base_xpath = %(#{Nokogiri::CSS.xpath_for(css)[0]})
# init_xpath = %(#{base_xpath}[../input or ../textarea])
# new_xpath = ['submit', 'image', 'radio', 'checkbox', 'hidden', 'file'].inject(init_xpath) do |tmp_xpath, attr|
# %(#{tmp_xpath}[not(@type='#{attr}')])
# end
# new_xpath
# 高速化のためベタ書き
"#{base_xpath}[../input or ../textarea][not(@type='submit')][not(@type='image')][not(@type='radio')][not(@type='checkbox')][not(@type='hidden')][not(@type='file')]"
end
# CSSセレクタをXPathに変換し、さらにボタンの条件を付加する
# @param [String] css CSSセレクタ
# @return [String] 変換後のXPath
def convert_css_to_button_xpath(css)
base_xpath = %(#{Nokogiri::CSS.xpath_for(css)[0]})
# xpath_input = %(#{base_xpath}[../input][@type='#{['submit', 'reset', 'image', 'button'].join("' or @type='")}'])
# xpath_button = %(#{base_xpath}[../button])
# "(#{xpath_input} | #{xpath_button})”
# 高速化のためベタ書き
"(#{base_xpath}[../input][@type='submit' or @type='reset' or @type='image' or @type='button'] | #{base_xpath}[../button])"
end
# CSSセレクタをXPathに変換し、さらにリンクの条件を付加する
# @param [String] css CSSセレクタ
# @return [String] 変換後のXPath
def convert_css_to_link_xpath(css)
base_xpath = %(#{Nokogiri::CSS.xpath_for(css)[0]})
"#{base_xpath}[../a][@href]"
end
少々解説します。Nokogiri::CSS.xpath_for は、
複数条件CSSに対応するため、返り値はXPath文字列を要素に持つ配列になります。入力欄を選ぶという状況的に、1つのみになると考えられるので最初の要素を選択すればよいから、Nokogiri::CSS.xpath_for(css)[0]としています。
変換したXPathの後ろに例えば [../input or ../textarea] といった要素指定を付加しています。これは、要素自身がinput要素あるいはtextarea要素であることを後から無理やり指定するためです。スマートではない形式ですが、CSSセレクタを変換して条件を付加するという状況なので仕方ありません。
最初はメソッド中のコメントアウトしてある部分のようにかなり動的にXPathを生成していましたが、テスト実行を少しでも早くするために、文字列をベタ書きしておくことにしました。
さて、使い方としてはCSSセレクタを渡してXPathに変換し、その条件でノードを取得して動作を行えばよいです。Cucumberによる例を挙げると、こんな感じでステップ定義に使えます。何やら余計なものが色々付いているように見えますが、その辺の詳細は
以前に書いたこの記事を参照。
もし(/^".*?\((.*?)\)" 欄に "(.*?)" と入力(?:し(、)|した)(\(非同期\))?( debug)?$/) do |t_css, t_with, t_eos, t_async, debug|
show_debug_info if debug
xpath = convert_css_to_fillable_xpath(t_css)
page.find(:xpath, xpath, :visible => true, :wait => t_async ? async_time : 0).set(t_with)
puts '' if t_eos.blank?
end
もし(/^".*?\((.*?)\)" ボタンを押下(?:し(、)|した)(\(非同期\))?( debug)?$/) do |t_css, t_eos, t_async, debug|
show_debug_info if debug
xpath = convert_css_to_button_xpath(t_css)
page.find(:xpath, xpath, :visible => true, :wait => t_async ? async_time : 0).click
puts '' if t_eos.blank?
end
もし(/^".*?\((.*?)\)" リンクを押下(?:し(、)|した)(\(非同期\))?( debug)?$/) do |t_css, t_eos, t_async, debug|
show_debug_info if debug
xpath = convert_css_to_link_xpath(t_css)
page.find(:xpath, xpath, :visible => true, :wait => t_async ? async_time : 0).click
puts '' if t_eos.blank?
end
Gerkinによるfeature定義としては、例えば
もし "アカウント名(#account_name)" 欄に “foo" と入力し、
かつ "パスワード(#password)" 欄に “bar" と入力し、
かつ "ログイン(#login-button)" ボタンを押下した
ならば・・・(以下略)
のようになります。
※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。