Slide 1

Slide 1 text

Ambitious Capybara Eduardo Gutierrez github.com/ecbypi | @ecbypi

Slide 2

Slide 2 text

Brought to you by…

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

Little Known Facts • Capybaras (Hydrochoerus hydrochaeris) originate from South America • Pronunciation of the name is under heavy debate: capybara vs capybeara

Slide 5

Slide 5 text

Advanced Facts • Capybaras are semi-aquatic • They can roam over areas spanning 25 acres • Very friendly animals, many people have them as pets

Slide 6

Slide 6 text

Balancing Act Readability Performance Maintainability

Slide 7

Slide 7 text

Balancing Act Maintainability VS Readability • Some may argue these are very similar • Imagine a large DSL that is very readable but potentially hard to maintain (i.e. Rails’ routing DSL) • On the other hand, imagine well factored code that encompasses a complex problem, but still requires a stiff learning curve

Slide 8

Slide 8 text

Keeping it Ambitious Readability Performance Maintainability Our Ideal

Slide 9

Slide 9 text

Keeping it Ambitious Readability Performance Maintainability Sweet Spot

Slide 10

Slide 10 text

And for capybara? Readability • Written as stories a user or developer new to the project could understand Maintainability • Growable / refactor-able to introduce new patterns and features Performance • Fast feedback, whether on CI on a developer machine or iterating on a feature

Slide 11

Slide 11 text

Readability • Avoid raw selectors in feature specs as much as possible, especially CSS classes • Leverage capybara DSL as much as possible by using semantic markup • Is the user story described by your feature actually readable as a user story

Slide 12

Slide 12 text

Readability Let’s refactor a feature test scenario "sign in the user" do create(:user, email: "[email protected]") visit sign_in_page fill_in "user_email", with: "[email protected]" fill_in "user_password", with: "password" click_button "Sign In” expect(current_path).to eq profile_path end

Slide 13

Slide 13 text

Readability scenario "sign in the user" do create(:user, email: "[email protected]") visit sign_in_page fill_in "user_email", with: "[email protected]" fill_in "user_password", with: "password" click_button "Sign In” expect(current_path).to eq profile_path end These tests are using DOM IDs to fill in inputs

Slide 14

Slide 14 text

Readability = simple_form_for :user, url: :sign_in do |f| = f.input :email, label: false, placeholder: "email..." = f.input :password, label: false, placeholder: "password..." = f.button :submit, value: "Sign In” Template removes labels in favor of the placeholders

Slide 15

Slide 15 text

Readability = simple_form_for :user, url: :sign_in do |f| = f.input :email, label_html: { class: "hide" }, placeholder: "email..." = f.input :password, label_html: { class: "hide" }, placeholder: "password..." = f.button :submit, value: "Sign In” Can use a utility class to hide them instead

Slide 16

Slide 16 text

Readability scenario "sign in the user" do create(:user, email: "[email protected]") visit sign_in_page fill_in "Email", with: "[email protected]" fill_in "Password", with: "password" click_button "Sign In" expect(current_path).to eq profile_path end Tests now are now more readable

Slide 17

Slide 17 text

Readability Randomized with seed 36225 . Finished in 3.26 seconds (files took 3.43 seconds to load) 1 example, 0 failures Tests still pass

Slide 18

Slide 18 text

Readability scenario "sign in the user", :js do create(:user, email: "[email protected]") visit sign_in_page fill_in "Email", with: "[email protected]" fill_in "Password", with: "password" click_button "Sign In" expect(current_path).to eq profile_path end Test will fail when using `js: true` because Capybara ignores hidden elements in these tests

Slide 19

Slide 19 text

Readability Randomized with seed 3941 F Failures: 1) Login page can sign in the user Failure/Error: fill_in Capybara::ElementNotFound: Unable to find field "Email" # ./spec/features/sign_in_spec:6:in `block (2 levels) in ' Finished in 3.72 seconds (files took 3.24 seconds to load) 1 example, 1 failure

Slide 20

Slide 20 text

Readability = simple_form_for :user, url: :sign_in do |f| = f.input :email, label_html: { class: "accessible-hide" }, placeholder: "email..." = f.input :password, label_html: { class: "accessible-hide" }, placeholder: "password..." = f.button :submit, value: "Sign In” Use an accessible method of hiding elements

Slide 21

Slide 21 text

Readability .accessible-hide { position: absolute !important; height: 1px; width: 1px; overflow: hidden; clip: rect(1px 1px 1px 1px); /* IE6, IE7 */ clip: rect(1px, 1px, 1px, 1px); } This will be visible to screen readers and capybara http://snook.ca/archives/html_and_css/hiding-content-for-accessibility

Slide 22

Slide 22 text

Readability Randomized with seed 7651 . Finished in 2.75 seconds (files took 3.28 seconds to load) 1 example, 0 failures

Slide 23

Slide 23 text

Readability Let’s work on another test: scenario "search users by birthdate", :js do create( :user, name: "John", birthdate: Time.zone.local(1985, 10, 30) ) visit search_path find(".open-datepicker").click # fill in datepicker with Wed, Oct 30, 1985 click_button "Search" expect(page).to have_css ".search-result:contains('John')" end

Slide 24

Slide 24 text

Readability scenario "search users by birthdate", :js do create( :user, name: "John", birthdate: Time.zone.local(1985, 10, 30) ) visit search_path find(".open-datepicker").click # fill in datepicker with Wed, Oct 30, 1985 click_button "Search" expect(page).to have_css ".search-result:contains('John')" end ASIDE: We’ll fill this in later

Slide 25

Slide 25 text

Readability .input %label{ for: "birthday" } Birthday of Contact %input{ type: :hidden, id: "birthday" } %input{ type: :text, disabled: "" } %button.hide{ type: :button } %button{ type: :button } Simplified datepicker markup

Slide 26

Slide 26 text

Readability scenario "search users by birthdate", :js do create( :user, name: "John", birthdate: Time.zone.local(1985, 10, 30) ) visit search_path find(".open-datepicker").click # fill in datepicker with Wed, Oct 30, 1985 click_button "Search" expect(page).to have_css ".search-result:contains('John')" end Using hard-coded CSS / Sizzle selectors

Slide 27

Slide 27 text

Readability scenario "search users by birthdate", :js do create( :user, name: "John", birthdate: Time.zone.local(1985, 10, 30) ) visit search_path # MAYBE ? Unfortunately no... click_button ".open-datepicker" # fill in datepicker with Wed, Oct 30, 1985 click_button "Search" expect(page).to have_css ".search-result:contains('John')" end

Slide 28

Slide 28 text

Readability # XPath::HTML.button(locator) # Trimmed and reformatted def button(locator) locator = locator.to_s button = descendant(:input)[ attr(:type).one_of('submit', 'reset', 'image', 'button') ] [ attr(:id).equals(locator) | attr(:value).is(locator) | attr(:title).is(locator) ] # ... end `title` is a valid attribute to lookup buttons

Slide 29

Slide 29 text

Readability .input %label{ for: "birthday" } Birthday of Contact %input{ type: :hidden, id: "birthday" } %input{ type: :text, disabled: "" } %button.hide{ title: "Clear Input", type: :button } %button{ title: "Open Calendar", type: :button }

Slide 30

Slide 30 text

Readability scenario "search users by birthdate", :js do create( :user, name: "John", birthdate: Time.zone.local(1985, 10, 30) ) visit search_path click_button "Open Calendar" # fill in datepicker with Wed, Oct 30, 1985 click_button "Search" expect(page).to have_css ".search-result:contains('John')" end

Slide 31

Slide 31 text

Readability scenario "search users by birthdate", :js do create( :user, name: "John", birthdate: Time.zone.local(1985, 10, 30) ) visit search_path click_button "Open Calendar" # fill in datepicker with Wed, Oct 30, 1985 click_button "Search" expect(page).to have_css ".search-result", text: "John" end capybara queries can use `:text` option instead

Slide 32

Slide 32 text

Readability %ul - @results.each do |result| %li.search-result = result.name What do we have to hook into here besides tag names or CSS classes?

Slide 33

Slide 33 text

Readability How about attributes? According to the HTML5 spec: • Custom data attributes are intended to store custom data private to the page or application, for which there are no more appropriate attributes or elements. data-

Slide 34

Slide 34 text

Readability Lets add our own custom selectors for elements: Capybara.add_selector(:models_element) do xpath { |name| XPath.css("[data-collection='#{name}']") } end Capybara.add_selector(:model_element) do xpath { |name| XPath.css("[data-resource='#{name}']") } end

Slide 35

Slide 35 text

Readability And define helpers to make them easier to use: module ResourceDomElements def model_element(model_name, options = {}) find(:model_element, model_name, options) end def have_model_element(model_name, options) have_selector :model_element, model_name, options end end

Slide 36

Slide 36 text

Readability And metaprogram even more painless helpers module ResourceDomElements RESOURCES = %w( search_result ) RESOURCES.each do |model_name| class_eval <<-METHODS, __FILE__, __LINE__ + 1 def #{model_name}_element(options = {}) model_element('#{model_name}', options) end def have_#{model_name}_element(options = {}) have_model_element('#{model_name}', options) end METHODS end end

Slide 37

Slide 37 text

Readability %ul - @results.each do |result| %li.search-result{ data: { resource: :search_result } } = result.name

Slide 38

Slide 38 text

Readability scenario "search users by birthdate", :js do create( :user, name: "John", birthdate: Time.zone.local(1985, 10, 30) ) visit search_path click_button "Open Calendar" # fill in datepicker with Wed, Oct 30, 1985 click_button "Search" expect(page).to have_search_result_element text: "John" end

Slide 39

Slide 39 text

Filling in the Datepicker scenario "search users by birthdate", :js do create( :user, name: "John", birthdate: Time.zone.local(1985, 10, 30) ) visit search_path click_button "Open Calendar" # fill in datepicker with Wed, Oct 30, 1985 click_button "Search" expect(page).to have_search_result_element text: "John" end

Slide 40

Slide 40 text

Filling in the Datepicker Click right table cell Change selects

Slide 41

Slide 41 text

Filling in the Datepicker

Slide 42

Slide 42 text

Filling in the Datepicker

Slide 43

Slide 43 text

Capybara::Element find(:button, "Submit") # => Capybara::Element

Slide 44

Slide 44 text

Capybara::Element def click_button(locator, options={}) find(:button, locator, options).click end

Slide 45

Slide 45 text

Filling in the Datepicker find( 'select.ui-datepicker-year option', text: birthdate.year.to_s ).select_option

Slide 46

Slide 46 text

Filling in the Datepicker find( 'select.ui-datepicker-month option', text: birthdate.strftime('%b') ).select_option

Slide 47

Slide 47 text

Filling in the Datepicker find( a.ui-state-default:not(.ui-priority-secondary)', text: birthdate.day.to_s, match: :first ).click

Slide 48

Slide 48 text

Filling in the Datepicker scenario "search users by birthdate", :js do birthdate = Time.zone.local(1985, 10, 30) create(:user, name: "John", birthdate: birthdate) visit search_path click_button "Open Calendar" within "#ui-datepicker-div" do find( 'select.ui-datepicker-year option', text: birthdate.year.to_s ).select_option find( 'select.ui-datepicker-month option', text: birthdate.strftime('%b') ).select_option find( 'a.ui-state-default:not(.ui-priority-secondary)', text: birthdate.day.to_s, match: :first ).click end click_button "Search" expect(page).to have_search_result_element text: "John" end

Slide 49

Slide 49 text

Maintainability scenario "search users by birthdate", :js do birthdate = Time.zone.local(1985, 10, 30) create(:user, name: "John", birthdate: birthdate) visit search_path click_button "Open Calendar" within "#ui-datepicker-div" do find( 'select.ui-datepicker-year option', text: birthdate.year.to_s ).select_option find( 'select.ui-datepicker-month option', text: birthdate.strftime('%b') ).select_option find( 'a.ui-state-default:not(.ui-priority-secondary)', text: birthdate.day.to_s, match: :first ).click end click_button "Search" expect(page).to have_search_result_element text: "John" end All in one test, likely to be used elsewhere

Slide 50

Slide 50 text

Maintainability scenario "search users by birthdate", :js do birthdate = Time.zone.local(1985, 10, 30) create(:user, name: "John", birthdate: birthdate) visit search_path choose_date birthdate, from: "Birthday of Contact" click_button "Search" expect(page).to have_search_result_element text: "John" end

Slide 51

Slide 51 text

Maintainability module DatepickerSteps def choose_date(date, options) within "[data-datepicker-input]", text: options[:from] do click_button "Open Calendar" end within "#ui-datepicker-div" do find( 'select.ui-datepicker-year option', text: date.year.to_s ).select_option find( 'select.ui-datepicker-month option', text: date.strftime('%b') ).select_option find( 'a.ui-state-default:not(.ui-priority-secondary)', text: date.day.to_s, match: :first ).click end end end Extract another steps module

Slide 52

Slide 52 text

Maintainability scenario "search users by birthdate", :js do birthdate = Time.zone.local(1985, 10, 30) create(:user, name: "John", birthdate: birthdate) visit search_path choose_date birthdate, from: "Birthday of Contact" click_button "Search" expect(page).to have_search_result_element text: “John" expect(page).to have_datepicker_input( "Birthday of Contact", with: birthdate ) end

Slide 53

Slide 53 text

Maintainability module DatepickerSteps def have_datepicker_input(label_text, options = {}) with = options[:with].strftime("%a, %B %d, %Y") have_field label_text, with: value, disabled: true end end

Slide 54

Slide 54 text

Maintainability module DatepickerSteps def have_datepicker_input(label_text, options = {}) with = options[:with].strftime("%a, %B %d, %Y") have_field label_text, with: value, disabled: true end end

Slide 55

Slide 55 text

Capybara::Selector Capybara.add_selector(:field) do xpath { |locator| XPath::HTML.field(locator) } filter(:checked, boolean: true) { |node, value| not(value ^ node.checked?) } filter(:unchecked, boolean: true) { |node, value| (value ^ node.checked?) } filter(:disabled, default: false, boolean: true) { |node, value| not(value ^ node.disabled?) } filter(:with) { |node, with| node.value == with.to_s } filter(:type) do |node, type| if ['textarea', 'select'].include?(type) node.tag_name == type else node[:type] == type end end describe do |options| desc, states = "", [] desc << " of type #{options[:type].inspect}" if options[:type] desc << " with value #{options[:with].to_s.inspect}" if options.has_key?(:with) states << 'checked' if options[:checked] || (options.has_key?(:unchecked) && ! options[:unchecked]) states << 'not checked' if options[:unchecked] || (options.has_key?(:checked) && ! options[:checked]) states << 'disabled' if options[:disabled] desc << " that is #{states.join(' and ')}" unless states.empty? desc end end https://github.com/jnicklas/capybara/blob/master/lib/capybara/selector.rb#L114-L137

Slide 56

Slide 56 text

No content

Slide 57

Slide 57 text

Performance within helper • Use to scope multiple assertions in the same part of the DOM • Avoids searching through the same parts of the DOM multiple times within

Slide 58

Slide 58 text

Performance - scenario "add new address from the addresses listing", :js do visit addresses_page expect(page).to have_address_element count: 30 click_button "Add Address" fill_in "Street", with: "77 Mass Ave" fill_in "City", with: "Cambridge" select "MA", from: "State" fill_in "Postal Code", with: "02139" select "United States", from: "Country" click_button "Add Address" expect(page).to have_address_element text: "77 Mass Ave" end within

Slide 59

Slide 59 text

Performance - scenario "add new address from the addresses listing", :js do visit addresses_page expect(page).to have_address_element count: 30 click_button "Add Address" within modal_dialogue(text: "New Address") do fill_in "Street", with: "77 Mass Ave" fill_in "City", with: "Cambridge" select "MA", from: "State" fill_in "Postal Code", with: "02139" select "United States", from: "Country" click_button "Add Address" end expect(page).to have_address_element text: "77 Mass Ave" end within

Slide 60

Slide 60 text

Be wary of elements not found in current scope within modal_dialogue(text: "New Address") do fill_in "Street", with: "77 Mass Ave" fill_in "City", with: "Cambridge" select "MA", from: "State" fill_in "Postal Code", with: "02139" select "United States", from: "Country" choose_date 2.months.from_now, from: "Expires At" # => raises `Capybara::ElementNotFound` click_button "Add Address" end Performance - within date picker does not exist in modal_dialogue

Slide 61

Slide 61 text

Performance Write features as QA scripts • Avoid single test for each happy and sad path • Setup / teardown of each test can be expensive • Most useful on pages with a significant amount of markup (~1200+)

Slide 62

Slide 62 text

Performance - QA Scripts def language_fluency_form find(".new_language_ability") end scenario "allows viewing, adding, editing and removing them" do spanish = create(:language, name: "Spanish") create(:language, name: "French") applicant = create(:applicant) create( :language_ability, applicant: applicant, language: spanish, ability: 3 ) sign_in_applicant applicant visit language_abilities_path expect(current_path).to eq language_abilities_path within language_ability_element(text: "Spanish") do expect(page).to have_content "3" click_button "Edit" expect(page).to have_select "Change Fluency", selected: "3" expect(page).to have_button "Save" click_button "Cancel" expect(page).not_to have_select "Change Fluency" expect(page).to have_button "Edit" expect(page).to have_button "Delete" end within language_fluency_form do expect(page).to have_css( "option[disabled]", text: "Spanish", visible: false ) click_button "Add Language" expect(page).to have_css( ".input-error-message", count: 2, text: I18n.t("errors.messages.blank") ) chosen_select "French", from: "Language" choose "4" click_button "Add Language" expect(page).to have_chosen_select "Language", selected: "" expect(page).not_to have_field "Fluency" expect(page).to have_css( "option[disabled]", text: "French", visible: false ) end within language_ability_element(text: "French") do expect(page).to have_content "4" click_button "Edit" select "5", from: "Change Fluency" click_button "Save" expect(page).not_to have_select "Change Fluency" expect(page).to have_content "5" end within language_ability_element(text: "Spanish") do click_button "Delete" A single integration test for a page of the app

Slide 63

Slide 63 text

Performance - QA Scripts scenario "allows viewing, adding, editing and removing them" do spanish = create(:language, name: "Spanish") create(:language, name: "French") applicant = create(:applicant) create( :language_ability, applicant: applicant, language: spanish, ability: 3 ) sign_in_applicant applicant visit language_abilities_path expect(current_path).to eq language_abilities_path Create seed records

Slide 64

Slide 64 text

Performance - QA Scripts scenario "allows viewing, adding, editing and removing them" do spanish = create(:language, name: "Spanish") create(:language, name: "French") applicant = create(:applicant) create( :language_ability, applicant: applicant, language: spanish, ability: 3 ) sign_in_applicant applicant visit language_abilities_path expect(current_path).to eq language_abilities_path Create seed records Visit page under test

Slide 65

Slide 65 text

Performance - QA Scripts within language_ability_element(text: "Spanish") do expect(page).to have_content "3" click_button "Edit" expect(page).to have_select "Change Fluency", selected: "3" expect(page).to have_button "Save" click_button "Cancel" expect(page).not_to have_select "Change Fluency" expect(page).to have_button "Edit" expect(page).to have_button "Delete" end Check editing existing record

Slide 66

Slide 66 text

Performance - QA Scripts within language_ability_element(text: "Spanish") do expect(page).to have_content "3" click_button "Edit" expect(page).to have_select "Change Fluency", selected: "3" expect(page).to have_button "Save" click_button "Cancel" expect(page).not_to have_select "Change Fluency" expect(page).to have_button "Edit" expect(page).to have_button "Delete" end Ensure cancel works Check editing existing record

Slide 67

Slide 67 text

Performance - QA Scripts within language_fluency_form do expect(page).to have_css( "option[disabled]", text: "Spanish", visible: false ) click_button "Add Language" expect(page).to have_css( ".input-error-message", count: 2, text: I18n.t("errors.messages.blank") ) chosen_select "French", from: "Language" choose "4" click_button "Add Language" expect(page).to have_chosen_select "Language", selected: "" expect(page).not_to have_field "Fluency" expect(page).to have_css( "option[disabled]", text: "French", visible: false ) end Ensure selected languages are disabled Make sure client-side validations run

Slide 68

Slide 68 text

Performance - QA Scripts within language_fluency_form do expect(page).to have_css( "option[disabled]", text: "Spanish", visible: false ) click_button "Add Language" expect(page).to have_css( ".input-error-message", count: 2, text: I18n.t("errors.messages.blank") ) chosen_select "French", from: "Language" choose "4" click_button "Add Language" expect(page).to have_chosen_select "Language", selected: "" expect(page).not_to have_field "Fluency" expect(page).to have_css( "option[disabled]", text: "French", visible: false ) end Ensure selected languages are disabled Make sure client-side validations run Check form is updated correctly

Slide 69

Slide 69 text

Advanced Capybara::Window enables working between multiple windows within a single spec • SOA applications spread out across different domains • Client-side applications built in two repositories

Slide 70

Slide 70 text

Advanced scenario "ensure removed inventory is removed from the API", :js do CMS_URL = "http://localhost:3000" API_URL = "http://localhost:3001" cms_window = window_opened_by do visit CMS_URL create_product!(name: "Foo") end store_window = window_opened_by do visit API_URL expect(page).to have_product_element text: "Foo" end within_window cms_window do within product_element(text: "Foo") do click_button "Out of Stock" end end within_window store_window do visit API_URL expect(page).not_to have_product_element text: "Foo" end end

Slide 71

Slide 71 text

Resources • Initial inspiration: http://www.elabs.se/blog/51- simple-tricks-to-clean-up-your-capybara-tests • `ResourceDomElements`: https://gist.github.com/ ecbypi/3151818#file-resource_dom_elements-rb • Selectors API / options: https://github.com/jnicklas/ capybara/blob/master/lib/capybara/ selector.rb#L114-L242