capybara-chrome 0.1.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
[](http://www.youtube.com/watch?v=SLmkx5z-lAA)
|
95
|
+
|
96
|
+
A video demonstrating debugging an rspec test with `byebug`
|
97
|
+
|
98
|
+
[](http://www.youtube.com/watch?v=McEQG9YEAdE)
|
99
|
+
|
100
|
+
A video showing capybara-chrome running against a netcat backend
|
101
|
+
|
102
|
+
[](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
|