apparition 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +20 -0
- data/README.md +251 -0
- data/lib/capybara/apparition.rb +20 -0
- data/lib/capybara/apparition/browser.rb +532 -0
- data/lib/capybara/apparition/chrome_client.rb +235 -0
- data/lib/capybara/apparition/command.rb +21 -0
- data/lib/capybara/apparition/cookie.rb +51 -0
- data/lib/capybara/apparition/dev_tools_protocol/session.rb +29 -0
- data/lib/capybara/apparition/dev_tools_protocol/target.rb +52 -0
- data/lib/capybara/apparition/dev_tools_protocol/target_manager.rb +37 -0
- data/lib/capybara/apparition/driver.rb +505 -0
- data/lib/capybara/apparition/errors.rb +230 -0
- data/lib/capybara/apparition/frame.rb +90 -0
- data/lib/capybara/apparition/frame_manager.rb +81 -0
- data/lib/capybara/apparition/inspector.rb +49 -0
- data/lib/capybara/apparition/keyboard.rb +383 -0
- data/lib/capybara/apparition/launcher.rb +218 -0
- data/lib/capybara/apparition/mouse.rb +47 -0
- data/lib/capybara/apparition/network_traffic.rb +9 -0
- data/lib/capybara/apparition/network_traffic/error.rb +12 -0
- data/lib/capybara/apparition/network_traffic/request.rb +47 -0
- data/lib/capybara/apparition/network_traffic/response.rb +49 -0
- data/lib/capybara/apparition/node.rb +844 -0
- data/lib/capybara/apparition/page.rb +711 -0
- data/lib/capybara/apparition/utility.rb +15 -0
- data/lib/capybara/apparition/version.rb +7 -0
- data/lib/capybara/apparition/web_socket_client.rb +80 -0
- metadata +245 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 9aa4f4946fd5bbdd57c5547b6dd179c2c9f91737e4810bc331ac7155b9efb542
|
4
|
+
data.tar.gz: 8bdbca6fc6c5598a8827627f10b4dc9dfbbe94df5b57ea006a80d13a8bf4402a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 47c7ddf86e36760c0d26c64e59a157d5e32871e106977dd166acceeca7cd3b07e27131d02e6ec1487d74cba6cdebad8831ea6b6a2e79fcf4d00dbe53fd02be70
|
7
|
+
data.tar.gz: 50d6f7f64911974d0f17687500a2059efba3ccd3f609f5c96f7fb09ba37a193f77d537c13c50b3fbf7a27d033ea3c0b591ee33023a991870637f211db5705ed3
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2019 Thomas Walpole
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,251 @@
|
|
1
|
+
# Apparition - A Chrome driver for Capybara #
|
2
|
+
|
3
|
+
[![Build Status](https://secure.travis-ci.org/twalpole/apparition.svg)](http://travis-ci.org/twalpole/apparition)
|
4
|
+
|
5
|
+
Apparition is a driver for [Capybara](https://github.com/jnicklas/capybara). It allows you to
|
6
|
+
run your Capybara tests in the Chrome browser via CDP (no selenium or chromedriver needed) in a headless or
|
7
|
+
headed configuration. It started as a fork of Poltergeist and attempts to maintain as much compatibility
|
8
|
+
with the Poltergeist API as possible, with the thought to add a capybara-webkit compatibility wrapper at some future point in time.
|
9
|
+
|
10
|
+
## Getting help ##
|
11
|
+
|
12
|
+
Questions should be posted [on Stack
|
13
|
+
Overflow, using the 'capybara' tag](http://stackoverflow.com/questions/tagged/capybara) and mentioning
|
14
|
+
you are using the apparition driver.
|
15
|
+
|
16
|
+
Bug reports should be posted [on GitHub](https://github.com/twalpole/apparition/issues) (and be sure
|
17
|
+
to read the bug reporting guidance below).
|
18
|
+
|
19
|
+
## Installation ##
|
20
|
+
|
21
|
+
Add either
|
22
|
+
|
23
|
+
``` ruby
|
24
|
+
gem 'apparition'
|
25
|
+
```
|
26
|
+
|
27
|
+
or
|
28
|
+
|
29
|
+
``` ruby
|
30
|
+
gem apparition', github: 'twalpole/apparition'
|
31
|
+
```
|
32
|
+
|
33
|
+
to your Gemfile and run `bundle install`.
|
34
|
+
|
35
|
+
In your test setup add:
|
36
|
+
|
37
|
+
``` ruby
|
38
|
+
require 'capybara/apparition'
|
39
|
+
Capybara.javascript_driver = :apparition
|
40
|
+
```
|
41
|
+
|
42
|
+
If you were previously using the `:rack_test` driver, be aware that your app will now run in a separate thread and this can have
|
43
|
+
consequences for transactional tests. [See the Capybara README for more detail](https://github.com/teamcapybara/capybara/blob/master/README.md#transactions-and-database-setup).
|
44
|
+
|
45
|
+
## What's supported? ##
|
46
|
+
|
47
|
+
Apparition supports all Capybara features, and the following extended features:
|
48
|
+
|
49
|
+
* `page.status_code`
|
50
|
+
* `page.response_headers`
|
51
|
+
* `page.driver.render_base64(format, options)`
|
52
|
+
* `page.driver.scroll_to(left, top)`
|
53
|
+
* `page.driver.basic_authorize(user, password)`
|
54
|
+
* `page.driver.set_proxy(ip, port, type, user, password)`
|
55
|
+
* cookie handling
|
56
|
+
* extra headers
|
57
|
+
|
58
|
+
There are some additional features:
|
59
|
+
|
60
|
+
### Taking screenshots with some extensions ###
|
61
|
+
|
62
|
+
You can grab screenshots of the page at any point by calling
|
63
|
+
`save_screenshot('/path/to/file.png')`.
|
64
|
+
|
65
|
+
By default, only the viewport will be rendered (the part of the page that is in
|
66
|
+
view). To render the entire page, use `save_screenshot('/path/to/file.png',
|
67
|
+
full: true)`.
|
68
|
+
|
69
|
+
You also have an ability to render selected element. Pass option `selector` with
|
70
|
+
any valid CSS element selector to make a screenshot bounded by that element
|
71
|
+
`save_screenshot('/path/to/file.png', selector: '#id')`.
|
72
|
+
|
73
|
+
If the desired image format is not identifiable from the filename passed you can
|
74
|
+
also pass in a `format:` option with accepable values being `:png` or `:jpeg`
|
75
|
+
|
76
|
+
If, for some reason, you need a base64 encoded screenshot you can simply call
|
77
|
+
`render_base64` which will return your encoded image. Additional options are the
|
78
|
+
same as for `save_screenshot`.
|
79
|
+
|
80
|
+
### Clicking precise coordinates ###
|
81
|
+
|
82
|
+
Sometimes its desirable to click a very specific area of the screen. You can accomplish this with
|
83
|
+
`page.driver.click(x, y)`, where x and y are the screen coordinates.
|
84
|
+
|
85
|
+
### Manipulating request headers ###
|
86
|
+
|
87
|
+
You can manipulate HTTP request headers with these methods:
|
88
|
+
|
89
|
+
``` ruby
|
90
|
+
page.driver.headers # => {}
|
91
|
+
page.driver.headers = { "User-Agent" => "Apparition" }
|
92
|
+
page.driver.add_headers("Referer" => "https://example.com")
|
93
|
+
page.driver.headers # => { "User-Agent" => "Apparition", "Referer" => "https://example.com" }
|
94
|
+
```
|
95
|
+
|
96
|
+
Notice that `headers=` will overwrite already set headers. You should use
|
97
|
+
`add_headers` if you want to add a few more. These headers will apply to all
|
98
|
+
subsequent HTTP requests (including requests for assets, AJAX, etc). They will
|
99
|
+
be automatically cleared at the end of the test. You have ability to set headers
|
100
|
+
only for the initial request:
|
101
|
+
|
102
|
+
``` ruby
|
103
|
+
page.driver.headers = { "User-Agent" => "Apparition" }
|
104
|
+
page.driver.add_header("Referer", "http://example.com", permanent: false)
|
105
|
+
page.driver.headers # => { "User-Agent" => "Apparition", "Referer" => "http://example.com" }
|
106
|
+
visit(login_path)
|
107
|
+
page.driver.headers # => { "User-Agent" => "Apparition" }
|
108
|
+
```
|
109
|
+
|
110
|
+
This way your temporary headers will be sent only for the initial request, and related 30x redirects. All
|
111
|
+
subsequent request will only contain your permanent headers. If the temporary
|
112
|
+
headers should not be sent on related 30x redirects, specify `permanent: :no_redirect`.
|
113
|
+
|
114
|
+
### Inspecting network traffic ###
|
115
|
+
|
116
|
+
You can inspect the network traffic (i.e. what resources have been
|
117
|
+
loaded) on the current page by calling `page.driver.network_traffic`.
|
118
|
+
This returns an array of request objects. A request object has a
|
119
|
+
`response_parts` method containing data about the response chunks.
|
120
|
+
|
121
|
+
You can inspect requests that were blocked by a whitelist or blacklist
|
122
|
+
by calling `page.driver.network_traffic(:blocked)`. This returns an array of
|
123
|
+
request objects. The `response_parts` portion of these requests will always
|
124
|
+
be empty.
|
125
|
+
|
126
|
+
Please note that network traffic is not cleared when you visit new page.
|
127
|
+
You can manually clear the network traffic by calling `page.driver.clear_network_traffic`
|
128
|
+
or `page.driver.reset`
|
129
|
+
|
130
|
+
### Manipulating cookies ###
|
131
|
+
|
132
|
+
The following methods are used to inspect and manipulate cookies:
|
133
|
+
|
134
|
+
* `page.driver.cookies` - a hash of cookies accessible to the current
|
135
|
+
page. The keys are cookie names. The values are `Cookie` objects, with
|
136
|
+
the following methods: `name`, `value`, `domain`, `path`, `secure?`,
|
137
|
+
`httponly?`, `samesite`, `expires`.
|
138
|
+
* `page.driver.set_cookie(name, value, options = {})` - set a cookie.
|
139
|
+
The options hash can take the following keys: `:domain`, `:path`,
|
140
|
+
`:secure`, `:httponly`, `:samesite`, `:expires`. `:expires` should be a
|
141
|
+
`Time` object.
|
142
|
+
* `page.driver.remove_cookie(name)` - remove a cookie
|
143
|
+
* `page.driver.clear_cookies` - clear all cookies
|
144
|
+
|
145
|
+
## Customization ##
|
146
|
+
|
147
|
+
You can customize the way that Capybara sets up Apparition via the following code in your
|
148
|
+
test setup:
|
149
|
+
|
150
|
+
``` ruby
|
151
|
+
Capybara.register_driver :apparition do |app|
|
152
|
+
Capybara::Apparition::Driver.new(app, options)
|
153
|
+
end
|
154
|
+
```
|
155
|
+
|
156
|
+
`options` is a hash of options. The following options are supported:
|
157
|
+
|
158
|
+
* `:headless` (Boolean) - When false, run the browser visibly
|
159
|
+
* `:debug` (Boolean) - When true, debug output is logged to `STDERR`.
|
160
|
+
* `:logger` (Object responding to `puts`) - When present, debug output is written to this object
|
161
|
+
* `:browser_logger` (`IO` object) - Where the `STDOUT` from Chromium is written to. This is
|
162
|
+
where your `console.log` statements will show up. Default: `STDOUT`
|
163
|
+
* `:timeout` (Numeric) - The number of seconds we'll wait for a response
|
164
|
+
when communicating with Chrome. Default is 30.
|
165
|
+
* `:inspector` (Boolean, String) - See 'Remote Debugging', above.
|
166
|
+
* `:js_errors` (Boolean) - When false, JavaScript errors do not get re-raised in Ruby.
|
167
|
+
* `:window_size` (Array) - The dimensions of the browser window in which to test, expressed
|
168
|
+
as a 2-element array, e.g. [1024, 768]. Default: [1024, 768]
|
169
|
+
* `:screen_size` (Array) - The dimensions the window size will be set to when Window#maximize is called in headless mode. Expressed
|
170
|
+
as a 2-element array, e.g. [1600, 1200]. Default: [1366, 768]
|
171
|
+
* `:extensions` (Array) - An array of JS files to be preloaded into
|
172
|
+
the browser. Useful for faking or mocking APIs.
|
173
|
+
* `:url_blacklist` (Array) - Default session url blacklist - expressed as an array of strings to match against requested URLs.
|
174
|
+
* `:url_whitelist` (Array) - Default session url whitelist - expressed as an array of strings to match against requested URLs.
|
175
|
+
* `:browser_options` (Hash) - Extra command line options to pass to Chrome when starting
|
176
|
+
|
177
|
+
### URL Blacklisting & Whitelisting ###
|
178
|
+
Apparition supports URL blacklisting, which allows you
|
179
|
+
to prevent scripts from running on designated domains:
|
180
|
+
|
181
|
+
```ruby
|
182
|
+
page.driver.browser.url_blacklist = ['http://www.example.com']
|
183
|
+
```
|
184
|
+
|
185
|
+
and also URL whitelisting, which allows scripts to only run
|
186
|
+
on designated domains:
|
187
|
+
|
188
|
+
```ruby
|
189
|
+
page.driver.browser.url_whitelist = ['http://www.example.com']
|
190
|
+
```
|
191
|
+
|
192
|
+
If you are experiencing slower run times, consider creating a
|
193
|
+
URL whitelist of domains that are essential or a blacklist of
|
194
|
+
domains that are not essential, such as ad networks or analytics,
|
195
|
+
to your testing environment.
|
196
|
+
|
197
|
+
|
198
|
+
### Timing problems ###
|
199
|
+
|
200
|
+
Sometimes tests pass and fail sporadically. This is often because there
|
201
|
+
is some problem synchronising events properly. It's often
|
202
|
+
straightforward to verify this by adding `sleep` statements into your
|
203
|
+
test to allow sufficient time for the page to settle.
|
204
|
+
|
205
|
+
If you have these types of problems, read through the [Capybara
|
206
|
+
documentation on asynchronous
|
207
|
+
JavaScript](https://github.com/jnicklas/capybara#asynchronous-javascript-ajax-and-friends)
|
208
|
+
which explains the tools that Capybara provides for dealing with this.
|
209
|
+
|
210
|
+
### Filing a bug ###
|
211
|
+
|
212
|
+
If you can provide specific steps to reproduce your problem, or have
|
213
|
+
specific information that might help track down the problem, then please file a bug on Github.
|
214
|
+
|
215
|
+
Include as much information as possible. For example:
|
216
|
+
|
217
|
+
* Specific steps to reproduce where possible (failing tests are even
|
218
|
+
better)
|
219
|
+
* The output obtained from running Apparition with `:debug` turned on
|
220
|
+
* Screenshots
|
221
|
+
* Stack traces if there are any Ruby on JavaScript exceptions generated
|
222
|
+
* The Apparition, Capybara, and Chrome version numbers used
|
223
|
+
* The operating system name and version used
|
224
|
+
|
225
|
+
## Changes ##
|
226
|
+
|
227
|
+
Version history and a list of next-release features and fixes can be found in
|
228
|
+
the [changelog](CHANGELOG.md).
|
229
|
+
|
230
|
+
## License ##
|
231
|
+
|
232
|
+
Copyright (c) 2019 Thomas Walpole
|
233
|
+
|
234
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
235
|
+
a copy of this software and associated documentation files (the
|
236
|
+
"Software"), to deal in the Software without restriction, including
|
237
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
238
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
239
|
+
permit persons to whom the Software is furnished to do so, subject to
|
240
|
+
the following conditions:
|
241
|
+
|
242
|
+
The above copyright notice and this permission notice shall be
|
243
|
+
included in all copies or substantial portions of the Software.
|
244
|
+
|
245
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
246
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
247
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
248
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
249
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
250
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
251
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'capybara'
|
4
|
+
|
5
|
+
module Capybara
|
6
|
+
module Apparition
|
7
|
+
require 'capybara/apparition/utility'
|
8
|
+
require 'capybara/apparition/driver'
|
9
|
+
require 'capybara/apparition/browser'
|
10
|
+
require 'capybara/apparition/node'
|
11
|
+
require 'capybara/apparition/inspector'
|
12
|
+
require 'capybara/apparition/network_traffic'
|
13
|
+
require 'capybara/apparition/errors'
|
14
|
+
require 'capybara/apparition/cookie'
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
Capybara.register_driver :apparition do |app|
|
19
|
+
Capybara::Apparition::Driver.new(app)
|
20
|
+
end
|
@@ -0,0 +1,532 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'capybara/apparition/errors'
|
4
|
+
require 'capybara/apparition/command'
|
5
|
+
require 'capybara/apparition/dev_tools_protocol/target_manager'
|
6
|
+
require 'capybara/apparition/page'
|
7
|
+
require 'json'
|
8
|
+
require 'time'
|
9
|
+
|
10
|
+
module Capybara::Apparition
|
11
|
+
class Browser
|
12
|
+
attr_reader :client, :logger, :paper_size
|
13
|
+
|
14
|
+
def initialize(client, logger = nil)
|
15
|
+
@client = client
|
16
|
+
@logger = logger
|
17
|
+
@current_page_handle = nil
|
18
|
+
@targets = Capybara::Apparition::DevToolsProtocol::TargetManager.new
|
19
|
+
@context_id = nil
|
20
|
+
@js_errors = true
|
21
|
+
|
22
|
+
initialize_handlers
|
23
|
+
|
24
|
+
command('Target.setDiscoverTargets', discover: true)
|
25
|
+
while @current_page_handle.nil?
|
26
|
+
puts 'waiting for target...'
|
27
|
+
sleep 0.1
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def restart
|
32
|
+
puts 'handle client restart'
|
33
|
+
# client.restart
|
34
|
+
|
35
|
+
self.debug = @debug if defined?(@debug)
|
36
|
+
self.js_errors = @js_errors if defined?(@js_errors)
|
37
|
+
self.extensions = @extensions if @extensions
|
38
|
+
end
|
39
|
+
|
40
|
+
def visit(url)
|
41
|
+
current_page.visit url
|
42
|
+
end
|
43
|
+
|
44
|
+
def current_url
|
45
|
+
current_page.current_url
|
46
|
+
end
|
47
|
+
|
48
|
+
def status_code
|
49
|
+
current_page.status_code
|
50
|
+
end
|
51
|
+
|
52
|
+
def body
|
53
|
+
current_page.content
|
54
|
+
end
|
55
|
+
|
56
|
+
def source
|
57
|
+
# Is this still useful?
|
58
|
+
# command 'source'
|
59
|
+
end
|
60
|
+
|
61
|
+
def title
|
62
|
+
# Updated info doesn't have correct title when changed programmatically
|
63
|
+
# current_target.title
|
64
|
+
current_page.title
|
65
|
+
end
|
66
|
+
|
67
|
+
def frame_title
|
68
|
+
current_page.frame_title
|
69
|
+
end
|
70
|
+
|
71
|
+
def frame_url
|
72
|
+
current_page.frame_url
|
73
|
+
end
|
74
|
+
|
75
|
+
def find(method, selector)
|
76
|
+
current_page.find(method, selector)
|
77
|
+
end
|
78
|
+
|
79
|
+
def click_coordinates(x, y)
|
80
|
+
current_page.click_at(x, y)
|
81
|
+
end
|
82
|
+
|
83
|
+
def evaluate(script, *args)
|
84
|
+
current_page.evaluate(script, *args)
|
85
|
+
end
|
86
|
+
|
87
|
+
def evaluate_async(script, wait_time, *args)
|
88
|
+
current_page.evaluate_async(script, wait_time, *args)
|
89
|
+
end
|
90
|
+
|
91
|
+
def execute(script, *args)
|
92
|
+
current_page.execute(script, *args)
|
93
|
+
end
|
94
|
+
|
95
|
+
def switch_to_frame(frame)
|
96
|
+
case frame
|
97
|
+
when Capybara::Node::Base
|
98
|
+
current_page.push_frame(frame)
|
99
|
+
when :parent
|
100
|
+
current_page.pop_frame
|
101
|
+
when :top
|
102
|
+
current_page.pop_frame(top: true)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def window_handle
|
107
|
+
@current_page_handle
|
108
|
+
end
|
109
|
+
|
110
|
+
def window_handles
|
111
|
+
@targets.window_handles
|
112
|
+
end
|
113
|
+
|
114
|
+
def switch_to_window(handle)
|
115
|
+
target = @targets.get(handle)
|
116
|
+
raise NoSuchWindowError unless target&.page
|
117
|
+
|
118
|
+
target.page.wait_for_loaded
|
119
|
+
@current_page_handle = handle
|
120
|
+
end
|
121
|
+
|
122
|
+
def open_new_window
|
123
|
+
context_id = @context_id || current_target.info['browserContextId']
|
124
|
+
info = command('Target.createTarget', url: 'about:blank', browserContextId: context_id)
|
125
|
+
target_id = info['targetId']
|
126
|
+
target = DevToolsProtocol::Target.new(self, info.merge('type' => 'page', 'inherit' => current_page))
|
127
|
+
target.page # Ensure page object construction happens
|
128
|
+
@targets.add(target_id, target)
|
129
|
+
target_id
|
130
|
+
end
|
131
|
+
|
132
|
+
def close_window(handle)
|
133
|
+
@targets.delete(handle)
|
134
|
+
@current_page_handle = nil if @current_page_handle == handle
|
135
|
+
command('Target.closeTarget', targetId: handle)
|
136
|
+
end
|
137
|
+
|
138
|
+
def within_window(locator)
|
139
|
+
original = window_handle
|
140
|
+
handle = find_window_handle(locator)
|
141
|
+
switch_to_window(handle)
|
142
|
+
yield
|
143
|
+
ensure
|
144
|
+
switch_to_window(original)
|
145
|
+
end
|
146
|
+
|
147
|
+
def reset
|
148
|
+
command('Target.disposeBrowserContext', browserContextId: @context_id) if @context_id
|
149
|
+
|
150
|
+
@context_id = command('Target.createBrowserContext')['browserContextId']
|
151
|
+
target_id = command('Target.createTarget', url: 'about:blank', browserContextId: @context_id)['targetId']
|
152
|
+
|
153
|
+
start = Time.now
|
154
|
+
until @targets.get(target_id)&.page&.usable?
|
155
|
+
if Time.now - start > 5
|
156
|
+
puts 'Timedout waiting for reset'
|
157
|
+
# byebug
|
158
|
+
raise TimeoutError.new('reset')
|
159
|
+
end
|
160
|
+
sleep 0.01
|
161
|
+
end
|
162
|
+
@current_page_handle = target_id
|
163
|
+
true
|
164
|
+
end
|
165
|
+
|
166
|
+
def scroll_to(left, top)
|
167
|
+
current_page.scroll_to(left, top)
|
168
|
+
end
|
169
|
+
|
170
|
+
def render(path, options = {})
|
171
|
+
options[:format] ||= File.extname(path).downcase[1..-1]
|
172
|
+
check_render_options!(options)
|
173
|
+
options[:full] = !!options[:full]
|
174
|
+
img_data = current_page.render(options)
|
175
|
+
File.open(path, 'wb') { |f| f.write(Base64.decode64(img_data)) }
|
176
|
+
end
|
177
|
+
|
178
|
+
def render_base64(_format, options = {})
|
179
|
+
check_render_options!(options)
|
180
|
+
options[:full] = !!options[:full]
|
181
|
+
current_page.render(options)
|
182
|
+
end
|
183
|
+
|
184
|
+
# def set_zoom_factor(zoom_factor)
|
185
|
+
# TODO: implement if needed
|
186
|
+
# command 'set_zoom_factor', zoom_factor
|
187
|
+
# end
|
188
|
+
|
189
|
+
def set_paper_size(size)
|
190
|
+
@paper_size = size
|
191
|
+
end
|
192
|
+
|
193
|
+
def resize(width, height, screen: nil)
|
194
|
+
current_page.set_viewport width: width, height: height, screen: screen
|
195
|
+
end
|
196
|
+
|
197
|
+
def fullscreen
|
198
|
+
current_page.fullscreen
|
199
|
+
end
|
200
|
+
|
201
|
+
def maximize
|
202
|
+
current_page.maximize
|
203
|
+
end
|
204
|
+
|
205
|
+
def network_traffic(type = nil)
|
206
|
+
case type
|
207
|
+
when :blocked
|
208
|
+
current_page.network_traffic.select(&:blocked?)
|
209
|
+
else
|
210
|
+
current_page.network_traffic
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
def clear_network_traffic
|
215
|
+
current_page.clear_network_traffic
|
216
|
+
end
|
217
|
+
|
218
|
+
def set_proxy(ip, port, type, user, password)
|
219
|
+
args = [ip, port, type]
|
220
|
+
args << user if user
|
221
|
+
args << password if password
|
222
|
+
# TODO: Implement via CDP if possible
|
223
|
+
# command('set_proxy', *args)
|
224
|
+
end
|
225
|
+
|
226
|
+
def equals(page_id, id, other_id)
|
227
|
+
# TODO: Implement if still needed
|
228
|
+
# command('equals', page_id, id, other_id)
|
229
|
+
end
|
230
|
+
|
231
|
+
def get_headers
|
232
|
+
current_page.extra_headers
|
233
|
+
end
|
234
|
+
|
235
|
+
def set_headers(headers)
|
236
|
+
@targets.pages.each do |page|
|
237
|
+
page.perm_headers = headers
|
238
|
+
page.temp_headers = {}
|
239
|
+
page.temp_no_redirect_headers = {}
|
240
|
+
page.update_headers
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
def add_headers(headers)
|
245
|
+
current_page.perm_headers.merge! headers
|
246
|
+
current_page.update_headers
|
247
|
+
end
|
248
|
+
|
249
|
+
def add_header(header, permanent: true, **_options)
|
250
|
+
if permanent == true
|
251
|
+
@targets.pages.each do |page|
|
252
|
+
page.perm_headers.merge! header
|
253
|
+
page.update_headers
|
254
|
+
end
|
255
|
+
else
|
256
|
+
if permanent.to_s == 'no_redirect'
|
257
|
+
current_page.temp_no_redirect_headers.merge! header
|
258
|
+
else
|
259
|
+
current_page.temp_headers.merge! header
|
260
|
+
end
|
261
|
+
current_page.update_headers
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
def response_headers
|
266
|
+
current_page.response_headers
|
267
|
+
end
|
268
|
+
|
269
|
+
def cookies
|
270
|
+
current_page.command('Network.getCookies')['cookies'].each_with_object({}) do |c, h|
|
271
|
+
h[c['name']] = Cookie.new(c)
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
def set_cookie(cookie)
|
276
|
+
if cookie[:expires]
|
277
|
+
# cookie[:expires] = cookie[:expires].to_i * 1000
|
278
|
+
cookie[:expires] = cookie[:expires].to_i
|
279
|
+
end
|
280
|
+
|
281
|
+
current_page.command('Network.setCookie', cookie)
|
282
|
+
end
|
283
|
+
|
284
|
+
def remove_cookie(name)
|
285
|
+
current_page.command('Network.deleteCookies', name: name, url: current_url)
|
286
|
+
end
|
287
|
+
|
288
|
+
def clear_cookies
|
289
|
+
current_page.command('Network.clearBrowserCookies')
|
290
|
+
end
|
291
|
+
|
292
|
+
def cookies_enabled=(flag)
|
293
|
+
current_page.command('Emulation.setDocumentCookieDisabled', disabled: !flag)
|
294
|
+
end
|
295
|
+
|
296
|
+
def set_http_auth(user = nil, password = nil)
|
297
|
+
current_page.credentials = if user.nil? && password.nil?
|
298
|
+
nil
|
299
|
+
else
|
300
|
+
{ username: user, password: password }
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
attr_accessor :js_errors
|
305
|
+
|
306
|
+
def extensions=(filenames)
|
307
|
+
@extensions = filenames
|
308
|
+
Array(filenames).each do |name|
|
309
|
+
begin
|
310
|
+
current_page.command('Page.addScriptToEvaluateOnNewDocument', source: File.read(name))
|
311
|
+
rescue Errno::ENOENT
|
312
|
+
raise ::Capybara::Apparition::BrowserError.new('name' => "Unable to load extension: #{name}", 'args' => nil)
|
313
|
+
end
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
def url_whitelist=(whitelist)
|
318
|
+
current_page&.url_whitelist = whitelist
|
319
|
+
end
|
320
|
+
|
321
|
+
def url_blacklist=(blacklist)
|
322
|
+
current_page&.url_blacklist = blacklist
|
323
|
+
end
|
324
|
+
|
325
|
+
attr_writer :debug
|
326
|
+
|
327
|
+
def clear_memory_cache
|
328
|
+
current_page.command('Network.clearBrowserCache')
|
329
|
+
end
|
330
|
+
|
331
|
+
def command(name, params = {})
|
332
|
+
cmd = Command.new(name, params)
|
333
|
+
log cmd.message
|
334
|
+
|
335
|
+
response = client.send_cmd(name, params, async: false)
|
336
|
+
log response
|
337
|
+
|
338
|
+
response || raise(Capybara::Apparition::ObsoleteNode.new(nil, nil))
|
339
|
+
rescue DeadClient
|
340
|
+
restart
|
341
|
+
raise
|
342
|
+
end
|
343
|
+
|
344
|
+
def command_for_session(session_id, name, params, async: false)
|
345
|
+
cmd = Command.new(name, params)
|
346
|
+
log cmd.message
|
347
|
+
|
348
|
+
response = client.send_cmd_to_session(session_id, name, params, async: async)
|
349
|
+
log response
|
350
|
+
|
351
|
+
response
|
352
|
+
rescue DeadClient
|
353
|
+
restart
|
354
|
+
raise
|
355
|
+
end
|
356
|
+
|
357
|
+
def go_back
|
358
|
+
current_page.go_back
|
359
|
+
end
|
360
|
+
|
361
|
+
def go_forward
|
362
|
+
current_page.go_forward
|
363
|
+
end
|
364
|
+
|
365
|
+
def refresh
|
366
|
+
current_page.refresh
|
367
|
+
end
|
368
|
+
|
369
|
+
def accept_alert
|
370
|
+
current_page.add_modal(alert: true)
|
371
|
+
end
|
372
|
+
|
373
|
+
def accept_confirm
|
374
|
+
current_page.add_modal(confirm: true)
|
375
|
+
end
|
376
|
+
|
377
|
+
def dismiss_confirm
|
378
|
+
current_page.add_modal(confirm: false)
|
379
|
+
end
|
380
|
+
|
381
|
+
#
|
382
|
+
# press "OK" with text (response) or default value
|
383
|
+
#
|
384
|
+
def accept_prompt(response)
|
385
|
+
current_page.add_modal(prompt: response)
|
386
|
+
end
|
387
|
+
|
388
|
+
#
|
389
|
+
# press "Cancel"
|
390
|
+
#
|
391
|
+
def dismiss_prompt
|
392
|
+
current_page.add_modal(prompt: false)
|
393
|
+
end
|
394
|
+
|
395
|
+
def modal_message
|
396
|
+
current_page.modal_messages.shift
|
397
|
+
end
|
398
|
+
|
399
|
+
def current_page
|
400
|
+
current_target.page
|
401
|
+
end
|
402
|
+
|
403
|
+
private
|
404
|
+
|
405
|
+
def current_target
|
406
|
+
@targets.get(@current_page_handle) || begin
|
407
|
+
puts "No current page: #{@current_page_handle}"
|
408
|
+
@current_page_handle = nil
|
409
|
+
raise NoSuchWindowError
|
410
|
+
end
|
411
|
+
end
|
412
|
+
|
413
|
+
def log(message)
|
414
|
+
logger&.puts message
|
415
|
+
end
|
416
|
+
|
417
|
+
def check_render_options!(options)
|
418
|
+
options[:format] = :jpeg if options[:format].to_s == 'jpg'
|
419
|
+
return unless options[:full] && options.key?(:selector)
|
420
|
+
|
421
|
+
warn "Ignoring :selector in #render since :full => true was given at #{caller(1..1)}"
|
422
|
+
options.delete(:selector)
|
423
|
+
end
|
424
|
+
|
425
|
+
def find_window_handle(locator)
|
426
|
+
return locator if window_handles.include? locator
|
427
|
+
|
428
|
+
window_handles.each do |handle|
|
429
|
+
switch_to_window(handle)
|
430
|
+
return handle if evaluate('window.name') == locator
|
431
|
+
end
|
432
|
+
raise NoSuchWindowError
|
433
|
+
end
|
434
|
+
|
435
|
+
KEY_ALIASES = {
|
436
|
+
command: :Meta,
|
437
|
+
equals: :Equal,
|
438
|
+
control: :Control,
|
439
|
+
ctrl: :Control,
|
440
|
+
multiply: 'numpad*',
|
441
|
+
add: 'numpad+',
|
442
|
+
divide: 'numpad/',
|
443
|
+
subtract: 'numpad-',
|
444
|
+
decimal: 'numpad.',
|
445
|
+
left: 'ArrowLeft',
|
446
|
+
right: 'ArrowRight',
|
447
|
+
down: 'ArrowDown',
|
448
|
+
up: 'ArrowUp'
|
449
|
+
}.freeze
|
450
|
+
|
451
|
+
def normalize_keys(keys)
|
452
|
+
keys.map do |key_desc|
|
453
|
+
case key_desc
|
454
|
+
when Array
|
455
|
+
# [:Shift, "s"] => { modifier: "shift", keys: "S" }
|
456
|
+
# [:Shift, "string"] => { modifier: "shift", keys: "STRING" }
|
457
|
+
# [:Ctrl, :Left] => { modifier: "ctrl", key: 'Left' }
|
458
|
+
# [:Ctrl, :Shift, :Left] => { modifier: "ctrl,shift", key: 'Left' }
|
459
|
+
# [:Ctrl, :Left, :Left] => { modifier: "ctrl", key: [:Left, :Left] }
|
460
|
+
keys_chunks = key_desc.chunk do |k|
|
461
|
+
k.is_a?(Symbol) && %w[shift ctrl control alt meta command].include?(k.to_s.downcase)
|
462
|
+
end
|
463
|
+
modifiers = modifiers_from_chunks(keys_chunks)
|
464
|
+
letters = normalize_keys(_keys.next[1].map { |k| k.is_a?(String) ? k.upcase : k })
|
465
|
+
{ modifier: modifiers, keys: letters }
|
466
|
+
when Symbol
|
467
|
+
symbol_to_desc(key_desc)
|
468
|
+
when String
|
469
|
+
key_desc # Plain string, nothing to do
|
470
|
+
end
|
471
|
+
end
|
472
|
+
end
|
473
|
+
|
474
|
+
def modifiers_from_chunks(chunks)
|
475
|
+
if chunks.peek[0]
|
476
|
+
chunks.next[1].map do |k|
|
477
|
+
k = k.to_s.downcase
|
478
|
+
k = 'control' if k == 'ctrl'
|
479
|
+
k = 'meta' if k == 'command'
|
480
|
+
k
|
481
|
+
end.join(',')
|
482
|
+
else
|
483
|
+
''
|
484
|
+
end
|
485
|
+
end
|
486
|
+
|
487
|
+
def symbol_to_desc(symbol)
|
488
|
+
if symbol == :space
|
489
|
+
res = ' '
|
490
|
+
else
|
491
|
+
key = KEY_ALIASES.fetch(symbol.downcase, symbol)
|
492
|
+
if (match = key.to_s.match(/numpad(.)/))
|
493
|
+
res = { keys: match[1], modifier: 'keypad' }
|
494
|
+
elsif !/^[A-Z]/.match?(key)
|
495
|
+
key = key.to_s.split('_').map(&:capitalize).join
|
496
|
+
end
|
497
|
+
end
|
498
|
+
res || { key: key }
|
499
|
+
end
|
500
|
+
|
501
|
+
def initialize_handlers
|
502
|
+
@client.on 'Target.targetCreated' do |info|
|
503
|
+
puts "Target Created Info: #{info}" if ENV['DEBUG']
|
504
|
+
target_info = info['targetInfo']
|
505
|
+
if !@targets.target?(target_info['targetId'])
|
506
|
+
@targets.add(target_info['targetId'], DevToolsProtocol::Target.new(self, target_info))
|
507
|
+
puts "**** Target Added #{info}" if ENV['DEBUG']
|
508
|
+
elsif ENV['DEBUG']
|
509
|
+
puts "Target already existed #{info}"
|
510
|
+
end
|
511
|
+
@current_page_handle ||= target_info['targetId'] if target_info['type'] == 'page'
|
512
|
+
end
|
513
|
+
|
514
|
+
@client.on 'Target.targetDestroyed' do |info|
|
515
|
+
puts "**** Target Destroyed Info: #{info}" if ENV['DEBUG']
|
516
|
+
@targets.delete(info['targetId'])
|
517
|
+
end
|
518
|
+
|
519
|
+
@client.on 'Target.targetInfoChanged' do |info|
|
520
|
+
puts "**** Target Info Changed: #{info}" if ENV['DEBUG']
|
521
|
+
target_info = info['targetInfo']
|
522
|
+
target = @targets.get(target_info['targetId'])
|
523
|
+
if target
|
524
|
+
target.info.merge!(target_info)
|
525
|
+
else
|
526
|
+
puts '****No target for the info change- creating****' if ENV['DEBUG']
|
527
|
+
@targets.add(target_info['targetId'], DevToolsProtocol::Target.new(self, target_info))
|
528
|
+
end
|
529
|
+
end
|
530
|
+
end
|
531
|
+
end
|
532
|
+
end
|