apparition 0.0.1

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.
@@ -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