diff --git a/rb/lib/selenium/webdriver/common.rb b/rb/lib/selenium/webdriver/common.rb index 4d2aad2c8e68e..627be8197edc3 100644 --- a/rb/lib/selenium/webdriver/common.rb +++ b/rb/lib/selenium/webdriver/common.rb @@ -75,3 +75,4 @@ require 'selenium/webdriver/common/takes_screenshot' require 'selenium/webdriver/common/driver' require 'selenium/webdriver/common/element' +require 'selenium/webdriver/common/shadow_root' diff --git a/rb/lib/selenium/webdriver/common/driver.rb b/rb/lib/selenium/webdriver/common/driver.rb index 16ac0b7e49d21..c23fd5e22f81d 100644 --- a/rb/lib/selenium/webdriver/common/driver.rb +++ b/rb/lib/selenium/webdriver/common/driver.rb @@ -297,7 +297,9 @@ def capabilities # @see SearchContext # - def ref; end + def ref + [:driver, nil] + end private diff --git a/rb/lib/selenium/webdriver/common/element.rb b/rb/lib/selenium/webdriver/common/element.rb index 44bdf4a0f4d8f..7356ee4a01fdb 100644 --- a/rb/lib/selenium/webdriver/common/element.rb +++ b/rb/lib/selenium/webdriver/common/element.rb @@ -143,7 +143,7 @@ def attribute(name) # def dom_attribute(name) - bridge.element_dom_attribute self, name + bridge.element_dom_attribute @id, name end # @@ -157,7 +157,7 @@ def dom_attribute(name) # def property(name) - bridge.element_property self, name + bridge.element_property @id, name end # @@ -167,7 +167,7 @@ def property(name) # def aria_role - bridge.element_aria_role self + bridge.element_aria_role @id end # @@ -177,7 +177,7 @@ def aria_role # def accessible_name - bridge.element_aria_label self + bridge.element_aria_label @id end # @@ -317,6 +317,16 @@ def size bridge.element_size @id end + # + # Returns the shadow root of an element. + # + # @return [WebDriver::ShadowRoot] + # + + def shadow_root + bridge.shadow_root @id + end + #-------------------------------- sugar -------------------------------- # @@ -336,14 +346,13 @@ def size # alias_method :[], :attribute - # - # for SearchContext and execute_script # # @api private + # @see SearchContext # def ref - @id + [:element, @id] end # @@ -379,7 +388,7 @@ def selectable? end def screenshot - bridge.element_screenshot(self) + bridge.element_screenshot(@id) end end # Element end # WebDriver diff --git a/rb/lib/selenium/webdriver/common/error.rb b/rb/lib/selenium/webdriver/common/error.rb index be95063c92fac..93fc532700837 100644 --- a/rb/lib/selenium/webdriver/common/error.rb +++ b/rb/lib/selenium/webdriver/common/error.rb @@ -61,6 +61,12 @@ class UnknownCommandError < WebDriverError; end class StaleElementReferenceError < WebDriverError; end + # + # A command failed because the referenced shadow root is no longer attached to the DOM. + # + + class DetachedShadowRootError < WebDriverError; end + # # The target element is in an invalid state, rendering it impossible to interact with, for # example if you click a disabled element. @@ -93,6 +99,12 @@ class TimeoutError < WebDriverError; end class NoSuchWindowError < WebDriverError; end + # + # The element does not have a shadow root. + # + + class NoSuchShadowRootError < WebDriverError; end + # # An illegal attempt was made to set a cookie under a different domain than the current page. # diff --git a/rb/lib/selenium/webdriver/common/shadow_root.rb b/rb/lib/selenium/webdriver/common/shadow_root.rb new file mode 100644 index 0000000000000..af0519a20fc4f --- /dev/null +++ b/rb/lib/selenium/webdriver/common/shadow_root.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +module Selenium + module WebDriver + class ShadowRoot + ROOT_KEY = 'shadow-6066-11e4-a52e-4f735466cecf' + + include SearchContext + + # + # Creates a new shadow root + # + # @api private + # + + def initialize(bridge, id) + @bridge = bridge + @id = id + end + + def inspect + format '#<%s:0x%x id=%s>', class: self.class, hash: hash * 2, id: @id.inspect + end + + def ==(other) + other.is_a?(self.class) && ref == other.ref + end + alias_method :eql?, :== + + def hash + @id.hash ^ @bridge.hash + end + + # + # @api private + # @see SearchContext + # + + def ref + [:shadow_root, @id] + end + + # + # Convert to a ShadowRoot JSON Object for transmission over the wire. + # @see https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol#basic-terms-and-concepts + # + # @api private + # + + def to_json(*) + JSON.generate as_json + end + + # + # For Rails 3 - http://jonathanjulian.com/2010/04/rails-to_json-or-as_json/ + # + # @api private + # + + def as_json(*) + {ROOT_KEY => @id} + end + + private + + attr_reader :bridge + + end # ShadowRoot + end # WebDriver +end # Selenium diff --git a/rb/lib/selenium/webdriver/remote/bridge.rb b/rb/lib/selenium/webdriver/remote/bridge.rb index e64a3fc4057d7..f5b58e2fd42f6 100644 --- a/rb/lib/selenium/webdriver/remote/bridge.rb +++ b/rb/lib/selenium/webdriver/remote/bridge.rb @@ -269,7 +269,7 @@ def screenshot end def element_screenshot(element) - execute :take_element_screenshot, id: element.ref + execute :take_element_screenshot, id: element end # @@ -431,7 +431,7 @@ def clear_element(element) end def submit_element(element) - form = find_element_by('xpath', "./ancestor-or-self::form", element) + form = find_element_by('xpath', "./ancestor-or-self::form", [:element, element]) execute_script("var e = arguments[0].ownerDocument.createEvent('Event');" \ "e.initEvent('submit', true, true);" \ 'if (arguments[0].dispatchEvent(e)) { arguments[0].submit() }', form.as_json) @@ -451,19 +451,19 @@ def element_attribute(element, name) end def element_dom_attribute(element, name) - execute :get_element_attribute, id: element.ref, name: name + execute :get_element_attribute, id: element, name: name end def element_property(element, name) - execute :get_element_property, id: element.ref, name: name + execute :get_element_property, id: element, name: name end def element_aria_role(element) - execute :get_element_aria_role, id: element.ref + execute :get_element_aria_role, id: element end def element_aria_label(element) - execute :get_element_aria_label, id: element.ref + execute :get_element_aria_label, id: element end def element_value(element) @@ -524,13 +524,17 @@ def active_element alias_method :switch_to_active_element, :active_element - def find_element_by(how, what, parent = nil) + def find_element_by(how, what, parent_ref = []) how, what = convert_locator(how, what) return execute_atom(:findElements, Support::RelativeLocator.new(what).as_json).first if how == 'relative' - id = if parent - execute :find_child_element, {id: parent}, {using: how, value: what.to_s} + parent_type, parent_id = parent_ref + id = case parent_type + when :element + execute :find_child_element, {id: parent_id}, {using: how, value: what.to_s} + when :shadow_root + execute :find_shadow_child_element, {id: parent_id}, {using: how, value: what.to_s} else execute :find_element, {}, {using: how, value: what.to_s} end @@ -538,13 +542,17 @@ def find_element_by(how, what, parent = nil) Element.new self, element_id_from(id) end - def find_elements_by(how, what, parent = nil) + def find_elements_by(how, what, parent_ref = []) how, what = convert_locator(how, what) return execute_atom :findElements, Support::RelativeLocator.new(what).as_json if how == 'relative' - ids = if parent - execute :find_child_elements, {id: parent}, {using: how, value: what.to_s} + parent_type, parent_id = parent_ref + ids = case parent_type + when :element + execute :find_child_elements, {id: parent_id}, {using: how, value: what.to_s} + when :shadow_root + execute :find_shadow_child_elements, {id: parent_id}, {using: how, value: what.to_s} else execute :find_elements, {}, {using: how, value: what.to_s} end @@ -552,6 +560,11 @@ def find_elements_by(how, what, parent = nil) ids.map { |id| Element.new self, element_id_from(id) } end + def shadow_root(element) + id = execute :get_element_shadow_root, id: element + ShadowRoot.new self, shadow_root_id_from(id) + end + private # @@ -599,7 +612,11 @@ def unwrap_script_result(arg) end def element_id_from(id) - id['ELEMENT'] || id['element-6066-11e4-a52e-4f735466cecf'] + id['ELEMENT'] || id[Element::ELEMENT_KEY] + end + + def shadow_root_id_from(id) + id[ShadowRoot::ROOT_KEY] end def prepare_capabilities_payload(capabilities) diff --git a/rb/lib/selenium/webdriver/remote/commands.rb b/rb/lib/selenium/webdriver/remote/commands.rb index 3dd7843b20624..3338170533d7a 100644 --- a/rb/lib/selenium/webdriver/remote/commands.rb +++ b/rb/lib/selenium/webdriver/remote/commands.rb @@ -77,7 +77,10 @@ class Bridge find_elements: [:post, 'session/:session_id/elements'], find_child_element: [:post, 'session/:session_id/element/:id/element'], find_child_elements: [:post, 'session/:session_id/element/:id/elements'], + find_shadow_child_element: [:post, 'session/:session_id/shadow/:id/element'], + find_shadow_child_elements: [:post, 'session/:session_id/shadow/:id/elements'], get_active_element: [:get, 'session/:session_id/element/active'], + get_element_shadow_root: [:get, 'session/:session_id/element/:id/shadow'], is_element_selected: [:get, 'session/:session_id/element/:id/selected'], get_element_attribute: [:get, 'session/:session_id/element/:id/attribute/:name'], get_element_property: [:get, 'session/:session_id/element/:id/property/:name'], diff --git a/rb/lib/selenium/webdriver/support/event_firing_bridge.rb b/rb/lib/selenium/webdriver/support/event_firing_bridge.rb index 1767f430d4387..136ca869e78d0 100644 --- a/rb/lib/selenium/webdriver/support/event_firing_bridge.rb +++ b/rb/lib/selenium/webdriver/support/event_firing_bridge.rb @@ -76,7 +76,7 @@ def find_element_by(how, what, parent = nil) @delegate.find_element_by how, what, parent end - Element.new self, e.ref + Element.new self, e.ref.last end def find_elements_by(how, what, parent = nil) @@ -84,7 +84,7 @@ def find_elements_by(how, what, parent = nil) @delegate.find_elements_by(how, what, parent) end - es.map { |e| Element.new self, e.ref } + es.map { |e| Element.new self, e.ref.last } end def execute_script(script, *args) diff --git a/rb/spec/unit/selenium/webdriver/support/event_firing_spec.rb b/rb/spec/unit/selenium/webdriver/support/event_firing_spec.rb index b08b1b0b4dc64..7e71420d4fae9 100644 --- a/rb/spec/unit/selenium/webdriver/support/event_firing_spec.rb +++ b/rb/spec/unit/selenium/webdriver/support/event_firing_spec.rb @@ -62,20 +62,20 @@ module Support context 'finding elements' do it 'fires events for find_element' do expect(listener).to receive(:before_find).with('id', 'foo', instance_of(Driver)) - allow(bridge).to receive(:find_element_by).with('id', 'foo', nil).and_return(element) + allow(bridge).to receive(:find_element_by).with('id', 'foo', [:driver, nil]).and_return(element) expect(listener).to receive(:after_find).with('id', 'foo', instance_of(Driver)) driver.find_element(id: 'foo') - expect(bridge).to have_received(:find_element_by).with('id', 'foo', nil) + expect(bridge).to have_received(:find_element_by).with('id', 'foo', [:driver, nil]) end it 'fires events for find_elements' do expect(listener).to receive(:before_find).with('class name', 'foo', instance_of(Driver)) - allow(bridge).to receive(:find_elements_by).with('class name', 'foo', nil).and_return([element]) + allow(bridge).to receive(:find_elements_by).with('class name', 'foo', [:driver, nil]).and_return([element]) expect(listener).to receive(:after_find).with('class name', 'foo', instance_of(Driver)) driver.find_elements(class: 'foo') - expect(bridge).to have_received(:find_elements_by).with('class name', 'foo', nil) + expect(bridge).to have_received(:find_elements_by).with('class name', 'foo', [:driver, nil]) end end