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
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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).
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -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__)
|
data/bin/setup
ADDED
@@ -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
|