From 2575ea3594749832ad1896cbad5b13b6a122468d Mon Sep 17 00:00:00 2001 From: Alexander Brandon Coles Date: Tue, 18 Feb 2025 12:14:34 -0300 Subject: [PATCH 1/5] Add initial support for Node#obscured? Ports `Capybara::Selenium::Node#obscured?` to Cuprite. N.B. Support for nested iframes is not yet implemented. --- lib/capybara/cuprite/browser.rb | 11 ++++++++++ lib/capybara/cuprite/javascripts/index.js | 12 +++++++++++ lib/capybara/cuprite/node.rb | 4 ++++ spec/features/session_spec.rb | 25 +++++++++++++++++++++++ spec/spec_helper.rb | 6 +----- 5 files changed, 53 insertions(+), 5 deletions(-) diff --git a/lib/capybara/cuprite/browser.rb b/lib/capybara/cuprite/browser.rb index 5aad56df..eaca4097 100644 --- a/lib/capybara/cuprite/browser.rb +++ b/lib/capybara/cuprite/browser.rb @@ -210,6 +210,17 @@ def path(node) evaluate_on(node: node, expression: "_cuprite.path(this)") end + def obscured?(node, x = nil, y = nil) + value = if x && y + evaluate_on(node: node, expression: "_cuprite.isObscured(this, #{x}, #{y})") + else + evaluate_on(node: node, expression: "_cuprite.isObscured(this)") + end + return true if value == true + + false + end + def all_text(node) node.text end diff --git a/lib/capybara/cuprite/javascripts/index.js b/lib/capybara/cuprite/javascripts/index.js index 81bc8db8..8f33811d 100644 --- a/lib/capybara/cuprite/javascripts/index.js +++ b/lib/capybara/cuprite/javascripts/index.js @@ -115,6 +115,18 @@ class Cuprite { return `//${selectors.join("/")}`; } + isObscured(node, x, y) { + let rect = node.getBoundingClientRect(); + if (x == null) x = rect.width/2; + if (y == null) y = rect.height/2; + + let px = rect.left + x; + let py = rect.top + y; + let e = document.elementFromPoint(px, py); + + return !node.contains(e) || { x: px, y: py }; + } + set(node, value) { if (node.readOnly) return; diff --git a/lib/capybara/cuprite/node.rb b/lib/capybara/cuprite/node.rb index f7e68f44..82fc9992 100644 --- a/lib/capybara/cuprite/node.rb +++ b/lib/capybara/cuprite/node.rb @@ -213,6 +213,10 @@ def path command(:path) end + def obscured?(x: nil, y: nil) + command(:obscured?, x, y) + end + def inspect %(#<#{self.class} @node=#{@node.inspect}>) end diff --git a/spec/features/session_spec.rb b/spec/features/session_spec.rb index ea3cd45d..b502e288 100644 --- a/spec/features/session_spec.rb +++ b/spec/features/session_spec.rb @@ -314,6 +314,31 @@ end end + describe "Node#obscured?" do + context "when the element is not in the viewport of parent element" do + before do + @session.visit("/cuprite/scroll") + end + + it "is is a boolean" do + expect(@session.find_link("Link outside viewport").obscured?).to be true + expect(@session.find_link("Below the fold").obscured?).to be true + end + end + + context "when the element is only overlapped by descendants" do + before do + @session.visit("/with_html") + end + + # copied from https://github.com/teamcapybara/capybara/blob/master/lib/capybara/spec/session/node_spec.rb#L328 + # as this example is currently disabled on CI in the upstream suite + it "is not obscured" do + expect(@session.first(:css, "p:not(.para)")).not_to be_obscured + end + end + end + it "has no trouble clicking elements when the size of a document changes" do @session.visit("/cuprite/long_page") @session.find(:css, "#penultimate").click diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index ff85a004..f71259a4 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -37,7 +37,7 @@ module TestSessions RSpec.configure do |config| config.define_derived_metadata do |metadata| regexes = <<~REGEXP.split("\n").map { |s| Regexp.quote(s.strip) }.join("|") - node #obscured? + node #obscured? should work in nested iframes node #drag_to should work with jsTree node #drag_to should drag and drop an object node #drag_to should drag and drop if scrolling is needed @@ -68,10 +68,6 @@ module TestSessions node #path reports when element in shadow dom node #shadow_root node #set should submit single text input forms if ended with - #all with obscured filter should only find nodes on top in the viewport when false - #all with obscured filter should not find nodes on top outside the viewport when false - #all with obscured filter should find top nodes outside the viewport when true - #all with obscured filter should only find non-top nodes when true #fill_in should fill in a color field #fill_in should handle carriage returns with line feeds in a textarea correctly #has_field with valid should be false if field is invalid From 2866448d365b2cb735884a22fcf31da17f5345fc Mon Sep 17 00:00:00 2001 From: Alexander Brandon Coles Date: Mon, 17 Feb 2025 18:44:32 -0300 Subject: [PATCH 2/5] Make Node#obscured? support nested iframes --- lib/capybara/cuprite/browser.rb | 17 ++++++++++++++++- lib/capybara/cuprite/page.rb | 16 ++++++++-------- spec/spec_helper.rb | 1 - 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/lib/capybara/cuprite/browser.rb b/lib/capybara/cuprite/browser.rb index eaca4097..bdc0ceb3 100644 --- a/lib/capybara/cuprite/browser.rb +++ b/lib/capybara/cuprite/browser.rb @@ -218,7 +218,7 @@ def obscured?(node, x = nil, y = nil) end return true if value == true - false + frame_obscured_at?(*value.values_at("x", "y")) end def all_text(node) @@ -268,6 +268,21 @@ def attach_page(target_id = nil) target.page = Page.new(target.client, context_id: target.context_id, target_id: target.id) target.page end + + def frame_obscured_at?(x, y) + frame = page.active_frame + return false if frame.main? + + frame_node = frame.evaluate("window.frameElement") + return false unless frame_node + + switch_to_frame(:parent) + begin + obscured?(frame_node, x, y) + ensure + switch_to_frame(frame) + end + end end end end diff --git a/lib/capybara/cuprite/page.rb b/lib/capybara/cuprite/page.rb index f3e2bf3e..e80855d4 100644 --- a/lib/capybara/cuprite/page.rb +++ b/lib/capybara/cuprite/page.rb @@ -138,6 +138,14 @@ def closed? false end + def active_frame + if @frame_stack.empty? + main_frame + else + @frames[@frame_stack.last] + end + end + private def prepare_page @@ -180,14 +188,6 @@ def find_position(node, **options) raise end - - def active_frame - if @frame_stack.empty? - main_frame - else - @frames[@frame_stack.last] - end - end end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index f71259a4..e41daddd 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -37,7 +37,6 @@ module TestSessions RSpec.configure do |config| config.define_derived_metadata do |metadata| regexes = <<~REGEXP.split("\n").map { |s| Regexp.quote(s.strip) }.join("|") - node #obscured? should work in nested iframes node #drag_to should work with jsTree node #drag_to should drag and drop an object node #drag_to should drag and drop if scrolling is needed From a5a2b63eee3cc83ee17182c7c7319cb6905704ef Mon Sep 17 00:00:00 2001 From: Alexander Brandon Coles Date: Sun, 23 Feb 2025 13:23:25 -0300 Subject: [PATCH 3/5] Prefer RSpec predicate matchers Co-authored-by: Ivan Kuchin --- spec/features/session_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/features/session_spec.rb b/spec/features/session_spec.rb index b502e288..96a5b784 100644 --- a/spec/features/session_spec.rb +++ b/spec/features/session_spec.rb @@ -321,8 +321,8 @@ end it "is is a boolean" do - expect(@session.find_link("Link outside viewport").obscured?).to be true - expect(@session.find_link("Below the fold").obscured?).to be true + expect(@session.find_link("Link outside viewport")).to be_obscured + expect(@session.find_link("Below the fold")).to be_obscured end end From da3a8ae708332e61fd50b68535919ceae8686021 Mon Sep 17 00:00:00 2001 From: Alexander Brandon Coles Date: Wed, 5 Mar 2025 16:07:02 -0300 Subject: [PATCH 4/5] Rework Node#obscured? frame support in JavaScript Teaches `isObscured` how to traverse nested frames. --- lib/capybara/cuprite/browser.rb | 28 ++++----------------- lib/capybara/cuprite/javascripts/index.js | 30 ++++++++++++++++++++--- lib/capybara/cuprite/page.rb | 16 ++++++------ 3 files changed, 39 insertions(+), 35 deletions(-) diff --git a/lib/capybara/cuprite/browser.rb b/lib/capybara/cuprite/browser.rb index bdc0ceb3..86a8767b 100644 --- a/lib/capybara/cuprite/browser.rb +++ b/lib/capybara/cuprite/browser.rb @@ -211,14 +211,11 @@ def path(node) end def obscured?(node, x = nil, y = nil) - value = if x && y - evaluate_on(node: node, expression: "_cuprite.isObscured(this, #{x}, #{y})") - else - evaluate_on(node: node, expression: "_cuprite.isObscured(this)") - end - return true if value == true - - frame_obscured_at?(*value.values_at("x", "y")) + if x && y + evaluate_on(node: node, expression: "_cuprite.isObscured(this, #{x}, #{y})") + else + evaluate_on(node: node, expression: "_cuprite.isObscured(this)") + end end def all_text(node) @@ -268,21 +265,6 @@ def attach_page(target_id = nil) target.page = Page.new(target.client, context_id: target.context_id, target_id: target.id) target.page end - - def frame_obscured_at?(x, y) - frame = page.active_frame - return false if frame.main? - - frame_node = frame.evaluate("window.frameElement") - return false unless frame_node - - switch_to_frame(:parent) - begin - obscured?(frame_node, x, y) - ensure - switch_to_frame(frame) - end - end end end end diff --git a/lib/capybara/cuprite/javascripts/index.js b/lib/capybara/cuprite/javascripts/index.js index 8f33811d..187a71ee 100644 --- a/lib/capybara/cuprite/javascripts/index.js +++ b/lib/capybara/cuprite/javascripts/index.js @@ -115,16 +115,38 @@ class Cuprite { return `//${selectors.join("/")}`; } + /** + * Returns true if the node is obscured in the viewport. + * + * @param {Element} node + * @param {number} [x] the center position + * @param {number} [y] the center position + * @return {boolean} true if the node is obscured, false otherwise + */ isObscured(node, x, y) { + let win = window; let rect = node.getBoundingClientRect(); - if (x == null) x = rect.width/2; - if (y == null) y = rect.height/2; + x = x ?? rect.width / 2; + y = y ?? rect.height / 2; let px = rect.left + x; let py = rect.top + y; - let e = document.elementFromPoint(px, py); - return !node.contains(e) || { x: px, y: py }; + while (win) { + let topNode = win.document.elementFromPoint(px, py); + + if (node !== topNode && !node.contains(topNode)) return true; + + node = win.frameElement; + if (!node) return false; + + rect = node.getBoundingClientRect(); + px = rect.left + px; + py = rect.top + py; + win = win.parent; + } + + return false; } set(node, value) { diff --git a/lib/capybara/cuprite/page.rb b/lib/capybara/cuprite/page.rb index e80855d4..f3e2bf3e 100644 --- a/lib/capybara/cuprite/page.rb +++ b/lib/capybara/cuprite/page.rb @@ -138,14 +138,6 @@ def closed? false end - def active_frame - if @frame_stack.empty? - main_frame - else - @frames[@frame_stack.last] - end - end - private def prepare_page @@ -188,6 +180,14 @@ def find_position(node, **options) raise end + + def active_frame + if @frame_stack.empty? + main_frame + else + @frames[@frame_stack.last] + end + end end end end From 91eb89fbb3a9384faabff021e6d4002c456bcf17 Mon Sep 17 00:00:00 2001 From: Alexander Brandon Coles Date: Wed, 5 Mar 2025 17:19:21 -0300 Subject: [PATCH 5/5] Remove Node#obscured? parameters Although `Capybara::Selenium::Node#obscured?` accepts `x` and `y` arguments, these parameters do not appear to part of the public Capybara API. --- lib/capybara/cuprite/browser.rb | 8 ++------ lib/capybara/cuprite/javascripts/index.js | 11 +++-------- lib/capybara/cuprite/node.rb | 4 ++-- 3 files changed, 7 insertions(+), 16 deletions(-) diff --git a/lib/capybara/cuprite/browser.rb b/lib/capybara/cuprite/browser.rb index 86a8767b..d7ce914e 100644 --- a/lib/capybara/cuprite/browser.rb +++ b/lib/capybara/cuprite/browser.rb @@ -210,12 +210,8 @@ def path(node) evaluate_on(node: node, expression: "_cuprite.path(this)") end - def obscured?(node, x = nil, y = nil) - if x && y - evaluate_on(node: node, expression: "_cuprite.isObscured(this, #{x}, #{y})") - else - evaluate_on(node: node, expression: "_cuprite.isObscured(this)") - end + def obscured?(node) + evaluate_on(node: node, expression: "_cuprite.isObscured(this)") end def all_text(node) diff --git a/lib/capybara/cuprite/javascripts/index.js b/lib/capybara/cuprite/javascripts/index.js index 187a71ee..74b96ae5 100644 --- a/lib/capybara/cuprite/javascripts/index.js +++ b/lib/capybara/cuprite/javascripts/index.js @@ -119,18 +119,13 @@ class Cuprite { * Returns true if the node is obscured in the viewport. * * @param {Element} node - * @param {number} [x] the center position - * @param {number} [y] the center position * @return {boolean} true if the node is obscured, false otherwise */ - isObscured(node, x, y) { + isObscured(node) { let win = window; let rect = node.getBoundingClientRect(); - x = x ?? rect.width / 2; - y = y ?? rect.height / 2; - - let px = rect.left + x; - let py = rect.top + y; + let px = rect.left + rect.width / 2; + let py = rect.top + rect.height / 2; while (win) { let topNode = win.document.elementFromPoint(px, py); diff --git a/lib/capybara/cuprite/node.rb b/lib/capybara/cuprite/node.rb index 82fc9992..d4f12395 100644 --- a/lib/capybara/cuprite/node.rb +++ b/lib/capybara/cuprite/node.rb @@ -213,8 +213,8 @@ def path command(:path) end - def obscured?(x: nil, y: nil) - command(:obscured?, x, y) + def obscured? + command(:obscured?) end def inspect