Skip to content

Commit d44b41b

Browse files
committed
Implement Shadow DOM API
This adds new method `Element#shadow_root` which returns an instance of `ShadowRoot` class. It can then be used to locate elements within this shadow root. Note that currently no drivers support WebDriver's Shadow DOM API. Consider this implementation a draft because there was no way to test it.
1 parent ee0193d commit d44b41b

File tree

9 files changed

+159
-28
lines changed

9 files changed

+159
-28
lines changed

rb/lib/selenium/webdriver/common.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,4 @@
7575
require 'selenium/webdriver/common/takes_screenshot'
7676
require 'selenium/webdriver/common/driver'
7777
require 'selenium/webdriver/common/element'
78+
require 'selenium/webdriver/common/shadow_root'

rb/lib/selenium/webdriver/common/driver.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,9 @@ def capabilities
297297
# @see SearchContext
298298
#
299299

300-
def ref; end
300+
def ref
301+
[:driver, nil]
302+
end
301303

302304
private
303305

rb/lib/selenium/webdriver/common/element.rb

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ def attribute(name)
143143
#
144144

145145
def dom_attribute(name)
146-
bridge.element_dom_attribute self, name
146+
bridge.element_dom_attribute @id, name
147147
end
148148

149149
#
@@ -157,7 +157,7 @@ def dom_attribute(name)
157157
#
158158

159159
def property(name)
160-
bridge.element_property self, name
160+
bridge.element_property @id, name
161161
end
162162

163163
#
@@ -167,7 +167,7 @@ def property(name)
167167
#
168168

169169
def aria_role
170-
bridge.element_aria_role self
170+
bridge.element_aria_role @id
171171
end
172172

173173
#
@@ -177,7 +177,7 @@ def aria_role
177177
#
178178

179179
def accessible_name
180-
bridge.element_aria_label self
180+
bridge.element_aria_label @id
181181
end
182182

183183
#
@@ -317,6 +317,16 @@ def size
317317
bridge.element_size @id
318318
end
319319

320+
#
321+
# Returns the shadow root of an element.
322+
#
323+
# @return [WebDriver::ShadowRoot]
324+
#
325+
326+
def shadow_root
327+
bridge.shadow_root @id
328+
end
329+
320330
#-------------------------------- sugar --------------------------------
321331

322332
#
@@ -336,14 +346,13 @@ def size
336346
#
337347
alias_method :[], :attribute
338348

339-
#
340-
# for SearchContext and execute_script
341349
#
342350
# @api private
351+
# @see SearchContext
343352
#
344353

345354
def ref
346-
@id
355+
[:element, @id]
347356
end
348357

349358
#
@@ -379,7 +388,7 @@ def selectable?
379388
end
380389

381390
def screenshot
382-
bridge.element_screenshot(self)
391+
bridge.element_screenshot(@id)
383392
end
384393
end # Element
385394
end # WebDriver

rb/lib/selenium/webdriver/common/error.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,12 @@ class UnknownCommandError < WebDriverError; end
6161

6262
class StaleElementReferenceError < WebDriverError; end
6363

64+
#
65+
# A command failed because the referenced shadow root is no longer attached to the DOM.
66+
#
67+
68+
class DetachedShadowRootError < WebDriverError; end
69+
6470
#
6571
# The target element is in an invalid state, rendering it impossible to interact with, for
6672
# example if you click a disabled element.
@@ -93,6 +99,12 @@ class TimeoutError < WebDriverError; end
9399

94100
class NoSuchWindowError < WebDriverError; end
95101

102+
#
103+
# The element does not have a shadow root.
104+
#
105+
106+
class NoSuchShadowRootError < WebDriverError; end
107+
96108
#
97109
# An illegal attempt was made to set a cookie under a different domain than the current page.
98110
#
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# frozen_string_literal: true
2+
3+
# Licensed to the Software Freedom Conservancy (SFC) under one
4+
# or more contributor license agreements. See the NOTICE file
5+
# distributed with this work for additional information
6+
# regarding copyright ownership. The SFC licenses this file
7+
# to you under the Apache License, Version 2.0 (the
8+
# "License"); you may not use this file except in compliance
9+
# with the License. You may obtain a copy of the License at
10+
#
11+
# http://www.apache.org/licenses/LICENSE-2.0
12+
#
13+
# Unless required by applicable law or agreed to in writing,
14+
# software distributed under the License is distributed on an
15+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
# KIND, either express or implied. See the License for the
17+
# specific language governing permissions and limitations
18+
# under the License.
19+
20+
module Selenium
21+
module WebDriver
22+
class ShadowRoot
23+
ROOT_KEY = 'shadow-6066-11e4-a52e-4f735466cecf'
24+
25+
include SearchContext
26+
27+
#
28+
# Creates a new shadow root
29+
#
30+
# @api private
31+
#
32+
33+
def initialize(bridge, id)
34+
@bridge = bridge
35+
@id = id
36+
end
37+
38+
def inspect
39+
format '#<%<class>s:0x%<hash>x id=%<id>s>', class: self.class, hash: hash * 2, id: @id.inspect
40+
end
41+
42+
def ==(other)
43+
other.is_a?(self.class) && ref == other.ref
44+
end
45+
alias_method :eql?, :==
46+
47+
def hash
48+
@id.hash ^ @bridge.hash
49+
end
50+
51+
#
52+
# @api private
53+
# @see SearchContext
54+
#
55+
56+
def ref
57+
[:shadow_root, @id]
58+
end
59+
60+
#
61+
# Convert to a ShadowRoot JSON Object for transmission over the wire.
62+
# @see https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol#basic-terms-and-concepts
63+
#
64+
# @api private
65+
#
66+
67+
def to_json(*)
68+
JSON.generate as_json
69+
end
70+
71+
#
72+
# For Rails 3 - http://jonathanjulian.com/2010/04/rails-to_json-or-as_json/
73+
#
74+
# @api private
75+
#
76+
77+
def as_json(*)
78+
{ROOT_KEY => @id}
79+
end
80+
81+
private
82+
83+
attr_reader :bridge
84+
85+
end # ShadowRoot
86+
end # WebDriver
87+
end # Selenium

rb/lib/selenium/webdriver/remote/bridge.rb

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ def screenshot
269269
end
270270

271271
def element_screenshot(element)
272-
execute :take_element_screenshot, id: element.ref
272+
execute :take_element_screenshot, id: element
273273
end
274274

275275
#
@@ -431,7 +431,7 @@ def clear_element(element)
431431
end
432432

433433
def submit_element(element)
434-
form = find_element_by('xpath', "./ancestor-or-self::form", element)
434+
form = find_element_by('xpath', "./ancestor-or-self::form", [:element, element])
435435
execute_script("var e = arguments[0].ownerDocument.createEvent('Event');" \
436436
"e.initEvent('submit', true, true);" \
437437
'if (arguments[0].dispatchEvent(e)) { arguments[0].submit() }', form.as_json)
@@ -451,19 +451,19 @@ def element_attribute(element, name)
451451
end
452452

453453
def element_dom_attribute(element, name)
454-
execute :get_element_attribute, id: element.ref, name: name
454+
execute :get_element_attribute, id: element, name: name
455455
end
456456

457457
def element_property(element, name)
458-
execute :get_element_property, id: element.ref, name: name
458+
execute :get_element_property, id: element, name: name
459459
end
460460

461461
def element_aria_role(element)
462-
execute :get_element_aria_role, id: element.ref
462+
execute :get_element_aria_role, id: element
463463
end
464464

465465
def element_aria_label(element)
466-
execute :get_element_aria_label, id: element.ref
466+
execute :get_element_aria_label, id: element
467467
end
468468

469469
def element_value(element)
@@ -524,34 +524,47 @@ def active_element
524524

525525
alias_method :switch_to_active_element, :active_element
526526

527-
def find_element_by(how, what, parent = nil)
527+
def find_element_by(how, what, parent_ref = [])
528528
how, what = convert_locator(how, what)
529529

530530
return execute_atom(:findElements, Support::RelativeLocator.new(what).as_json).first if how == 'relative'
531531

532-
id = if parent
533-
execute :find_child_element, {id: parent}, {using: how, value: what.to_s}
532+
parent_type, parent_id = parent_ref
533+
id = case parent_type
534+
when :element
535+
execute :find_child_element, {id: parent_id}, {using: how, value: what.to_s}
536+
when :shadow_root
537+
execute :find_shadow_child_element, {id: parent_id}, {using: how, value: what.to_s}
534538
else
535539
execute :find_element, {}, {using: how, value: what.to_s}
536540
end
537541

538542
Element.new self, element_id_from(id)
539543
end
540544

541-
def find_elements_by(how, what, parent = nil)
545+
def find_elements_by(how, what, parent_ref = [])
542546
how, what = convert_locator(how, what)
543547

544548
return execute_atom :findElements, Support::RelativeLocator.new(what).as_json if how == 'relative'
545549

546-
ids = if parent
547-
execute :find_child_elements, {id: parent}, {using: how, value: what.to_s}
550+
parent_type, parent_id = parent_ref
551+
ids = case parent_type
552+
when :element
553+
execute :find_child_elements, {id: parent_id}, {using: how, value: what.to_s}
554+
when :shadow_root
555+
execute :find_shadow_child_elements, {id: parent_id}, {using: how, value: what.to_s}
548556
else
549557
execute :find_elements, {}, {using: how, value: what.to_s}
550558
end
551559

552560
ids.map { |id| Element.new self, element_id_from(id) }
553561
end
554562

563+
def shadow_root(element)
564+
id = execute :get_element_shadow_root, id: element
565+
ShadowRoot.new self, shadow_root_id_from(id)
566+
end
567+
555568
private
556569

557570
#
@@ -599,7 +612,11 @@ def unwrap_script_result(arg)
599612
end
600613

601614
def element_id_from(id)
602-
id['ELEMENT'] || id['element-6066-11e4-a52e-4f735466cecf']
615+
id['ELEMENT'] || id[Element::ELEMENT_KEY]
616+
end
617+
618+
def shadow_root_id_from(id)
619+
id[ShadowRoot::ROOT_KEY]
603620
end
604621

605622
def prepare_capabilities_payload(capabilities)

rb/lib/selenium/webdriver/remote/commands.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,10 @@ class Bridge
7777
find_elements: [:post, 'session/:session_id/elements'],
7878
find_child_element: [:post, 'session/:session_id/element/:id/element'],
7979
find_child_elements: [:post, 'session/:session_id/element/:id/elements'],
80+
find_shadow_child_element: [:post, 'session/:session_id/shadow/:id/element'],
81+
find_shadow_child_elements: [:post, 'session/:session_id/shadow/:id/elements'],
8082
get_active_element: [:get, 'session/:session_id/element/active'],
83+
get_element_shadow_root: [:get, 'session/:session_id/element/:id/shadow'],
8184
is_element_selected: [:get, 'session/:session_id/element/:id/selected'],
8285
get_element_attribute: [:get, 'session/:session_id/element/:id/attribute/:name'],
8386
get_element_property: [:get, 'session/:session_id/element/:id/property/:name'],

rb/lib/selenium/webdriver/support/event_firing_bridge.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,15 +76,15 @@ def find_element_by(how, what, parent = nil)
7676
@delegate.find_element_by how, what, parent
7777
end
7878

79-
Element.new self, e.ref
79+
Element.new self, e.ref.last
8080
end
8181

8282
def find_elements_by(how, what, parent = nil)
8383
es = dispatch(:find, how, what, driver) do
8484
@delegate.find_elements_by(how, what, parent)
8585
end
8686

87-
es.map { |e| Element.new self, e.ref }
87+
es.map { |e| Element.new self, e.ref.last }
8888
end
8989

9090
def execute_script(script, *args)

rb/spec/unit/selenium/webdriver/support/event_firing_spec.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,20 +62,20 @@ module Support
6262
context 'finding elements' do
6363
it 'fires events for find_element' do
6464
expect(listener).to receive(:before_find).with('id', 'foo', instance_of(Driver))
65-
allow(bridge).to receive(:find_element_by).with('id', 'foo', nil).and_return(element)
65+
allow(bridge).to receive(:find_element_by).with('id', 'foo', [:driver, nil]).and_return(element)
6666
expect(listener).to receive(:after_find).with('id', 'foo', instance_of(Driver))
6767

6868
driver.find_element(id: 'foo')
69-
expect(bridge).to have_received(:find_element_by).with('id', 'foo', nil)
69+
expect(bridge).to have_received(:find_element_by).with('id', 'foo', [:driver, nil])
7070
end
7171

7272
it 'fires events for find_elements' do
7373
expect(listener).to receive(:before_find).with('class name', 'foo', instance_of(Driver))
74-
allow(bridge).to receive(:find_elements_by).with('class name', 'foo', nil).and_return([element])
74+
allow(bridge).to receive(:find_elements_by).with('class name', 'foo', [:driver, nil]).and_return([element])
7575
expect(listener).to receive(:after_find).with('class name', 'foo', instance_of(Driver))
7676

7777
driver.find_elements(class: 'foo')
78-
expect(bridge).to have_received(:find_elements_by).with('class name', 'foo', nil)
78+
expect(bridge).to have_received(:find_elements_by).with('class name', 'foo', [:driver, nil])
7979
end
8080
end
8181

0 commit comments

Comments
 (0)