teamcapybara/capybara

View on GitHub
lib/capybara/selenium/extensions/find.rb

Summary

Maintainability
A
2 hrs
Test Coverage
# frozen_string_literal: true

module Capybara
  module Selenium
    module Find
      def find_xpath(selector, uses_visibility: false, styles: nil, position: false, **_options)
        find_by(:xpath, selector, uses_visibility: uses_visibility, texts: [], styles: styles, position: position)
      end

      def find_css(selector, uses_visibility: false, texts: [], styles: nil, position: false, **_options)
        find_by(:css, selector, uses_visibility: uses_visibility, texts: texts, styles: styles, position: position)
      end

    private

      def find_by(format, selector, uses_visibility:, texts:, styles:, position:)
        els = find_context.find_elements(format, selector)
        hints = []

        if (els.size > 2) && !ENV['DISABLE_CAPYBARA_SELENIUM_OPTIMIZATIONS']
          els = filter_by_text(els, texts) unless texts.empty?
          hints = gather_hints(els, uses_visibility: uses_visibility, styles: styles, position: position)
        end
        els.map.with_index { |el, idx| build_node(el, hints[idx] || {}) }
      end

      def gather_hints(elements, uses_visibility:, styles:, position:)
        hints_js, functions = build_hints_js(uses_visibility, styles, position)
        return [] unless functions.any?

        es_context.execute_script(hints_js, elements).map! do |results|
          hint = {}
          hint[:style] = results.pop if functions.include?(:style_func)
          hint[:position] = results.pop if functions.include?(:position_func)
          hint[:visible] = results.pop if functions.include?(:vis_func)
          hint
        end
      rescue ::Selenium::WebDriver::Error::StaleElementReferenceError,
             ::Capybara::NotSupportedByDriverError
        # warn 'Unexpected Stale Element Error - skipping optimization'
        []
      end

      def filter_by_text(elements, texts)
        es_context.execute_script <<~JS, elements, texts
          var texts = arguments[1];
          return arguments[0].filter(function(el){
            var content = el.textContent.toLowerCase();
            return texts.every(function(txt){ return content.indexOf(txt.toLowerCase()) != -1 });
          })
        JS
      end

      def build_hints_js(uses_visibility, styles, position)
        functions = []
        hints_js = +''

        if uses_visibility && !is_displayed_atom.empty?
          hints_js << <<~VISIBILITY_JS
            var vis_func = #{is_displayed_atom};
          VISIBILITY_JS
          functions << :vis_func
        end

        if position
          hints_js << <<~POSITION_JS
            var position_func = function(el){
              return el.getBoundingClientRect();
            };
          POSITION_JS
          functions << :position_func
        end

        if styles.is_a? Hash
          hints_js << <<~STYLE_JS
            var style_func = function(el){
              var el_styles = window.getComputedStyle(el);
              return #{styles.keys.map(&:to_s)}.reduce(function(res, style){
                res[style] = el_styles[style];
                return res;
              }, {});
            };
          STYLE_JS
          functions << :style_func
        end

        hints_js << <<~EACH_JS
          return arguments[0].map(function(el){
            return [#{functions.join(',')}].map(function(fn){ return fn.call(null, el) });
          });
        EACH_JS

        [hints_js, functions]
      end

      def es_context
        respond_to?(:execute_script) ? self : driver
      end

      def is_displayed_atom # rubocop:disable Naming/PredicateName
        @@is_displayed_atom ||= begin # rubocop:disable Style/ClassVars
          browser.send(:bridge).send(:read_atom, 'isDisplayed')
                                rescue StandardError
                                  # If the atom doesn't exist or other error
                                  ''
        end
      end
    end
  end
end