# frozen_string_literal: true module Capybara module Selenium module Find def find_xpath(selector, uses_visibility: false, styles: nil, **_options) find_by(:xpath, selector, uses_visibility: uses_visibility, texts: [], styles: styles) end def find_css(selector, uses_visibility: false, texts: [], styles: nil, **_options) find_by(:css, selector, uses_visibility: uses_visibility, texts: texts, styles: styles) end private def find_by(format, selector, uses_visibility:, texts:, styles:) els = find_context.find_elements(format, selector) hints = [] if (els.size > 2) && !ENV['DISABLE_CAPYBARA_SELENIUM_OPTIMIZATIONS'] begin els = filter_by_text(els, texts) unless texts.empty? hints_js, functions = build_hints_js(uses_visibility, styles) unless functions.empty? hints = es_context.execute_script(hints_js, els).map! do |results| hint = {} hint[:style] = results.pop if functions.include?(:style_func) hint[:visible] = results.pop if functions.include?(:vis_func) hint end end rescue ::Selenium::WebDriver::Error::StaleElementReferenceError, ::Capybara::NotSupportedByDriverError # warn 'Unexpected Stale Element Error - skipping optimization' hints = [] end end els.map.with_index { |el, idx| build_node(el, hints[idx] || {}) } 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) 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 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