Class: Capybara::Chrome::Browser

Inherits:
Object
  • Object
show all
Includes:
Debug, Service
Defined in:
lib/capybara/chrome/browser.rb

Constant Summary collapse

RECOGNIZED_SCHEME =
/^https?/

Constants included from Service

Service::CHROME_ARGS

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Service

#chrome_args, #chrome_path, #chrome_pid, #chrome_running?, find_available_port, #os, #restart_chrome, #start_chrome, #stop_chrome, #wait_for_chrome

Methods included from Debug

#debug, #info

Constructor Details

#initialize(driver, host: "127.0.0.1", port: nil) ⇒ Browser

Returns a new instance of Browser.



13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# File 'lib/capybara/chrome/browser.rb', line 13

def initialize(driver, host: "127.0.0.1", port: nil)
  @driver = driver
  @chrome_pid = nil
  @chrome_host = host
  @chrome_port = port || find_available_port(host)
  @remote = nil
  @responses = {}
  @last_response = nil
  @frame_mutex = Mutex.new
  @network_mutex = Mutex.new
  @console_messages = []
  @error_messages = []
  @js_dialog_handlers = Hash.new {|h,key| h[key] = []}
  @unrecognized_scheme_requests = []
  @loader_ids = []
  @loaded_loaders = {}
end

Instance Attribute Details

#chrome_portObject

Returns the value of attribute chrome_port.



12
13
14
# File 'lib/capybara/chrome/browser.rb', line 12

def chrome_port
  @chrome_port
end

#console_messagesObject (readonly)

Returns the value of attribute console_messages.



11
12
13
# File 'lib/capybara/chrome/browser.rb', line 11

def console_messages
  @console_messages
end

#driverObject (readonly)

Returns the value of attribute driver.



11
12
13
# File 'lib/capybara/chrome/browser.rb', line 11

def driver
  @driver
end

#error_messagesObject (readonly)

Returns the value of attribute error_messages.



11
12
13
# File 'lib/capybara/chrome/browser.rb', line 11

def error_messages
  @error_messages
end

#remoteObject (readonly)

Returns the value of attribute remote.



11
12
13
# File 'lib/capybara/chrome/browser.rb', line 11

def remote
  @remote
end

Instance Method Details

#accept_modal(type, text_or_options = nil, options = {}, &block) ⇒ Object



314
315
316
317
# File 'lib/capybara/chrome/browser.rb', line 314

def accept_modal(type, text_or_options=nil, options={}, &block)
  @js_dialog_handlers[type.to_s] << {accept: true}
  block.call if block
end

#after_remote_startObject



261
262
263
264
265
266
267
268
269
# File 'lib/capybara/chrome/browser.rb', line 261

def after_remote_start
  track_network_events
  enable_console_log
  # enable_lifecycle_events
  enable_js_dialog
  enable_script_debug
  enable_network_interception
  set_viewport(width: 1680, height: 1050)
end

#current_urlObject



141
142
143
# File 'lib/capybara/chrome/browser.rb', line 141

def current_url
  document_root["documentURL"]
end

#dismiss_modal(type, text_or_options = nil, options = {}, &block) ⇒ Object



319
320
321
322
323
# File 'lib/capybara/chrome/browser.rb', line 319

def dismiss_modal(type, text_or_options=nil, options={}, &block)
  @js_dialog_handlers[type.to_s] << {accept: false}
  block.call if block
  debug [type, text_or_options, options]
end

#document_rootObject



166
167
168
# File 'lib/capybara/chrome/browser.rb', line 166

def document_root
  @document_root = get_document["root"]
end

#enable_console_logObject



325
326
327
328
329
330
331
332
333
334
335
# File 'lib/capybara/chrome/browser.rb', line 325

def enable_console_log
  remote.send_cmd! "Console.enable"
  remote.on "Console.messageAdded" do |params|
    str = "#{params["message"]["source"]}:#{params["message"]["line"]} #{params["message"]["text"]}"
    if params["message"]["level"] == "error"
      @error_messages << str
    else
      @console_messages << str
    end
  end
end

#enable_js_dialogObject



296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
# File 'lib/capybara/chrome/browser.rb', line 296

def enable_js_dialog
  remote.on("Page.javascriptDialogOpening") do |params|
    debug ["Dialog Opening", params]
    handler = @js_dialog_handlers[params["type"]].last
    if handler
      debug ["have handler", handler]
      args = {accept: handler[:accept]}
      args.merge!(promptText: handler[:prompt_text]) if params[:type] == "prompt"
      remote.send_cmd("Page.handleJavaScriptDialog", args)
      @js_dialog_handlers[params["type"]].delete(params["type"].size - 1)
    else
      puts "WARNING: Accepting unhandled modal. Use #accept_modal or #dismiss_modal to handle this modal properly."
      puts "Details: #{params.inspect}"
      remote.send_cmd("Page.handleJavaScriptDialog", accept: true)
    end
  end
end

#enable_lifecycle_eventsObject



337
338
339
340
341
342
343
344
345
346
347
# File 'lib/capybara/chrome/browser.rb', line 337

def enable_lifecycle_events
  remote.send_cmd! "Page.setLifecycleEventsEnabled", enabled: true
  remote.on("Page.lifecycleEvent") do |params|
    if params["name"] == "init"
      @loader_ids.push(params["loaderId"])
    elsif params["name"] == "load"
      @loaded_loaders[params["loaderId"]] = true
    elsif params["name"] == "networkIdle"
    end
  end
end

#enable_network_interceptionObject



275
276
277
278
279
280
281
282
283
284
285
286
# File 'lib/capybara/chrome/browser.rb', line 275

def enable_network_interception
  remote.send_cmd! "Network.setRequestInterception", patterns: [{urlPattern: "*"}]
  remote.on("Network.requestIntercepted") do |params|
    if Capybara::Chrome.configuration.block_url?(params["request"]["url"]) || (Capybara::Chrome.configuration.skip_image_loading? && params["resourceType"] == "Image")
      # p ["blocking", params["request"]["url"]]
      remote.send_cmd "Network.continueInterceptedRequest", interceptionId: params["interceptionId"], errorReason: "ConnectionRefused"
    else
      # p ["allowing", params["request"]["url"]]
      remote.send_cmd "Network.continueInterceptedRequest", interceptionId: params["interceptionId"]
    end
  end
end

#enable_script_debugObject



288
289
290
291
292
293
294
# File 'lib/capybara/chrome/browser.rb', line 288

def enable_script_debug
  remote.send_cmd "Debugger.enable"
  remote.on("Debugger.scriptFailedToParse") do |params|
    puts "\n\n!!! ERROR: SCRIPT FAILED TO PARSE !!!\n\n"
    p params
  end
end

#evaluate_async_script(script, *args) ⇒ Object



61
62
63
# File 'lib/capybara/chrome/browser.rb', line 61

def evaluate_async_script(script, *args)
  raise "i dunno"
end

#evaluate_script(script, *args) ⇒ Object



36
37
38
39
# File 'lib/capybara/chrome/browser.rb', line 36

def evaluate_script(script, *args)
  val = execute_script(script, *args)
  val["result"]["value"]
end

#execute_script(script, *args) ⇒ Object



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/capybara/chrome/browser.rb', line 41

def execute_script(script, *args)
  default_options = {expression: script, includeCommandLineAPI: true, awaitPromise: true}
  opts = args[0].respond_to?(:merge) ? args[0] : {}
  opts = default_options.merge(opts)
  val = remote.send_cmd "Runtime.evaluate", opts
  debug script, val
  if details = val["exceptionDetails"]
    if details["exception"]["className"] == "NodeNotFoundError"
      raise Capybara::ElementNotFound
    else
      raise JSException.new(details["exception"].inspect)
    end
  end
  val
end

#execute_script!(script, options = {}) ⇒ Object



57
58
59
# File 'lib/capybara/chrome/browser.rb', line 57

def execute_script!(script, options={})
  remote.send_cmd!("Runtime.evaluate", {expression: script, includeCommandLineAPI: true}.merge(options))
end

#find_css(query) ⇒ Object



184
185
186
187
188
# File 'lib/capybara/chrome/browser.rb', line 184

def find_css(query)
  debug query
  nodes = query_selector_all(query)
  nodes
end

#find_xpath(query, index = nil) ⇒ Object



233
234
235
236
237
238
239
240
241
242
243
# File 'lib/capybara/chrome/browser.rb', line 233

def find_xpath(query, index=nil)
  wait_for_load
  query = query.dup
  query.gsub!('"', '\"')
  result = if index
             evaluate_script %( window.ChromeRemoteHelper && ChromeRemoteHelper.findXPathWithin(#{index}, "#{query}") )
           else
             evaluate_script %( window.ChromeRemoteHelper && ChromeRemoteHelper.findXPath("#{query}") )
           end
  get_node_results result
end

#get_documentObject



162
163
164
# File 'lib/capybara/chrome/browser.rb', line 162

def get_document
  val = remote.send_cmd "DOM.getDocument"
end

#get_node_results(result) ⇒ Object



222
223
224
225
226
227
228
229
230
231
# File 'lib/capybara/chrome/browser.rb', line 222

def get_node_results(result)
  vals = result.split(",")
  nodes = []
  if vals.any?
    nodes = result.split(",").map do |id|
      Node.new driver, self, id.to_i
    end
  end
  nodes
end

#has_body?(resp) ⇒ Boolean

Returns:

  • (Boolean)


150
151
152
153
154
155
156
157
158
159
160
# File 'lib/capybara/chrome/browser.rb', line 150

def has_body?(resp)
  debug
  if resp["root"] && resp["root"]["children"]
    resp["root"]["children"].detect do |child|
      next unless child.has_key?("children")
      child["children"].detect do |grandchild|
        grandchild["localName"] == "body"
      end
    end
  end
end

#header(key, value) ⇒ Object



369
370
371
372
373
374
375
# File 'lib/capybara/chrome/browser.rb', line 369

def header(key, value)
  if key.downcase == "user-agent"
    remote.send_cmd!("Network.setUserAgentOverride", userAgent: value)
  else
    remote.send_cmd!("Network.setExtraHTTPHeaders", headers: {key => value})
  end
end

#htmlObject



178
179
180
181
182
# File 'lib/capybara/chrome/browser.rb', line 178

def html
  val = root_node.html
  debug "root", val.size
  val
end

#last_responseObject



124
125
126
# File 'lib/capybara/chrome/browser.rb', line 124

def last_response
  @last_response
end

#last_response_or_errObject



128
129
130
131
132
133
134
135
# File 'lib/capybara/chrome/browser.rb', line 128

def last_response_or_err
  loop do
    break last_response if last_response
    remote.read_and_process(0.01)
  end
rescue Timeout::Error
  raise Capybara::ExpectationNotMet
end

#loader_loaded?(loader_id) ⇒ Boolean

Returns:

  • (Boolean)


349
350
351
# File 'lib/capybara/chrome/browser.rb', line 349

def loader_loaded?(loader_id)
  @loaded_loaders[loader_id]
end

#query_selector_all(query, index = nil) ⇒ Object



190
191
192
193
194
195
196
197
198
199
200
# File 'lib/capybara/chrome/browser.rb', line 190

def query_selector_all(query, index=nil)
  wait_for_load
  query = query.dup
  query.gsub!('"', '\"')
  result = if index
             evaluate_script %( window.ChromeRemoteHelper && ChromeRemoteHelper.findCssWithin(#{index}, "#{query}") )
           else
             evaluate_script %( window.ChromeRemoteHelper && ChromeRemoteHelper.findCss("#{query}") )
           end
  get_node_results result
end

#render(path, width = nil, height = nil) ⇒ Object



359
360
361
362
363
364
365
366
367
# File 'lib/capybara/chrome/browser.rb', line 359

def render(path, width=nil, height=nil)
  response = remote.send_cmd "Page.getLayoutMetrics"
  width = response["contentSize"]["width"]
  height = response["contentSize"]["height"]
  response = remote.send_cmd "Page.captureScreenshot", clip: {width: width, height: height, x: 0, y: 0, scale: 1}
  File.open path, "wb" do |f|
    f.write Base64.decode64(response["data"])
  end
end

#request_nodes(object_id) ⇒ Object

object_id represents a script that returned of an array of nodes

Raises:

  • (Capybara::ExpectationNotMet)


203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
# File 'lib/capybara/chrome/browser.rb', line 203

def request_nodes(object_id)
  nodes = []
  results = remote.send_cmd("Runtime.getProperties", objectId: object_id, ownProperties: true)
  raise Capybara::ExpectationNotMet if results.nil?
  results["result"].each do |prop|
    if prop["value"]["subtype"] == "node"
      lookup = remote.send_cmd("DOM.requestNode", objectId: prop["value"]["objectId"])
      raise Capybara::ExpectationNotMet if lookup.nil?
      id = lookup["nodeId"]
      if id == 0
        raise Capybara::ExpectationNotMet
      else
        nodes << Node.new(driver, self, id)
      end
    end
  end
  nodes
end

#resetObject



377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
# File 'lib/capybara/chrome/browser.rb', line 377

def reset
  unset_root_node
  @responses.clear
  @last_response = nil
  @console_messages.clear
  @error_messages.clear
  @js_dialog_handlers.clear
  @unrecognized_scheme_requests.clear
  remote.reset
  remote.send_cmd! "Network.clearBrowserCookies"
  remote.send_cmd! "Runtime.discardConsoleEntries"
  remote.send_cmd! "Network.setExtraHTTPHeaders", headers: {}
  remote.send_cmd! "Network.setUserAgentOverride", userAgent: ""
  visit "about:blank"
end

#root_nodeObject



170
171
172
# File 'lib/capybara/chrome/browser.rb', line 170

def root_node
  @root_node = find_css("html")[0]
end

#save_screenshot(path, options = {}) ⇒ Object



353
354
355
356
357
# File 'lib/capybara/chrome/browser.rb', line 353

def save_screenshot(path, options={})
  options[:width]  ||= 1000
  options[:height] ||= 10
  render path, options[:width], options[:height]
end

#set_viewport(width:, height:, device_scale_factor: 1, mobile: false) ⇒ Object



271
272
273
# File 'lib/capybara/chrome/browser.rb', line 271

def set_viewport(width:, height:, device_scale_factor: 1, mobile: false)
  remote.send_cmd!("Emulation.setDeviceMetricsOverride", width: width, height: height, deviceScaleFactor: device_scale_factor, mobile: mobile)
end

#startObject



31
32
33
34
# File 'lib/capybara/chrome/browser.rb', line 31

def start
  start_chrome
  start_remote
end

#start_remoteObject



254
255
256
257
258
259
# File 'lib/capybara/chrome/browser.rb', line 254

def start_remote
  # @remote = ChromeRemoteClient.new(::ChromeRemote.send(:get_ws_url, {host: "localhost", port: @chrome_port}))
  @remote = RDPClient.new chrome_host: @chrome_host, chrome_port: @chrome_port, browser: self
  remote.start
  after_remote_start
end

#status_codeObject



137
138
139
# File 'lib/capybara/chrome/browser.rb', line 137

def status_code
  last_response_or_err["status"]
end

#titleObject



245
246
247
248
249
250
251
252
# File 'lib/capybara/chrome/browser.rb', line 245

def title
  nodes = find_xpath("/html/head/title")
  if nodes && nodes.first
    nodes[0].text
  else
    ""
  end
end

#track_network_eventsObject



99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/capybara/chrome/browser.rb', line 99

def track_network_events
  return if @track_network_events
  remote.on("Network.requestWillBeSent") do |req|
    if req["type"] == "Document"
      if !RECOGNIZED_SCHEME.match req["request"]["url"]
        puts "ADDING SCHEME"
        @unrecognized_scheme_requests << req["request"]["url"]
      else
        @last_response = nil
      end
    end
  end
  remote.on("Network.responseReceived") do |params|
    debug params["response"]["url"], params["requestId"], params["loaderId"], params["type"]
    if params["type"] == "Document"
      @responses[params["requestId"]] = params["response"]
      @last_response = params["response"]
    end
  end
  remote.on("Network.loadingFailed") do |params|
    debug ["loadingFailed", params]
  end
  @track_network_events = true
end

#unrecognized_scheme_requestsObject



145
146
147
148
# File 'lib/capybara/chrome/browser.rb', line 145

def unrecognized_scheme_requests
  remote.read_and_process(1)
  @unrecognized_scheme_requests
end

#unset_root_nodeObject



174
175
176
# File 'lib/capybara/chrome/browser.rb', line 174

def unset_root_node
  @root_node = nil
end

#visit(path, attributes = {}) ⇒ Object



73
74
75
76
77
78
79
80
81
82
# File 'lib/capybara/chrome/browser.rb', line 73

def visit(path, attributes={})
  uri = URI.parse(path)
  if uri.scheme.nil?
    uri.host = Capybara.current_session.server.host unless uri.host.present?
    uri.port = Capybara.current_session.server.port unless uri.port.present?
  end
  debug ["visit #{uri}"]
  @last_navigate = remote.send_cmd "Page.navigate", url: uri.to_s, transitionType: "typed"
  wait_for_load
end

#wait_for_loadObject



65
66
67
68
69
70
71
# File 'lib/capybara/chrome/browser.rb', line 65

def wait_for_load
  remote.send_cmd "DOM.getDocument"
  loop do
    val = evaluate_script %(window.ChromeRemotePageLoaded), awaitPromise: false
    break val if val
  end
end

#with_retry(n: 10, timeout: 0.05, &block) ⇒ Object



84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/capybara/chrome/browser.rb', line 84

def with_retry(n:10, timeout: 0.05, &block)
  skip_retry = [Errno::EPIPE, EOFError, ResponseTimeoutError]
  begin
    block.call
  rescue => e
    if n == 0 || skip_retry.detect {|klass| e.instance_of?(klass)}
      raise e
    else
      puts "RETRYING #{e}"
      sleep timeout
      with_retry(n: n-1, timeout: timeout, &block)
    end
  end
end