apparition 0.1.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
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