LoginSignup
16
15

More than 5 years have passed since last update.

Capybara のアクションにCSSセレクタを使う

Posted at

背景・事情

Capybaraでユーザの動作をシミュレートする場合、Capybara::Node::Actionsモジュールのメソッドが便利ですよね。

例えば、入力欄に文字を入力する#fill_in、ラジオボタンによる選択肢を選ぶ#choose、プルダウンメニューによる選択肢を選ぶ#select、チェックボックスのチェックを入れる#check、リンクをクリックする#click_linkなどのメソッドがあります。

これらのメソッドを使ってユーザのアクションをシミュレートしていくわけですが、そのアクションの対象となる要素を指定する方法として、メソッドの引数にlocatorというものを渡します。このlocatorはメソッドによって異なりますが、例えば「name属性の値・id属性の値・対応するlabel要素のテキストのいずれか」といった具合になっています。

しかし、ここはlocatorじゃなくCSSセレクタを使いたい、と思うわけです。

CSSセレクタが使えればテストコードの記述の柔軟性も上がりますし、テスト中の要素の指定方法としてCSSセレクタに記述の形式を統一しておきたいのです。

ということで、Capybara::Node::Actionsモジュールのメソッドについて、CSSセレクタを使いつつ同じ動作を実現するための方法を書いてみようと思います。

実現方法に至るまでの説明

Capybara::Node::Actionsモジュールのメソッドの動作を知るために、Capybaraのソースコードをちょっと覗いてみます。例えばCapybara::Node::Actions#click_linkの定義部分はこんな感じです。

def click_link(locator, options={})
  find(:link, locator, options).click
end

Capybara::Node::Finders#findメソッドによって要素を指定して、Capybara::Node::Element#clickメソッドを呼んでいます。これは他のアクションも同様で、#findで要素を指定した後、#click#setなどを呼んでいます。

#findメソッドにはCSSセレクタを使うことができるので、要素の指定方法を要素の種類とlocatorの組からCSSセレクタに置き換えればアクションの動作自体は実現できます。

ですが、それだと要素の種類は限定できません。例えば、#click_linkメソッドの場合、アクションの対象をリンクに限定してくれるわけです。単純にCSSセレクタに置き換えるだけだと、選択される要素の種類を限定することができません。

そこで、もう少し調べてみます。ソースコードを追っていくと、#findメソッドによる要素指定には、XPath::HTMLモジュールのメソッドが使われているようです。例えば、#click_linkメソッドに使われているのは、XPath::HTML.linkメソッドです。

irbで実行して、どんなXPathが生成されるかを見てみます。

irb(main):001:0> require 'xpath'
=> true
irb(main):002:0> XPath::HTML.link('locator').to_xpath
=> ".//a[./@href][(((./@id = 'locator' or contains(normalize-space(string(.)), 'locator')) or contains(./@title, 'locator')) or .//img[contains(./@alt, 'locator')])]"

.//a[./@href]の部分が要素の種類を限定している部分で、残りの部分は「id属性・titile属性・(子孫img要素の)alt属性のいずれかにlocatorを含む」という条件を付与していますね。

CSSセレクタに要素の種類を限定している部分を組み合わせれば目的は達成できそうです。もちろんXPathとCSSセレクタはそのままでは結合はできませんが、Nokogiri::CSS.xpath_forメソッドを使うとCSSセレクタをXPathに変換できるので結合できます。

ということで、#click_linkに相当する動作をCSSセレクタで行うメソッドは、こんな風に定義できます。

def click_link_css(css_selector)
  base_xpath = Nokogiri::CSS.xpath_for(css_selector)[0]
  xpath = "#{base_xpath}[../a][@href]"
  find(:xpath, xpath).click
end

この要領でCapybara::Node::Actionsモジュールのメソッドに相当するメソッドを独自に定義して使えばいいんじゃないか、ということです。

というわけで具体例

以下のような感じでメソッド定義して使えばいいんじゃないかと思います。

def click_link_css(css_selector)
  base_xpath = Nokogiri::CSS.xpath_for(css_selector)[0]
  xpath = "#{base_xpath}[../a][@href]"
  find(:xpath, xpath).click
end

def click_button_css(css_selector)
  base_xpath = Nokogiri::CSS.xpath_for(css_selector)[0]
  xpath = "(#{base_xpath}[../input][@type='submit' or @type='reset' or @type='image' or @type='button'] | #{base_xpath}[../button])"
  find(:xpath, xpath).click
end

def click_link_or_button_css(css_selector)
  base_xpath = Nokogiri::CSS.xpath_for(css_selector)[0]
  xpath = "(#{base_xpath}[../a][@href] | #{base_xpath}[../input][@type='submit' or @type='reset' or @type='image' or @type='button'] | #{base_xpath}[../button])"
  find(:xpath, xpath).click
end

def click_on_css(css_selector)
  click_link_or_button_css(css_selector)
end

def fill_in_css(css_selector, with)
  base_xpath = Nokogiri::CSS.xpath_for(css_selector)[0]
  xpath = "#{base_xpath}[../input or ../textarea][not(@type='submit')][not(@type='image')][not(@type='radio')][not(@type='checkbox')][not(@type='hidden')][not(@type='file')]"
  find(:xpath, xpath).set(with)
end

def choose_css(css_selector)
  base_xpath = Nokogiri::CSS.xpath_for(css_selector)[0]
  xpath = "#{base_xpath}[../input][@type='radio']"
  find(:xpath, xpath).set(true)
end

def check_css(css_selector)
  base_xpath = Nokogiri::CSS.xpath_for(css_selector)[0]
  xpath = "#{base_xpath}[../input][@type='checkbox']"
  find(:xpath, xpath).set(true)
end

def uncheck_css(css_selector)
  base_xpath = Nokogiri::CSS.xpath_for(css_selector)[0]
  xpath = "#{base_xpath}[../input][@type='checkbox']"
  find(:xpath, xpath).set(false)
end

def select_css(css_selector, value)
  base_xpath = Nokogiri::CSS.xpath_for(css_selector)[0]
  xpath = "#{base_xpath}[../select]//option[@value='#{value}']"
  find(:xpath, xpath).select_option
end

def unselect_css(css_selector, value)
  base_xpath = Nokogiri::CSS.xpath_for(css_selector)[0]
  xpath = "#{base_xpath}[../select]//option[@value='#{value}']"
  find(:xpath, xpath).unselect_option
end

def attach_file_css(css_selector, file_path)
  base_xpath = Nokogiri::CSS.xpath_for(css_selector)[0]
  xpath = "#{base_xpath}[../input][@type='file']"
  find(:xpath, xpath).set(file_path)
end

補足というか言い訳

CSSセレクタを使いたいだけなら、#findメソッドでCSSセレクタを使って絞り込んでからメソッドチェーンでアクションを繋げばいいじゃん、という意見もありそうです。例えばこんな感じですね。

find('div.foo').fill_in('FirstName', :with => 'Hoge')

でもやっぱりlocatorによる指定が必要になるので、これがlabelのテキストだったりすると文言を変更したらテストコードも変更しなきゃいけないし、idを指定するくらいなら直接CSSセレクタでアクションの対象要素を指定できたほういいよなー、なんて思っちゃいます。

参考
https://github.com/jnicklas/capybara/blob/2.4.4/lib/capybara/node/actions.rb
https://github.com/jnicklas/capybara/blob/2.4.4/lib/capybara/selector.rb
https://github.com/jnicklas/xpath/blob/2.0.0/lib/xpath/html.rb

16
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
15