apparition 0.0.1 → 0.0.2

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.
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara::Apparition
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
25
+ end
26
+ end
27
+
28
+ def drag_by(x, y, delay: 0.1)
29
+ pos = visible_center
30
+ raise ::Capybara::Apparition::MouseEventImpossible.new(self, 'args' => ['hover']) if pos.nil?
31
+
32
+ other_pos = { x: pos[:x] + x, y: pos[:y] + y }
33
+ raise ::Capybara::Apparition::MouseEventFailed.new(self, 'args' => ['drag', test['selector'], pos]) unless mouse_event_test?(pos)
34
+
35
+ @page.mouse.move_to(pos).down
36
+ sleep delay
37
+ @page.mouse.move_to(other_pos.merge(button: 'left'))
38
+ sleep delay
39
+ @page.mouse.up
40
+ end
41
+
42
+ private
43
+
44
+ def html5_drag_to(element)
45
+ driver.execute_script MOUSEDOWN_TRACKER
46
+ scroll_if_needed
47
+ @page.mouse.move_to(element.visible_center).down
48
+ if driver.evaluate_script('window.capybara_mousedown_prevented')
49
+ element.scroll_if_needed
50
+ @page.mouse.move_to(element.visible_center).up
51
+ else
52
+ driver.execute_script HTML5_DRAG_DROP_SCRIPT, self, element
53
+ @page.mouse.up(element.visible_center)
54
+ end
55
+ end
56
+
57
+ def html5_draggable?
58
+ native.property('draggable')
59
+ end
60
+
61
+ MOUSEDOWN_TRACKER = <<~JS
62
+ document.addEventListener('mousedown', ev => {
63
+ window.capybara_mousedown_prevented = ev.defaultPrevented;
64
+ }, { once: true, passive: true })
65
+ JS
66
+
67
+ HTML5_DRAG_DROP_SCRIPT = <<~JS
68
+ var source = arguments[0];
69
+ var target = arguments[1];
70
+
71
+ function rectCenter(rect){
72
+ return new DOMPoint(
73
+ (rect.left + rect.right)/2,
74
+ (rect.top + rect.bottom)/2
75
+ );
76
+ }
77
+
78
+ function pointOnRect(pt, rect) {
79
+ var rectPt = rectCenter(rect);
80
+ var slope = (rectPt.y - pt.y) / (rectPt.x - pt.x);
81
+
82
+ if (pt.x <= rectPt.x) { // left side
83
+ var minXy = slope * (rect.left - pt.x) + pt.y;
84
+ if (rect.top <= minXy && minXy <= rect.bottom)
85
+ return new DOMPoint(rect.left, minXy);
86
+ }
87
+
88
+ if (pt.x >= rectPt.x) { // right side
89
+ var maxXy = slope * (rect.right - pt.x) + pt.y;
90
+ if (rect.top <= maxXy && maxXy <= rect.bottom)
91
+ return new DOMPoint(rect.right, maxXy);
92
+ }
93
+
94
+ if (pt.y <= rectPt.y) { // top side
95
+ var minYx = (rectPt.top - pt.y) / slope + pt.x;
96
+ if (rect.left <= minYx && minYx <= rect.right)
97
+ return new DOMPoint(minYx, rect.top);
98
+ }
99
+
100
+ if (pt.y >= rectPt.y) { // bottom side
101
+ var maxYx = (rect.bottom - pt.y) / slope + pt.x;
102
+ if (rect.left <= maxYx && maxYx <= rect.right)
103
+ return new DOMPoint(maxYx, rect.bottom);
104
+ }
105
+
106
+ return new DOMPoint(pt.x,pt.y);
107
+ }
108
+
109
+ var dt = new DataTransfer();
110
+ var opts = { cancelable: true, bubbles: true, dataTransfer: dt };
111
+
112
+ if (source.tagName == 'A'){
113
+ dt.setData('text/uri-list', source.href);
114
+ dt.setData('text', source.href);
115
+ }
116
+ if (source.tagName == 'IMG'){
117
+ dt.setData('text/uri-list', source.src);
118
+ dt.setData('text', source.src);
119
+ }
120
+
121
+ var dragEvent = new DragEvent('dragstart', opts);
122
+ source.dispatchEvent(dragEvent);
123
+ target.scrollIntoView({behavior: 'instant', block: 'center', inline: 'center'});
124
+ var targetRect = target.getBoundingClientRect();
125
+ var sourceCenter = rectCenter(source.getBoundingClientRect());
126
+
127
+ // fire 2 dragover events to simulate dragging with a direction
128
+ var entryPoint = pointOnRect(sourceCenter, targetRect)
129
+ var dragOverOpts = Object.assign({clientX: entryPoint.x, clientY: entryPoint.y}, opts);
130
+ var dragOverEvent = new DragEvent('dragover', dragOverOpts);
131
+ target.dispatchEvent(dragOverEvent);
132
+
133
+ var targetCenter = rectCenter(targetRect);
134
+ dragOverOpts = Object.assign({clientX: targetCenter.x, clientY: targetCenter.y}, opts);
135
+ dragOverEvent = new DragEvent('dragover', dragOverOpts);
136
+ target.dispatchEvent(dragOverEvent);
137
+
138
+ var dragLeaveEvent = new DragEvent('dragleave', opts);
139
+ target.dispatchEvent(dragLeaveEvent);
140
+ if (dragOverEvent.defaultPrevented) {
141
+ var dropEvent = new DragEvent('drop', opts);
142
+ target.dispatchEvent(dropEvent);
143
+ }
144
+ var dragEndEvent = new DragEvent('dragend', opts);
145
+ source.dispatchEvent(dragEndEvent);
146
+ JS
147
+ end
148
+ end
@@ -12,20 +12,24 @@ module Capybara::Apparition
12
12
  attr_accessor :perm_headers, :temp_headers, :temp_no_redirect_headers
13
13
  attr_reader :network_traffic
14
14
 
15
- def self.create(browser, session, id, ignore_https_errors: nil, screenshot_task_queue: nil, js_errors: false)
15
+ def self.create(browser, session, id, ignore_https_errors: false, screenshot_task_queue: nil, js_errors: false)
16
16
  session.command 'Page.enable'
17
- session.command 'Page.setLifecycleEventsEnabled', enabled: true
18
- session.command 'Page.setDownloadBehavior', behavior: 'allow', downloadPath: Capybara.save_path
17
+
18
+ # Provides a lot of info - but huge overhead
19
+ # session.command 'Page.setLifecycleEventsEnabled', enabled: true
19
20
 
20
21
  page = Page.new(browser, session, id, ignore_https_errors, screenshot_task_queue, js_errors)
21
22
 
22
23
  session.command 'Network.enable'
23
24
  session.command 'Runtime.enable'
24
25
  session.command 'Security.enable'
25
- session.command 'Security.setOverrideCertificateErrors', override: true if ignore_https_errors
26
+ # session.command 'Security.setOverrideCertificateErrors', override: true if ignore_https_errors
27
+ session.command 'Security.setIgnoreCertificateErrors', ignore: !!ignore_https_errors
26
28
  session.command 'DOM.enable'
27
29
  # session.command 'Log.enable'
28
-
30
+ if Capybara.save_path
31
+ session.command 'Page.setDownloadBehavior', behavior: 'allow', downloadPath: Capybara.save_path
32
+ end
29
33
  page
30
34
  end
31
35
 
@@ -121,17 +125,23 @@ module Capybara::Apparition
121
125
  params = {}
122
126
  params[:paperWidth] = @browser.paper_size[:width].to_f if @browser.paper_size
123
127
  params[:paperHeight] = @browser.paper_size[:height].to_f if @browser.paper_size
128
+ params[:scale] = @browser.zoom_factor if @browser.zoom_factor
124
129
  command('Page.printToPDF', params)
125
130
  else
126
- if options[:selector]
131
+ clip_options = if options[:selector]
127
132
  pos = evaluate("document.querySelector('#{options.delete(:selector)}').getBoundingClientRect().toJSON();")
128
- options[:clip] = %w[x y width height].each_with_object('scale' => 1) { |key, hash| hash[key] = pos[key] }
133
+ %w[x y width height].each_with_object({}) { |key, hash| hash[key] = pos[key] }
129
134
  elsif options[:full]
130
- options[:clip] = evaluate <<~JS
135
+ evaluate <<~JS
131
136
  { width: document.documentElement.clientWidth, height: document.documentElement.clientHeight}
132
137
  JS
133
- options[:clip].merge!(x: 0, y: 0, scale: 1)
138
+ else
139
+ evaluate <<~JS
140
+ { width: window.innerWidth, height: window.innerHeight }
141
+ JS
134
142
  end
143
+ options[:clip] = { x: 0, y: 0, scale: 1 }.merge(clip_options)
144
+ options[:clip][:scale] = @browser.zoom_factor || 1
135
145
  command('Page.captureScreenshot', options)
136
146
  end['data']
137
147
  end
@@ -139,10 +149,11 @@ module Capybara::Apparition
139
149
  def push_frame(frame_el)
140
150
  node = command('DOM.describeNode', objectId: frame_el.base.id)
141
151
  frame_id = node['node']['frameId']
142
- start = Time.now
152
+
153
+ timer = Capybara::Helpers.timer(expire_in: 10)
143
154
  while (frame = @frames.get(frame_id)).nil? || frame.loading?
144
155
  # Wait for the frame creation messages to be processed
145
- if Time.now - start > 10
156
+ if timer.expired?
146
157
  puts 'Timed out waiting from frame to be ready'
147
158
  # byebug
148
159
  raise TimeoutError.new('push_frame')
@@ -217,10 +228,10 @@ module Capybara::Apparition
217
228
  attr_reader :status_code
218
229
 
219
230
  def wait_for_loaded(allow_obsolete: false)
220
- start = Time.now
231
+ timer = Capybara::Helpers.timer(expire_in: 10)
221
232
  cf = current_frame
222
233
  until cf.usable? || (allow_obsolete && cf.obsolete?) || @js_error
223
- if Time.now - start > 10
234
+ if timer.expired?
224
235
  puts 'Timedout waiting for page to be loaded'
225
236
  # byebug
226
237
  raise TimeoutError.new('wait_for_loaded')
@@ -248,6 +259,9 @@ module Capybara::Apparition
248
259
  navigate_opts = { url: url, transitionType: 'reload' }
249
260
  navigate_opts[:referrer] = extra_headers['Referer'] if extra_headers['Referer']
250
261
  response = command('Page.navigate', navigate_opts)
262
+
263
+ raise StatusFailError, 'args' => [url, response['errorText']] if response['errorText']
264
+
251
265
  main_frame.loading(response['loaderId'])
252
266
  wait_for_loaded
253
267
  rescue TimeoutError
@@ -306,11 +320,11 @@ module Capybara::Apparition
306
320
  end
307
321
 
308
322
  def command(name, **params)
309
- @browser.command_for_session(@session.session_id, name, params)
323
+ @browser.command_for_session(@session.session_id, name, params).result
310
324
  end
311
325
 
312
326
  def async_command(name, **params)
313
- @browser.command_for_session(@session.session_id, name, params, async: true)
327
+ @browser.command_for_session(@session.session_id, name, params).discard_result
314
328
  end
315
329
 
316
330
  def extra_headers
@@ -378,6 +392,8 @@ module Capybara::Apparition
378
392
 
379
393
  @session.on 'Page.windowOpen' do |params|
380
394
  puts "**** windowOpen was called with: #{params}" if ENV['DEBUG']
395
+ # TODO: find a better way to handle this
396
+ sleep 0.4 # wait a bit so the window has time to start loading
381
397
  end
382
398
 
383
399
  @session.on 'Page.frameAttached' do |params|
@@ -399,16 +415,31 @@ module Capybara::Apparition
399
415
  end
400
416
  end
401
417
 
402
- @session.on 'Page.lifecycleEvent' do |params|
403
- puts "Lifecycle: #{params['name']} - frame: #{params['frameId']} - loader: #{params['loaderId']}" if ENV['DEBUG']
404
- case params['name']
405
- when 'init'
406
- @frames.get(params['frameId'])&.loading(params['loaderId'])
407
- when 'firstMeaningfulPaintCandidate',
408
- 'networkIdle'
409
- frame = @frames.get(params['frameId'])
410
- frame.loaded! if frame.loader_id == params['loaderId']
411
- end
418
+ @session.on 'Page.frameStartedLoading' do |params|
419
+ @frames.get(params['frameId'])&.loading(-1)
420
+ end
421
+
422
+ @session.on 'Page.frameStoppedLoading' do |params|
423
+ @frames.get(params['frameId'])&.loaded!
424
+ end
425
+
426
+ # @session.on 'Page.lifecycleEvent' do |params|
427
+ # # Provides a lot of useful info - but lots of overhead
428
+ # puts "Lifecycle: #{params['name']} - frame: #{params['frameId']} - loader: #{params['loaderId']}" if ENV['DEBUG']
429
+ # case params['name']
430
+ # when 'init'
431
+ # @frames.get(params['frameId'])&.loading(params['loaderId'])
432
+ # when 'firstMeaningfulPaint',
433
+ # 'networkIdle'
434
+ # @frames.get(params['frameId']).tap do |frame|
435
+ # frame.loaded! if frame.loader_id == params['loaderId']
436
+ # end
437
+ # end
438
+ # end
439
+
440
+ @session.on('Page.domContentEventFired') do |params|
441
+ # TODO: Really need something better than this
442
+ main_frame.loaded! if @status_code != 200
412
443
  end
413
444
 
414
445
  @session.on 'Page.navigatedWithinDocument' do |params|
@@ -466,7 +497,7 @@ module Capybara::Apparition
466
497
  @session.on 'Network.loadingFailed' do |params|
467
498
  req = @network_traffic.find { |request| request.request_id == params['requestId'] }
468
499
  req&.blocked_params = params if params['blockedReason']
469
- puts "Loading Failed - request: #{params['requestId']}: #{params['errorText']}" if params['type'] == 'Document'
500
+ puts "Loading Failed - request: #{params['requestId']} : #{params['errorText']}" if params['type'] == 'Document'
470
501
  end
471
502
 
472
503
  @session.on 'Network.requestIntercepted' do |params|
@@ -484,6 +515,10 @@ module Capybara::Apparition
484
515
  )
485
516
  end
486
517
 
518
+ # @session.on 'Security.certificateError' do |params|
519
+ # async_command 'Network.continueInterceptedRequest', interceptionId: id, **params
520
+ # end
521
+
487
522
  # @session.on 'Log.entryAdded' do |params|
488
523
  # log_entry = params['entry']
489
524
  # if params.values_at('source', 'level') == ['javascript', 'error']
@@ -634,7 +669,6 @@ module Capybara::Apparition
634
669
  def decode_result(result, object_cache = {})
635
670
  if result['type'] == 'object'
636
671
  if result['subtype'] == 'array'
637
- # remoteObject = @browser.command('Runtime.getProperties',
638
672
  remote_object = command('Runtime.getProperties',
639
673
  objectId: result['objectId'],
640
674
  ownProperties: true)
@@ -655,7 +689,7 @@ module Capybara::Apparition
655
689
 
656
690
  # releasePromises = [helper.releaseObject(@element._client, remoteObject)]
657
691
  end
658
-
692
+ command('Runtime.releaseObject', objectId: result['objectId'])
659
693
  return results
660
694
  elsif result['subtype'] == 'node'
661
695
  return result
@@ -667,10 +701,10 @@ module Capybara::Apparition
667
701
  .find { |prop| prop['name'] == '[[StableObjectId]]' }
668
702
  .dig('value', 'value')
669
703
  # We could actually return cyclic objects here but Capybara would need to be updated to support
670
- return '(cyclic structure)' if object_cache.key?(stable_id)
671
-
672
704
  # return object_cache[stable_id] if object_cache.key?(stable_id)
673
705
 
706
+ return '(cyclic structure)' if object_cache.key?(stable_id)
707
+
674
708
  object_cache[stable_id] = {}
675
709
  properties = remote_object['result']
676
710
 
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara::Apparition
4
+ class ChromeClient
5
+ class Response
6
+ def initialize(client, *msg_ids, send_time: nil)
7
+ @send_time = send_time
8
+ @msg_ids = msg_ids
9
+ @client = client
10
+ end
11
+
12
+ def result
13
+ response = @msg_ids.map do |id|
14
+ resp = @client.send(:wait_for_msg_response, id)
15
+ handle_error(resp['error']) if resp['error']
16
+ resp
17
+ end.last
18
+ puts "Processed msg: #{@msg_ids.last} in #{Time.now - @send_time} seconds" if ENV['DEBUG']
19
+
20
+ response['result']
21
+ end
22
+
23
+ def discard_result
24
+ @msg_ids.each { |id| @client.add_async_id id }
25
+ @result_time = Time.now
26
+ nil
27
+ end
28
+
29
+ private
30
+
31
+ def handle_error(error)
32
+ case error['code']
33
+ when -32_000
34
+ raise WrongWorld.new(nil, error)
35
+ else
36
+ raise CDPError.new(error)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Capybara
4
4
  module Apparition
5
- VERSION = '0.0.1'
5
+ VERSION = '0.0.2'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: apparition
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Thomas Walpole
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-01-23 00:00:00.000000000 Z
11
+ date: 2019-01-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: capybara
@@ -197,7 +197,6 @@ files:
197
197
  - lib/capybara/apparition.rb
198
198
  - lib/capybara/apparition/browser.rb
199
199
  - lib/capybara/apparition/chrome_client.rb
200
- - lib/capybara/apparition/command.rb
201
200
  - lib/capybara/apparition/cookie.rb
202
201
  - lib/capybara/apparition/dev_tools_protocol/session.rb
203
202
  - lib/capybara/apparition/dev_tools_protocol/target.rb
@@ -215,7 +214,9 @@ files:
215
214
  - lib/capybara/apparition/network_traffic/request.rb
216
215
  - lib/capybara/apparition/network_traffic/response.rb
217
216
  - lib/capybara/apparition/node.rb
217
+ - lib/capybara/apparition/node/drag.rb
218
218
  - lib/capybara/apparition/page.rb
219
+ - lib/capybara/apparition/response.rb
219
220
  - lib/capybara/apparition/utility.rb
220
221
  - lib/capybara/apparition/version.rb
221
222
  - lib/capybara/apparition/web_socket_client.rb
@@ -238,7 +239,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
238
239
  - !ruby/object:Gem::Version
239
240
  version: '0'
240
241
  requirements: []
241
- rubygems_version: 3.0.2
242
+ rubygems_version: 3.0.1
242
243
  signing_key:
243
244
  specification_version: 4
244
245
  summary: Chrome driver using CDP for Capybara
@@ -1,21 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'securerandom'
4
-
5
- module Capybara::Apparition
6
- class Command
7
- attr_reader :id
8
- attr_reader :name
9
- attr_accessor :args
10
-
11
- def initialize(name, params = {})
12
- @id = SecureRandom.uuid
13
- @name = name
14
- @params = params
15
- end
16
-
17
- def message
18
- JSON.dump('id' => @id, 'name' => @name, 'params' => @params)
19
- end
20
- end
21
- end