apparition 0.3.0 → 0.4.0

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