Kayaのブログ

プログラミング学習用です

Capybaraの注意点

エラーを解決していくなかでcapybaraの抑えるべき注意点が少しわかったのでまとめておく

within

1) Task Task一覧 正常系 Project詳細からTask一覧ページにアクセスした場合、Taskが表示されること
     Failure/Error: expect(page).to have_content task.title
       expected to find text "Task" in "Name: quod\nStatus: doing\nRelease date: 2019-08-31\nView Todos\nEdit | Back"

該当箇所を確認してみる

# spec/system/task_spec.rb
it 'Project詳細からTask一覧ページにアクセスした場合、Taskが表示されること' do
        visit project_path(project)
        click_link 'View Todos'
        expect(page).to have_content task.title
        expect(Task.count).to eq 1
        expect(current_path).to eq project_tasks_path(project)
end

ブラウザ上で確認してみたところ、どうやらclick_link 'View Todos'により、別タブが作成されていることがわかった

作成された先でテストが行われない状態になっているためにエラーとなっていたみたい

Capybaraと仲良くなる(タブ・ウィンドウの操作について) - Qiita

teamcapybara/capybara

セレクタの スコープ を制限することもできます。その場合はCapybaraの within を使ってページの一部分に含まれる要素を操作します。

テスト実行しているタブはそのままに、別タブをスコープとして操作できる

# spec/system/task_spec.rb
it 'Project詳細からTask一覧ページにアクセスした場合、Taskが表示されること' do
        visit project_path(project)
        click_link 'View Todos'
        within_window(windows.last) do
          expect(page).to have_content task.title
          expect(Task.count).to eq 1
          expect(current_path).to eq project_tasks_path(project)
        end
end

はじめ、下記のようになると思った(別タブに移動する)

# spec/system/task_spec.rb
it 'Project詳細からTask一覧ページにアクセスした場合、Taskが表示されること' do
        visit project_path(project)
        click_link 'View Todos'
        switch_to_window(windows.last)
        expect(page).to have_content task.title
        expect(Task.count).to eq 1
        expect(current_path).to eq project_tasks_path(project)
end

ただ、これだと元タブのテストをするために再度タブ移動するかスコープ定義しなくてはならない


strftime

2) Task Task編集 正常系 Taskを編集した場合、一覧画面で編集後の内容が表示されること
     Failure/Error: expect(find('.task_list')).to have_content(Time.current.strftime('%Y-%m-%d'))
       expected to find text "2021-05-31" in "Task doing 5/31 22:44 Show Edit Destroy"

エラーメッセージより、日付表記が間違っているとわかった

Time#strftime (Ruby 3.0.0 リファレンスマニュアル)

expect(find('.task_list')).to have_content(Time.current.strftime('%-m/%d %-H:%M'))

ただ、時刻表示の仕様が変わってもテストが失敗しないようにしたい(毎度変えるの面倒)

⇒ 表示時刻は、viewで使用しているメソッド(⇒ helper)を呼び出すようにすれば変える箇所が減る

# app/helpers/application_helper.rb
module ApplicationHelper
  def short_time(datetime)
    datetime.strftime("%-m/%d %-H:%M")
  end
end

それを呼び出す

# spec/rails_helper.rb
config.include ApplicationHelper
# spec/system/task_spec.rb
require 'rails_helper' 

it 'Taskを編集した場合、一覧画面で編集後の内容が表示されること' do
        visit edit_project_task_path(task.project, task)
        fill_in 'Deadline', with: Time.current
        click_button 'Update Task'
        click_link 'Back'
        expect(find('.task_list')).to have_content(short_time(Time.current))
        expect(current_path).to eq project_tasks_path(task.project)
end

○ short_time(Time.current)について

fill_in 'Deadline', with: Time.current

よく分からないまま試しにTime.currentを入れてみたら通ったため、改めて整理する

今回、編集後のtime(現在の)が、deadline timeに表示されることを確認したい

expect(find('.task_list')).to have_content(short_time(Time.current))

このコードで合っている

別解

expect(find('.task_list')).to have_content(short_time(task.reload.deadline))

表記は異なるが意味は同じ

注意点

rspecでupdateのテストを通せない初心者が疑うべきこと - Qiita

short_time(task.deadline)としてしまうと、テストが通らない

⇒ DBの値が更新されてもすでに読み込まれた taskインスタンスの値は更新されないから

⇒ (task.reload.deadline)とすることでインスタンスの値を更新できる


find

 3) Task Task削除 正常系 Taskが削除されること
     Failure/Error: expect(page).not_to have_content task.title
       expected not to find text "Task" in "Task was successfully destroyed.\nTasks of corporis\nTitle Status Deadline\n\nNew Task"

問題点が分からなかったためデバッグしてみた

[97, 106] in /Users/kaya/environment/rspec/rspec_app_exam/spec/system/task_spec.rb
    97:         task = FactoryBot.create(:task, project_id: project.id)
    98:         visit project_tasks_path(project)
    99:         click_link 'Destroy'
   100:         page.driver.browser.switch_to.alert.accept
   101:         byebug
=> 102:         expect(page).not_to have_content task.title
   103:         expect(Task.count).to eq 0
   104:         expect(current_path).to eq project_tasks_path(project)
   105:       end
   106:     end
(byebug) page.methods
[:choose, :query, :reset!, :find, :refresh, :visit, :server, :title, :current_path, :click_link, :html, :windows, :fill_in, :switch_to_window, :all, :find_all, :click_button, :select, :driver, :first, :quit, :within, :attach_file, :scroll_to, :scroll_by, :click_link_or_button, :find_button, :find_by_id, :find_field, :find_link, :has_content?, :has_css?, :has_no_content?, :has_no_text?, :has_no_css?, :has_no_xpath?, :has_xpath?, :uncheck, :has_link?, :source, :has_button?, :has_no_button?, :has_field?, :has_no_field?, :has_checked_field?, :has_unchecked_field?, :has_no_table?, :has_table?, :unselect, :has_no_link?, :document, :has_no_select?, :has_select?, :has_no_selector?, :has_selector?, :has_no_checked_field?, :click_on, :assert_selector, :has_no_unchecked_field?, :assert_all_of_selectors, :assert_no_selector, :assert_any_of_selectors, :assert_none_of_selectors, :assert_text, :refute_selector, :text, :assert_no_text, :assert_no_title, :assert_title, :has_no_title?, :current_url, :current_host, :mode, :execute_script, :status_code, :go_back, :within_element, :within_fieldset, :within_table, :within_frame, :switch_to_frame, :go_forward, :current_window, :within_window, :window_opened_by, :open_new_window, :save_page, :save_and_open_page, :evaluate_script, :reset_session!, :response_headers, :save_and_open_screenshot, :save_screenshot, :inspect, :has_text?, :accept_alert, :accept_confirm, :dismiss_confirm, :accept_prompt, :dismiss_prompt, :synchronized, :configure, :has_title?, :raise_server_error!, :cleanup!, :synchronized=, :evaluate_async_script, :using_wait_time, :current_scope, :check, :config, :app, :body, :assert_no_current_path, :has_current_path?, :has_no_current_path?, :assert_current_path, :to_json, :deep_dup, :duplicable?, :with_options, :in?, :presence_in, :to_param, :to_yaml, :to_query, :blank?, :`, :html_safe?, :instance_values, :instance_variable_names, :present?, :presence, :dclone, :acts_like?, :as_json, :pretty_print_inspect, :pretty_print, :pretty_print_instance_variables, :pretty_print_cycle, :try, :try!, :require_or_load, :require_dependency, :load_dependency, :unloadable, :instance_variable_defined?, :remove_instance_variable, :instance_of?, :kind_of?, :is_a?, :tap, :instance_variable_get, :instance_variable_set, :protected_methods, :instance_variables, :private_methods, :method, :public_method, :public_send, :singleton_method, :class_eval, :define_singleton_method, :debugger, :pretty_inspect, :remote_byebug, :extend, :byebug, :to_enum, :enum_for, :<=>, :===, :=~, :!~, :gem, :eql?, :respond_to?, :freeze, :object_id, :send, :to_s, :display, :class, :nil?, :hash, :dup, :singleton_class, :clone, :then, :itself, :yield_self, :untaint, :taint, :tainted?, :trust, :untrust, :untrusted?, :singleton_methods, :frozen?, :methods, :public_methods, :received_message?, :as_null_object, :==, :stub, :!=, :should, :should_not, :equal?, :!, :instance_eval, :instance_exec, :__id__, :should_receive, :null_object?, :stub_chain, :unstub, :should_not_receive, :__send__]
(byebug) page.body
"<html><head>\n    <title>Exam01Bugfix</title>\n    \n    \n\n    <link rel=\"stylesheet\" media=\"all\" href=\"/assets/application-1b47595487db05535588106b3df861a47054f7dbcb769490b6c97a7bc1135a9a.css\" data-turbolinks-track=\"reload\">\n    <script src=\"/assets/application-9dcb1626850e8ac8a42c5643d632ef67f743043da887857b887306ce35674069.js\" data-turbolinks-track=\"reload\"></script>\n  </head>\n\n  <body>\n    <p id=\"notice\">Task was successfully destroyed.</p>\n\n<h1>Tasks of fuga</h1>\n\n<table>\n  <thead>\n    <tr>\n      <th>Title</th>\n      <th>Status</th>\n      <th>Deadline</th>\n      <th colspan=\"3\"></th>\n    </tr>\n  </thead>\n\n  <tbody class=\"task_list\">\n  </tbody>\n</table>\n\n<br>\n\n<a href=\"/projects/1/tasks/new\">New Task</a>\n\n  \n\n</body></html>"
(byebug) page.current_path
"/projects/1/tasks"

page.methodsの、bodyのclassがわかった(ブラウザの検証でも確認できる)

<tbody class=\"task_list\">

使えるRSpec入門・その4「どんなブラウザ操作も自由自在!逆引きCapybara大辞典」 - Qiita

class でリンクを指定し、そのリンクのhref属性を検証する例

link = find('.settings-link') expect(link[:href]).to eq edit_user_path

このため、今回はこのように書き換えるのが良い

expect(find('.task_list')).not_to have_content task.title

○ classではなくidで指定する場合は異なるため注意

id で指定する場合は、ドット(.)ではなくシャープ(#)で指定します。 ⇒ find('#settings-link').click

※ expect(page)がよくなかった理由(?)

Capybaraを使う上で気をつけること

検索対象がページ全体になり,ノイズが混ざる可能性がある.さらにもし見つからなかった場合にpage内のテキスト全体がエラーログに表示されてしまいわかりづらい. 出来る限り要素を絞ると良い

xpath

今回、

expect(page).to have_no_xpath task.title

expect(page).to have_no_css task.title

を使用し記述することでエラーを解消することもできるよう

XPathとは?基本概念や書き方をわかりやすく解説!

ただ、今回はより簡潔に記述するため、これらは使用しない