JezK
Edit File: finders.rb
# frozen_string_literal: true module Capybara module Node module Finders ## # # Find an {Capybara::Node::Element} based on the given arguments. {#find} will raise an error if the element # is not found. # # page.find('#foo').find('.bar') # page.find(:xpath, './/div[contains(., "bar")]') # page.find('li', text: 'Quox').click_link('Delete') # # @param (see #all) # # @macro waiting_behavior # # @!macro system_filters # @option options [String, Regexp] text Only find elements which contain this text or match this regexp # @option options [String, Boolean] exact_text # When String the elements contained text must match exactly, when Boolean controls whether the `text` option must match exactly. # Defaults to {Capybara.configure exact_text}. # @option options [Boolean] normalize_ws # Whether the `text`/`exact_text` options are compared against elment text with whitespace normalized or as returned by the driver. # Defaults to {Capybara.configure default_normalize_ws}. # @option options [Boolean, Symbol] visible 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. # @option options [Boolean] obscured Only find elements with the specified obscured state: # * true - only find elements whose centerpoint is not in the viewport or is obscured by another non-descendant element. # * false - only find elements whose centerpoint is in the viewport and is not obscured by other non-descendant elements. # @option options [String, Regexp] id Only find elements with an id that matches the value passed # @option options [String, Array<String>, Regexp] class Only find elements with matching class/classes. # * Absence of a class can be checked by prefixing the class name with `!` # * If you need to check for existence of a class name that starts with `!` then prefix with `!!` # # class:['a', '!b', '!!!c'] # limit to elements with class 'a' and '!c' but not class 'b' # # @option options [String, Regexp, Hash] style Only find elements with matching style. String and Regexp will be checked against text of the elements `style` attribute, while a Hash will be compared against the elements full style # @option options [Boolean] exact Control whether `is` expressions in the given XPath match exactly or partially. Defaults to {Capybara.configure exact}. # @option options [Symbol] match The matching strategy to use. Defaults to {Capybara.configure match}. # # @return [Capybara::Node::Element] The found element # @raise [Capybara::ElementNotFound] If the element can't be found before time expires # def find(*args, **options, &optional_filter_block) options[:session_options] = session_options synced_resolve Capybara::Queries::SelectorQuery.new(*args, options, &optional_filter_block) end ## # # Find an {Capybara::Node::Element} based on the given arguments that is also an ancestor of the element called on. # {#ancestor} will raise an error if the element is not found. # # {#ancestor} takes the same options as {#find}. # # element.ancestor('#foo').find('.bar') # element.ancestor(:xpath, './/div[contains(., "bar")]') # element.ancestor('ul', text: 'Quox').click_link('Delete') # # @param (see #find) # # @macro waiting_behavior # # @return [Capybara::Node::Element] The found element # @raise [Capybara::ElementNotFound] If the element can't be found before time expires # def ancestor(*args, **options, &optional_filter_block) options[:session_options] = session_options synced_resolve Capybara::Queries::AncestorQuery.new(*args, options, &optional_filter_block) end ## # # Find an {Capybara::Node::Element} based on the given arguments that is also a sibling of the element called on. # {#sibling} will raise an error if the element is not found. # # {#sibling} takes the same options as {#find}. # # element.sibling('#foo').find('.bar') # element.sibling(:xpath, './/div[contains(., "bar")]') # element.sibling('ul', text: 'Quox').click_link('Delete') # # @param (see #find) # # @macro waiting_behavior # # @return [Capybara::Node::Element] The found element # @raise [Capybara::ElementNotFound] If the element can't be found before time expires # def sibling(*args, **options, &optional_filter_block) options[:session_options] = session_options synced_resolve Capybara::Queries::SiblingQuery.new(*args, options, &optional_filter_block) end ## # # Find a form field on the page. The field can be found by its name, id or label text. # # @overload find_field([locator], **options) # @param [String] locator name, id, {Capybara.configure test_id} attribute, placeholder or text of associated label element # # @macro waiting_behavior # # # @option options [Boolean] checked Match checked field? # @option options [Boolean] unchecked Match unchecked field? # @option options [Boolean, Symbol] disabled (false) Match disabled field? # * true - only finds a disabled field # * false - only finds an enabled field # * :all - finds either an enabled or disabled field # @option options [Boolean] readonly Match readonly field? # @option options [String, Regexp] with Value of field to match on # @option options [String] type Type of field to match on # @option options [Boolean] multiple Match fields that can have multiple values? # @option options [String, Regexp] id Match fields that match the id attribute # @option options [String] name Match fields that match the name attribute # @option options [String] placeholder Match fields that match the placeholder attribute # @option options [String, Array<String>, Regexp] class Match fields that match the class(es) passed # @return [Capybara::Node::Element] The found element # def find_field(locator = nil, **options, &optional_filter_block) find(:field, locator, options, &optional_filter_block) end ## # # Find a link on the page. The link can be found by its id or text. # # @overload find_link([locator], **options) # @param [String] locator id, {Capybara.configure test_id} attribute, title, text, or alt of enclosed img element # # @macro waiting_behavior # # @option options [String,Regexp,nil] href Value to match against the links href, if `nil` finds link placeholders (`<a>` elements with no href attribute), if `false` ignores the href # @option options [String, Regexp] id Match links with the id provided # @option options [String] title Match links with the title provided # @option options [String] alt Match links with a contained img element whose alt matches # @option options [String, Array<String>, Regexp] class Match links that match the class(es) provided # @return [Capybara::Node::Element] The found element # def find_link(locator = nil, **options, &optional_filter_block) find(:link, locator, options, &optional_filter_block) end ## # # Find a button on the page. # This can be any `<input>` element of type submit, reset, image, button or it can be a # `<button>` element. All buttons can be found by their id, name, {Capybara.configure test_id} attribute, value, or title. # `<button>` elements can also be found by their text content, and image `<input>` elements by their alt attribute. # # @overload find_button([locator], **options) # @param [String] locator id, name, {Capybara.configure test_id} attribute, value, title, text content, alt of image # # @macro waiting_behavior # # @option options [Boolean, Symbol] disabled (false) Match disabled button? # * true - only finds a disabled button # * false - only finds an enabled button # * :all - finds either an enabled or disabled button # @option options [String, Regexp] id Match buttons with the id provided # @option options [String] name Match buttons with the name provided # @option options [String] title Match buttons with the title provided # @option options [String] value Match buttons with the value provided # @option options [String, Array<String>, Regexp] class Match buttons that match the class(es) provided # @return [Capybara::Node::Element] The found element # def find_button(locator = nil, **options, &optional_filter_block) find(:button, locator, options, &optional_filter_block) end ## # # Find a element on the page, given its id. # # @macro waiting_behavior # # @param [String] id id of element # # @return [Capybara::Node::Element] The found element # def find_by_id(id, **options, &optional_filter_block) find(:id, id, options, &optional_filter_block) end ## # @!method all([kind = Capybara.default_selector], locator = nil, **options) # # Find all elements on the page matching the given selector # and options. # # Both XPath and CSS expressions are supported, but Capybara # does not try to automatically distinguish between them. The # following statements are equivalent: # # page.all(:css, 'a#person_123') # page.all(:xpath, './/a[@id="person_123"]') # # If the type of selector is left out, Capybara uses # {Capybara.configure default_selector}. It's set to `:css` by default. # # page.all("a#person_123") # # Capybara.default_selector = :xpath # page.all('.//a[@id="person_123"]') # # The set of found elements can further be restricted by specifying # options. It's possible to select elements by their text or visibility: # # page.all('a', text: 'Home') # page.all('#menu li', visible: true) # # By default Capybara's waiting behavior will wait up to {Capybara.configure default_max_wait_time} # seconds for matching elements to be available and then return an empty result if none # are available. It is possible to set expectations on the number of results located and # Capybara will raise an exception if the number of elements located don't satisfy the # specified conditions. The expectations can be set using: # # page.assert_selector('p#foo', count: 4) # page.assert_selector('p#foo', maximum: 10) # page.assert_selector('p#foo', minimum: 1) # page.assert_selector('p#foo', between: 1..10) # # @param [Symbol] kind Optional selector type (:css, :xpath, :field, etc.). Defaults to {Capybara.configure default_selector}. # @param [String] locator The locator for the specified selector # @macro system_filters # @macro waiting_behavior # @option options [Integer] count Exact number of matches that are expected to be found # @option options [Integer] maximum Maximum number of matches that are expected to be found # @option options [Integer] minimum Minimum number of matches that are expected to be found # @option options [Range] between Number of matches found must be within the given range # @overload all([kind = Capybara.default_selector], locator = nil, **options) # @overload all([kind = Capybara.default_selector], locator = nil, **options, &filter_block) # @yieldparam element [Capybara::Node::Element] The element being considered for inclusion in the results # @yieldreturn [Boolean] Should the element be considered in the results? # @return [Capybara::Result] A collection of found elements # @raise [Capybara::ExpectationNotMet] The number of elements found doesn't match the specified conditions def all(*args, **options, &optional_filter_block) minimum_specified = options_include_minimum?(options) options = { minimum: 1 }.merge(options) unless minimum_specified options[:session_options] = session_options query = Capybara::Queries::SelectorQuery.new(*args, options, &optional_filter_block) result = nil begin synchronize(query.wait) do result = query.resolve_for(self) raise Capybara::ExpectationNotMet, result.failure_message unless result.matches_count? result end rescue Capybara::ExpectationNotMet raise if minimum_specified || (result.compare_count == 1) Result.new([], nil) end end alias_method :find_all, :all ## # # Find the first element on the page matching the given selector # and options. By default {#first} will wait up to {Capybara.configure default_max_wait_time} # seconds for matching elements to appear and then raise an error if no matching # element is found, or `nil` if the provided count options allow for empty results. # # @overload first([kind], locator, options) # @param [Symbol] kind The type of selector # @param [String] locator The selector # @param [Hash] options Additional options; see {#all} # @return [Capybara::Node::Element] The found element or nil # @raise [Capybara::ElementNotFound] If element(s) matching the provided options can't be found before time expires # def first(*args, **options, &optional_filter_block) options = { minimum: 1 }.merge(options) unless options_include_minimum?(options) all(*args, options, &optional_filter_block).first end private def synced_resolve(query) synchronize(query.wait) do if prefer_exact?(query) result = query.resolve_for(self, true) result = query.resolve_for(self, false) if result.empty? && query.supports_exact? && !query.exact? else result = query.resolve_for(self) end raise Capybara::Ambiguous, "Ambiguous match, found #{result.size} elements matching #{query.applied_description}" if ambiguous?(query, result) raise Capybara::ElementNotFound, "Unable to find #{query.applied_description}" if result.empty? result.first end.tap(&:allow_reload!) end def ambiguous?(query, result) %i[one smart].include?(query.match) && (result.size > 1) end def prefer_exact?(query) %i[smart prefer_exact].include?(query.match) end def options_include_minimum?(opts) %i[count minimum between].any? { |key| opts.key?(key) } end def parent first(:xpath, './parent::*', minimum: 0) end end end end