apparition 0.0.6 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 594a3f464bdb932d89f32f9bc0c3ce132ae5322c167e96016b0e66e292a8d71f
4
- data.tar.gz: 4e2bf086855cb7aad5d38727efb96c9945f325e6c4b53f4ab17b4caa5a4caa35
3
+ metadata.gz: b51cb57f961f9825c56ce38377bbf4fd5477a5cdad6d75a2dac1aab827b81561
4
+ data.tar.gz: 29e9b0c81cdcd3be2820a8d9a2719708750d3099f56a6c403c474a77bb254bce
5
5
  SHA512:
6
- metadata.gz: d4111cb452f2f30f69619bbdfe499b1a44aa00208e255264797440dfed1dd5c98c4045a31ebd6572b902bf16ed62c4873dbfc5229d5ec273296c7ba688cb4c70
7
- data.tar.gz: 1c160f3115ec245d2ddb834053913627f1ed34fe681fd8cb892d0a2b9bcd1af7d61f673b392582c84d248d5341239b4b2343ee5eee14c357fd018396f6e919d8
6
+ metadata.gz: 95be5f025a29554e4909b5e4500d2694f580b20ee92b9cf555c7fcf2a9bc1962871d95c81730d9800a55d8cc38f99656713f88cd10f0f7bd8e880083819c0edc
7
+ data.tar.gz: 34c0d53702b1b0be001cd65ebf0bfcb17ab6d3e86505851a23bbe39bd9bd80d4c0cb6d3d95a990804bd3ee123090ebce155892ddadd2ab4484782219ad0535b3
data/README.md CHANGED
@@ -158,8 +158,7 @@ end
158
158
  * `:headless` (Boolean) - When false, run the browser visibly
159
159
  * `:debug` (Boolean) - When true, debug output is logged to `STDERR`.
160
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`
161
+ * `:browser_logger` (`IO` object) - This is where your `console.log` statements will show up. Default: `STDOUT`
163
162
  * `:timeout` (Numeric) - The number of seconds we'll wait for a response
164
163
  when communicating with Chrome. Default is 30.
165
164
  * `:inspector` (Boolean, String) - See 'Remote Debugging', above.
@@ -4,6 +4,13 @@ require 'capybara/apparition/errors'
4
4
  require 'capybara/apparition/dev_tools_protocol/target_manager'
5
5
  require 'capybara/apparition/page'
6
6
  require 'capybara/apparition/console'
7
+ require 'capybara/apparition/browser/header'
8
+ require 'capybara/apparition/browser/window'
9
+ require 'capybara/apparition/browser/render'
10
+ require 'capybara/apparition/browser/cookie'
11
+ require 'capybara/apparition/browser/modal'
12
+ require 'capybara/apparition/browser/frame'
13
+ require 'capybara/apparition/browser/auth'
7
14
  require 'json'
8
15
  require 'time'
9
16
 
@@ -23,7 +30,7 @@ module Capybara::Apparition
23
30
  def initialize(client, logger = nil)
24
31
  @client = client
25
32
  @current_page_handle = nil
26
- @targets = Capybara::Apparition::DevToolsProtocol::TargetManager.new
33
+ @targets = Capybara::Apparition::DevToolsProtocol::TargetManager.new(self)
27
34
  @context_id = nil
28
35
  @js_errors = true
29
36
  @ignore_https_errors = false
@@ -38,6 +45,7 @@ module Capybara::Apparition
38
45
  puts 'waiting for target...'
39
46
  sleep 0.1
40
47
  end
48
+ @context_id = current_target.context_id
41
49
  end
42
50
 
43
51
  def restart
@@ -64,101 +72,43 @@ module Capybara::Apparition
64
72
  current_page.click_at(x, y)
65
73
  end
66
74
 
67
- def switch_to_frame(frame)
68
- case frame
69
- when Capybara::Node::Base
70
- current_page.push_frame(frame)
71
- when :parent
72
- current_page.pop_frame
73
- when :top
74
- current_page.pop_frame(top: true)
75
- end
76
- end
77
-
78
- def window_handle
79
- @current_page_handle
80
- end
81
-
82
- def window_handles
83
- @targets.window_handles
84
- end
85
-
86
- def switch_to_window(handle)
87
- target = @targets.get(handle)
88
- raise NoSuchWindowError unless target&.page
75
+ include Header
76
+ include Window
77
+ include Render
78
+ include Cookie
79
+ include Modal
80
+ include Frame
81
+ include Auth
89
82
 
90
- target.page.wait_for_loaded
91
- @current_page_handle = handle
92
- end
93
-
94
- def open_new_window
95
- context_id = @context_id || current_target.info['browserContextId']
96
- info = command('Target.createTarget', url: 'about:blank', browserContextId: context_id)
97
- target_id = info['targetId']
98
- target = DevToolsProtocol::Target.new(self, info.merge('type' => 'page', 'inherit' => current_page))
99
- target.page # Ensure page object construction happens
100
- @targets.add(target_id, target)
101
- target_id
102
- end
83
+ def reset
84
+ current_page_targets = @targets.of_type('page').values
103
85
 
104
- def close_window(handle)
105
- @current_page_handle = nil if @current_page_handle == handle
106
- win_target = @targets.delete(handle)
107
- warn 'Window was already closed unexpectedly' if win_target.nil?
108
- win_target&.close
109
- end
86
+ new_context_id = command('Target.createBrowserContext')['browserContextId']
87
+ new_target_response = client.send_cmd('Target.createTarget', url: 'about:blank', browserContextId: new_context_id)
110
88
 
111
- def within_window(locator)
112
- original = window_handle
113
- handle = find_window_handle(locator)
114
- switch_to_window(handle)
115
- yield
116
- ensure
117
- switch_to_window(original)
118
- end
119
-
120
- def reset
121
- command('Target.disposeBrowserContext', browserContextId: @context_id) if @context_id
89
+ current_page_targets.each do |target|
90
+ begin
91
+ client.send_cmd('Target.disposeBrowserContext', browserContextId: target.context_id).discard_result
92
+ rescue WrongWorld
93
+ puts 'Unknown browserContextId'
94
+ end
95
+ @targets.delete(target.id)
96
+ end
122
97
 
123
- @context_id = command('Target.createBrowserContext')['browserContextId']
124
- target_id = command('Target.createTarget', url: 'about:blank', browserContextId: @context_id)['targetId']
98
+ new_target_id = new_target_response['targetId']
125
99
 
126
- timer = Capybara::Helpers.timer(expire_in: 5)
127
- until @targets.get(target_id)&.page&.usable?
100
+ timer = Capybara::Helpers.timer(expire_in: 10)
101
+ until @targets.get(new_target_id)&.page&.usable?
128
102
  if timer.expired?
129
103
  puts 'Timedout waiting for reset'
130
- # byebug
131
104
  raise TimeoutError.new('reset')
132
105
  end
133
106
  sleep 0.01
134
107
  end
135
- @current_page_handle = target_id
108
+ @current_page_handle = new_target_id
136
109
  true
137
110
  end
138
111
 
139
- def render(path, options = {})
140
- check_render_options!(options, path)
141
- img_data = current_page.render(options)
142
- File.open(path, 'wb') { |f| f.write(Base64.decode64(img_data)) }
143
- end
144
-
145
- def render_base64(options = {})
146
- check_render_options!(options)
147
- current_page.render(options)
148
- end
149
-
150
- attr_writer :zoom_factor
151
-
152
- def paper_size=(size)
153
- @paper_size = if size.is_a? Hash
154
- size
155
- else
156
- PAPER_SIZES.fetch(size) do
157
- raise_errors ArgumentError, "Unknwon paper size: #{size}"
158
- end
159
- end
160
- end
161
-
162
112
  def resize(width, height, screen: nil)
163
113
  current_page.set_viewport width: width, height: height, screen: screen
164
114
  end
@@ -172,95 +122,6 @@ module Capybara::Apparition
172
122
  end
173
123
  end
174
124
 
175
- def headers
176
- current_page.extra_headers
177
- end
178
-
179
- def headers=(headers)
180
- @targets.pages.each do |page|
181
- page.perm_headers = headers.dup
182
- page.temp_headers = {}
183
- page.temp_no_redirect_headers = {}
184
- page.update_headers
185
- end
186
- end
187
-
188
- def add_headers(headers)
189
- current_page.perm_headers.merge! headers
190
- current_page.update_headers
191
- end
192
-
193
- def add_header(header, permanent: true, **_options)
194
- if permanent == true
195
- @targets.pages.each do |page|
196
- page.perm_headers.merge! header
197
- page.update_headers
198
- end
199
- else
200
- if permanent.to_s == 'no_redirect'
201
- current_page.temp_no_redirect_headers.merge! header
202
- else
203
- current_page.temp_headers.merge! header
204
- end
205
- current_page.update_headers
206
- end
207
- end
208
-
209
- def cookies
210
- CookieJar.new(
211
- # current_page.command('Network.getCookies')['cookies'].map { |c| Cookie.new(c) }
212
- self
213
- )
214
- end
215
-
216
- def all_cookies
217
- CookieJar.new(
218
- # current_page.command('Network.getAllCookies')['cookies'].map { |c| Cookie.new(c) }
219
- self
220
- )
221
- end
222
-
223
- def get_raw_cookies
224
- current_page.command('Network.getAllCookies')['cookies'].map { |c| Cookie.new(c) }
225
- end
226
-
227
- def set_cookie(cookie)
228
- if cookie[:expires]
229
- # cookie[:expires] = cookie[:expires].to_i * 1000
230
- cookie[:expires] = cookie[:expires].to_i
231
- end
232
-
233
- current_page.command('Network.setCookie', cookie)
234
- end
235
-
236
- def remove_cookie(name)
237
- current_page.command('Network.deleteCookies', name: name, url: current_url)
238
- end
239
-
240
- def clear_cookies
241
- current_page.command('Network.clearBrowserCookies')
242
- end
243
-
244
- def cookies_enabled=(flag)
245
- current_page.command('Emulation.setDocumentCookieDisabled', disabled: !flag)
246
- end
247
-
248
- def set_proxy_auth(user, password)
249
- @proxy_auth = if user.nil? && password.nil?
250
- nil
251
- else
252
- { username: user, password: password }
253
- end
254
- end
255
-
256
- def set_http_auth(user = nil, password = nil)
257
- current_page.credentials = if user.nil? && password.nil?
258
- nil
259
- else
260
- { username: user, password: password }
261
- end
262
- end
263
-
264
125
  attr_accessor :js_errors, :ignore_https_errors
265
126
 
266
127
  def extensions=(filenames)
@@ -305,30 +166,6 @@ module Capybara::Apparition
305
166
  raise
306
167
  end
307
168
 
308
- def accept_alert
309
- current_page.add_modal(alert: true)
310
- end
311
-
312
- def accept_confirm
313
- current_page.add_modal(confirm: true)
314
- end
315
-
316
- def dismiss_confirm
317
- current_page.add_modal(confirm: false)
318
- end
319
-
320
- def accept_prompt(response)
321
- current_page.add_modal(prompt: response)
322
- end
323
-
324
- def dismiss_prompt
325
- current_page.add_modal(prompt: false)
326
- end
327
-
328
- def modal_message
329
- current_page.modal_messages.shift
330
- end
331
-
332
169
  def current_page
333
170
  current_target.page
334
171
  end
@@ -342,6 +179,7 @@ module Capybara::Apparition
342
179
  def current_target
343
180
  @targets.get(@current_page_handle) || begin
344
181
  puts "No current page: #{@current_page_handle}"
182
+ puts caller
345
183
  @current_page_handle = nil
346
184
  raise NoSuchWindowError
347
185
  end
@@ -351,98 +189,79 @@ module Capybara::Apparition
351
189
  @logger&.puts message if ENV['DEBUG']
352
190
  end
353
191
 
354
- def check_render_options!(options, path = nil)
355
- options[:format] ||= File.extname(path).downcase[1..-1] if path
356
- options[:format] = :jpeg if options[:format].to_s == 'jpg'
357
- options[:full] = !!options[:full]
358
- return unless options[:full] && options.key?(:selector)
359
-
360
- warn "Ignoring :selector in #render since :full => true was given at #{caller(1..1)}"
361
- options.delete(:selector)
362
- end
363
-
364
- def find_window_handle(locator)
365
- return locator if window_handles.include? locator
366
-
367
- window_handles.each do |handle|
368
- switch_to_window(handle)
369
- return handle if evaluate('window.name') == locator
370
- end
371
- raise NoSuchWindowError
372
- end
373
-
374
- KEY_ALIASES = {
375
- command: :Meta,
376
- equals: :Equal,
377
- control: :Control,
378
- ctrl: :Control,
379
- multiply: 'numpad*',
380
- add: 'numpad+',
381
- divide: 'numpad/',
382
- subtract: 'numpad-',
383
- decimal: 'numpad.',
384
- left: 'ArrowLeft',
385
- right: 'ArrowRight',
386
- down: 'ArrowDown',
387
- up: 'ArrowUp'
388
- }.freeze
389
-
390
- def normalize_keys(keys)
391
- keys.map do |key_desc|
392
- case key_desc
393
- when Array
394
- # [:Shift, "s"] => { modifier: "shift", keys: "S" }
395
- # [:Shift, "string"] => { modifier: "shift", keys: "STRING" }
396
- # [:Ctrl, :Left] => { modifier: "ctrl", key: 'Left' }
397
- # [:Ctrl, :Shift, :Left] => { modifier: "ctrl,shift", key: 'Left' }
398
- # [:Ctrl, :Left, :Left] => { modifier: "ctrl", key: [:Left, :Left] }
399
- keys_chunks = key_desc.chunk do |k|
400
- k.is_a?(Symbol) && %w[shift ctrl control alt meta command].include?(k.to_s.downcase)
401
- end
402
- modifiers = modifiers_from_chunks(keys_chunks)
403
- letters = normalize_keys(_keys.next[1].map { |k| k.is_a?(String) ? k.upcase : k })
404
- { modifier: modifiers, keys: letters }
405
- when Symbol
406
- symbol_to_desc(key_desc)
407
- when String
408
- key_desc # Plain string, nothing to do
409
- end
410
- end
411
- end
412
-
413
- def modifiers_from_chunks(chunks)
414
- if chunks.peek[0]
415
- chunks.next[1].map do |k|
416
- k = k.to_s.downcase
417
- k = 'control' if k == 'ctrl'
418
- k = 'meta' if k == 'command'
419
- k
420
- end.join(',')
421
- else
422
- ''
423
- end
424
- end
425
-
426
- def symbol_to_desc(symbol)
427
- if symbol == :space
428
- res = ' '
429
- else
430
- key = KEY_ALIASES.fetch(symbol.downcase, symbol)
431
- if (match = key.to_s.match(/numpad(.)/))
432
- res = { keys: match[1], modifier: 'keypad' }
433
- elsif !/^[A-Z]/.match?(key)
434
- key = key.to_s.split('_').map(&:capitalize).join
435
- end
436
- end
437
- res || { key: key }
438
- end
192
+ # KEY_ALIASES = {
193
+ # command: :Meta,
194
+ # equals: :Equal,
195
+ # control: :Control,
196
+ # ctrl: :Control,
197
+ # multiply: 'numpad*',
198
+ # add: 'numpad+',
199
+ # divide: 'numpad/',
200
+ # subtract: 'numpad-',
201
+ # decimal: 'numpad.',
202
+ # left: 'ArrowLeft',
203
+ # right: 'ArrowRight',
204
+ # down: 'ArrowDown',
205
+ # up: 'ArrowUp'
206
+ # }.freeze
207
+ #
208
+ # def normalize_keys(keys)
209
+ # keys.map do |key_desc|
210
+ # case key_desc
211
+ # when Array
212
+ # # [:Shift, "s"] => { modifier: "shift", keys: "S" }
213
+ # # [:Shift, "string"] => { modifier: "shift", keys: "STRING" }
214
+ # # [:Ctrl, :Left] => { modifier: "ctrl", key: 'Left' }
215
+ # # [:Ctrl, :Shift, :Left] => { modifier: "ctrl,shift", key: 'Left' }
216
+ # # [:Ctrl, :Left, :Left] => { modifier: "ctrl", key: [:Left, :Left] }
217
+ # keys_chunks = key_desc.chunk do |k|
218
+ # k.is_a?(Symbol) && %w[shift ctrl control alt meta command].include?(k.to_s.downcase)
219
+ # end
220
+ # modifiers = modifiers_from_chunks(keys_chunks)
221
+ # letters = normalize_keys(_keys.next[1].map { |k| k.is_a?(String) ? k.upcase : k })
222
+ # { modifier: modifiers, keys: letters }
223
+ # when Symbol
224
+ # symbol_to_desc(key_desc)
225
+ # when String
226
+ # key_desc # Plain string, nothing to do
227
+ # end
228
+ # end
229
+ # end
230
+ #
231
+ # def modifiers_from_chunks(chunks)
232
+ # if chunks.peek[0]
233
+ # chunks.next[1].map do |k|
234
+ # k = k.to_s.downcase
235
+ # k = 'control' if k == 'ctrl'
236
+ # k = 'meta' if k == 'command'
237
+ # k
238
+ # end.join(',')
239
+ # else
240
+ # ''
241
+ # end
242
+ # end
243
+ #
244
+ # def symbol_to_desc(symbol)
245
+ # if symbol == :space
246
+ # res = ' '
247
+ # else
248
+ # key = KEY_ALIASES.fetch(symbol.downcase, symbol)
249
+ # if (match = key.to_s.match(/numpad(.)/))
250
+ # res = { keys: match[1], modifier: 'keypad' }
251
+ # elsif !/^[A-Z]/.match?(key)
252
+ # key = key.to_s.split('_').map(&:capitalize).join
253
+ # end
254
+ # end
255
+ # res || { key: key }
256
+ # end
439
257
 
440
258
  def initialize_handlers
441
259
  @client.on 'Target.targetCreated' do |info|
442
260
  puts "Target Created Info: #{info}" if ENV['DEBUG']
443
261
  target_info = info['targetInfo']
444
262
  if !@targets.target?(target_info['targetId'])
445
- @targets.add(target_info['targetId'], DevToolsProtocol::Target.new(self, target_info))
263
+ # @targets.add(target_info['targetId'], DevToolsProtocol::Target.new(self, target_info))
264
+ @targets.add(target_info['targetId'], target_info)
446
265
  puts "**** Target Added #{info}" if ENV['DEBUG']
447
266
  elsif ENV['DEBUG']
448
267
  puts "Target already existed #{info}"
@@ -460,22 +279,12 @@ module Capybara::Apparition
460
279
  target_info = info['targetInfo']
461
280
  target = @targets.get(target_info['targetId'])
462
281
  if target
463
- target.info.merge!(target_info)
282
+ target.update(target_info)
464
283
  else
465
284
  puts '****No target for the info change- creating****' if ENV['DEBUG']
466
- @targets.add(target_info['targetId'], DevToolsProtocol::Target.new(self, target_info))
285
+ @targets.add(target_info['targetId'], target_info)
467
286
  end
468
287
  end
469
288
  end
470
-
471
- PAPER_SIZES = {
472
- 'A3' => { width: 11.69, height: 16.53 },
473
- 'A4' => { width: 8.27, height: 11.69 },
474
- 'A5' => { width: 5.83, height: 8.27 },
475
- 'Legal' => { width: 8.5, height: 14 },
476
- 'Letter' => { width: 8.5, height: 11 },
477
- 'Tabloid' => { width: 11, height: 17 },
478
- 'Ledger' => { width: 17, height: 11 }
479
- }.freeze
480
289
  end
481
290
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara::Apparition
4
+ class Browser
5
+ module Auth
6
+ def set_proxy_auth(user, password)
7
+ set_auth(:proxy, user, password)
8
+ end
9
+
10
+ def set_http_auth(user, password)
11
+ set_auth(:http, user, password)
12
+ end
13
+
14
+ private
15
+
16
+ def set_auth(type, user, password)
17
+ creds = user.nil? && password.nil? ? nil : { username: user, password: password }
18
+
19
+ case type
20
+ when :http
21
+ current_page.credentials = creds
22
+ when :proxy
23
+ @proxy_auth = creds
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'capybara/apparition/cookie_jar'
4
+
5
+ module Capybara::Apparition
6
+ class Browser
7
+ module Cookie
8
+ def cookies
9
+ CookieJar.new(
10
+ # current_page.command('Network.getCookies')['cookies'].map { |c| Cookie.new(c) }
11
+ self
12
+ )
13
+ end
14
+
15
+ def all_cookies
16
+ CookieJar.new(
17
+ # current_page.command('Network.getAllCookies')['cookies'].map { |c| Cookie.new(c) }
18
+ self
19
+ )
20
+ end
21
+
22
+ def get_raw_cookies
23
+ current_page.command('Network.getAllCookies')['cookies'].map do |c|
24
+ Capybara::Apparition::Cookie.new(c)
25
+ end
26
+ end
27
+
28
+ def set_cookie(cookie)
29
+ if cookie[:expires]
30
+ # cookie[:expires] = cookie[:expires].to_i * 1000
31
+ cookie[:expires] = cookie[:expires].to_i
32
+ end
33
+
34
+ current_page.command('Network.setCookie', cookie)
35
+ end
36
+
37
+ def remove_cookie(name)
38
+ current_page.command('Network.deleteCookies', name: name, url: current_url)
39
+ end
40
+
41
+ def clear_cookies
42
+ current_page.command('Network.clearBrowserCookies')
43
+ end
44
+
45
+ def cookies_enabled=(flag)
46
+ current_page.command('Emulation.setDocumentCookieDisabled', disabled: !flag)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara::Apparition
4
+ class Browser
5
+ module Frame
6
+ def switch_to_frame(frame)
7
+ case frame
8
+ when Capybara::Node::Base
9
+ current_page.push_frame(frame)
10
+ when :parent
11
+ current_page.pop_frame
12
+ when :top
13
+ current_page.pop_frame(top: true)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara::Apparition
4
+ class Browser
5
+ module Header
6
+ def headers
7
+ current_page.extra_headers
8
+ end
9
+
10
+ def headers=(headers)
11
+ @targets.pages.each do |page|
12
+ page.perm_headers = headers.dup
13
+ page.temp_headers = {}
14
+ page.temp_no_redirect_headers = {}
15
+ page.update_headers
16
+ end
17
+ end
18
+
19
+ def add_headers(headers)
20
+ current_page.perm_headers.merge! headers
21
+ current_page.update_headers
22
+ end
23
+
24
+ def add_header(header, permanent: true, **_options)
25
+ if permanent == true
26
+ @targets.pages.each do |page|
27
+ page.perm_headers.merge! header
28
+ page.update_headers
29
+ end
30
+ else
31
+ if permanent.to_s == 'no_redirect'
32
+ current_page.temp_no_redirect_headers.merge! header
33
+ else
34
+ current_page.temp_headers.merge! header
35
+ end
36
+ current_page.update_headers
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara::Apparition
4
+ class Browser
5
+ module Modal
6
+ def accept_alert
7
+ current_page.add_modal(alert: true)
8
+ end
9
+
10
+ def accept_confirm
11
+ current_page.add_modal(confirm: true)
12
+ end
13
+
14
+ def dismiss_confirm
15
+ current_page.add_modal(confirm: false)
16
+ end
17
+
18
+ def accept_prompt(response)
19
+ current_page.add_modal(prompt: response)
20
+ end
21
+
22
+ def dismiss_prompt
23
+ current_page.add_modal(prompt: false)
24
+ end
25
+
26
+ def modal_message
27
+ current_page.modal_messages.shift
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara::Apparition
4
+ class Browser
5
+ module Render
6
+ def render(path, options = {})
7
+ check_render_options!(options, path)
8
+ img_data = current_page.render(options)
9
+ File.open(path, 'wb') { |f| f.write(Base64.decode64(img_data)) }
10
+ end
11
+
12
+ def render_base64(options = {})
13
+ check_render_options!(options)
14
+ current_page.render(options)
15
+ end
16
+
17
+ attr_writer :zoom_factor
18
+
19
+ def paper_size=(size)
20
+ @paper_size = if size.is_a? Hash
21
+ size
22
+ else
23
+ PAPER_SIZES.fetch(size) do
24
+ raise_errors ArgumentError, "Unknwon paper size: #{size}"
25
+ end
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def check_render_options!(options, path = nil)
32
+ options[:format] ||= File.extname(path).downcase[1..-1] if path
33
+ options[:format] = :jpeg if options[:format].to_s == 'jpg'
34
+ options[:full] = !!options[:full]
35
+ return unless options[:full] && options.key?(:selector)
36
+
37
+ warn "Ignoring :selector in #render since :full => true was given at #{caller(1..1)}"
38
+ options.delete(:selector)
39
+ end
40
+
41
+ PAPER_SIZES = {
42
+ 'A3' => { width: 11.69, height: 16.53 },
43
+ 'A4' => { width: 8.27, height: 11.69 },
44
+ 'A5' => { width: 5.83, height: 8.27 },
45
+ 'Legal' => { width: 8.5, height: 14 },
46
+ 'Letter' => { width: 8.5, height: 11 },
47
+ 'Tabloid' => { width: 11, height: 17 },
48
+ 'Ledger' => { width: 17, height: 11 }
49
+ }.freeze
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara::Apparition
4
+ class Browser
5
+ module Window
6
+ def window_handle
7
+ @current_page_handle
8
+ end
9
+
10
+ def window_handles
11
+ @targets.window_handles
12
+ end
13
+
14
+ def switch_to_window(handle)
15
+ target = @targets.get(handle)
16
+ raise NoSuchWindowError unless target&.page
17
+
18
+ target.page.wait_for_loaded
19
+ @current_page_handle = handle
20
+ end
21
+
22
+ def open_new_window
23
+ context_id = current_target.context_id
24
+ info = command('Target.createTarget', url: 'about:blank', browserContextId: context_id)
25
+ target_id = info['targetId']
26
+ target = DevToolsProtocol::Target.new(self, info.merge('type' => 'page', 'inherit' => current_page))
27
+ target.page # Ensure page object construction happens
28
+ begin
29
+ puts "Adding #{target_id} - #{target.info}" if ENV['DEBUG']
30
+ @targets.add(target_id, target)
31
+ rescue ArgumentError
32
+ puts 'Target already existed' if ENV['DEBUG']
33
+ end
34
+ target_id
35
+ end
36
+
37
+ def close_window(handle)
38
+ @current_page_handle = nil if @current_page_handle == handle
39
+ win_target = @targets.delete(handle)
40
+ warn 'Window was already closed unexpectedly' if win_target.nil?
41
+ win_target&.close
42
+ end
43
+
44
+ def within_window(locator)
45
+ original = window_handle
46
+ handle = find_window_handle(locator)
47
+ switch_to_window(handle)
48
+ yield
49
+ ensure
50
+ switch_to_window(original)
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def find_window_handle(locator)
57
+ return locator if window_handles.include? locator
58
+
59
+ window_handles.each do |handle|
60
+ switch_to_window(handle)
61
+ return handle if evaluate('window.name') == locator
62
+ end
63
+ raise NoSuchWindowError
64
+ end
65
+ end
66
+ end
@@ -5,24 +5,36 @@ require 'capybara/apparition/dev_tools_protocol/session'
5
5
  module Capybara::Apparition
6
6
  module DevToolsProtocol
7
7
  class Target
8
- attr_accessor :info
9
-
10
8
  def initialize(browser, info)
11
9
  @browser = browser
12
- @info = info
10
+ @info = info.dup
13
11
  @page = nil
14
12
  end
15
13
 
14
+ def info
15
+ @info.dup.freeze
16
+ end
17
+
18
+ def update(new_info)
19
+ @info ||= {}
20
+ @info.merge!(new_info)
21
+ info
22
+ end
23
+
16
24
  def id
17
- info['targetId']
25
+ @info['targetId']
26
+ end
27
+
28
+ def context_id
29
+ @info['browserContextId']
18
30
  end
19
31
 
20
32
  def title
21
- info['title']
33
+ @info['title']
22
34
  end
23
35
 
24
36
  def url
25
- info['url']
37
+ @info['url']
26
38
  end
27
39
 
28
40
  def page
@@ -30,7 +42,7 @@ module Capybara::Apparition
30
42
  if info['type'] == 'page'
31
43
  Page.create(@browser, create_session, id,
32
44
  ignore_https_errors: @browser.ignore_https_errors,
33
- js_errors: @browser.js_errors).inherit(info.delete('inherit'))
45
+ js_errors: @browser.js_errors).inherit(@info.delete('inherit'))
34
46
  else
35
47
  nil
36
48
  end
@@ -5,7 +5,8 @@ require 'capybara/apparition/dev_tools_protocol/target'
5
5
  module Capybara::Apparition
6
6
  module DevToolsProtocol
7
7
  class TargetManager
8
- def initialize
8
+ def initialize(browser)
9
+ @browser = browser
9
10
  @targets = {}
10
11
  end
11
12
 
@@ -13,8 +14,18 @@ module Capybara::Apparition
13
14
  @targets[id]
14
15
  end
15
16
 
17
+ def of_type(type)
18
+ @targets.select { |_id, target| target.info['type'] == type }
19
+ end
20
+
16
21
  def add(id, target)
17
- @targets[id] = target
22
+ raise ArgumentError, 'Target already exists' if @targets.key?(id)
23
+
24
+ @targets[id] = if target.is_a? DevToolsProtocol::Target
25
+ target
26
+ else
27
+ DevToolsProtocol::Target.new(@browser, target)
28
+ end
18
29
  end
19
30
 
20
31
  def delete(id)
@@ -135,7 +135,12 @@ module Capybara::Apparition
135
135
  end
136
136
 
137
137
  def reset!
138
- browser.reset
138
+ begin
139
+ browser.reset
140
+ rescue TimeoutError
141
+ puts 'Reset timed out - retrying'
142
+ browser.reset
143
+ end
139
144
  browser.url_blacklist = options[:url_blacklist] || []
140
145
  browser.url_whitelist = options[:url_whitelist] || []
141
146
  @started = false
@@ -227,6 +227,7 @@ module Capybara::Apparition
227
227
  rescue EOFError # rubocop:disable Lint/HandleExceptions
228
228
  end
229
229
  end
230
+ # @listener.abort_on_exception = true
230
231
  end
231
232
  end
232
233
  end
@@ -26,6 +26,10 @@ module Capybara::Apparition
26
26
  nil
27
27
  end
28
28
 
29
+ def [](key)
30
+ result[key]
31
+ end
32
+
29
33
  private
30
34
 
31
35
  def handle_error(error)
@@ -101,7 +101,7 @@ module Capybara::Apparition
101
101
  evaluate_on GET_STYLES_JS, value: styles
102
102
  end
103
103
 
104
- def set(value, **_options)
104
+ def set(value, **options)
105
105
  if tag_name == 'input'
106
106
  case self[:type]
107
107
  when 'radio'
@@ -118,13 +118,13 @@ module Capybara::Apparition
118
118
  when 'datetime-local'
119
119
  set_datetime_local(value)
120
120
  else
121
- set_text(value.to_s)
121
+ set_text(value.to_s, delay: options.fetch(:delay, 0))
122
122
  end
123
123
  elsif tag_name == 'textarea'
124
124
  set_text(value.to_s)
125
125
  elsif self[:isContentEditable]
126
126
  delete_text
127
- send_keys(value.to_s)
127
+ send_keys(value.to_s, delay: options.fetch(:delay, 0))
128
128
  end
129
129
  end
130
130
 
@@ -233,9 +233,9 @@ module Capybara::Apparition
233
233
  false
234
234
  end
235
235
 
236
- def send_keys(*keys)
236
+ def send_keys(*keys, delay: 0, **_opts)
237
237
  click unless evaluate_on CURRENT_NODE_SELECTED_JS
238
- @page.keyboard.type(keys)
238
+ @page.keyboard.type(keys, delay: delay)
239
239
  end
240
240
  alias_method :send_key, :send_keys
241
241
 
@@ -410,16 +410,16 @@ module Capybara::Apparition
410
410
  DevToolsProtocol::RemoteObject.new(@page, response['result'] || response['object']).value
411
411
  end
412
412
 
413
- def set_text(value, clear: nil, **_unused)
413
+ def set_text(value, clear: nil, delay: 0, **_unused)
414
414
  value = value.to_s
415
415
  if value.empty? && clear.nil?
416
416
  evaluate_on CLEAR_ELEMENT_JS
417
417
  elsif clear == :backspace
418
418
  # Clear field by sending the correct number of backspace keys.
419
419
  backspaces = [:backspace] * self.value.to_s.length
420
- send_keys(*([:end] + backspaces + [value]))
420
+ send_keys(*([:end] + backspaces + [value]), delay: delay)
421
421
  elsif clear.is_a? Array
422
- send_keys(*clear, value)
422
+ send_keys(*clear, value, delay: delay)
423
423
  else
424
424
  # Clear field by JavaScript assignment of the value property.
425
425
  # Script can change a readonly element which user input cannot, so
@@ -429,7 +429,7 @@ module Capybara::Apparition
429
429
  arguments[0].value = ''
430
430
  }
431
431
  JS
432
- send_keys(value)
432
+ send_keys(value, delay: delay)
433
433
  end
434
434
  end
435
435
 
@@ -439,7 +439,11 @@ module Capybara::Apparition
439
439
 
440
440
  def set_date(value)
441
441
  value = SettableValue.new(value)
442
- return set_text(value) unless value.dateable?
442
+ unless value.dateable?
443
+ # click(x:5, y:10)
444
+ # debug
445
+ return set_text(value)
446
+ end
443
447
 
444
448
  # TODO: this would be better if locale can be detected and correct keystrokes sent
445
449
  update_value_js(value.to_date_str)
@@ -133,11 +133,13 @@ module Capybara::Apparition
133
133
 
134
134
  def render(options)
135
135
  wait_for_loaded
136
+ pixel_ratio = evaluate('window.devicePixelRatio')
137
+ scale = (@browser.zoom_factor || 1).to_f / pixel_ratio
136
138
  if options[:format].to_s == 'pdf'
137
139
  params = {}
138
140
  params[:paperWidth] = @browser.paper_size[:width].to_f if @browser.paper_size
139
141
  params[:paperHeight] = @browser.paper_size[:height].to_f if @browser.paper_size
140
- params[:scale] = @browser.zoom_factor if @browser.zoom_factor
142
+ params[:scale] = scale
141
143
  command('Page.printToPDF', params)
142
144
  else
143
145
  clip_options = if options[:selector]
@@ -152,8 +154,7 @@ module Capybara::Apparition
152
154
  { width: window.innerWidth, height: window.innerHeight }
153
155
  JS
154
156
  end
155
- options[:clip] = { x: 0, y: 0, scale: 1 }.merge(clip_options)
156
- options[:clip][:scale] = @browser.zoom_factor || 1
157
+ options[:clip] = { x: 0, y: 0, scale: scale }.merge(clip_options)
157
158
  command('Page.captureScreenshot', options)
158
159
  end['data']
159
160
  end
@@ -595,6 +596,9 @@ module Capybara::Apparition
595
596
  unless @temp_no_redirect_headers.empty? || !navigation
596
597
  headers.delete_if { |name, value| @temp_no_redirect_headers[name] == value }
597
598
  end
599
+ if (accept = perm_headers.keys.find { |k| k =~ /accept/i })
600
+ headers[accept] = perm_headers[accept]
601
+ end
598
602
 
599
603
  if @url_blacklist.any? { |r| url.match Regexp.escape(r).gsub('\*', '.*?') }
600
604
  block_request(interception_id, 'Failed')
@@ -10,8 +10,8 @@ module Capybara::Apparition
10
10
  @pressed_keys = {}
11
11
  end
12
12
 
13
- def type(keys)
14
- type_with_modifiers(Array(keys))
13
+ def type(keys, delay:)
14
+ type_with_modifiers(Array(keys), delay: delay)
15
15
  end
16
16
 
17
17
  def press(key)
@@ -66,14 +66,19 @@ module Capybara::Apparition
66
66
 
67
67
  private
68
68
 
69
- def type_with_modifiers(keys)
69
+ def type_with_modifiers(keys, delay:)
70
70
  old_pressed_keys, @pressed_keys = @pressed_keys, {}
71
71
 
72
72
  Array(keys).each do |sequence|
73
73
  case sequence
74
- when Array then type_with_modifiers(sequence)
75
- when String then sequence.each_char { |char| press char }
76
- else press sequence
74
+ when Array then type_with_modifiers(sequence, delay: delay)
75
+ when String then sequence.each_char do |char|
76
+ press char
77
+ sleep delay
78
+ end
79
+ else
80
+ press sequence
81
+ sleep delay
77
82
  end
78
83
  end
79
84
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Capybara
4
4
  module Apparition
5
- VERSION = '0.0.6'
5
+ VERSION = '0.1.0'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: apparition
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.6
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Thomas Walpole
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-01-31 00:00:00.000000000 Z
11
+ date: 2019-02-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: backports
@@ -224,6 +224,13 @@ files:
224
224
  - README.md
225
225
  - lib/capybara/apparition.rb
226
226
  - lib/capybara/apparition/browser.rb
227
+ - lib/capybara/apparition/browser/auth.rb
228
+ - lib/capybara/apparition/browser/cookie.rb
229
+ - lib/capybara/apparition/browser/frame.rb
230
+ - lib/capybara/apparition/browser/header.rb
231
+ - lib/capybara/apparition/browser/modal.rb
232
+ - lib/capybara/apparition/browser/render.rb
233
+ - lib/capybara/apparition/browser/window.rb
227
234
  - lib/capybara/apparition/console.rb
228
235
  - lib/capybara/apparition/cookie.rb
229
236
  - lib/capybara/apparition/cookie_jar.rb