capybara-chrome 0.1.22
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.travis.yml +5 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +110 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/capybara-chrome.gemspec +31 -0
- data/lib/capybara-chrome.rb +1 -0
- data/lib/capybara/chrome.rb +56 -0
- data/lib/capybara/chrome/browser.rb +393 -0
- data/lib/capybara/chrome/configuration.rb +77 -0
- data/lib/capybara/chrome/debug.rb +17 -0
- data/lib/capybara/chrome/driver.rb +38 -0
- data/lib/capybara/chrome/errors.rb +15 -0
- data/lib/capybara/chrome/node.rb +343 -0
- data/lib/capybara/chrome/rdp_client.rb +204 -0
- data/lib/capybara/chrome/rdp_socket.rb +29 -0
- data/lib/capybara/chrome/rdp_web_socket_client.rb +51 -0
- data/lib/capybara/chrome/repeat_timeout.rb +15 -0
- data/lib/capybara/chrome/service.rb +109 -0
- data/lib/capybara/chrome/version.rb +5 -0
- data/lib/chrome_remote_helper.js +340 -0
- metadata +154 -0
@@ -0,0 +1,77 @@
|
|
1
|
+
module Capybara::Chrome
|
2
|
+
class Configuration
|
3
|
+
|
4
|
+
DEFAULT_ALLOWED_URLS = [
|
5
|
+
%r(.*127\.0\.0\.1),
|
6
|
+
%r(.*localhost),
|
7
|
+
"data:*,*"
|
8
|
+
].freeze
|
9
|
+
DEFAULT_MAX_WAIT_TIME = 10
|
10
|
+
DEFAULT_DOWNLOAD_PATH = "/tmp"
|
11
|
+
# set Capybara::Chrome::Configuration.chrome_port = 9222 for easy debugging
|
12
|
+
DEFAULT_CHROME_PORT = nil
|
13
|
+
DEFAULT_TRAP_INTERRUPT = true
|
14
|
+
DEFAULT_DEBUG = false
|
15
|
+
|
16
|
+
attr_accessor :max_wait_time, :download_path, :chrome_port, :trap_interrupt, :debug
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
@allowed_urls = DEFAULT_ALLOWED_URLS.dup
|
20
|
+
@blocked_urls = []
|
21
|
+
@max_wait_time = DEFAULT_MAX_WAIT_TIME
|
22
|
+
@download_path = DEFAULT_DOWNLOAD_PATH
|
23
|
+
@chrome_port = DEFAULT_CHROME_PORT
|
24
|
+
@trap_interrupt = DEFAULT_TRAP_INTERRUPT
|
25
|
+
@debug = DEFAULT_DEBUG
|
26
|
+
end
|
27
|
+
|
28
|
+
def block_unknown_urls
|
29
|
+
@block_unknown_urls = true
|
30
|
+
end
|
31
|
+
|
32
|
+
def allow_unknown_urls
|
33
|
+
allow_url(/.*/)
|
34
|
+
end
|
35
|
+
|
36
|
+
def re_url(url)
|
37
|
+
url.is_a?(Regexp) ? url : Regexp.new(Regexp.escape(url))
|
38
|
+
end
|
39
|
+
|
40
|
+
def allow_url(url)
|
41
|
+
@allowed_urls << re_url(url)
|
42
|
+
end
|
43
|
+
|
44
|
+
def block_url(url)
|
45
|
+
@blocked_urls << re_url(url)
|
46
|
+
end
|
47
|
+
|
48
|
+
def url_match?(pattern, url)
|
49
|
+
pattern === url
|
50
|
+
end
|
51
|
+
|
52
|
+
def url_allowed?(url)
|
53
|
+
@allowed_urls.detect {|pattern| url_match?(pattern, url)}
|
54
|
+
end
|
55
|
+
|
56
|
+
def block_url?(url)
|
57
|
+
if url_allowed?(url)
|
58
|
+
false
|
59
|
+
else
|
60
|
+
@block_unknown_urls || @blocked_urls.detect {|pattern| url_match?(pattern, url)}
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def skip_image_loading
|
65
|
+
@skip_image_loading = true
|
66
|
+
end
|
67
|
+
|
68
|
+
def skip_image_loading?
|
69
|
+
@skip_image_loading
|
70
|
+
end
|
71
|
+
|
72
|
+
def trap_interrupt?
|
73
|
+
@trap_interrupt
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Capybara
|
2
|
+
module Chrome
|
3
|
+
module Debug
|
4
|
+
def debug(*args)
|
5
|
+
if Capybara::Chrome.configuration.debug
|
6
|
+
p [caller_locations(1,1)[0].label, *args, Time.now.to_i]
|
7
|
+
end
|
8
|
+
return args[0]
|
9
|
+
end
|
10
|
+
|
11
|
+
def info(*args)
|
12
|
+
p [caller_locations(1,1)[0].label, *args, Time.now.to_i]
|
13
|
+
return args[0]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Capybara::Chrome
|
2
|
+
class Driver < Capybara::Driver::Base
|
3
|
+
extend Forwardable
|
4
|
+
|
5
|
+
def initialize(app, options={})
|
6
|
+
@app = app
|
7
|
+
@options = options
|
8
|
+
@session = nil
|
9
|
+
end
|
10
|
+
|
11
|
+
def_delegators :browser, :visit, :find_css, :html, :evaluate_script, :evaluate_async_script, :execute_script, :status_code, :save_screenshot, :render, :current_url, :header, :console_messages, :error_messages, :dismiss_modal, :accept_modal, :title, :unrecognized_scheme_requests, :wait_for_load
|
12
|
+
|
13
|
+
def browser
|
14
|
+
@browser ||= Browser.new(self, port: @options[:port])
|
15
|
+
end
|
16
|
+
|
17
|
+
def find_xpath(query)
|
18
|
+
nodes = browser.find_xpath(query)
|
19
|
+
end
|
20
|
+
|
21
|
+
def start
|
22
|
+
browser.start
|
23
|
+
end
|
24
|
+
|
25
|
+
def needs_server?
|
26
|
+
true
|
27
|
+
end
|
28
|
+
|
29
|
+
def wait?
|
30
|
+
true
|
31
|
+
end
|
32
|
+
|
33
|
+
def reset!
|
34
|
+
browser.reset
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,343 @@
|
|
1
|
+
module Capybara::Chrome
|
2
|
+
class Node < ::Capybara::Driver::Node
|
3
|
+
include Debug
|
4
|
+
attr_reader :browser, :id
|
5
|
+
|
6
|
+
KEY_DATA = Hash.new do |h, k|
|
7
|
+
h[k] = {text: k.to_s}
|
8
|
+
end.merge!({
|
9
|
+
cancel: { key_code: 3, code: "Abort", key: "Cancel"},
|
10
|
+
help: { key_code: 6, code: "Help", key: "Help"},
|
11
|
+
backspace: { key_code: 8, code: "Backspace", key: "Backspace"},
|
12
|
+
tab: { key_code: 9, code: "Tab", key: "Tab"},
|
13
|
+
delete: { key_code: 46, code: "Delete", key: "Delete"},
|
14
|
+
home: { key_code: 36, code: "Home", key: "Home"},
|
15
|
+
end: { key_code: 35, code: "End", key: "End"},
|
16
|
+
left: { key_code: 37, code: "ArrowLeft", key: "ArrowLeft"},
|
17
|
+
right: { key_code: 39, code: "ArrowRight", key: "ArrowRight"},
|
18
|
+
up: { key_code: 38, code: "ArrowUp", key: "ArrowUp"},
|
19
|
+
down: { key_code: 40, code: "ArrowDown", key: "ArrowDown"},
|
20
|
+
return: { key_code: 13, code: "Enter", key: "Enter"},
|
21
|
+
enter: { key_code: 13, code: "Enter", key: "Enter"},
|
22
|
+
"\r" => { key_code: 13, code: "Enter", key: "Enter", text: "\r"},
|
23
|
+
"\n" => { key_code: 13, code: "Enter", key: "Enter", text: "\r"},
|
24
|
+
space: { key_code: 32, code: "Space", key: " ", text: " "},
|
25
|
+
shift: { key_code: 16, code: "Shift", key: "ShiftLeft", location: 1},
|
26
|
+
control: { key_code: 17, code: "ControlLeft", key: "Control", text: "\r"},
|
27
|
+
alt: { key_code: 18, code: "AltLeft", key: "Alt", location: 1},
|
28
|
+
meta: { key_code: 91, code: "MetaLeft", key: "Meta", location: 1},
|
29
|
+
})
|
30
|
+
|
31
|
+
def initialize(driver, browser, id)
|
32
|
+
raise "hell" if id == 0
|
33
|
+
@driver = driver
|
34
|
+
@browser = browser
|
35
|
+
@id = id
|
36
|
+
@mouse_x = 0
|
37
|
+
@mouse_y = 0
|
38
|
+
end
|
39
|
+
|
40
|
+
def html
|
41
|
+
browser.evaluate_script %( ChromeRemoteHelper.waitDOMContentLoaded(); )
|
42
|
+
browser.evaluate_script %( ChromeRemoteHelper.onSelfValue(#{id}, "outerHTML") )
|
43
|
+
end
|
44
|
+
|
45
|
+
def all_text
|
46
|
+
browser.evaluate_script %( ChromeRemoteHelper.waitDOMContentLoaded(); )
|
47
|
+
text = browser.evaluate_script %( ChromeRemoteHelper.onSelfValue(#{id}, "textContent") )
|
48
|
+
if Capybara::VERSION.to_f < 3.0
|
49
|
+
Capybara::Helpers.normalize_whitespace(text)
|
50
|
+
else
|
51
|
+
text.gsub(/[\u200b\u200e\u200f]/, '')
|
52
|
+
.gsub(/[\ \n\f\t\v\u2028\u2029]+/, ' ')
|
53
|
+
.gsub(/\A[[:space:]&&[^\u00a0]]+/, "")
|
54
|
+
.gsub(/[[:space:]&&[^\u00a0]]+\z/, "")
|
55
|
+
.tr("\u00a0", ' ')
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def visible_text
|
60
|
+
raw_text.to_s.gsub(/\ +/, ' ')
|
61
|
+
.gsub(/[\ \n]*\n[\ \n]*/, "\n")
|
62
|
+
.gsub(/\A[[:space:]&&[^\u00a0]]+/, "")
|
63
|
+
.gsub(/[[:space:]&&[^\u00a0]]+\z/, "")
|
64
|
+
.tr("\u00a0", ' ')
|
65
|
+
end
|
66
|
+
alias text visible_text
|
67
|
+
|
68
|
+
def raw_text
|
69
|
+
browser.evaluate_script %( ChromeRemoteHelper.nodeText(#{id}) )
|
70
|
+
end
|
71
|
+
|
72
|
+
def visible?
|
73
|
+
browser.evaluate_script %( ChromeRemoteHelper.nodeVisible(#{id}) )
|
74
|
+
end
|
75
|
+
|
76
|
+
def is_connected?
|
77
|
+
# on_self_value %( return this.isConnected )
|
78
|
+
val = browser.evaluate_script %( ChromeRemoteHelper.onSelfValue(#{id}, "isConnected") )
|
79
|
+
end
|
80
|
+
|
81
|
+
def get_dimensions
|
82
|
+
browser.wait_for_load
|
83
|
+
val = browser.evaluate_script %( ChromeRemoteHelper.nodeGetDimensions(#{id}) )
|
84
|
+
val = JSON.parse(val) rescue {}
|
85
|
+
val
|
86
|
+
end
|
87
|
+
|
88
|
+
def expect_node_at_position(x,y)
|
89
|
+
in_position = browser.evaluate_script %( ChromeRemoteHelper.nodeIsNodeAtPosition(#{id}, {relativeX: #{x}, relativeY: #{y}}) )
|
90
|
+
if !in_position
|
91
|
+
raise "Element '<#{tag_name}>' not at expected position: #{x},#{y}"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def move_mouse(x, y, steps: 1)
|
96
|
+
if steps >= 1
|
97
|
+
(0..x).step(steps).each do |dx|
|
98
|
+
send_cmd! "Input.dispatchMouseEvent", type: "mouseMoved", x: dx, y: 0
|
99
|
+
end
|
100
|
+
(0..y).step(steps).each do |dy|
|
101
|
+
send_cmd! "Input.dispatchMouseEvent", type: "mouseMoved", x: x, y: dy
|
102
|
+
end
|
103
|
+
end
|
104
|
+
send_cmd! "Input.dispatchMouseEvent", type: "mouseMoved", x: x, y: y
|
105
|
+
end
|
106
|
+
|
107
|
+
def click
|
108
|
+
browser.with_retry do
|
109
|
+
on_self("this.scrollIntoViewIfNeeded();");
|
110
|
+
dim = get_dimensions
|
111
|
+
if dim["width"] == 0 && dim["height"] == 0
|
112
|
+
puts "DIM IS 0"
|
113
|
+
puts html
|
114
|
+
raise Capybara::ElementNotFound
|
115
|
+
end
|
116
|
+
xd = [1, dim["width"]/2, dim["width"]/3]
|
117
|
+
yd = [1, dim["height"]/2, dim["height"]/3]
|
118
|
+
strategy = rand(0..xd.size-1)
|
119
|
+
cx = (dim["x"] + xd[strategy]).floor
|
120
|
+
cy = (dim["y"] + yd[strategy]).floor
|
121
|
+
move_mouse(cx, cy, steps: 0)
|
122
|
+
expect_node_at_position(cx, cy)
|
123
|
+
send_cmd! "Input.dispatchMouseEvent", type: "mousePressed", x: cx, y: cy, clickCount: 1, button: "left"
|
124
|
+
send_cmd! "Input.dispatchMouseEvent", type: "mouseReleased", x: cx, y: cy, clickCount: 1, button: "left"
|
125
|
+
vv = browser.wait_for_load
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def find_css(query)
|
130
|
+
browser.query_selector_all(query, id)
|
131
|
+
end
|
132
|
+
|
133
|
+
def find_xpath(query)
|
134
|
+
browser.find_xpath query, id
|
135
|
+
end
|
136
|
+
|
137
|
+
def path
|
138
|
+
browser.evaluate_script %( ChromeRemoteHelper.nodePathForNode(#{id}) )
|
139
|
+
end
|
140
|
+
alias get_xpath path
|
141
|
+
|
142
|
+
def disabled?
|
143
|
+
val = browser.evaluate_script %( ChromeRemoteHelper.onSelfValue(#{id}, "disabled") )
|
144
|
+
debug val
|
145
|
+
val
|
146
|
+
end
|
147
|
+
|
148
|
+
TEXT_TYPES = %w(date email number password search tel text textarea url)
|
149
|
+
def set(value, options={})
|
150
|
+
value = value.to_s.gsub('"', '\"')
|
151
|
+
type = browser.evaluate_script %( ChromeRemoteHelper.nodeSetType(#{id}) )
|
152
|
+
|
153
|
+
if type == "file"
|
154
|
+
send_cmd "DOM.setFileInputFiles", files: [value.to_s], nodeId: node_id
|
155
|
+
elsif TEXT_TYPES.include?(type)
|
156
|
+
script = "this.value = ''; this.focus();"
|
157
|
+
if value.blank?
|
158
|
+
script << %(ChromeRemoteHelper.dispatchEvent(this, "change"))
|
159
|
+
end
|
160
|
+
on_self script
|
161
|
+
type_string(value.to_s)
|
162
|
+
else
|
163
|
+
browser.evaluate_script %( ChromeRemoteHelper.nodeSet(#{id}, "#{value}", "#{type}") )
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def node_id
|
168
|
+
browser.get_document
|
169
|
+
val = browser.execute_script %(ChromeRemoteHelper.onSelf(#{id}, "return this;"))
|
170
|
+
send_cmd("DOM.requestNode", objectId: val["result"]["objectId"])["nodeId"]
|
171
|
+
end
|
172
|
+
|
173
|
+
def type_string(string)
|
174
|
+
ary = string.chars
|
175
|
+
ary.each do |char|
|
176
|
+
char.tr!("\n", "\r")
|
177
|
+
send_cmd! "Input.dispatchKeyEvent", {type: "keyDown", text: char}
|
178
|
+
send_cmd! "Input.dispatchKeyEvent", {type: "keyUp"}
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def send_keys(*keys)
|
183
|
+
keys.each do |key|
|
184
|
+
if key.is_a? Array
|
185
|
+
mods, new_keys = get_modifiers(key)
|
186
|
+
new_keys.each do |k|
|
187
|
+
send_key_data(get_key_data(k), modifiers: mods)
|
188
|
+
end
|
189
|
+
else
|
190
|
+
send_key_data(get_key_data(key))
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
def get_key_data(key)
|
196
|
+
KEY_DATA[key]
|
197
|
+
end
|
198
|
+
|
199
|
+
def send_key_data(data, modifiers: 0)
|
200
|
+
type = data[:text] ? "keyDown" : "rawKeyDown"
|
201
|
+
send_cmd! "Input.dispatchKeyEvent", {type: type, text: data[:text].to_s, windowsVirtualKeyCode: data[:key_code].to_i, code: data[:code].to_s, key: data[:key].to_s, modifiers: modifiers}
|
202
|
+
send_cmd! "Input.dispatchKeyEvent", {type: "keyUp", modifiers: modifiers}
|
203
|
+
end
|
204
|
+
|
205
|
+
def get_modifiers(ary)
|
206
|
+
mods = []
|
207
|
+
keys = []
|
208
|
+
ary.each do |k|
|
209
|
+
case k
|
210
|
+
when :alt
|
211
|
+
mods << 1
|
212
|
+
when :control
|
213
|
+
mods << 2
|
214
|
+
when :meta, :command
|
215
|
+
mods << 4
|
216
|
+
when :shift
|
217
|
+
mods << 8
|
218
|
+
else
|
219
|
+
keys << k
|
220
|
+
end
|
221
|
+
end
|
222
|
+
[mods.inject(&:|), keys]
|
223
|
+
end
|
224
|
+
|
225
|
+
def focus
|
226
|
+
val = browser.evaluate_script %( ChromeRemoteHelper.onSelfValue(#{id}, "focus") )
|
227
|
+
end
|
228
|
+
|
229
|
+
def value(*args)
|
230
|
+
debug args
|
231
|
+
val = browser.evaluate_script %( ChromeRemoteHelper.onSelfValue(#{id}, "value") )
|
232
|
+
end
|
233
|
+
|
234
|
+
def checked?
|
235
|
+
val = browser.evaluate_script %( ChromeRemoteHelper.onSelfValue(#{id}, "checked") )
|
236
|
+
end
|
237
|
+
|
238
|
+
def selected?
|
239
|
+
val = browser.evaluate_script %( ChromeRemoteHelper.onSelfValue(#{id}, "selected") )
|
240
|
+
end
|
241
|
+
|
242
|
+
def disabled?
|
243
|
+
val = browser.evaluate_script %( ChromeRemoteHelper.onSelfValue(#{id}, "disabled") )
|
244
|
+
end
|
245
|
+
|
246
|
+
def select_option(*args)
|
247
|
+
on_self_value %(
|
248
|
+
if (this.disabled)
|
249
|
+
return;
|
250
|
+
|
251
|
+
var selectNode = this.parentNode;
|
252
|
+
if (selectNode.tagName == "OPTGROUP")
|
253
|
+
selectNode = selectNode.parentNode;
|
254
|
+
|
255
|
+
ChromeRemoteHelper.dispatchEvent(selectNode, "mousedown");
|
256
|
+
selectNode.focus();
|
257
|
+
ChromeRemoteHelper.dispatchEvent(selectNode, "input");
|
258
|
+
|
259
|
+
if (!this.selected) {
|
260
|
+
this.selected = true;
|
261
|
+
ChromeRemoteHelper.dispatchEvent(selectNode, "change");
|
262
|
+
}
|
263
|
+
|
264
|
+
ChromeRemoteHelper.dispatchEvent(selectNode, "mouseup");
|
265
|
+
ChromeRemoteHelper.dispatchEvent(selectNode, "click");
|
266
|
+
)
|
267
|
+
end
|
268
|
+
|
269
|
+
def trigger(event_name)
|
270
|
+
on_self %( ChromeRemoteHelper.dispatchEvent(this, '#{event_name}') )
|
271
|
+
end
|
272
|
+
|
273
|
+
def method_missing(method, *args)
|
274
|
+
debug ["method missing", method, args]
|
275
|
+
raise "method missing #{method} #{args.inspect}"
|
276
|
+
end
|
277
|
+
|
278
|
+
def [](attr)
|
279
|
+
on_self_value %(return this.getAttribute("#{attr}") )
|
280
|
+
end
|
281
|
+
|
282
|
+
def node_description
|
283
|
+
# return @node_description if @node_description
|
284
|
+
@node_description = send_cmd("DOM.describeNode", nodeId: id)
|
285
|
+
debug ["node_desc", @node_description, id]
|
286
|
+
if @node_description.nil?
|
287
|
+
raise Capybara::ExpectationNotMet
|
288
|
+
end
|
289
|
+
@node_description
|
290
|
+
end
|
291
|
+
|
292
|
+
def tag_name
|
293
|
+
# node_description["node"]["localName"]
|
294
|
+
on_self_value("return this.tagName.toLowerCase()")
|
295
|
+
end
|
296
|
+
alias local_name tag_name
|
297
|
+
|
298
|
+
def send_cmd(cmd, args={})
|
299
|
+
browser.remote.send_cmd(cmd, args)
|
300
|
+
end
|
301
|
+
def send_cmd!(cmd, args={})
|
302
|
+
browser.remote.send_cmd!(cmd, args)
|
303
|
+
end
|
304
|
+
|
305
|
+
def on_self_value(function_body, options={})
|
306
|
+
function_body = function_body.gsub('"', '\"').gsub(/\s+/, " ")
|
307
|
+
browser.evaluate_script %(ChromeRemoteHelper.onSelf(#{id}, "#{function_body}"))
|
308
|
+
end
|
309
|
+
|
310
|
+
def on_self(function_body, options={})
|
311
|
+
function_body = function_body.gsub('"', '\"').gsub(/\s+/, " ")
|
312
|
+
browser.evaluate_script %( window.ChromeRemoteHelper && ChromeRemoteHelper.waitDOMContentLoaded(); )
|
313
|
+
browser.evaluate_script %(window.ChromeRemoteHelper && ChromeRemoteHelper.onSelf(#{id}, "#{function_body}"))
|
314
|
+
end
|
315
|
+
|
316
|
+
def on_self!(function_body, options={})
|
317
|
+
function_body = function_body.gsub('"', '\"').gsub(/\s+/, " ")
|
318
|
+
|
319
|
+
browser.evaluate_script %( ChromeRemoteHelper.waitDOMContentLoaded(); )
|
320
|
+
|
321
|
+
browser.execute_script! %(ChromeRemoteHelper.onSelf(#{id}, "#{function_body}"))
|
322
|
+
end
|
323
|
+
|
324
|
+
def remote_object_id
|
325
|
+
remote_object["object"]["objectId"]
|
326
|
+
end
|
327
|
+
|
328
|
+
def remote_object
|
329
|
+
return @remote_object if @remote_object
|
330
|
+
@remote_object = send_cmd("DOM.resolveNode", nodeId: id)
|
331
|
+
debug @remote_object, id
|
332
|
+
if @remote_object.nil?
|
333
|
+
raise Capybara::ExpectationNotMet
|
334
|
+
end
|
335
|
+
@remote_object
|
336
|
+
end
|
337
|
+
|
338
|
+
def request_node(remote_object_id)
|
339
|
+
@request_node ||= send_cmd("DOM.requestNode", objectId: remote_object_id)
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
end
|