From 9ce269c3ba0476d028ca02cc2a71885cb138f563 Mon Sep 17 00:00:00 2001 From: Thomas Walpole Date: Sun, 13 Jan 2019 15:28:47 -0800 Subject: [PATCH] Use find hints to optimize Selenium style filter --- lib/capybara/node/document.rb | 1 - lib/capybara/node/element.rb | 2 +- lib/capybara/queries/selector_query.rb | 13 ++--- lib/capybara/selenium/extensions/find.rb | 67 ++++++++++++++++++------ 4 files changed, 59 insertions(+), 24 deletions(-) diff --git a/lib/capybara/node/document.rb b/lib/capybara/node/document.rb index b5c6d22c..9c2f18b2 100644 --- a/lib/capybara/node/document.rb +++ b/lib/capybara/node/document.rb @@ -43,7 +43,6 @@ module Capybara def scroll_to(*args) find(:xpath, '//body').scroll_to(*args) end - end end end diff --git a/lib/capybara/node/element.rb b/lib/capybara/node/element.rb index 373c13cc..92041a30 100644 --- a/lib/capybara/node/element.rb +++ b/lib/capybara/node/element.rb @@ -405,7 +405,7 @@ module Capybara when Symbol synchronize { base.scroll_to(nil, pos_or_el_or_x) } unless pos_or_el_or_x == :current when Capybara::Node::Element - synchronize { base.scroll_to(pos_or_el_or_x.base, align)} + synchronize { base.scroll_to(pos_or_el_or_x.base, align) } else synchronize { base.scroll_to(nil, nil, [pos_or_el_or_x, y]) } end diff --git a/lib/capybara/queries/selector_query.rb b/lib/capybara/queries/selector_query.rb index 8bd6c8e8..0f94482e 100644 --- a/lib/capybara/queries/selector_query.rb +++ b/lib/capybara/queries/selector_query.rb @@ -180,19 +180,20 @@ module Capybara end def find_nodes_by_selector_format(node, exact) - options = {} - options[:uses_visibility] = true unless visible == :all - options[:texts] = text_fragments unless selector.format == :xpath + hints = {} + hints[:uses_visibility] = true unless visible == :all + hints[:texts] = text_fragments unless selector.format == :xpath + hints[:styles] = options[:style] if use_default_style_filter? if selector.format == :css if node.method(:find_css).arity != 1 - node.find_css(css, **options) + node.find_css(css, **hints) else node.find_css(css) end elsif selector.format == :xpath if node.method(:find_xpath).arity != 1 - node.find_xpath(xpath(exact), **options) + node.find_xpath(xpath(exact), **hints) else node.find_xpath(xpath(exact)) end @@ -388,7 +389,7 @@ module Capybara end def matches_style?(node, styles) - @actual_styles = node.style(*styles.keys) + @actual_styles = node.initial_cache[:style] || node.style(*styles.keys) styles.all? do |style, value| if value.is_a? Regexp @actual_styles[style.to_s] =~ value diff --git a/lib/capybara/selenium/extensions/find.rb b/lib/capybara/selenium/extensions/find.rb index 91b28581..37857808 100644 --- a/lib/capybara/selenium/extensions/find.rb +++ b/lib/capybara/selenium/extensions/find.rb @@ -3,32 +3,63 @@ module Capybara module Selenium module Find - def find_xpath(selector, uses_visibility: false, **_options) - find_by(:xpath, selector, uses_visibility: uses_visibility, texts: []) + 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: [], **_options) - find_by(:css, selector, uses_visibility: uses_visibility, texts: texts) + 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:) + def find_by(format, selector, uses_visibility:, texts:, styles:) els = find_context.find_elements(format, selector) - els = filter_by_text(els, texts) if (els.size > 1) && !texts.empty? - visibilities = uses_visibility && els.size > 1 ? element_visibilities(els) : [] - els.map.with_index { |el, idx| build_node(el, visible: visibilities[idx]) } - end + hints = [] + if (els.size > 2) && !ENV['DISABLE_CAPYBARA_SELENIUM_OPTIMIZATIONS'] + els = filter_by_text(els, texts) unless texts.empty? - def element_visibilities(elements) - es_context = respond_to?(:execute_script) ? self : driver - es_context.execute_script <<~JS, elements - return arguments[0].map(#{is_displayed_atom}) - JS + hints_js = +'' + functions = [] + if uses_visibility + 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 + + unless functions.empty? + hints_js << <<~EACH_JS + return arguments[0].map(function(el){ + return [#{functions.join(',')}].map(function(fn){ return fn.call(null, el) }); }); + EACH_JS + + hints = es_context.execute_script hints_js, els + hints.map! do |results| + result = {} + result[:style] = results.pop if styles.is_a? Hash + result[:visible] = results.pop if uses_visibility + result + end + end + end + els.map.with_index { |el, idx| build_node(el, hints[idx] || {}) } end def filter_by_text(elements, texts) - es_context = respond_to?(:execute_script) ? self : driver es_context.execute_script <<~JS, elements, texts var texts = arguments[1] return arguments[0].filter(function(el){ @@ -38,8 +69,12 @@ module Capybara JS end + def es_context + respond_to?(:execute_script) ? self : driver + end + def is_displayed_atom # rubocop:disable Naming/PredicateName - @is_displayed_atom ||= browser.send(:bridge).send(:read_atom, 'isDisplayed') + @@is_displayed_atom ||= browser.send(:bridge).send(:read_atom, 'isDisplayed') end end end