capybara-chrome 0.1.22

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 8541435afe87267dc82259cc09f0c1ca7484588a
4
+ data.tar.gz: b80fe620c9549091dab72dfe7e62a505dbbbe83b
5
+ SHA512:
6
+ metadata.gz: c90f45a45e4f3f96ce46c48956b73afd65c01bb39b4b3830c9905fb3b101f4ccbe31f5806b581b74cc6d533ad9495b8f655a9af1270fa45e867a1c200dd118c1
7
+ data.tar.gz: c678cd3da799220fb700e873562a8586ec850b86460049c92cbb65b28e2921fc2a5e0cc511cfb647614e8406f4f90a3f50b6c2c0b4094e4ac1abce1e25f189bf
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.2.2
5
+ before_install: gem install bundler -v 1.16.1
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in capybara-chrome.gemspec
6
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 Sandro Turriate
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,110 @@
1
+ # Capybara::Chrome
2
+
3
+ Use [Capybara](https://github.com/teamcapybara/capybara) to drive Chrome in headless mode via the [debugging protocol](https://chromedevtools.github.io/devtools-protocol/).
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'capybara-chrome'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install capybara-chrome
20
+
21
+ ## Usage
22
+
23
+ ```ruby
24
+ Capybara.javascript_driver = :chrome
25
+ Capybara::Chrome.configuration.chrome_port = 9222 # optional
26
+ ```
27
+
28
+ The standard port for the debugging protocol is `9222`. Visit `localhost:9222` in a Chrome tab to watch the tests execute. Note, the port will be random by default.
29
+
30
+ ### Using thin
31
+
32
+ I like using [thin](https://github.com/macournoyer/thin) instead of WEBrick as
33
+ my Capybara server. It's a little faster, gives me greater control of logging,
34
+ shows errors, and allows me to disable signal handlers.
35
+ Below are my settings:
36
+
37
+ ```ruby
38
+ Capybara.register_server :thin do |app, port, host|
39
+ require "rack/handler/thin"
40
+ Thin::Logging.silent = false
41
+ # Thin::Logging.debug = true # uncomment to see request and response codes
42
+ # Thin::Logging.trace = true # uncomment to see full requests/responses
43
+ Rack::Handler::Thin.run(app, Host: host, Port: port, signals: false)
44
+ end
45
+ Capybara.server = :thin
46
+ ```
47
+
48
+ ### Debugging
49
+
50
+ Use `byebug` instead of `binding.pry` when debugging a test. The Pry debugger tends to hang when `visit` is called.
51
+
52
+ ### Using the repl
53
+
54
+ You can use the capybara-chrome browser without providing a rack app. This can be helpful in debugging.
55
+
56
+ ```
57
+ [2] pry(main)> driver = Capybara::Chrome::Driver.new(nil, port:9222); driver.start; browser = driver.browser
58
+ [3] pry(main)> browser.visit "http://google.com"
59
+ => true
60
+ [4] pry(main)> browser.current_url
61
+ => "https://www.google.com/?gws_rd=ssl"
62
+ [5] pry(main)>
63
+
64
+ ```
65
+
66
+ Further, you can run a local netcat server and point the capybara-chrome browser to it to see the entire request that's being sent.
67
+
68
+ Terminal one contains the browser:
69
+
70
+ ```
71
+ [1] pry(main)> driver = Capybara::Chrome::Driver.new(nil, port:9222); driver.start; browser = driver.browser
72
+ [2] pry(main)> browser.header "x-foo", "bar"
73
+ [3] pry(main)> browser.visit "http://localhost:8000"
74
+ ```
75
+
76
+ Terminal two prints the request
77
+
78
+ ```
79
+ $ while true; do { echo -e "HTTP/1.1 200 OK \r\n"; echo "hi"; } | nc -l 8000; done
80
+ GET / HTTP/1.1
81
+ Host: localhost:8000
82
+ Connection: keep-alive
83
+ Upgrade-Insecure-Requests: 1
84
+ User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/68.0.3440.106 Safari/537.36
85
+ x-foo: bar
86
+ Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
87
+ Accept-Encoding: gzip, deflate
88
+ ```
89
+
90
+ ### Videos
91
+
92
+ A video showing capybara-chrome running in a browser tab
93
+
94
+ [![Preview of visualizing capybara-chrome](http://img.youtube.com/vi/SLmkx5z-lAA/0.jpg)](http://www.youtube.com/watch?v=SLmkx5z-lAA)
95
+
96
+ A video demonstrating debugging an rspec test with `byebug`
97
+
98
+ [![Preview of debugging capybara-chrome](http://img.youtube.com/vi/McEQG9YEAdE/0.jpg)](http://www.youtube.com/watch?v=McEQG9YEAdE)
99
+
100
+ A video showing capybara-chrome running against a netcat backend
101
+
102
+ [![Preview of debugging capybara-chrome with netcat](http://img.youtube.com/vi/B1__LeLyXBo/0.jpg)](http://www.youtube.com/watch?v=B1__LeLyXBo)
103
+
104
+ ## Contributing
105
+
106
+ Bug reports and pull requests are welcome on GitHub at https://github.com/carezone/capybara-chrome.
107
+
108
+ ## License
109
+
110
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
7
+
8
+ task :tag do
9
+ puts `git tag #{Capybara::Chrome::VERSION} && git tag | sort -V | tail`
10
+ end
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "capybara/chrome"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,31 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "capybara/chrome/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "capybara-chrome"
8
+ spec.version = Capybara::Chrome::VERSION
9
+ spec.authors = ["Sandro Turriate"]
10
+ spec.email = ["sandro.turriate@gmail.com"]
11
+
12
+ spec.summary = %q{Chrome driver for capybara using remote debugging protocol.}
13
+ spec.description = %q{Chrome driver for capybara using remote debugging protocol.}
14
+ spec.homepage = "https://github.com/carezone/capybara-chrome"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
+ f.match(%r{^(test|spec|features)/})
19
+ end
20
+ spec.bindir = "exe"
21
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
+ spec.require_paths = ["lib"]
23
+
24
+ spec.add_runtime_dependency("capybara")
25
+ spec.add_runtime_dependency("json")
26
+ spec.add_runtime_dependency("websocket-driver")
27
+
28
+ spec.add_development_dependency "bundler", "~> 1.16"
29
+ spec.add_development_dependency "rake", "~> 10.0"
30
+ spec.add_development_dependency "rspec", "~> 3.0"
31
+ end
@@ -0,0 +1 @@
1
+ require "capybara/chrome"
@@ -0,0 +1,56 @@
1
+ require "capybara/chrome/version"
2
+ require "capybara"
3
+ require "websocket/driver"
4
+
5
+ module Capybara
6
+ module Chrome
7
+ require "capybara/chrome/errors"
8
+ autoload :Configuration, "capybara/chrome/configuration"
9
+
10
+ autoload :Driver, "capybara/chrome/driver"
11
+ autoload :Browser, "capybara/chrome/browser"
12
+ autoload :Node, "capybara/chrome/node"
13
+ autoload :Service, "capybara/chrome/service"
14
+
15
+ autoload :RDPClient, "capybara/chrome/rdp_client"
16
+ autoload :RDPWebSocketClient, "capybara/chrome/rdp_web_socket_client"
17
+ autoload :RDPSocket, "capybara/chrome/rdp_socket"
18
+
19
+ autoload :Debug, "capybara/chrome/debug"
20
+
21
+ def self.configure(reset: false)
22
+ @configuration = nil if reset
23
+ yield configuration
24
+ end
25
+
26
+ def self.configuration
27
+ @configuration ||= Configuration.new
28
+ end
29
+
30
+ def self.wants_to_quit
31
+ @wants_to_quit
32
+ end
33
+
34
+ def self.trap_interrupt
35
+ previous_interrupt = trap("INT") do
36
+ @wants_to_quit = true
37
+ if previous_interrupt.respond_to?(:call)
38
+ previous_interrupt.call
39
+ else
40
+ exit 1
41
+ end
42
+ end
43
+ end
44
+
45
+ Capybara.register_driver :chrome do |app|
46
+ driver = Capybara::Chrome::Driver.new(app, port: configuration.chrome_port)
47
+ if driver.browser.chrome_running?
48
+ driver = Capybara::Chrome::Driver.new(app)
49
+ end
50
+ driver.start
51
+ Capybara::Chrome.trap_interrupt if Capybara::Chrome.configuration.trap_interrupt?
52
+ driver
53
+ end
54
+
55
+ end
56
+ end
@@ -0,0 +1,393 @@
1
+ module Capybara::Chrome
2
+
3
+ class Browser
4
+ require 'rbconfig'
5
+
6
+ RECOGNIZED_SCHEME = /^https?/
7
+
8
+ include Debug
9
+ include Service
10
+
11
+ attr_reader :remote, :driver, :console_messages, :error_messages
12
+ attr_accessor :chrome_port
13
+ def initialize(driver, host: "127.0.0.1", port: nil)
14
+ @driver = driver
15
+ @chrome_pid = nil
16
+ @chrome_host = host
17
+ @chrome_port = port || find_available_port(host)
18
+ @remote = nil
19
+ @responses = {}
20
+ @last_response = nil
21
+ @frame_mutex = Mutex.new
22
+ @network_mutex = Mutex.new
23
+ @console_messages = []
24
+ @error_messages = []
25
+ @js_dialog_handlers = Hash.new {|h,key| h[key] = []}
26
+ @unrecognized_scheme_requests = []
27
+ @loader_ids = []
28
+ @loaded_loaders = {}
29
+ end
30
+
31
+ def start
32
+ start_chrome
33
+ start_remote
34
+ end
35
+
36
+ def evaluate_script(script, *args)
37
+ val = execute_script(script, *args)
38
+ val["result"]["value"]
39
+ end
40
+
41
+ def execute_script(script, *args)
42
+ default_options = {expression: script, includeCommandLineAPI: true, awaitPromise: true}
43
+ opts = args[0].respond_to?(:merge) ? args[0] : {}
44
+ opts = default_options.merge(opts)
45
+ val = remote.send_cmd "Runtime.evaluate", opts
46
+ debug script, val
47
+ if details = val["exceptionDetails"]
48
+ if details["exception"]["className"] == "NodeNotFoundError"
49
+ raise Capybara::ElementNotFound
50
+ else
51
+ raise JSException.new(details["exception"].inspect)
52
+ end
53
+ end
54
+ val
55
+ end
56
+
57
+ def execute_script!(script, options={})
58
+ remote.send_cmd!("Runtime.evaluate", {expression: script, includeCommandLineAPI: true}.merge(options))
59
+ end
60
+
61
+ def evaluate_async_script(script, *args)
62
+ raise "i dunno"
63
+ end
64
+
65
+ def wait_for_load
66
+ remote.send_cmd "DOM.getDocument"
67
+ loop do
68
+ val = evaluate_script %(window.ChromeRemotePageLoaded), awaitPromise: false
69
+ break val if val
70
+ end
71
+ end
72
+
73
+ def visit(path, attributes={})
74
+ uri = URI.parse(path)
75
+ if uri.scheme.nil?
76
+ uri.host = Capybara.current_session.server.host unless uri.host.present?
77
+ uri.port = Capybara.current_session.server.port unless uri.port.present?
78
+ end
79
+ debug ["visit #{uri}"]
80
+ @last_navigate = remote.send_cmd "Page.navigate", url: uri.to_s, transitionType: "typed"
81
+ wait_for_load
82
+ end
83
+
84
+ def with_retry(n:10, timeout: 0.05, &block)
85
+ skip_retry = [Errno::EPIPE, EOFError, ResponseTimeoutError]
86
+ begin
87
+ block.call
88
+ rescue => e
89
+ if n == 0 || skip_retry.detect {|klass| e.instance_of?(klass)}
90
+ raise e
91
+ else
92
+ puts "RETRYING #{e}"
93
+ sleep timeout
94
+ with_retry(n: n-1, timeout: timeout, &block)
95
+ end
96
+ end
97
+ end
98
+
99
+ def track_network_events
100
+ return if @track_network_events
101
+ remote.on("Network.requestWillBeSent") do |req|
102
+ if req["type"] == "Document"
103
+ if !RECOGNIZED_SCHEME.match req["request"]["url"]
104
+ puts "ADDING SCHEME"
105
+ @unrecognized_scheme_requests << req["request"]["url"]
106
+ else
107
+ @last_response = nil
108
+ end
109
+ end
110
+ end
111
+ remote.on("Network.responseReceived") do |params|
112
+ debug params["response"]["url"], params["requestId"], params["loaderId"], params["type"]
113
+ if params["type"] == "Document"
114
+ @responses[params["requestId"]] = params["response"]
115
+ @last_response = params["response"]
116
+ end
117
+ end
118
+ remote.on("Network.loadingFailed") do |params|
119
+ debug ["loadingFailed", params]
120
+ end
121
+ @track_network_events = true
122
+ end
123
+
124
+ def last_response
125
+ @last_response
126
+ end
127
+
128
+ def last_response_or_err
129
+ loop do
130
+ break last_response if last_response
131
+ remote.read_and_process(0.01)
132
+ end
133
+ rescue Timeout::Error
134
+ raise Capybara::ExpectationNotMet
135
+ end
136
+
137
+ def status_code
138
+ last_response_or_err["status"]
139
+ end
140
+
141
+ def current_url
142
+ document_root["documentURL"]
143
+ end
144
+
145
+ def unrecognized_scheme_requests
146
+ remote.read_and_process(1)
147
+ @unrecognized_scheme_requests
148
+ end
149
+
150
+ def has_body?(resp)
151
+ debug
152
+ if resp["root"] && resp["root"]["children"]
153
+ resp["root"]["children"].detect do |child|
154
+ next unless child.has_key?("children")
155
+ child["children"].detect do |grandchild|
156
+ grandchild["localName"] == "body"
157
+ end
158
+ end
159
+ end
160
+ end
161
+
162
+ def get_document
163
+ val = remote.send_cmd "DOM.getDocument"
164
+ end
165
+
166
+ def document_root
167
+ @document_root = get_document["root"]
168
+ end
169
+
170
+ def root_node
171
+ @root_node = find_css("html")[0]
172
+ end
173
+
174
+ def unset_root_node
175
+ @root_node = nil
176
+ end
177
+
178
+ def html
179
+ val = root_node.html
180
+ debug "root", val.size
181
+ val
182
+ end
183
+
184
+ def find_css(query)
185
+ debug query
186
+ nodes = query_selector_all(query)
187
+ nodes
188
+ end
189
+
190
+ def query_selector_all(query, index=nil)
191
+ wait_for_load
192
+ query = query.dup
193
+ query.gsub!('"', '\"')
194
+ result = if index
195
+ evaluate_script %( window.ChromeRemoteHelper && ChromeRemoteHelper.findCssWithin(#{index}, "#{query}") )
196
+ else
197
+ evaluate_script %( window.ChromeRemoteHelper && ChromeRemoteHelper.findCss("#{query}") )
198
+ end
199
+ get_node_results result
200
+ end
201
+
202
+ # object_id represents a script that returned of an array of nodes
203
+ def request_nodes(object_id)
204
+ nodes = []
205
+ results = remote.send_cmd("Runtime.getProperties", objectId: object_id, ownProperties: true)
206
+ raise Capybara::ExpectationNotMet if results.nil?
207
+ results["result"].each do |prop|
208
+ if prop["value"]["subtype"] == "node"
209
+ lookup = remote.send_cmd("DOM.requestNode", objectId: prop["value"]["objectId"])
210
+ raise Capybara::ExpectationNotMet if lookup.nil?
211
+ id = lookup["nodeId"]
212
+ if id == 0
213
+ raise Capybara::ExpectationNotMet
214
+ else
215
+ nodes << Node.new(driver, self, id)
216
+ end
217
+ end
218
+ end
219
+ nodes
220
+ end
221
+
222
+ def get_node_results(result)
223
+ vals = result.split(",")
224
+ nodes = []
225
+ if vals.any?
226
+ nodes = result.split(",").map do |id|
227
+ Node.new driver, self, id.to_i
228
+ end
229
+ end
230
+ nodes
231
+ end
232
+
233
+ def find_xpath(query, index=nil)
234
+ wait_for_load
235
+ query = query.dup
236
+ query.gsub!('"', '\"')
237
+ result = if index
238
+ evaluate_script %( window.ChromeRemoteHelper && ChromeRemoteHelper.findXPathWithin(#{index}, "#{query}") )
239
+ else
240
+ evaluate_script %( window.ChromeRemoteHelper && ChromeRemoteHelper.findXPath("#{query}") )
241
+ end
242
+ get_node_results result
243
+ end
244
+
245
+ def title
246
+ nodes = find_xpath("/html/head/title")
247
+ if nodes && nodes.first
248
+ nodes[0].text
249
+ else
250
+ ""
251
+ end
252
+ end
253
+
254
+ def start_remote
255
+ # @remote = ChromeRemoteClient.new(::ChromeRemote.send(:get_ws_url, {host: "localhost", port: @chrome_port}))
256
+ @remote = RDPClient.new chrome_host: @chrome_host, chrome_port: @chrome_port, browser: self
257
+ remote.start
258
+ after_remote_start
259
+ end
260
+
261
+ def after_remote_start
262
+ track_network_events
263
+ enable_console_log
264
+ # enable_lifecycle_events
265
+ enable_js_dialog
266
+ enable_script_debug
267
+ enable_network_interception
268
+ set_viewport(width: 1680, height: 1050)
269
+ end
270
+
271
+ def set_viewport(width:, height:, device_scale_factor: 1, mobile: false)
272
+ remote.send_cmd!("Emulation.setDeviceMetricsOverride", width: width, height: height, deviceScaleFactor: device_scale_factor, mobile: mobile)
273
+ end
274
+
275
+ def enable_network_interception
276
+ remote.send_cmd! "Network.setRequestInterception", patterns: [{urlPattern: "*"}]
277
+ remote.on("Network.requestIntercepted") do |params|
278
+ if Capybara::Chrome.configuration.block_url?(params["request"]["url"]) || (Capybara::Chrome.configuration.skip_image_loading? && params["resourceType"] == "Image")
279
+ # p ["blocking", params["request"]["url"]]
280
+ remote.send_cmd "Network.continueInterceptedRequest", interceptionId: params["interceptionId"], errorReason: "ConnectionRefused"
281
+ else
282
+ # p ["allowing", params["request"]["url"]]
283
+ remote.send_cmd "Network.continueInterceptedRequest", interceptionId: params["interceptionId"]
284
+ end
285
+ end
286
+ end
287
+
288
+ def enable_script_debug
289
+ remote.send_cmd "Debugger.enable"
290
+ remote.on("Debugger.scriptFailedToParse") do |params|
291
+ puts "\n\n!!! ERROR: SCRIPT FAILED TO PARSE !!!\n\n"
292
+ p params
293
+ end
294
+ end
295
+
296
+ def enable_js_dialog
297
+ remote.on("Page.javascriptDialogOpening") do |params|
298
+ debug ["Dialog Opening", params]
299
+ handler = @js_dialog_handlers[params["type"]].last
300
+ if handler
301
+ debug ["have handler", handler]
302
+ args = {accept: handler[:accept]}
303
+ args.merge!(promptText: handler[:prompt_text]) if params[:type] == "prompt"
304
+ remote.send_cmd("Page.handleJavaScriptDialog", args)
305
+ @js_dialog_handlers[params["type"]].delete(params["type"].size - 1)
306
+ else
307
+ puts "WARNING: Accepting unhandled modal. Use #accept_modal or #dismiss_modal to handle this modal properly."
308
+ remote.send_cmd("Page.handleJavaScriptDialog", accept: true)
309
+ end
310
+ end
311
+ end
312
+
313
+ def accept_modal(type, text_or_options=nil, options={}, &block)
314
+ @js_dialog_handlers[type.to_s] << {accept: true}
315
+ block.call if block
316
+ end
317
+
318
+ def dismiss_modal(type, text_or_options=nil, options={}, &block)
319
+ @js_dialog_handlers[type.to_s] << {accept: false}
320
+ block.call if block
321
+ debug [type, text_or_options, options]
322
+ end
323
+
324
+ def enable_console_log
325
+ remote.send_cmd! "Console.enable"
326
+ remote.on "Console.messageAdded" do |params|
327
+ str = "#{params["message"]["source"]}:#{params["message"]["line"]} #{params["message"]["text"]}"
328
+ if params["message"]["level"] == "error"
329
+ @error_messages << str
330
+ else
331
+ @console_messages << str
332
+ end
333
+ end
334
+ end
335
+
336
+ def enable_lifecycle_events
337
+ remote.send_cmd! "Page.setLifecycleEventsEnabled", enabled: true
338
+ remote.on("Page.lifecycleEvent") do |params|
339
+ if params["name"] == "init"
340
+ @loader_ids.push(params["loaderId"])
341
+ elsif params["name"] == "load"
342
+ @loaded_loaders[params["loaderId"]] = true
343
+ elsif params["name"] == "networkIdle"
344
+ end
345
+ end
346
+ end
347
+
348
+ def loader_loaded?(loader_id)
349
+ @loaded_loaders[loader_id]
350
+ end
351
+
352
+ def save_screenshot(path, options={})
353
+ options[:width] ||= 1000
354
+ options[:height] ||= 10
355
+ render path, options[:width], options[:height]
356
+ end
357
+
358
+ def render(path, width=nil, height=nil)
359
+ response = remote.send_cmd "Page.getLayoutMetrics"
360
+ width = response["contentSize"]["width"]
361
+ height = response["contentSize"]["height"]
362
+ response = remote.send_cmd "Page.captureScreenshot", clip: {width: width, height: height, x: 0, y: 0, scale: 1}
363
+ File.open path, "wb" do |f|
364
+ f.write Base64.decode64(response["data"])
365
+ end
366
+ end
367
+
368
+ def header(key, value)
369
+ if key.downcase == "user-agent"
370
+ remote.send_cmd!("Network.setUserAgentOverride", userAgent: value)
371
+ else
372
+ remote.send_cmd!("Network.setExtraHTTPHeaders", headers: {key => value})
373
+ end
374
+ end
375
+
376
+ def reset
377
+ unset_root_node
378
+ @responses.clear
379
+ @last_response = nil
380
+ @console_messages.clear
381
+ @error_messages.clear
382
+ @js_dialog_handlers.clear
383
+ @unrecognized_scheme_requests.clear
384
+ remote.reset
385
+ remote.send_cmd! "Network.clearBrowserCookies"
386
+ remote.send_cmd! "Runtime.discardConsoleEntries"
387
+ remote.send_cmd! "Network.setExtraHTTPHeaders", headers: {}
388
+ remote.send_cmd! "Network.setUserAgentOverride", userAgent: ""
389
+ visit "about:blank"
390
+ end
391
+ end
392
+
393
+ end