apparition 0.1.0 → 0.6.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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +40 -4
  3. data/lib/capybara/apparition.rb +0 -2
  4. data/lib/capybara/apparition/browser.rb +75 -133
  5. data/lib/capybara/apparition/browser/cookie.rb +4 -16
  6. data/lib/capybara/apparition/browser/header.rb +2 -2
  7. data/lib/capybara/apparition/browser/launcher.rb +25 -0
  8. data/lib/capybara/apparition/browser/launcher/local.rb +213 -0
  9. data/lib/capybara/apparition/browser/launcher/remote.rb +55 -0
  10. data/lib/capybara/apparition/browser/page_manager.rb +90 -0
  11. data/lib/capybara/apparition/browser/window.rb +29 -29
  12. data/lib/capybara/apparition/configuration.rb +100 -0
  13. data/lib/capybara/apparition/console.rb +8 -1
  14. data/lib/capybara/apparition/dev_tools_protocol/remote_object.rb +23 -7
  15. data/lib/capybara/apparition/dev_tools_protocol/session.rb +3 -4
  16. data/lib/capybara/apparition/driver.rb +107 -35
  17. data/lib/capybara/apparition/driver/chrome_client.rb +13 -8
  18. data/lib/capybara/apparition/driver/response.rb +1 -1
  19. data/lib/capybara/apparition/driver/web_socket_client.rb +1 -0
  20. data/lib/capybara/apparition/errors.rb +3 -3
  21. data/lib/capybara/apparition/network_traffic/error.rb +1 -0
  22. data/lib/capybara/apparition/network_traffic/request.rb +5 -5
  23. data/lib/capybara/apparition/node.rb +142 -50
  24. data/lib/capybara/apparition/node/drag.rb +165 -65
  25. data/lib/capybara/apparition/page.rb +180 -142
  26. data/lib/capybara/apparition/page/frame.rb +3 -0
  27. data/lib/capybara/apparition/page/frame_manager.rb +2 -1
  28. data/lib/capybara/apparition/page/keyboard.rb +29 -7
  29. data/lib/capybara/apparition/page/mouse.rb +20 -6
  30. data/lib/capybara/apparition/utility.rb +1 -1
  31. data/lib/capybara/apparition/version.rb +1 -1
  32. metadata +53 -23
  33. data/lib/capybara/apparition/dev_tools_protocol/target.rb +0 -64
  34. data/lib/capybara/apparition/dev_tools_protocol/target_manager.rb +0 -48
  35. data/lib/capybara/apparition/driver/launcher.rb +0 -217
@@ -92,7 +92,7 @@ module Capybara::Apparition
92
92
  def send_msg(command, params)
93
93
  msg_id, msg = generate_msg(command, params)
94
94
  @send_mutex.synchronize do
95
- puts "#{Time.now.to_i}: sending msg: #{msg}" if ENV['DEBUG']
95
+ puts "#{Time.now.to_i}: sending msg: #{msg}" if ENV['DEBUG'] == 'V'
96
96
  @ws.send_msg(msg)
97
97
  end
98
98
  msg_id
@@ -141,7 +141,7 @@ module Capybara::Apparition
141
141
 
142
142
  def read_msg
143
143
  msg = JSON.parse(@ws.read_msg)
144
- puts "#{Time.now.to_i}: got msg: #{msg}" if ENV['DEBUG']
144
+ puts "#{Time.now.to_i}: got msg: #{msg}" if ENV['DEBUG'] == 'V'
145
145
  # Check if it's an event and push on event queue
146
146
  @events.push msg.dup if msg['method']
147
147
 
@@ -162,7 +162,7 @@ module Capybara::Apparition
162
162
  @msg_mutex.synchronize do
163
163
  @message_available.wait(@msg_mutex, 0.1)
164
164
  (@responses.keys & @async_ids).each do |msg_id|
165
- puts "Cleaning up response for #{msg_id}" if ENV['DEBUG'] == 'v'
165
+ puts "Cleaning up response for #{msg_id}" if ENV['DEBUG'] == 'V'
166
166
  @responses.delete(msg_id)
167
167
  @async_ids.delete(msg_id)
168
168
  end
@@ -206,7 +206,7 @@ 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
+ handler.call(**event['params'].transform_keys(&method(:snake_sym)))
210
210
  end
211
211
  end
212
212
 
@@ -222,12 +222,17 @@ module Capybara::Apparition
222
222
  @async_response_handler.abort_on_exception = true
223
223
 
224
224
  @listener = Thread.new do
225
- begin
226
- listen
227
- rescue EOFError # rubocop:disable Lint/HandleExceptions
228
- end
225
+ listen
226
+ rescue EOFError # rubocop:disable Lint/SuppressedException
229
227
  end
230
228
  # @listener.abort_on_exception = true
231
229
  end
230
+
231
+ def snake_sym(str)
232
+ str.gsub(/([a-z\d])([A-Z])/, '\1_\2')
233
+ .tr('-', '_')
234
+ .downcase
235
+ .to_sym
236
+ end
232
237
  end
233
238
  end
@@ -15,7 +15,7 @@ module Capybara::Apparition
15
15
  handle_error(resp['error']) if resp['error']
16
16
  resp
17
17
  end.last
18
- puts "Processed msg: #{@msg_ids.last} in #{Time.now - @send_time} seconds" if ENV['DEBUG']
18
+ puts "Processed msg: #{@msg_ids.last} in #{Time.now - @send_time} seconds" if ENV['DEBUG'] == 'V'
19
19
 
20
20
  response['result']
21
21
  end
@@ -63,6 +63,7 @@ module Capybara::Apparition
63
63
 
64
64
  class Socket
65
65
  attr_reader :url
66
+
66
67
  def initialize(url)
67
68
  @url = url
68
69
  uri = URI.parse(url)
@@ -45,7 +45,7 @@ module Capybara
45
45
  end
46
46
 
47
47
  def message
48
- 'There was an error inside the Puppeteer portion of Apparition. ' \
48
+ 'There was an error inside Apparition. ' \
49
49
  'If this is the error returned, and not the cause of a more detailed error response, ' \
50
50
  'this is probably a bug, so please report it. ' \
51
51
  "\n\n#{name}: #{error_parameters}"
@@ -57,7 +57,7 @@ module Capybara
57
57
  # response['args'].first.map { |data| JSErrorItem.new(data['message'], data['stack']) }
58
58
  # end
59
59
  def javascript_errors
60
- [response]
60
+ [message: response]
61
61
  end
62
62
 
63
63
  def message
@@ -65,7 +65,7 @@ module Capybara
65
65
  "If you don't care about these errors, you can ignore them by " \
66
66
  'setting js_errors: false in your Apparition configuration (see ' \
67
67
  'documentation for details).' \
68
- "\n\n#{javascript_errors.map(&:to_s).join("\n")}"
68
+ "\n\n#{javascript_errors.map { |err| err[:message] }.join("\n")}"
69
69
  end
70
70
  end
71
71
 
@@ -3,6 +3,7 @@
3
3
  module Capybara::Apparition::NetworkTraffic
4
4
  class Error
5
5
  attr_reader :url, :code, :description
6
+
6
7
  def initialize(url:, code:, description:)
7
8
  @url = url
8
9
  @code = code
@@ -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?
@@ -11,8 +11,8 @@ module Capybara::Apparition
11
11
 
12
12
  attr_reader :page_id
13
13
 
14
- def initialize(driver, page, remote_object)
15
- super(driver, self)
14
+ def initialize(driver, page, remote_object, initial_cache)
15
+ super(driver, self, initial_cache)
16
16
  @page = page
17
17
  @remote_object = remote_object
18
18
  end
@@ -30,12 +30,13 @@ module Capybara::Apparition
30
30
  def find(method, selector)
31
31
  js = method == :css ? FIND_CSS_JS : FIND_XPATH_JS
32
32
  evaluate_on(js, value: selector).map do |r_o|
33
- Capybara::Apparition::Node.new(driver, @page, r_o['objectId'])
33
+ tag_name = r_o['description'].split(/[.#]/, 2)[0]
34
+ Capybara::Apparition::Node.new(driver, @page, r_o['objectId'], tag_name: tag_name)
34
35
  end
35
36
  rescue ::Capybara::Apparition::BrowserError => e
36
- raise unless e.name =~ /is not a valid (XPath expression|selector)/
37
+ raise unless /is not a valid (XPath expression|selector)/.match? e.name
37
38
 
38
- raise Capybara::Apparition::InvalidSelector, [method, selector]
39
+ raise Capybara::Apparition::InvalidSelector, 'args' => [method, selector]
39
40
  end
40
41
 
41
42
  def find_xpath(selector)
@@ -71,6 +72,18 @@ module Capybara::Apparition
71
72
  visible_text
72
73
  end
73
74
 
75
+ # capybara-webkit method
76
+ def inner_html
77
+ self[:innerHTML]
78
+ end
79
+
80
+ # capybara-webkit method
81
+ def inner_html=(value)
82
+ driver.execute_script <<~JS, self, value
83
+ arguments[0].innerHTML = arguments[1]
84
+ JS
85
+ end
86
+
74
87
  def property(name)
75
88
  evaluate_on('name => this[name]', value: name)
76
89
  end
@@ -117,11 +130,18 @@ module Capybara::Apparition
117
130
  set_time(value)
118
131
  when 'datetime-local'
119
132
  set_datetime_local(value)
133
+ when 'color'
134
+ set_color(value)
135
+ when 'range'
136
+ set_range(value)
120
137
  else
121
- set_text(value.to_s, delay: options.fetch(:delay, 0))
138
+ set_text(value.to_s, **{ delay: 0 }.merge(options))
122
139
  end
123
140
  elsif tag_name == 'textarea'
124
141
  set_text(value.to_s)
142
+ elsif tag_name == 'select'
143
+ warn "Setting the value of a select element via 'set' is deprecated, please use 'select' or 'select_option'."
144
+ evaluate_on '()=>{ this.value = arguments[0] }', value: value.to_s
125
145
  elsif self[:isContentEditable]
126
146
  delete_text
127
147
  send_keys(value.to_s, delay: options.fetch(:delay, 0))
@@ -150,6 +170,21 @@ module Capybara::Apparition
150
170
  evaluate_on VISIBLE_JS
151
171
  end
152
172
 
173
+ def obscured?(**)
174
+ pos = visible_center(allow_scroll: false)
175
+ return true if pos.nil?
176
+
177
+ hit_node = @page.element_from_point(**pos)
178
+ return true if hit_node.nil?
179
+
180
+ begin
181
+ return evaluate_on('el => !this.contains(el)', objectId: hit_node['objectId'])
182
+ rescue WrongWorld # rubocop:disable Lint/SuppressedException
183
+ end
184
+
185
+ true
186
+ end
187
+
153
188
  def checked?
154
189
  self[:checked]
155
190
  end
@@ -162,20 +197,23 @@ module Capybara::Apparition
162
197
  evaluate_on ELEMENT_DISABLED_JS
163
198
  end
164
199
 
165
- def click(keys = [], button: 'left', count: 1, **options)
166
- pos = element_click_pos(options)
200
+ def click(keys = [], button: 'left', count: 1, delay: 0, **options)
201
+ pos = element_click_pos(**options)
167
202
  raise ::Capybara::Apparition::MouseEventImpossible.new(self, 'args' => ['click']) if pos.nil?
168
203
 
169
- test = mouse_event_test(pos)
204
+ test = mouse_event_test(**pos)
170
205
  raise ::Capybara::Apparition::MouseEventImpossible.new(self, 'args' => ['click']) if test.nil?
171
- raise ::Capybara::Apparition::MouseEventFailed.new(self, 'args' => ['click', test.selector, pos]) unless test.success
172
206
 
173
- @page.mouse.click_at pos.merge(button: button, count: count, modifiers: keys)
207
+ unless options[:x] && options[:y]
208
+ raise ::Capybara::Apparition::MouseEventFailed.new(self, 'args' => ['click', test.selector, pos]) unless test.success
209
+ end
210
+
211
+ @page.mouse.click_at(**pos.merge(button: button, count: count, modifiers: keys, delay: delay))
174
212
  if ENV['DEBUG']
175
213
  begin
176
- new_pos = element_click_pos(options)
214
+ new_pos = element_click_pos(**options)
177
215
  puts "Element moved from #{pos} to #{new_pos}" unless pos == new_pos
178
- rescue WrongWorld # rubocop:disable Lint/HandleExceptions
216
+ rescue WrongWorld # rubocop:disable Lint/SuppressedException
179
217
  end
180
218
  end
181
219
  # Wait a short time to see if click triggers page load
@@ -195,7 +233,7 @@ module Capybara::Apparition
195
233
  pos = visible_center
196
234
  raise ::Capybara::Apparition::MouseEventImpossible.new(self, 'args' => ['hover']) if pos.nil?
197
235
 
198
- @page.mouse.move_to(pos)
236
+ @page.mouse.move_to(**pos)
199
237
  end
200
238
 
201
239
  EVENTS = {
@@ -227,15 +265,19 @@ module Capybara::Apparition
227
265
  evaluate_on DISPATCH_EVENT_JS, { value: event_type }, { value: name }, value: opts.merge(options)
228
266
  end
229
267
 
268
+ def submit
269
+ evaluate_on '()=>{ this.submit() }'
270
+ end
271
+
230
272
  def ==(other)
231
273
  evaluate_on('el => this == el', objectId: other.id)
232
274
  rescue ObsoleteNode
233
275
  false
234
276
  end
235
277
 
236
- def send_keys(*keys, delay: 0, **_opts)
278
+ def send_keys(*keys, delay: 0, **opts)
237
279
  click unless evaluate_on CURRENT_NODE_SELECTED_JS
238
- @page.keyboard.type(keys, delay: delay)
280
+ _send_keys(*keys, delay: delay, **opts)
239
281
  end
240
282
  alias_method :send_key, :send_keys
241
283
 
@@ -243,9 +285,13 @@ module Capybara::Apparition
243
285
  evaluate_on GET_PATH_JS
244
286
  end
245
287
 
246
- def element_click_pos(x: nil, y: nil, **)
288
+ def element_click_pos(x: nil, y: nil, offset: nil, **)
247
289
  if x && y
248
- visible_top_left.tap do |p|
290
+ if offset == :center
291
+ visible_center
292
+ else
293
+ visible_top_left
294
+ end.tap do |p|
249
295
  p[:x] += x
250
296
  p[:y] += y
251
297
  end
@@ -255,7 +301,7 @@ module Capybara::Apparition
255
301
  end
256
302
 
257
303
  def visible_top_left
258
- rect = in_view_bounding_rect
304
+ rect = in_view_client_rect
259
305
  return nil if rect.nil?
260
306
 
261
307
  frame_offset = @page.current_frame_offset
@@ -288,8 +334,8 @@ module Capybara::Apparition
288
334
  end
289
335
  end
290
336
 
291
- def visible_center
292
- rect = in_view_bounding_rect
337
+ def visible_center(allow_scroll: true)
338
+ rect = in_view_client_rect(allow_scroll: allow_scroll)
293
339
  return nil if rect.nil?
294
340
 
295
341
  frame_offset = @page.current_frame_offset
@@ -332,12 +378,16 @@ module Capybara::Apparition
332
378
  end
333
379
 
334
380
  def top_left
335
- result = evaluate_on GET_BOUNDING_CLIENT_RECT_JS
381
+ result = evaluate_on GET_CLIENT_RECT_JS
336
382
  return nil if result.nil?
337
383
 
338
384
  { x: result['x'], y: result['y'] }
339
385
  end
340
386
 
387
+ def rect
388
+ evaluate_on GET_CLIENT_RECT_JS
389
+ end
390
+
341
391
  def scroll_by(x, y)
342
392
  evaluate_on <<~JS, { value: x }, value: y
343
393
  (x, y) => this.scrollBy(x,y)
@@ -382,9 +432,39 @@ module Capybara::Apparition
382
432
 
383
433
  private
384
434
 
385
- def in_view_bounding_rect
386
- evaluate_on('() => this.scrollIntoViewIfNeeded()')
387
- result = evaluate_on GET_BOUNDING_CLIENT_RECT_JS
435
+ def focus
436
+ @page.command('DOM.focus', objectId: id)
437
+ end
438
+
439
+ def keys_to_send(value, clear)
440
+ case clear
441
+ when :backspace
442
+ # Clear field by sending the correct number of backspace keys.
443
+ [:end] + ([:backspace] * self.value.to_s.length) + [value]
444
+ when :none
445
+ [value]
446
+ when Array
447
+ clear << value
448
+ else
449
+ # Clear field by JavaScript assignment of the value property.
450
+ # Script can change a readonly element which user input cannot, so
451
+ # don't execute if readonly.
452
+ driver.execute_script <<~JS, self
453
+ if (!arguments[0].readOnly) {
454
+ arguments[0].value = ''
455
+ }
456
+ JS
457
+ [value]
458
+ end
459
+ end
460
+
461
+ def _send_keys(*keys, delay: 0, **_opts)
462
+ @page.keyboard.type(keys, delay: delay)
463
+ end
464
+
465
+ def in_view_client_rect(allow_scroll: true)
466
+ evaluate_on('() => this.scrollIntoViewIfNeeded()') if allow_scroll
467
+ result = evaluate_on GET_CLIENT_RECT_JS
388
468
  result = result['model'] if result && result['model']
389
469
  result
390
470
  end
@@ -410,26 +490,21 @@ module Capybara::Apparition
410
490
  DevToolsProtocol::RemoteObject.new(@page, response['result'] || response['object']).value
411
491
  end
412
492
 
413
- def set_text(value, clear: nil, delay: 0, **_unused)
493
+ def set_text(value, clear: nil, delay: 0, rapid: nil, **_unused)
414
494
  value = value.to_s
415
495
  if value.empty? && clear.nil?
416
496
  evaluate_on CLEAR_ELEMENT_JS
417
- elsif clear == :backspace
418
- # Clear field by sending the correct number of backspace keys.
419
- backspaces = [:backspace] * self.value.to_s.length
420
- send_keys(*([:end] + backspaces + [value]), delay: delay)
421
- elsif clear.is_a? Array
422
- send_keys(*clear, value, delay: delay)
423
497
  else
424
- # Clear field by JavaScript assignment of the value property.
425
- # Script can change a readonly element which user input cannot, so
426
- # don't execute if readonly.
427
- driver.execute_script <<~JS, self unless clear == :none
428
- if (!arguments[0].readOnly) {
429
- arguments[0].value = ''
430
- }
431
- JS
432
- send_keys(value, delay: delay)
498
+ focus
499
+ if (rapid && (value.length >= 6)) || ((value.length > 30) && rapid != false)
500
+ _send_keys(*keys_to_send(value[0..2], clear), delay: delay)
501
+ driver.execute_script <<~JS, self, value[0...-3]
502
+ arguments[0].value = arguments[1]
503
+ JS
504
+ _send_keys(*keys_to_send(value[-3..-1], :none), delay: delay)
505
+ else
506
+ _send_keys(*keys_to_send(value, clear), delay: delay)
507
+ end
433
508
  end
434
509
  end
435
510
 
@@ -465,6 +540,14 @@ module Capybara::Apparition
465
540
  update_value_js(value.to_datetime_str)
466
541
  end
467
542
 
543
+ def set_color(value)
544
+ update_value_js(value.to_s)
545
+ end
546
+
547
+ def set_range(value)
548
+ update_value_js(value.to_s)
549
+ end
550
+
468
551
  def update_value_js(value)
469
552
  evaluate_on(<<~JS, value: value)
470
553
  value => {
@@ -488,7 +571,8 @@ module Capybara::Apparition
488
571
  r_o = @page.element_from_point(x: x, y: y)
489
572
  return nil unless r_o && r_o['objectId']
490
573
 
491
- hit_node = Capybara::Apparition::Node.new(driver, @page, r_o['objectId'])
574
+ tag_name = r_o['description'].split(/[.#]/, 2)[0]
575
+ hit_node = Capybara::Apparition::Node.new(driver, @page, r_o['objectId'], tag_name: tag_name)
492
576
  result = begin
493
577
  evaluate_on(<<~JS, objectId: hit_node.id)
494
578
  (hit_node) => {
@@ -658,11 +742,14 @@ module Capybara::Apparition
658
742
  sel = sel.parentNode;
659
743
  }
660
744
  let event_options = { bubbles: true, cancelable: true };
745
+ sel.dispatchEvent(new MouseEvent('mousedown', event_options));
661
746
  sel.dispatchEvent(new FocusEvent('focus', event_options));
662
-
663
- this.selected = true
664
-
665
- sel.dispatchEvent(new Event('change', event_options));
747
+ if (this.selected == false){
748
+ this.selected = true;
749
+ sel.dispatchEvent(new Event('change', event_options));
750
+ }
751
+ sel.dispatchEvent(new MouseEvent('mouseup', event_options));
752
+ sel.dispatchEvent(new MouseEvent('click', event_options));
666
753
  sel.dispatchEvent(new FocusEvent('blur', event_options));
667
754
  }
668
755
  JS
@@ -686,7 +773,7 @@ module Capybara::Apparition
686
773
  # if an area element, check visibility of relevant image
687
774
  VISIBLE_JS = <<~JS
688
775
  function(){
689
- el = this;
776
+ let el = this;
690
777
  if (el.tagName == 'AREA'){
691
778
  const map_name = document.evaluate('./ancestor::map/@name', el, null, XPathResult.STRING_TYPE, null).stringValue;
692
779
  el = document.querySelector(`img[usemap='#${map_name}']`);
@@ -704,7 +791,11 @@ module Capybara::Apparition
704
791
  (parseFloat(style.opacity) == 0)) {
705
792
  return false;
706
793
  }
707
- el = el.parentElement;
794
+ var parent = el.parentElement;
795
+ if (parent && (parent.tagName == 'DETAILS') && !parent.open && (el.tagName != 'SUMMARY')) {
796
+ return false;
797
+ }
798
+ el = parent;
708
799
  }
709
800
  return true;
710
801
  }
@@ -721,9 +812,10 @@ module Capybara::Apparition
721
812
  }
722
813
  JS
723
814
 
724
- GET_BOUNDING_CLIENT_RECT_JS = <<~JS
815
+ GET_CLIENT_RECT_JS = <<~JS
725
816
  function(){
726
- rect = this.getBoundingClientRect();
817
+ var rects = [...this.getClientRects()]
818
+ var rect = rects.find(r => (r.height && r.width)) || this.getBoundingClientRect();
727
819
  return rect.toJSON();
728
820
  }
729
821
  JS