apparition 0.3.0 → 0.4.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: f5675a80f435049166cf933b540e00b5569b0f516099597cdc071cc4c7767ff0
4
- data.tar.gz: 8a63bf45f5376a4eb0a65954bd3863062f1aea67b030cb3ee2b96c7cfe1d108d
3
+ metadata.gz: 350a10ffb794579fb942f37a1739bf104b0230bd831945cbc22a20e2273927fd
4
+ data.tar.gz: e0bbdc5f79d03fcdedabd5db29ff990ee57801087d9c33ef98e85f9de833be8d
5
5
  SHA512:
6
- metadata.gz: 7d0b64f71ec2e0a026bc5f0cea02588e550a5e27768b5368041bfbbd206e758654900cf22c35620d88f0618a76a12b874120513ae3d606e8ee481a5021bf3297
7
- data.tar.gz: b279c8871baedd04acf1e717fddbf2898ae9796e0c1152fd17c0ccc160deaa10cb0cc86243037448883e10da3a90844351c60e097e0b6b5aebb8e87b0e796f65
6
+ metadata.gz: 313247fbbaccd8197d36ccd68adede1a33c944f46a5b30dce143606d4cc00bc61a3aab99ba35820dc6e2e3cbb7c09432084f221a36fee2f355fbd7d59639bb6f
7
+ data.tar.gz: 73f91c766ab2bc02f2a58b07c95cce2a01bc71dff6ca8f6eb3374b36b5a8efbc73c95d26f48766e00d3fbd7f49003765b293af8725028afa64d798f2c5ebddb2
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![Build Status](https://secure.travis-ci.org/twalpole/apparition.svg)](http://travis-ci.org/twalpole/apparition)
4
4
 
5
- Apparition is a driver for [Capybara](https://github.com/jnicklas/capybara). It allows you to
5
+ Apparition is a driver for [Capybara](https://github.com/teamcapybara/capybara). It allows you to
6
6
  run your Capybara tests in the Chrome browser via CDP (no selenium or chromedriver needed) in a headless or
7
7
  headed configuration. It started as a fork of Poltergeist and attempts to maintain as much compatibility
8
8
  with the Poltergeist API as possible. Implementing the `capybara-webkit` specific driver methods has also begun.
@@ -205,7 +205,7 @@ test to allow sufficient time for the page to settle.
205
205
 
206
206
  If you have these types of problems, read through the [Capybara
207
207
  documentation on asynchronous
208
- JavaScript](https://github.com/jnicklas/capybara#asynchronous-javascript-ajax-and-friends)
208
+ JavaScript](https://github.com/teamcapybara/capybara#asynchronous-javascript-ajax-and-friends)
209
209
  which explains the tools that Capybara provides for dealing with this.
210
210
 
211
211
  ### Filing a bug ###
@@ -203,9 +203,9 @@ module Capybara::Apparition
203
203
  # @current_page_handle ||= target_info['targetId'] if target_info['type'] == 'page'
204
204
  # end
205
205
 
206
- @client.on 'Target.targetDestroyed' do |info|
207
- puts "**** Target Destroyed Info: #{info}" if ENV['DEBUG']
208
- @pages.delete(info['targetId'])
206
+ @client.on 'Target.targetDestroyed' do |target_id:, **info|
207
+ puts "**** Target Destroyed Info: #{target_id} - #{info}" if ENV['DEBUG']
208
+ @pages.delete(target_id)
209
209
  end
210
210
 
211
211
  # @client.on 'Target.targetInfoChanged' do |info|
@@ -3,7 +3,7 @@
3
3
  module Capybara::Apparition
4
4
  class Configuration
5
5
  class << self
6
- private # rubocop:disable Layout/IndentationWidth
6
+ private
7
7
 
8
8
  def instance
9
9
  @instance ||= new
@@ -25,10 +25,12 @@ module Capybara::Apparition
25
25
  elsif date?
26
26
  res = get_date_string(id)
27
27
  DateTime.parse(res)
28
- elsif object_class? || css_style?
29
- extract_properties_object(get_remote_object(id), object_cache)
30
28
  elsif window_class?
31
29
  { object_id: id }
30
+ elsif validity_state?
31
+ extract_properties_object(get_remote_object(id, false), object_cache)
32
+ elsif object_class? || css_style? || classname?
33
+ extract_properties_object(get_remote_object(id), object_cache)
32
34
  else
33
35
  params['value']
34
36
  end
@@ -46,6 +48,8 @@ module Capybara::Apparition
46
48
  def object_class?; classname == 'Object' end
47
49
  def css_style?; classname == 'CSSStyleDeclaration' end
48
50
  def window_class?; classname == 'Window' end
51
+ def validity_state?; classname == 'ValidityState' end
52
+ def classname?; !classname.nil? end
49
53
 
50
54
  def type; params['type'] end
51
55
  def subtype; params['subtype'] end
@@ -243,7 +243,7 @@ module Capybara::Apparition
243
243
  end
244
244
 
245
245
  def proxy_authorize(user = nil, password = nil)
246
- browser.set_proxy_aauth(user, password)
246
+ browser.set_proxy_auth(user, password)
247
247
  end
248
248
 
249
249
  def basic_authorize(user = nil, password = nil)
@@ -206,7 +206,9 @@ module Capybara::Apparition
206
206
  event_name = event['method']
207
207
  handlers[event_name].each do |handler|
208
208
  puts "Calling handler for #{event_name}" if ENV['DEBUG'] == 'V'
209
- handler.call(event['params'])
209
+ # TODO: Update this to use transform_keys when we dump Ruby 2.4
210
+ # handler.call(event['params'].transform_keys(&method(:snake_sym)))
211
+ handler.call(event['params'].each_with_object({}) { |(k, v), hash| hash[snake_sym(k)] = v })
210
212
  end
211
213
  end
212
214
 
@@ -229,5 +231,12 @@ module Capybara::Apparition
229
231
  end
230
232
  # @listener.abort_on_exception = true
231
233
  end
234
+
235
+ def snake_sym(str)
236
+ str.gsub(/([a-z\d])([A-Z])/, '\1_\2')
237
+ .tr('-', '_')
238
+ .downcase
239
+ .to_sym
240
+ end
232
241
  end
233
242
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'open3'
4
+
3
5
  module Capybara::Apparition
4
6
  class Browser
5
7
  class Launcher
@@ -44,23 +46,23 @@ module Capybara::Apparition
44
46
 
45
47
  def start
46
48
  @output = Queue.new
47
- @read_io, @write_io = IO.pipe
49
+
50
+ process_options = {}
51
+ process_options[:pgroup] = true unless Capybara::Apparition.windows?
52
+ cmd = [path] + @options.map { |k, v| v.nil? ? "--#{k}" : "--#{k}=#{v}" }
53
+
54
+ stdin, @stdout_stderr, wait_thr = Open3.popen2e(*cmd, process_options)
55
+ stdin.close
56
+
57
+ @pid = wait_thr.pid
48
58
 
49
59
  @out_thread = Thread.new do
50
- while !@read_io.eof? && (data = @read_io.readpartial(512))
60
+ while !@stdout_stderr.eof? && (data = @stdout_stderr.readpartial(512))
51
61
  @output << data
52
62
  end
53
63
  end
54
64
 
55
- process_options = { in: File::NULL }
56
- process_options[:pgroup] = true unless Capybara::Apparition.windows?
57
- process_options[:out] = process_options[:err] = @write_io if Capybara::Apparition.mri?
58
-
59
- redirect_stdout do
60
- cmd = [path] + @options.map { |k, v| v.nil? ? "--#{k}" : "--#{k}=#{v}" }
61
- @pid = ::Process.spawn(*cmd, process_options)
62
- ObjectSpace.define_finalizer(self, self.class.process_killer(@pid))
63
- end
65
+ ObjectSpace.define_finalizer(self, self.class.process_killer(@pid))
64
66
  end
65
67
 
66
68
  def stop
@@ -87,10 +89,13 @@ module Capybara::Apparition
87
89
  @ws_url ||= begin
88
90
  regexp = %r{DevTools listening on (ws://.*)}
89
91
  url = nil
92
+
93
+ sleep 3
90
94
  loop do
91
95
  break if (url = @output.pop.scan(regexp)[0])
92
96
  end
93
97
  @out_thread.kill
98
+ @out_thread.join # wait for thread to end before closing io
94
99
  close_io
95
100
  Addressable::URI.parse(url[0])
96
101
  end
@@ -98,36 +103,15 @@ module Capybara::Apparition
98
103
 
99
104
  private
100
105
 
101
- def redirect_stdout
102
- if Capybara::Apparition.mri?
103
- yield
104
- else
105
- begin
106
- prev = STDOUT.dup
107
- $stdout = @write_io
108
- STDOUT.reopen(@write_io)
109
- yield
110
- ensure
111
- STDOUT.reopen(prev)
112
- $stdout = STDOUT
113
- prev.close
114
- end
115
- end
116
- end
117
-
118
106
  def kill
119
107
  self.class.process_killer(@pid).call
120
108
  @pid = nil
121
109
  end
122
110
 
123
111
  def close_io
124
- [@write_io, @read_io].each do |io|
125
- begin
126
- io.close unless io.closed?
127
- rescue IOError
128
- raise unless RUBY_ENGINE == 'jruby'
129
- end
130
- end
112
+ @stdout_stderr.close unless @stdout_stderr.closed?
113
+ rescue IOError
114
+ raise unless RUBY_ENGINE == 'jruby'
131
115
  end
132
116
 
133
117
  def path
@@ -17,23 +17,23 @@ module Capybara::Apparition::NetworkTraffic
17
17
  end
18
18
 
19
19
  def request_id
20
- @data['requestId']
20
+ @data[:request_id]
21
21
  end
22
22
 
23
23
  def url
24
- @data.dig('request', 'url')
24
+ @data[:request]&.dig('url')
25
25
  end
26
26
 
27
27
  def method
28
- @data.dig('request', 'method')
28
+ @data[:request]&.dig('method')
29
29
  end
30
30
 
31
31
  def headers
32
- @data.dig('requst', 'headers')
32
+ @data[:request]&.dig('headers')
33
33
  end
34
34
 
35
35
  def time
36
- @data['timestamp'] && Time.parse(@data['timestamp'])
36
+ @data[:timestamp] && Time.parse(@data[:timestamp])
37
37
  end
38
38
 
39
39
  def blocked?
@@ -130,6 +130,8 @@ module Capybara::Apparition
130
130
  set_time(value)
131
131
  when 'datetime-local'
132
132
  set_datetime_local(value)
133
+ when 'color'
134
+ set_color(value)
133
135
  else
134
136
  set_text(value.to_s, { delay: 0 }.merge(options))
135
137
  end
@@ -199,7 +201,10 @@ module Capybara::Apparition
199
201
 
200
202
  test = mouse_event_test(pos)
201
203
  raise ::Capybara::Apparition::MouseEventImpossible.new(self, 'args' => ['click']) if test.nil?
202
- raise ::Capybara::Apparition::MouseEventFailed.new(self, 'args' => ['click', test.selector, pos]) unless test.success
204
+
205
+ unless options[:x] && options[:y]
206
+ raise ::Capybara::Apparition::MouseEventFailed.new(self, 'args' => ['click', test.selector, pos]) unless test.success
207
+ end
203
208
 
204
209
  @page.mouse.click_at pos.merge(button: button, count: count, modifiers: keys)
205
210
  if ENV['DEBUG']
@@ -278,9 +283,13 @@ module Capybara::Apparition
278
283
  evaluate_on GET_PATH_JS
279
284
  end
280
285
 
281
- def element_click_pos(x: nil, y: nil, **)
286
+ def element_click_pos(x: nil, y: nil, offset: nil, **)
282
287
  if x && y
283
- visible_top_left.tap do |p|
288
+ if offset == :center
289
+ visible_center
290
+ else
291
+ visible_top_left
292
+ end.tap do |p|
284
293
  p[:x] += x
285
294
  p[:y] += y
286
295
  end
@@ -290,7 +299,7 @@ module Capybara::Apparition
290
299
  end
291
300
 
292
301
  def visible_top_left
293
- rect = in_view_bounding_rect
302
+ rect = in_view_client_rect
294
303
  return nil if rect.nil?
295
304
 
296
305
  frame_offset = @page.current_frame_offset
@@ -324,7 +333,7 @@ module Capybara::Apparition
324
333
  end
325
334
 
326
335
  def visible_center(allow_scroll: true)
327
- rect = in_view_bounding_rect(allow_scroll: allow_scroll)
336
+ rect = in_view_client_rect(allow_scroll: allow_scroll)
328
337
  return nil if rect.nil?
329
338
 
330
339
  frame_offset = @page.current_frame_offset
@@ -367,12 +376,16 @@ module Capybara::Apparition
367
376
  end
368
377
 
369
378
  def top_left
370
- result = evaluate_on GET_BOUNDING_CLIENT_RECT_JS
379
+ result = evaluate_on GET_CLIENT_RECT_JS
371
380
  return nil if result.nil?
372
381
 
373
382
  { x: result['x'], y: result['y'] }
374
383
  end
375
384
 
385
+ def rect
386
+ evaluate_on GET_CLIENT_RECT_JS
387
+ end
388
+
376
389
  def scroll_by(x, y)
377
390
  evaluate_on <<~JS, { value: x }, value: y
378
391
  (x, y) => this.scrollBy(x,y)
@@ -447,9 +460,9 @@ module Capybara::Apparition
447
460
  @page.keyboard.type(keys, delay: delay)
448
461
  end
449
462
 
450
- def in_view_bounding_rect(allow_scroll: true)
463
+ def in_view_client_rect(allow_scroll: true)
451
464
  evaluate_on('() => this.scrollIntoViewIfNeeded()') if allow_scroll
452
- result = evaluate_on GET_BOUNDING_CLIENT_RECT_JS
465
+ result = evaluate_on GET_CLIENT_RECT_JS
453
466
  result = result['model'] if result && result['model']
454
467
  result
455
468
  end
@@ -517,6 +530,10 @@ module Capybara::Apparition
517
530
  update_value_js(value.to_datetime_str)
518
531
  end
519
532
 
533
+ def set_color(value)
534
+ update_value_js(value.to_s)
535
+ end
536
+
520
537
  def update_value_js(value)
521
538
  evaluate_on(<<~JS, value: value)
522
539
  value => {
@@ -760,7 +777,11 @@ module Capybara::Apparition
760
777
  (parseFloat(style.opacity) == 0)) {
761
778
  return false;
762
779
  }
763
- el = el.parentElement;
780
+ var parent = el.parentElement;
781
+ if (parent && (parent.tagName == 'DETAILS') && !parent.open && (el.tagName != 'SUMMARY')) {
782
+ return false;
783
+ }
784
+ el = parent;
764
785
  }
765
786
  return true;
766
787
  }
@@ -777,9 +798,10 @@ module Capybara::Apparition
777
798
  }
778
799
  JS
779
800
 
780
- GET_BOUNDING_CLIENT_RECT_JS = <<~JS
801
+ GET_CLIENT_RECT_JS = <<~JS
781
802
  function(){
782
- rect = this.getBoundingClientRect();
803
+ var rects = [...this.getClientRects()]
804
+ var rect = rects.find(r => (r.height && r.width)) || this.getBoundingClientRect();
783
805
  return rect.toJSON();
784
806
  }
785
807
  JS
@@ -2,26 +2,27 @@
2
2
 
3
3
  module Capybara::Apparition
4
4
  module Drag
5
- def drag_to(other, delay: 0.1)
6
- return html5_drag_to(other) if html5_draggable?
7
-
8
- pos = visible_center
9
- raise ::Capybara::Apparition::MouseEventImpossible.new(self, 'args' => ['drag_to']) if pos.nil?
10
-
11
- test = mouse_event_test(pos)
12
- raise ::Capybara::Apparition::MouseEventFailed.new(self, 'args' => ['drag', test.selector, pos]) unless test.success
13
-
14
- begin
15
- @page.mouse.move_to(pos).down
16
- sleep delay
17
-
18
- other_pos = other.visible_center
19
- raise ::Capybara::Apparition::MouseEventImpossible.new(self, 'args' => ['drag_to']) if other_pos.nil?
20
-
21
- @page.mouse.move_to(other_pos.merge(button: 'left'))
22
- sleep delay
23
- ensure
24
- @page.mouse.up
5
+ def drag_to(other, delay: 0.1, html5: nil)
6
+ driver.execute_script MOUSEDOWN_TRACKER
7
+ scroll_if_needed
8
+ m = @page.mouse
9
+ m.move_to(visible_center)
10
+ sleep delay
11
+ m.down
12
+ html5 = !driver.evaluate_script(LEGACY_DRAG_CHECK, self) if html5.nil?
13
+ if html5
14
+ driver.execute_script HTML5_DRAG_DROP_SCRIPT, self, other, delay
15
+ m.up(other.visible_center)
16
+ else
17
+ begin
18
+ other.scroll_if_needed
19
+ sleep delay
20
+ m.move_to(other.visible_center)
21
+ sleep delay
22
+ ensure
23
+ m.up
24
+ sleep delay
25
+ end
25
26
  end
26
27
  end
27
28
 
@@ -34,7 +35,7 @@ module Capybara::Apparition
34
35
 
35
36
  @page.mouse.move_to(pos).down
36
37
  sleep delay
37
- @page.mouse.move_to(other_pos.merge(button: 'left'))
38
+ @page.mouse.move_to(other_pos)
38
39
  sleep delay
39
40
  @page.mouse.up
40
41
  end
@@ -104,34 +105,29 @@ module Capybara::Apparition
104
105
  }
105
106
  JS
106
107
 
107
- private
108
-
109
- def html5_drag_to(element)
110
- driver.execute_script MOUSEDOWN_TRACKER
111
- scroll_if_needed
112
- @page.mouse.move_to(visible_center).down
113
- if driver.evaluate_script('window.capybara_mousedown_prevented')
114
- element.scroll_if_needed
115
- @page.mouse.move_to(element.visible_center).up
116
- else
117
- driver.execute_script HTML5_DRAG_DROP_SCRIPT, self, element
118
- @page.mouse.up(element.visible_center)
119
- end
120
- end
121
-
122
- def html5_draggable?
123
- native.property('draggable')
124
- end
125
-
126
108
  MOUSEDOWN_TRACKER = <<~JS
109
+ window.capybara_mousedown_prevented = null;
127
110
  document.addEventListener('mousedown', ev => {
128
111
  window.capybara_mousedown_prevented = ev.defaultPrevented;
129
112
  }, { once: true, passive: true })
130
113
  JS
131
114
 
115
+ LEGACY_DRAG_CHECK = <<~JS
116
+ (function(el){
117
+ if ([true, null].includes(window.capybara_mousedown_prevented)){
118
+ return true;
119
+ }
120
+ do {
121
+ if (el.draggable) return false;
122
+ } while (el = el.parentElement );
123
+ return true;
124
+ })(arguments[0])
125
+ JS
126
+
132
127
  HTML5_DRAG_DROP_SCRIPT = <<~JS
133
128
  var source = arguments[0];
134
129
  var target = arguments[1];
130
+ var step_delay = arguments[2] * 1000;
135
131
 
136
132
  function rectCenter(rect){
137
133
  return new DOMPoint(
@@ -171,9 +167,60 @@ module Capybara::Apparition
171
167
  return new DOMPoint(pt.x,pt.y);
172
168
  }
173
169
 
170
+ function dragStart() {
171
+ return new Promise( resolve => {
172
+ var dragEvent = new DragEvent('dragstart', opts);
173
+ source.dispatchEvent(dragEvent);
174
+ setTimeout(resolve, step_delay)
175
+ })
176
+ }
177
+
178
+ function dragEnter() {
179
+ return new Promise( resolve => {
180
+ target.scrollIntoView({behavior: 'instant', block: 'center', inline: 'center'});
181
+ let targetRect = target.getBoundingClientRect(),
182
+ sourceCenter = rectCenter(source.getBoundingClientRect());
183
+
184
+ // fire 2 dragover events to simulate dragging with a direction
185
+ let entryPoint = pointOnRect(sourceCenter, targetRect);
186
+ let dragOverOpts = Object.assign({clientX: entryPoint.x, clientY: entryPoint.y}, opts);
187
+ let dragOverEvent = new DragEvent('dragover', dragOverOpts);
188
+ target.dispatchEvent(dragOverEvent);
189
+ setTimeout(resolve, step_delay)
190
+ })
191
+ }
192
+
193
+ function dragOnto() {
194
+ return new Promise( resolve => {
195
+ var targetCenter = rectCenter(target.getBoundingClientRect());
196
+ dragOverOpts = Object.assign({clientX: targetCenter.x, clientY: targetCenter.y}, opts);
197
+ dragOverEvent = new DragEvent('dragover', dragOverOpts);
198
+ target.dispatchEvent(dragOverEvent);
199
+ setTimeout(resolve, step_delay, dragOverEvent.defaultPrevented);
200
+ })
201
+ }
202
+
203
+ function dragLeave(drop) {
204
+ return new Promise( resolve => {
205
+ var dragLeaveEvent = new DragEvent('dragleave', opts);
206
+ target.dispatchEvent(dragLeaveEvent);
207
+ if (drop) {
208
+ var dropEvent = new DragEvent('drop', opts);
209
+ target.dispatchEvent(dropEvent);
210
+ }
211
+ var dragEndEvent = new DragEvent('dragend', opts);
212
+ source.dispatchEvent(dragEndEvent);
213
+ setTimeout(resolve, step_delay);
214
+ })
215
+ }
216
+
174
217
  var dt = new DataTransfer();
175
218
  var opts = { cancelable: true, bubbles: true, dataTransfer: dt };
176
219
 
220
+ while (source && !source.draggable) {
221
+ source = source.parentElement;
222
+ }
223
+
177
224
  if (source.tagName == 'A'){
178
225
  dt.setData('text/uri-list', source.href);
179
226
  dt.setData('text', source.href);
@@ -183,31 +230,7 @@ module Capybara::Apparition
183
230
  dt.setData('text', source.src);
184
231
  }
185
232
 
186
- var dragEvent = new DragEvent('dragstart', opts);
187
- source.dispatchEvent(dragEvent);
188
- target.scrollIntoView({behavior: 'instant', block: 'center', inline: 'center'});
189
- var targetRect = target.getBoundingClientRect();
190
- var sourceCenter = rectCenter(source.getBoundingClientRect());
191
-
192
- // fire 2 dragover events to simulate dragging with a direction
193
- var entryPoint = pointOnRect(sourceCenter, targetRect)
194
- var dragOverOpts = Object.assign({clientX: entryPoint.x, clientY: entryPoint.y}, opts);
195
- var dragOverEvent = new DragEvent('dragover', dragOverOpts);
196
- target.dispatchEvent(dragOverEvent);
197
-
198
- var targetCenter = rectCenter(targetRect);
199
- dragOverOpts = Object.assign({clientX: targetCenter.x, clientY: targetCenter.y}, opts);
200
- dragOverEvent = new DragEvent('dragover', dragOverOpts);
201
- target.dispatchEvent(dragOverEvent);
202
-
203
- var dragLeaveEvent = new DragEvent('dragleave', opts);
204
- target.dispatchEvent(dragLeaveEvent);
205
- if (dragOverEvent.defaultPrevented) {
206
- var dropEvent = new DragEvent('drop', opts);
207
- target.dispatchEvent(dropEvent);
208
- }
209
- var dragEndEvent = new DragEvent('dragend', opts);
210
- source.dispatchEvent(dragEndEvent);
233
+ dragStart().then(dragEnter).then(dragOnto).then(dragLeave)
211
234
  JS
212
235
  end
213
236
  end
@@ -285,7 +285,6 @@ module Capybara::Apparition
285
285
  navigate_opts = { url: url, transitionType: 'reload' }
286
286
  navigate_opts[:referrer] = extra_headers['Referer'] if extra_headers['Referer']
287
287
  response = command('Page.navigate', navigate_opts)
288
-
289
288
  raise StatusFailError, 'args' => [url, response['errorText']] if response['errorText']
290
289
 
291
290
  main_frame.loading(response['loaderId'])
@@ -426,9 +425,9 @@ module Capybara::Apparition
426
425
  end
427
426
 
428
427
  def register_event_handlers
429
- @session.on 'Page.javascriptDialogOpening' do |params|
430
- type = params['type'].to_sym
431
- accept = accept_modal?(type, message: params['message'], manual: params['hasBrowserHandler'])
428
+ @session.on 'Page.javascriptDialogOpening' do |type:, message:, has_browser_handler:, **params|
429
+ type = type.to_sym
430
+ accept = accept_modal?(type, message: message, manual: has_browser_handler)
432
431
  next if accept.nil?
433
432
 
434
433
  if type == :prompt
@@ -436,7 +435,7 @@ module Capybara::Apparition
436
435
  when false
437
436
  async_command('Page.handleJavaScriptDialog', accept: false)
438
437
  when true
439
- async_command('Page.handleJavaScriptDialog', accept: true, promptText: params['defaultPrompt'])
438
+ async_command('Page.handleJavaScriptDialog', accept: true, promptText: params[:default_prompt])
440
439
  else
441
440
  async_command('Page.handleJavaScriptDialog', accept: true, promptText: accept)
442
441
  end
@@ -451,39 +450,38 @@ module Capybara::Apparition
451
450
  end
452
451
  end
453
452
 
454
- @session.on 'Page.windowOpen' do |params|
453
+ @session.on 'Page.windowOpen' do |**params|
455
454
  puts "**** windowOpen was called with: #{params}" if ENV['DEBUG']
456
455
  @browser.refresh_pages(opener: self)
457
456
  end
458
457
 
459
- @session.on 'Page.frameAttached' do |params|
458
+ @session.on 'Page.frameAttached' do |**params|
460
459
  puts "**** frameAttached called with #{params}" if ENV['DEBUG']
461
460
  # @frames.get(params["frameId"]) = Frame.new(params)
462
461
  end
463
462
 
464
- @session.on 'Page.frameDetached' do |params|
465
- @frames.delete(params['frameId'])
466
- puts "**** frameDetached called with #{params}" if ENV['DEBUG']
463
+ @session.on 'Page.frameDetached' do |frame_id:, **params|
464
+ @frames.delete(frame_id)
465
+ puts "**** frameDetached called with #{frame_id} : #{params}" if ENV['DEBUG']
467
466
  end
468
467
 
469
- @session.on 'Page.frameNavigated' do |params|
470
- puts "**** frameNavigated called with #{params}" if ENV['DEBUG']
471
- frame_params = params['frame']
472
- unless @frames.exists?(frame_params['id'])
473
- puts "**** creating frome for #{frame_params['id']}" if ENV['DEBUG']
474
- @frames.add(frame_params['id'], frame_params)
468
+ @session.on 'Page.frameNavigated' do |frame:|
469
+ puts "**** frameNavigated called with #{frame}" if ENV['DEBUG']
470
+ unless @frames.exists?(frame['id'])
471
+ puts "**** creating frame for #{frame['id']}" if ENV['DEBUG']
472
+ @frames.add(frame['id'], frame)
475
473
  end
476
- @frames.get(frame_params['id'])&.loading(frame_params['loaderId'] || -1)
474
+ @frames.get(frame['id'])&.loading(frame['loaderId'] || -1)
477
475
  end
478
476
 
479
- @session.on 'Page.frameStartedLoading' do |params|
480
- puts "Setting loading for #{params['frameId']}" if ENV['DEBUG']
481
- @frames.get(params['frameId'])&.loading(-1)
477
+ @session.on 'Page.frameStartedLoading' do |frame_id:|
478
+ puts "Setting loading for #{frame_id}" if ENV['DEBUG']
479
+ @frames.get(frame_id)&.loading(-1)
482
480
  end
483
481
 
484
- @session.on 'Page.frameStoppedLoading' do |params|
485
- puts "Setting loaded for #{params['frameId']}" if ENV['DEBUG']
486
- @frames.get(params['frameId'])&.loaded!
482
+ @session.on 'Page.frameStoppedLoading' do |frame_id:|
483
+ puts "Setting loaded for #{frame_id}" if ENV['DEBUG']
484
+ @frames.get(frame_id)&.loaded!
487
485
  end
488
486
 
489
487
  # @session.on 'Page.lifecycleEvent' do |params|
@@ -505,16 +503,14 @@ module Capybara::Apparition
505
503
  main_frame.loaded! if @status_code != 200
506
504
  end
507
505
 
508
- @session.on 'Page.navigatedWithinDocument' do |params|
509
- puts "**** navigatedWithinDocument called with #{params}" if ENV['DEBUG']
510
- frame_id = params['frameId']
511
- # @frames.get(frame_id).state = :loaded if frame_id == main_frame.id
506
+ @session.on 'Page.navigatedWithinDocument' do |frame_id:, **params|
507
+ puts "**** navigatedWithinDocument called with #{frame_id}: #{params}" if ENV['DEBUG']
512
508
  @frames.get(frame_id).loaded! if frame_id == main_frame.id
513
509
  end
514
510
 
515
- @session.on 'Runtime.executionContextCreated' do |params|
511
+ @session.on 'Runtime.executionContextCreated' do |context:|
516
512
  puts "**** executionContextCreated: #{params}" if ENV['DEBUG']
517
- context = params['context']
513
+ # context = params['context']
518
514
  frame_id = context.dig('auxData', 'frameId')
519
515
  if context.dig('auxData', 'isDefault') && frame_id
520
516
  if (frame = @frames.get(frame_id))
@@ -525,63 +521,92 @@ module Capybara::Apparition
525
521
  end
526
522
  end
527
523
 
528
- @session.on 'Runtime.executionContextDestroyed' do |params|
529
- puts "executionContextDestroyed: #{params}" if ENV['DEBUG']
530
- @frames.destroy_context(params['executionContextId'])
524
+ @session.on 'Runtime.executionContextDestroyed' do |execution_context_id:, **params|
525
+ puts "executionContextDestroyed: #{execution_context_id} : #{params}" if ENV['DEBUG']
526
+ @frames.destroy_context(execution_context_id)
531
527
  end
532
528
 
533
- @session.on 'Network.requestWillBeSent' do |params|
534
- @open_resource_requests[params['requestId']] = params.dig('request', 'url')
529
+ @session.on 'Network.requestWillBeSent' do |request_id:, request: nil, **|
530
+ @open_resource_requests[request_id] = request&.dig('url')
535
531
  end
536
532
 
537
- @session.on 'Network.responseReceived' do |params|
538
- @open_resource_requests.delete(params['requestId'])
533
+ @session.on 'Network.responseReceived' do |request_id:, **|
534
+ @open_resource_requests.delete(request_id)
539
535
  temp_headers.clear
540
536
  update_headers(async: true)
541
537
  end
542
538
 
543
- @session.on 'Network.requestWillBeSent' do |params|
539
+ @session.on 'Network.requestWillBeSent' do |**params|
544
540
  @network_traffic.push(NetworkTraffic::Request.new(params))
545
541
  end
546
542
 
547
- @session.on 'Network.responseReceived' do |params|
548
- req = @network_traffic.find { |request| request.request_id == params['requestId'] }
549
- req.response = NetworkTraffic::Response.new(params['response']) if req
543
+ @session.on 'Network.responseReceived' do |request_id:, response:, **|
544
+ req = @network_traffic.find { |request| request.request_id == request_id }
545
+ req.response = NetworkTraffic::Response.new(response) if req
550
546
  end
551
547
 
552
- @session.on 'Network.responseReceived' do |params|
553
- if params['type'] == 'Document'
554
- @response_headers[params['frameId']] = params['response']['headers']
555
- @status_code = params['response']['status']
548
+ @session.on 'Network.responseReceived' do |type:, frame_id: nil, response: nil, **|
549
+ if type == 'Document'
550
+ @response_headers[frame_id] = response['headers']
551
+ @status_code = response['status']
556
552
  end
557
553
  end
558
554
 
559
- @session.on 'Network.loadingFailed' do |params|
560
- req = @network_traffic.find { |request| request.request_id == params['requestId'] }
561
- req&.blocked_params = params if params['blockedReason']
562
- if params['type'] == 'Document'
563
- puts "Loading Failed - request: #{params['requestId']} : #{params['errorText']}" if ENV['DEBUG']
555
+ @session.on 'Network.loadingFailed' do |type:, request_id:, blocked_reason: nil, error_text: nil, **params|
556
+ req = @network_traffic.find { |request| request.request_id == request_id }
557
+ req&.blocked_params = params if blocked_reason
558
+ if type == 'Document'
559
+ puts "Loading Failed - request: #{request_id} : #{error_text}" if ENV['DEBUG']
564
560
  end
565
561
  end
566
562
 
567
- @session.on 'Network.requestIntercepted' do |params|
568
- request, interception_id = *params.values_at('request', 'interceptionId')
569
- if params['authChallenge']
570
- if params['authChallenge']['source'] == 'Proxy'
563
+ @session.on(
564
+ 'Network.requestIntercepted'
565
+ ) do |request:, interception_id:, auth_challenge: nil, is_navigation_request: nil, **|
566
+ if auth_challenge
567
+ if auth_challenge['source'] == 'Proxy'
571
568
  handle_proxy_auth(interception_id)
572
569
  else
573
570
  handle_user_auth(interception_id)
574
571
  end
575
572
  else
576
- process_intercepted_request(interception_id, request, params['isNavigationRequest'])
573
+ process_intercepted_request(interception_id, request, is_navigation_request)
574
+ end
575
+ end
576
+
577
+ @session.on 'Fetch.requestPaused' do |request:, request_id:, resource_type:, **|
578
+ process_intercepted_fetch(request_id, request, resource_type)
579
+ end
580
+
581
+ @session.on 'Fetch.authRequired' do |request_id:, auth_challenge: nil, **|
582
+ next unless auth_challenge
583
+
584
+ credentials_response = if auth_challenge['source'] == 'Proxy'
585
+ if @proxy_auth_attempts.include?(request_id)
586
+ puts 'Cancelling proxy auth' if ENV['DEBUG']
587
+ { response: 'CancelAuth' }
588
+ else
589
+ puts 'Replying with proxy auth credentials' if ENV['DEBUG']
590
+ @proxy_auth_attempts.push(request_id)
591
+ { response: 'ProvideCredentials' }.merge(@browser.proxy_auth || {})
592
+ end
593
+ elsif @auth_attempts.include?(request_id)
594
+ puts 'Cancelling auth' if ENV['DEBUG']
595
+ { response: 'CancelAuth' }
596
+ else
597
+ @auth_attempts.push(request_id)
598
+ puts 'Replying with auth credentials' if ENV['DEBUG']
599
+ { response: 'ProvideCredentials' }.merge(@credentials || {})
577
600
  end
601
+
602
+ async_command('Fetch.continueWithAuth', requestId: request_id, authChallengeResponse: credentials_response)
578
603
  end
579
604
 
580
- @session.on 'Runtime.consoleAPICalled' do |params|
605
+ @session.on 'Runtime.consoleAPICalled' do |**params|
581
606
  # {"type"=>"log", "args"=>[{"type"=>"string", "value"=>"hello"}], "executionContextId"=>2, "timestamp"=>1548722854903.285, "stackTrace"=>{"callFrames"=>[{"functionName"=>"", "scriptId"=>"15", "url"=>"http://127.0.0.1:53977/", "lineNumber"=>6, "columnNumber"=>22}]}}
582
- details = params.dig('stackTrace', 'callFrames')&.first
583
- @browser.console.log(params['type'],
584
- params['args'].map { |arg| arg['description'] || arg['value'] }.join(' ').to_s,
607
+ details = params.dig(:stack_trace, 'callFrames')&.first
608
+ @browser.console.log(params[:type],
609
+ params[:args].map { |arg| arg['description'] || arg['value'] }.join(' ').to_s,
585
610
  source: details['url'].empty? ? nil : details['url'],
586
611
  line_number: details['lineNumber'].zero? ? nil : details['lineNumber'],
587
612
  columnNumber: details['columnNumber'].zero? ? nil : details['columnNumber'])
@@ -600,15 +625,16 @@ module Capybara::Apparition
600
625
  end
601
626
 
602
627
  def register_js_error_handler
603
- @session.on 'Runtime.exceptionThrown' do |params|
604
- @js_error ||= params.dig('exceptionDetails', 'exception', 'description') if @raise_js_errors
628
+ @session.on 'Runtime.exceptionThrown' do |exception_details: nil, **|
629
+ @js_error ||= exception_details&.dig('exception', 'description') if @raise_js_errors
605
630
 
606
- details = params.dig('exceptionDetails', 'stackTrace', 'callFrames')&.first
631
+ details = exception_details&.dig('stackTrace', 'callFrames')&.first ||
632
+ exception_details || {}
607
633
  @browser.console.log('error',
608
- params.dig('exceptionDetails', 'exception', 'description'),
609
- source: details['url'].empty? ? nil : details['url'],
610
- line_number: details['lineNumber'].zero? ? nil : details['lineNumber'],
611
- columnNumber: details['columnNumber'].zero? ? nil : details['columnNumber'])
634
+ exception_details&.dig('exception', 'description'),
635
+ source: details['url'].to_s.empty? ? nil : details['url'],
636
+ line_number: details['lineNumber'].to_i.zero? ? nil : details['lineNumber'],
637
+ columnNumber: details['columnNumber'].to_i.zero? ? nil : details['columnNumber'])
612
638
  end
613
639
  end
614
640
 
@@ -619,6 +645,7 @@ module Capybara::Apparition
619
645
 
620
646
  def setup_network_interception
621
647
  async_command 'Network.setCacheDisabled', cacheDisabled: true
648
+ # async_command 'Fetch.enable', handleAuthRequests: true
622
649
  async_command 'Network.setRequestInterception', patterns: [{ urlPattern: '*' }]
623
650
  end
624
651
 
@@ -648,6 +675,37 @@ module Capybara::Apparition
648
675
  end
649
676
  end
650
677
 
678
+ def process_intercepted_fetch(interception_id, request, resource_type)
679
+ navigation = (resource_type == 'Document')
680
+ headers, url = request.values_at('headers', 'url')
681
+
682
+ unless @temp_headers.empty? || navigation # rubocop:disable Style/IfUnlessModifier
683
+ headers.delete_if { |name, value| @temp_headers[name] == value }
684
+ end
685
+ unless @temp_no_redirect_headers.empty? || !navigation
686
+ headers.delete_if { |name, value| @temp_no_redirect_headers[name] == value }
687
+ end
688
+ if (accept = perm_headers.keys.find { |k| /accept/i.match? k })
689
+ headers[accept] = perm_headers[accept]
690
+ end
691
+
692
+ if @url_blacklist.any? { |r| url.match Regexp.escape(r).gsub('\*', '.*?') }
693
+ async_command('Fetch.failRequest', errorReason: 'Failed', requestId: interception_id)
694
+ elsif @url_whitelist.any?
695
+ if @url_whitelist.any? { |r| url.match Regexp.escape(r).gsub('\*', '.*?') }
696
+ async_command('Fetch.continueRequest',
697
+ requestId: interception_id,
698
+ headers: headers.map { |k, v| { name: k, value: v } })
699
+ else
700
+ async_command('Fetch.failRequest', errorReason: 'Failed', requestId: interception_id)
701
+ end
702
+ else
703
+ async_command('Fetch.continueRequest',
704
+ requestId: interception_id,
705
+ headers: headers.map { |k, v| { name: k, value: v } })
706
+ end
707
+ end
708
+
651
709
  def continue_request(id, **params)
652
710
  async_command 'Network.continueInterceptedRequest', interceptionId: id, **params
653
711
  end
@@ -71,11 +71,13 @@ module Capybara::Apparition
71
71
 
72
72
  Array(keys).each do |sequence|
73
73
  case 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
74
+ when Array
75
+ type_with_modifiers(sequence, delay: delay)
76
+ when String
77
+ sequence.each_char do |char|
78
+ press char
79
+ sleep delay
80
+ end
79
81
  else
80
82
  press sequence
81
83
  sleep delay
@@ -6,6 +6,7 @@ module Capybara::Apparition
6
6
  @page = page
7
7
  @keyboard = keyboard
8
8
  @current_pos = { x: 0, y: 0 }
9
+ @current_buttons = BUTTONS[:none]
9
10
  end
10
11
 
11
12
  def click_at(x:, y:, button: 'left', count: 1, modifiers: [])
@@ -29,11 +30,13 @@ module Capybara::Apparition
29
30
  def down(button: 'left', **options)
30
31
  options = @current_pos.merge(button: button).merge(options)
31
32
  mouse_event('mousePressed', options)
33
+ @current_buttons |= BUTTONS[button.to_sym]
32
34
  self
33
35
  end
34
36
 
35
37
  def up(button: 'left', **options)
36
38
  options = @current_pos.merge(button: button).merge(options)
39
+ @current_buttons &= ~BUTTONS[button.to_sym]
37
40
  mouse_event('mouseReleased', options)
38
41
  self
39
42
  end
@@ -43,11 +46,21 @@ module Capybara::Apparition
43
46
  def mouse_event(type, x:, y:, button: 'none', count: 1)
44
47
  @page.command('Input.dispatchMouseEvent',
45
48
  type: type,
46
- button: button,
49
+ button: button.to_s,
50
+ buttons: @current_buttons,
47
51
  x: x,
48
52
  y: y,
49
53
  modifiers: @keyboard.modifiers,
50
54
  clickCount: count)
51
55
  end
56
+
57
+ BUTTONS = {
58
+ left: 1,
59
+ right: 2,
60
+ middle: 4,
61
+ back: 8,
62
+ forward: 16,
63
+ none: 0
64
+ }.freeze
52
65
  end
53
66
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Capybara
4
4
  module Apparition
5
- VERSION = '0.3.0'
5
+ VERSION = '0.4.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.3.0
4
+ version: 0.4.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-05-28 00:00:00.000000000 Z
11
+ date: 2019-08-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: capybara
@@ -44,20 +44,6 @@ dependencies:
44
44
  - - ">="
45
45
  - !ruby/object:Gem::Version
46
46
  version: 0.6.5
47
- - !ruby/object:Gem::Dependency
48
- name: byebug
49
- requirement: !ruby/object:Gem::Requirement
50
- requirements:
51
- - - ">="
52
- - !ruby/object:Gem::Version
53
- version: '0'
54
- type: :development
55
- prerelease: false
56
- version_requirements: !ruby/object:Gem::Requirement
57
- requirements:
58
- - - ">="
59
- - !ruby/object:Gem::Version
60
- version: '0'
61
47
  - !ruby/object:Gem::Dependency
62
48
  name: chunky_png
63
49
  requirement: !ruby/object:Gem::Requirement