apparition 0.0.1 → 0.0.2

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