From ceb4f864cc47363c342513b759d4de5194724afa Mon Sep 17 00:00:00 2001 From: Tilmann Singer Date: Tue, 25 Apr 2023 14:55:20 +0200 Subject: [PATCH] Add initial support for shadow_root Following capybara's support https://github.com/teamcapybara/capybara/pull/2546 --- lib/capybara/cuprite/javascripts/index.js | 34 +++++++++++++++-------- lib/capybara/cuprite/node.rb | 7 +++++ spec/features/session_spec.rb | 18 ++++++++++++ spec/spec_helper.rb | 2 +- 4 files changed, 49 insertions(+), 12 deletions(-) diff --git a/lib/capybara/cuprite/javascripts/index.js b/lib/capybara/cuprite/javascripts/index.js index 81bc8db8..4e6bde72 100644 --- a/lib/capybara/cuprite/javascripts/index.js +++ b/lib/capybara/cuprite/javascripts/index.js @@ -52,13 +52,17 @@ class Cuprite { if (this.isVisible(node)) { if (node.nodeName == "TEXTAREA") { return node.textContent; - } else { - if (node instanceof SVGElement) { - return node.textContent; - } else { - return node.innerText; - } } + if (node instanceof SVGElement) { + return node.textContent; + } + if (node instanceof ShadowRoot) { + return Array.from(node.children) + .map(child => this.visibleText(child)) + .filter(text => text) + .join(" "); + } + return node.innerText; } } @@ -74,11 +78,15 @@ class Cuprite { } while (node) { - style = window.getComputedStyle(node); - if (style.display === "none" || style.visibility === "hidden" || parseFloat(style.opacity) === 0) { - return false; + if (node instanceof ShadowRoot) { + node = node.host; + } else { + style = window.getComputedStyle(node); + if (style.display === "none" || style.visibility === "hidden" || parseFloat(style.opacity) === 0) { + return false; + } + node = node.parentElement ?? (node.getRootNode() instanceof ShadowRoot && node.getRootNode()); } - node = node.parentElement; } return true; @@ -94,6 +102,10 @@ class Cuprite { } path(node) { + if (node.getRootNode && node.getRootNode() instanceof ShadowRoot) { + return "(: Shadow DOM element - no XPath :)"; + }; + let nodes = [node]; let parent = node.parentNode; while (parent !== document && parent !== null) { @@ -280,7 +292,7 @@ class Cuprite { x -= frameOffset.left; y -= frameOffset.top; - let element = document.elementFromPoint(x, y); + let element = node.getRootNode().elementFromPoint(x, y); let el = element; while (el) { diff --git a/lib/capybara/cuprite/node.rb b/lib/capybara/cuprite/node.rb index 3efab57c..7d05ca5e 100644 --- a/lib/capybara/cuprite/node.rb +++ b/lib/capybara/cuprite/node.rb @@ -211,6 +211,13 @@ def path command(:path) end + def shadow_root + root = driver.evaluate_script <<~JS, self + arguments[0].shadowRoot + JS + root && self.class.new(driver, root.node) + end + def inspect %(#<#{self.class} @node=#{@node.inspect}>) end diff --git a/spec/features/session_spec.rb b/spec/features/session_spec.rb index db0e640d..55d2abaa 100644 --- a/spec/features/session_spec.rb +++ b/spec/features/session_spec.rb @@ -314,6 +314,24 @@ end end + describe "Node#shadow_root" do + it "produces error messages when failing" do + @session.visit("/with_shadow") + shadow_root = @session.find(:css, "#shadow_host").shadow_root + expect do + expect(shadow_root).to have_css("#shadow_content", text: "Not in the document") + end.to raise_error(/tag="#document-fragment"/) + end + + it "extends visibility check across shadow host boundary" do + @session.visit("/with_shadow") + shadow_root = @session.find(:css, "#shadow_host").shadow_root + expect(shadow_root).to have_css("a") + @session.execute_script %(document.getElementById("shadow_host").style.display = "none") + expect(shadow_root).to_not have_css("a") + 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 91e4577f..f55cb08d 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -66,8 +66,8 @@ module TestSessions node #visible? details non-summary descendants should be non-visible node #visible? works when details is toggled open and closed node #path reports when element in shadow dom - node #shadow_root node #set should submit single text input forms if ended with + node #shadow_root should produce error messages when failing #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