apparition 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.
@@ -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