apparition 0.0.4 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 69753c37a3e00ce9d57e86847601bef5e39a820ebef96db2c4ad7a6cb922f341
4
- data.tar.gz: 486caf74087a17e078b39e4960a0c707b090142f1831d12b9755ad8b2cbb0596
3
+ metadata.gz: 2df57d16d5cfa6f966f3b8de2146a63ca23877e4dddc2f81a12170ae37f5ec5d
4
+ data.tar.gz: b7b06eacc056f6b627b31ad091a7db9af5508b3f14ae69e7d9f1bedce6e8e63a
5
5
  SHA512:
6
- metadata.gz: 2f512ba7bbaca565d304a2ac0ede0d1c2b41fd3dc38d7f00b8dbe30b75bba2bbf3c35d7bcea2b00a17bdcbdb9d1702c27efcdfb9ef082b75de3988e7e48664c6
7
- data.tar.gz: e01438d8900cd67015e0b473e7f710528ac1d8d4597cb8d0f47c97a1971a914b63cb52013bc1958d46621be58123105e1ed1f3d8f9e033ca0d908490fb152f8f
6
+ metadata.gz: fd65552a378369a1d27852736c1cf2e1c51fb852a2c3e7c90b7c536f58608846bec86b471f009ef6eb9f9cbb803773d99c8af9b01575a0e5b2de953f1e0093f2
7
+ data.tar.gz: 77f8e17427d5621179b1707182e6df8bf3fc2bc52cb9f5f25e42ed31c6db945bd28b00c8f20ea09c2cf55e76476cf0fe467fd0b5aa8eece8f13a08611d9408e5
data/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  Apparition is a driver for [Capybara](https://github.com/jnicklas/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
- with the Poltergeist API as possible, with the thought to add a capybara-webkit compatibility wrapper at some future point in time.
8
+ with the Poltergeist API as possible. Implementing the `capybara-webkit` specific driver methods has also begun.
9
9
 
10
10
  ## Getting help ##
11
11
 
@@ -217,7 +217,7 @@ Include as much information as possible. For example:
217
217
 
218
218
  * Specific steps to reproduce where possible (failing tests are even
219
219
  better)
220
- * The output obtained from running Apparition with `:debug` turned on
220
+ * The output obtained from running Apparition with `:debug` turned on or ENV['DEBUG'] set
221
221
  * Screenshots
222
222
  * Stack traces if there are any Ruby on JavaScript exceptions generated
223
223
  * The Apparition, Capybara, and Chrome version numbers used
@@ -9,7 +9,7 @@ require 'time'
9
9
 
10
10
  module Capybara::Apparition
11
11
  class Browser
12
- attr_reader :client, :paper_size, :zoom_factor, :console
12
+ attr_reader :client, :paper_size, :zoom_factor, :console, :proxy_auth
13
13
  extend Forwardable
14
14
 
15
15
  delegate %i[visit current_url status_code
@@ -27,7 +27,9 @@ module Capybara::Apparition
27
27
  @context_id = nil
28
28
  @js_errors = true
29
29
  @ignore_https_errors = false
30
+ @logger = logger
30
31
  @console = Console.new(logger)
32
+ @proxy_auth = nil
31
33
 
32
34
  initialize_handlers
33
35
 
@@ -102,7 +104,7 @@ module Capybara::Apparition
102
104
  def close_window(handle)
103
105
  @current_page_handle = nil if @current_page_handle == handle
104
106
  win_target = @targets.delete(handle)
105
- warn "Window was already closed unexpectedly" if win_target.nil?
107
+ warn 'Window was already closed unexpectedly' if win_target.nil?
106
108
  win_target&.close
107
109
  end
108
110
 
@@ -205,9 +207,21 @@ module Capybara::Apparition
205
207
  end
206
208
 
207
209
  def cookies
208
- current_page.command('Network.getCookies')['cookies'].each_with_object({}) do |c, h|
209
- h[c['name']] = Cookie.new(c)
210
- end
210
+ CookieJar.new(
211
+ # current_page.command('Network.getCookies')['cookies'].map { |c| Cookie.new(c) }
212
+ self
213
+ )
214
+ end
215
+
216
+ def all_cookies
217
+ CookieJar.new(
218
+ # current_page.command('Network.getAllCookies')['cookies'].map { |c| Cookie.new(c) }
219
+ self
220
+ )
221
+ end
222
+
223
+ def get_raw_cookies
224
+ current_page.command('Network.getAllCookies')['cookies'].map { |c| Cookie.new(c) }
211
225
  end
212
226
 
213
227
  def set_cookie(cookie)
@@ -231,6 +245,14 @@ module Capybara::Apparition
231
245
  current_page.command('Emulation.setDocumentCookieDisabled', disabled: !flag)
232
246
  end
233
247
 
248
+ def set_proxy_auth(user, password)
249
+ @proxy_auth = if user.nil? && password.nil?
250
+ nil
251
+ else
252
+ { username: user, password: password }
253
+ end
254
+ end
255
+
234
256
  def set_http_auth(user = nil, password = nil)
235
257
  current_page.credentials = if user.nil? && password.nil?
236
258
  nil
@@ -311,8 +333,8 @@ module Capybara::Apparition
311
333
  current_target.page
312
334
  end
313
335
 
314
- def console_messages
315
- console.messages
336
+ def console_messages(type = nil)
337
+ console.messages(type)
316
338
  end
317
339
 
318
340
  private
@@ -326,7 +348,7 @@ module Capybara::Apparition
326
348
  end
327
349
 
328
350
  def log(message)
329
- logger&.puts message if ENV['DEBUG']
351
+ @logger&.puts message if ENV['DEBUG']
330
352
  end
331
353
 
332
354
  def check_render_options!(options, path = nil)
@@ -7,17 +7,19 @@ module Capybara::Apparition
7
7
  @messages = []
8
8
  end
9
9
 
10
- def log(type, message)
11
- @messages << OpenStruct.new(type: type, message: message)
12
- @logger&.puts message
10
+ def log(type, message, **options)
11
+ @messages << OpenStruct.new(type: type, message: message, **options)
12
+ @logger&.puts "#{type}: #{message}"
13
13
  end
14
14
 
15
15
  def clear
16
16
  @messages.clear
17
17
  end
18
18
 
19
- def messages
20
- @messages
19
+ def messages(type = nil)
20
+ return @messages if type.nil?
21
+
22
+ @messages.select { |msg| msg.type == type }
21
23
  end
22
24
  end
23
25
  end
@@ -48,9 +48,10 @@ module Capybara::Apparition
48
48
  Time.at @attributes['expires'] unless [nil, 0, -1].include? @attributes['expires']
49
49
  end
50
50
 
51
- def ==(value)
52
- return super unless value.is_a? String
53
- self.value == value
51
+ def ==(other)
52
+ return super unless other.is_a? String
53
+
54
+ value == other
54
55
  end
55
56
  end
56
57
  end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'capybara/apparition/cookie'
4
+
5
+ module Capybara::Apparition
6
+ class CookieJar
7
+ def initialize(browser)
8
+ @browser = browser
9
+ end
10
+
11
+ # def find(name, domain = nil, path = '/')
12
+ def find(name, domain = URI.parse(@browser.current_url).host, path = URI.parse(@browser.current_url).path)
13
+ # sort by path length because more specific take precendence
14
+ cookies.sort_by { |c| -c.path.length }.find do |cookie|
15
+ cookie.name.casecmp(name).zero? &&
16
+ (domain.nil? || match_domain?(cookie, domain)) &&
17
+ (path.nil? || match_path?(cookie, path))
18
+ end
19
+ end
20
+ alias_method :[], :find
21
+
22
+ private
23
+
24
+ def match_domain?(cookie, domain)
25
+ domain = '.' + domain
26
+ cookie_domain = cookie.domain
27
+ cookie_domain = '.' + cookie_domain unless cookie_domain.start_with?('.')
28
+ # cookie_domain.downcase.end_with? domain.downcase
29
+ domain.downcase.end_with? cookie_domain.downcase
30
+ end
31
+
32
+ def match_path?(cookie, path)
33
+ # cookie.path.start_with? path
34
+ path.start_with? cookie.path
35
+ end
36
+
37
+ def cookies
38
+ @browser.get_raw_cookies
39
+ end
40
+ end
41
+ end
@@ -14,16 +14,31 @@ module Capybara::Apparition
14
14
  end
15
15
 
16
16
  def command(name, **params)
17
- @browser.command_for_session(@session_id, name, params).result
17
+ send_cmd(name, params).result
18
+ end
19
+
20
+ def commands(*names)
21
+ responses = names.map { |name| send_cmd(name) }
22
+ responses.map(&:result)
18
23
  end
19
24
 
20
25
  def async_command(name, **params)
21
- @browser.command_for_session(@session_id, name, params).discard_result
26
+ send_cmd(name, params).discard_result
27
+ end
28
+
29
+ def async_commands(*names)
30
+ names.map { |name| async_command(name) }
22
31
  end
23
32
 
24
33
  def on(event_name, &block)
25
34
  connection.on(event_name, @session_id, &block)
26
35
  end
36
+
37
+ private
38
+
39
+ def send_cmd(name, **params)
40
+ @browser.command_for_session(@session_id, name, params)
41
+ end
27
42
  end
28
43
  end
29
44
  end
@@ -2,8 +2,8 @@
2
2
 
3
3
  require 'uri'
4
4
  require 'forwardable'
5
- require 'capybara/apparition/chrome_client'
6
- require 'capybara/apparition/launcher'
5
+ require 'capybara/apparition/driver/chrome_client'
6
+ require 'capybara/apparition/driver/launcher'
7
7
 
8
8
  module Capybara::Apparition
9
9
  class Driver < Capybara::Driver::Base
@@ -20,7 +20,7 @@ module Capybara::Apparition
20
20
  scroll_to
21
21
  network_traffic clear_network_traffic
22
22
  headers headers= add_headers
23
- cookies remove_cookie clear_cookies cookies_enabled=
23
+ cookies all_cookies remove_cookie clear_cookies cookies_enabled=
24
24
  clear_memory_cache
25
25
  go_back go_forward refresh
26
26
  console_messages] => :browser
@@ -196,13 +196,20 @@ module Capybara::Apparition
196
196
  end
197
197
  end
198
198
 
199
- def set_proxy(host, port, type = nil, user = nil, password = nil, bypass: [])
200
- # TODO: Look at implementing via the CDP Fetch domain
201
- raise ArgumentError, "Proxy auth is not yet implented" if user || password
199
+ def set_proxy(host, port, type = nil, user_ = nil, password_ = nil, user: nil, password: nil, bypass: [])
200
+ if user_ || password_
201
+ warn '#set_proxy: Passing `user` and `password` as positional arguments is deprecated. ' \
202
+ 'Please pass as keyword arguments.'
203
+ user ||= user_
204
+ password ||= password_
205
+ end
206
+
207
+ # TODO: Look at implementing via the CDP Fetch domain when available
202
208
  @options[:browser] ||= {}
203
- @options[:browser].merge!("proxy-server" => "#{type+"=" if type}#{host}:#{port}")
209
+ @options[:browser]['proxy-server'] = "#{type + '=' if type}#{host}:#{port}"
204
210
  bypass = Array(bypass).join(';')
205
- @options[:browser].merge!("proxy-bypass-list" => bypass) unless bypass.empty?
211
+ @options[:browser]['proxy-bypass-list'] = bypass unless bypass.empty?
212
+ browser.set_proxy_auth(user, password) if user || password
206
213
  end
207
214
 
208
215
  def add_header(name, value, options = {})
@@ -216,7 +223,7 @@ module Capybara::Apparition
216
223
  end
217
224
  end
218
225
 
219
- def set_cookie(name, value=nil, options = {})
226
+ def set_cookie(name, value = nil, options = {})
220
227
  name, value, options = parse_raw_cookie(name) if value.nil?
221
228
 
222
229
  options[:name] ||= name
@@ -232,10 +239,12 @@ module Capybara::Apparition
232
239
  browser.set_cookie(options)
233
240
  end
234
241
 
242
+ def proxy_authorize(user = nil, password = nil)
243
+ browser.set_proxy_aauth(user, password)
244
+ end
245
+
235
246
  def basic_authorize(user = nil, password = nil)
236
247
  browser.set_http_auth(user, password)
237
- # credentials = ["#{user}:#{password}"].pack('m*').strip
238
- # add_header('Authorization', "Basic #{credentials}")
239
248
  end
240
249
  alias_method :authenticate, :basic_authorize
241
250
 
@@ -336,7 +345,7 @@ module Capybara::Apparition
336
345
  end
337
346
 
338
347
  def within_frame(frame_selector)
339
- warn "Driver#within_frame is deprecated, please use Session#within_frame"
348
+ warn 'Driver#within_frame is deprecated, please use Session#within_frame'
340
349
 
341
350
  frame = case frame_selector
342
351
  when Capybara::Apparition::Node
@@ -347,7 +356,7 @@ module Capybara::Apparition
347
356
  find_css("iframe[name='#{frame_selector}']")[0]
348
357
  else
349
358
  raise TypeError, 'Unknown frame selector'
350
- command("FrameFocus")
359
+ # command('FrameFocus')
351
360
  end
352
361
 
353
362
  switch_to_frame(frame)
@@ -358,6 +367,10 @@ module Capybara::Apparition
358
367
  end
359
368
  end
360
369
 
370
+ def error_messages
371
+ console_messages('error')
372
+ end
373
+
361
374
  private
362
375
 
363
376
  def parse_raw_cookie(raw)
@@ -385,7 +398,8 @@ module Capybara::Apparition
385
398
  raise Capybara::ModalNotFound if modal_text.nil? || (expect_text && !modal_text.match(expect_regexp))
386
399
  rescue Capybara::ModalNotFound => e
387
400
  if timer.expired?
388
- raise e, 'Timed out waiting for modal dialog. Unable to find modal dialog.' if !found_text
401
+ raise e, 'Timed out waiting for modal dialog. Unable to find modal dialog.' unless found_text
402
+
389
403
  raise e, 'Unable to find modal dialog' \
390
404
  "#{" with #{expect_text}" if expect_text}" \
391
405
  "#{", did find modal with #{found_text}" if found_text}"
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'capybara/apparition/errors'
4
- require 'capybara/apparition/web_socket_client'
5
- require 'capybara/apparition/response'
4
+ require 'capybara/apparition/driver/web_socket_client'
5
+ require 'capybara/apparition/driver/response'
6
6
 
7
7
  module Capybara::Apparition
8
8
  class ChromeClient
@@ -135,7 +135,6 @@ module Capybara
135
135
  'The element you are trying to access is not from the current page'
136
136
  end
137
137
  end
138
- NodeNotAttachedError = ObsoleteNode
139
138
 
140
139
  class UnsupportedFeature < ClientError
141
140
  def name
@@ -46,7 +46,7 @@ module Capybara::Apparition
46
46
  end
47
47
 
48
48
  def all_text
49
- text = evaluate_on('function(){ return this.textContent }')
49
+ text = evaluate_on('() => this.textContent')
50
50
  text.to_s.gsub(/[\u200b\u200e\u200f]/, '')
51
51
  .gsub(/[\ \n\f\t\v\u2028\u2029]+/, ' ')
52
52
  .gsub(/\A[[:space:]&&[^\u00a0]]+/, '')
@@ -64,20 +64,21 @@ module Capybara::Apparition
64
64
  .tr("\u00a0", ' ')
65
65
  end
66
66
 
67
- def text # capybara-webkit method
68
- warn "Node#text is deprecated, please use Node#visible_text instead"
67
+ # capybara-webkit method
68
+ def text
69
+ warn 'Node#text is deprecated, please use Node#visible_text instead'
69
70
  visible_text
70
71
  end
71
72
 
72
73
  def property(name)
73
- evaluate_on('function(name){ return this[name] }', value: name)
74
+ evaluate_on('name => this[name]', value: name)
74
75
  end
75
76
 
76
77
  def attribute(name)
77
78
  if %w[checked selected].include?(name.to_s)
78
79
  property(name)
79
80
  else
80
- evaluate_on('function(name){ return this.getAttribute(name)}', value: name)
81
+ evaluate_on('name => this.getAttribute(name)', value: name)
81
82
  end
82
83
  end
83
84
 
@@ -95,6 +96,10 @@ module Capybara::Apparition
95
96
  evaluate_on GET_VALUE_JS
96
97
  end
97
98
 
99
+ def style(styles)
100
+ evaluate_on GET_STYLES_JS, value: styles
101
+ end
102
+
98
103
  def set(value, **_options)
99
104
  if tag_name == 'input'
100
105
  case self[:type]
@@ -137,7 +142,7 @@ module Capybara::Apparition
137
142
  end
138
143
 
139
144
  def tag_name
140
- @tag_name ||= evaluate_on('function(){ return this.tagName; }').downcase
145
+ @tag_name ||= evaluate_on('() => this.tagName').downcase
141
146
  end
142
147
 
143
148
  def visible?
@@ -169,7 +174,7 @@ module Capybara::Apparition
169
174
  begin
170
175
  new_pos = element_click_pos(options)
171
176
  puts "Element moved from #{pos} to #{new_pos}" unless pos == new_pos
172
- rescue WrongWorld
177
+ rescue WrongWorld # rubocop:disable Lint/HandleExceptions
173
178
  end
174
179
  end
175
180
  # Wait a short time to see if click triggers page load
@@ -204,19 +209,25 @@ module Capybara::Apparition
204
209
  mouseenter: ['MouseEvent'],
205
210
  mouseleave: ['MouseEvent'],
206
211
  mousemove: ['MouseEvent', { bubbles: true, cancelable: true }],
207
- submit: ['Event', { bubbles: true, cancelable: true }]
212
+ mouseover: ['MouseEvent', { bubbles: true, cancelable: true }],
213
+ mouseout: ['MouseEvent', { bubbles: true, cancelable: true }],
214
+ context_menu: ['MouseEvent', { bubble: true, cancelable: true }],
215
+ submit: ['Event', { bubbles: true, cancelable: true }],
216
+ change: ['Event', { bubbles: true, cacnelable: false }],
217
+ input: ['InputEvent', { bubbles: true, cacnelable: false }],
218
+ wheel: ['WheelEvent', { bubbles: true, cancelable: true }]
208
219
  }.freeze
209
220
 
210
- def trigger(name, **options)
211
- raise ArgumentError, 'Unknown event' unless EVENTS.key?(name.to_sym)
221
+ def trigger(name, event_type = nil, **options)
222
+ raise ArgumentError, 'Unknown event' unless EVENTS.key?(name.to_sym) || event_type
212
223
 
213
- event_type, opts = *EVENTS[name.to_sym], {}
224
+ event_type, opts = *EVENTS[name.to_sym], {} if event_type.nil?
214
225
 
215
226
  evaluate_on DISPATCH_EVENT_JS, { value: event_type }, { value: name }, value: opts.merge(options)
216
227
  end
217
228
 
218
229
  def ==(other)
219
- evaluate_on('function(el){ return this == el; }', objectId: other.id)
230
+ evaluate_on('el => this == el', objectId: other.id)
220
231
  rescue ObsoleteNode
221
232
  false
222
233
  end
@@ -328,8 +339,9 @@ module Capybara::Apparition
328
339
 
329
340
  def scroll_by(x, y)
330
341
  evaluate_on <<~JS, { value: x }, value: y
331
- function(x, y){ this.scrollBy(x,y); }
342
+ (x, y) => this.scrollBy(x,y)
332
343
  JS
344
+ self
333
345
  end
334
346
 
335
347
  def scroll_to(element, location, position = nil)
@@ -350,7 +362,7 @@ module Capybara::Apparition
350
362
  obsolete_checked_function = <<~JS
351
363
  function(){
352
364
  if (!this.ownerDocument.contains(this)) { throw 'ObsoleteNode' };
353
- return #{page_function.strip}.apply(this, arguments);
365
+ return (#{page_function.strip}).apply(this, arguments);
354
366
  }
355
367
  JS
356
368
  response = @page.command('Runtime.callFunctionOn',
@@ -371,7 +383,7 @@ module Capybara::Apparition
371
383
  private
372
384
 
373
385
  def in_view_bounding_rect
374
- evaluate_on('function(){ this.scrollIntoViewIfNeeded() }')
386
+ evaluate_on('() => this.scrollIntoViewIfNeeded()')
375
387
  result = evaluate_on GET_BOUNDING_CLIENT_RECT_JS
376
388
  result = result['model'] if result && result['model']
377
389
  result
@@ -422,7 +434,7 @@ module Capybara::Apparition
422
434
  value = value.to_s
423
435
  if value.empty? && clear.nil?
424
436
  evaluate_on <<~JS
425
- function() {
437
+ () => {
426
438
  this.focus();
427
439
  this.value = '';
428
440
  this.dispatchEvent(new Event('change', { bubbles: true }));
@@ -479,7 +491,7 @@ module Capybara::Apparition
479
491
 
480
492
  def update_value_js(value)
481
493
  evaluate_on(<<~JS, value: value)
482
- function(value){
494
+ value => {
483
495
  if (document.activeElement !== this){
484
496
  this.focus();
485
497
  }
@@ -503,7 +515,7 @@ module Capybara::Apparition
503
515
  hit_node = Capybara::Apparition::Node.new(driver, @page, r_o['objectId'])
504
516
  result = begin
505
517
  evaluate_on(<<~JS, objectId: hit_node.id)
506
- function(hit_node){
518
+ (hit_node) => {
507
519
  if ((hit_node == this) || this.contains(hit_node))
508
520
  return { status: 'success' };
509
521
  return { status: 'failure' };
@@ -513,38 +525,6 @@ module Capybara::Apparition
513
525
  { 'status': 'failure' }
514
526
  end
515
527
  OpenStruct.new(success: result['status'] == 'success', selector: r_o['description'])
516
-
517
- # frame_offset = @page.current_frame_offset
518
- # # return { status: 'failure' } if x < 0 || y < 0
519
- # result = evaluate_on(<<~JS, { value: x - frame_offset[:x] }, value: y - frame_offset[:y])
520
- # function(x,y){
521
- # const hit_node = document.elementFromPoint(x,y);
522
- # if ((hit_node == this) || this.contains(hit_node))
523
- # return { status: 'success' };
524
- #
525
- # const getSelector = function(element){
526
- # if (element == null)
527
- # return 'Element out of bounds';
528
- #
529
- # let selector = '';
530
- # if (element.tagName != 'HTML')
531
- # selector = getSelector(element.parentNode) + ' ';
532
- # selector += element.tagName.toLowerCase();
533
- # if (element.id)
534
- # selector += `#${element.id}`;
535
- #
536
- # for (let className of element.classList){
537
- # if (className != '')
538
- # selector += `.${className}`;
539
- # }
540
- # return selector;
541
- # }
542
- #
543
- # return { status: 'failure', selector: getSelector(hit_node) };
544
- # }
545
- # JS
546
- #
547
- # OpenStruct.new(success: result['status'] == 'success', selector: result['selector'])
548
528
  end
549
529
 
550
530
  def scroll_element_to_location(element, location)
@@ -558,7 +538,7 @@ module Capybara::Apparition
558
538
  else
559
539
  raise ArgumentError, "Invalid scroll_to location: #{location}"
560
540
  end
561
- element.evaluate_on "function(){ this.scrollIntoView(#{scroll_opts}) }"
541
+ element.evaluate_on "() => this.scrollIntoView(#{scroll_opts})"
562
542
  end
563
543
 
564
544
  def scroll_to_location(location)
@@ -570,12 +550,12 @@ module Capybara::Apparition
570
550
  when :center
571
551
  '(this.scrollHeight - this.clientHeight)/2'
572
552
  end
573
- evaluate_on "function(){ this.scrollTo(0, #{scroll_y}) }"
553
+ evaluate_on "() => this.scrollTo(0, #{scroll_y})"
574
554
  end
575
555
 
576
556
  def scroll_to_coords(x, y)
577
557
  evaluate_on <<~JS, { value: x }, value: y
578
- function(x,y){ this.scrollTo(x,y) }
558
+ (x,y) => this.scrollTo(x,y)
579
559
  JS
580
560
  end
581
561
 
@@ -831,5 +811,15 @@ module Capybara::Apparition
831
811
  this.dispatchEvent(event);
832
812
  }
833
813
  JS
814
+
815
+ GET_STYLES_JS = <<~JS
816
+ function(styles){
817
+ style = window.getComputedStyle(this);
818
+ return styles.reduce((res,name) => {
819
+ res[name] = style[name];
820
+ return res;
821
+ }, {})
822
+ }
823
+ JS
834
824
  end
835
825
  end
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'capybara/apparition/frame_manager'
4
- require 'capybara/apparition/mouse'
5
- require 'capybara/apparition/keyboard'
3
+ require 'capybara/apparition/page/frame_manager'
4
+ require 'capybara/apparition/page/mouse'
5
+ require 'capybara/apparition/page/keyboard'
6
6
 
7
7
  module Capybara::Apparition
8
8
  class Page
@@ -20,12 +20,13 @@ module Capybara::Apparition
20
20
 
21
21
  page = Page.new(browser, session, id, ignore_https_errors, screenshot_task_queue, js_errors)
22
22
 
23
- session.command 'Network.enable'
24
- session.command 'Runtime.enable'
25
- session.command 'Security.enable'
23
+ session.async_commands 'Network.enable', 'Runtime.enable', 'Security.enable', 'DOM.enable'
24
+ # session.command 'Network.enable'
25
+ # session.command 'Runtime.enable'
26
+ # session.command 'Security.enable'
26
27
  # session.command 'Security.setOverrideCertificateErrors', override: true if ignore_https_errors
27
28
  session.command 'Security.setIgnoreCertificateErrors', ignore: !!ignore_https_errors
28
- session.command 'DOM.enable'
29
+ # session.command 'DOM.enable'
29
30
  # session.command 'Log.enable'
30
31
  if Capybara.save_path
31
32
  session.command 'Page.setDownloadBehavior', behavior: 'allow', downloadPath: Capybara.save_path
@@ -46,18 +47,26 @@ module Capybara::Apparition
46
47
  @status_code = 0
47
48
  @url_blacklist = []
48
49
  @url_whitelist = []
50
+ @credentials = nil
49
51
  @auth_attempts = []
52
+ @proxy_credentials = nil
53
+ @proxy_auth_attempts = []
50
54
  @perm_headers = {}
51
55
  @temp_headers = {}
52
56
  @temp_no_redirect_headers = {}
53
57
  @viewport_size = nil
54
58
  @network_traffic = []
55
59
  @open_resource_requests = {}
60
+ @raise_js_errors = js_errors
56
61
  @js_error = nil
62
+ @modal_mutex = Mutex.new
63
+ @modal_closed = ConditionVariable.new
57
64
 
58
65
  register_event_handlers
59
66
 
60
- register_js_error_handler if js_errors
67
+ register_js_error_handler # if js_errors
68
+
69
+ setup_network_interception if browser.proxy_auth
61
70
  end
62
71
 
63
72
  def usable?
@@ -70,6 +79,7 @@ module Capybara::Apparition
70
79
  @response_headers = {}
71
80
  @status_code = 0
72
81
  @auth_attempts = []
82
+ @proxy_auth_attempts = []
73
83
  @perm_headers = {}
74
84
  end
75
85
 
@@ -78,6 +88,11 @@ module Capybara::Apparition
78
88
  @modals.push(modal_response)
79
89
  end
80
90
 
91
+ def proxy_credentials=(creds)
92
+ @proxy_credentials = creds
93
+ setup_network_interception
94
+ end
95
+
81
96
  def credentials=(creds)
82
97
  @credentials = creds
83
98
  setup_network_interception
@@ -151,7 +166,6 @@ module Capybara::Apparition
151
166
  # Wait for the frame creation messages to be processed
152
167
  if timer.expired?
153
168
  puts 'Timed out waiting from frame to be ready'
154
- # byebug
155
169
  raise TimeoutError.new('push_frame')
156
170
  end
157
171
  sleep 0.1
@@ -181,25 +195,41 @@ module Capybara::Apparition
181
195
 
182
196
  def execute(script, *args)
183
197
  wait_for_loaded
184
- _execute_script("function(){ #{script} }", *args)
198
+ _execute_script <<~JS, *args
199
+ function(){
200
+ #{script}
201
+ }
202
+ JS
185
203
  nil
186
204
  end
187
205
 
188
206
  def evaluate(script, *args)
189
207
  wait_for_loaded
190
- _execute_script("function(){ return #{script} }", *args)
208
+ _execute_script <<~JS, *args
209
+ function(){
210
+ let apparitionId=0;
211
+ return (function ider(obj){
212
+ if (obj && (typeof obj == 'object') && !obj.apparitionId){
213
+ obj.apparitionId = ++apparitionId;
214
+ Reflect.ownKeys(obj).forEach(key => ider(obj[key]))
215
+ }
216
+ return obj;
217
+ })((function(){ return #{script} }).apply(this, arguments))
218
+ }
219
+ JS
191
220
  end
192
221
 
193
222
  def evaluate_async(script, _wait_time, *args)
194
223
  wait_for_loaded
195
- _execute_script("function(){
196
- var args = Array.prototype.slice.call(arguments);
197
- return new Promise((resolve, reject)=>{
198
- args.push(resolve);
199
- var fn = function(){ #{script} };
200
- fn.apply(this, args);
201
- });
202
- }", *args)
224
+ _execute_script <<~JS, *args
225
+ function(){
226
+ var args = Array.prototype.slice.call(arguments);
227
+ return new Promise((resolve, reject)=>{
228
+ args.push(resolve);
229
+ (function(){ #{script} }).apply(this, args);
230
+ });
231
+ }
232
+ JS
203
233
  end
204
234
 
205
235
  def refresh
@@ -288,9 +318,15 @@ module Capybara::Apparition
288
318
  wait_for_loaded
289
319
  @viewport_size = { width: width, height: height }
290
320
  result = @browser.command('Browser.getWindowForTarget', targetId: @target_id)
291
- @browser.command('Browser.setWindowBounds',
292
- windowId: result['windowId'],
293
- bounds: { width: width, height: height })
321
+ begin
322
+ @browser.command('Browser.setWindowBounds',
323
+ windowId: result['windowId'],
324
+ bounds: { width: width, height: height })
325
+ rescue WrongWorld # TODO: Fix Error naming here
326
+ @browser.command('Browser.setWindowBounds', windowId: result['windowId'], bounds: { windowState: 'normal' })
327
+ retry
328
+ end
329
+
294
330
  metrics = {
295
331
  mobile: false,
296
332
  width: width,
@@ -339,7 +375,7 @@ module Capybara::Apparition
339
375
 
340
376
  def update_headers(async: false)
341
377
  method = async ? :async_command : :command
342
- if (ua = extra_headers.find { |k, _v| k=~/^User-Agent$/i })
378
+ if (ua = extra_headers.find { |k, _v| k =~ /^User-Agent$/i })
343
379
  send(method, 'Network.setUserAgentOverride', userAgent: ua[1])
344
380
  end
345
381
  send(method, 'Network.setExtraHTTPHeaders', headers: extra_headers)
@@ -378,23 +414,14 @@ module Capybara::Apparition
378
414
  def register_event_handlers
379
415
  @session.on 'Page.javascriptDialogOpening' do |params|
380
416
  type = params['type'].to_sym
381
- accept = if type == :beforeunload
382
- true
383
- else
384
- response = @modals.pop
385
- if !response&.key?(type)
386
- handle_unexpected_modal(type)
387
- else
388
- @modal_messages.push(params['message'])
389
- response[type]
390
- end
391
- end
417
+ accept = accept_modal?(type, message: params['message'], manual: params['hasBrowserHandler'])
418
+ next if accept.nil?
392
419
 
393
420
  if type == :prompt
394
421
  case accept
395
422
  when false
396
423
  async_command('Page.handleJavaScriptDialog', accept: false)
397
- when nil
424
+ when true
398
425
  async_command('Page.handleJavaScriptDialog', accept: true, promptText: params['defaultPrompt'])
399
426
  else
400
427
  async_command('Page.handleJavaScriptDialog', accept: true, promptText: accept)
@@ -404,6 +431,12 @@ module Capybara::Apparition
404
431
  end
405
432
  end
406
433
 
434
+ @session.on 'Page.javascriptDialogClosed' do
435
+ @modal_mutex.synchronize do
436
+ @modal_closed.signal
437
+ end
438
+ end
439
+
407
440
  @session.on 'Page.windowOpen' do |params|
408
441
  puts "**** windowOpen was called with: #{params}" if ENV['DEBUG']
409
442
  # TODO: find a better way to handle this
@@ -474,7 +507,6 @@ module Capybara::Apparition
474
507
  puts "unknown frame for context #{frame_id}"
475
508
  end
476
509
  end
477
- # command 'Network.setRequestInterception', patterns: [{urlPattern: '*'}]
478
510
  end
479
511
 
480
512
  @session.on 'Runtime.executionContextDestroyed' do |params|
@@ -519,15 +551,24 @@ module Capybara::Apparition
519
551
  @session.on 'Network.requestIntercepted' do |params|
520
552
  request, interception_id = *params.values_at('request', 'interceptionId')
521
553
  if params['authChallenge']
522
- handle_auth(interception_id)
554
+ if params['authChallenge']['source'] == 'Proxy'
555
+ handle_proxy_auth(interception_id)
556
+ else
557
+ handle_user_auth(interception_id)
558
+ end
523
559
  else
524
560
  process_intercepted_request(interception_id, request, params['isNavigationRequest'])
525
561
  end
526
562
  end
527
563
 
528
564
  @session.on 'Runtime.consoleAPICalled' do |params|
565
+ # {"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}]}}
566
+ details = params.dig('stackTrace', 'callFrames')&.first
529
567
  @browser.console.log(params['type'],
530
- "#{params['args'].map { |arg| arg['description'] || arg['value'] }.join(' ')}")
568
+ params['args'].map { |arg| arg['description'] || arg['value'] }.join(' ').to_s,
569
+ source: details['url'].empty? ? nil : details['url'],
570
+ line_number: details['lineNumber'].zero? ? nil : details['lineNumber'],
571
+ columnNumber: details['columnNumber'].zero? ? nil : details['columnNumber'])
531
572
  end
532
573
 
533
574
  # @session.on 'Security.certificateError' do |params|
@@ -544,7 +585,14 @@ module Capybara::Apparition
544
585
 
545
586
  def register_js_error_handler
546
587
  @session.on 'Runtime.exceptionThrown' do |params|
547
- @js_error ||= params.dig('exceptionDetails', 'exception', 'description')
588
+ @js_error ||= params.dig('exceptionDetails', 'exception', 'description') if @raise_js_errors
589
+
590
+ details = params.dig('exceptionDetails', 'stackTrace', 'callFrames')&.first
591
+ @browser.console.log('error',
592
+ params.dig('exceptionDetails', 'exception', 'description'),
593
+ source: details['url'].empty? ? nil : details['url'],
594
+ line_number: details['lineNumber'].zero? ? nil : details['lineNumber'],
595
+ columnNumber: details['columnNumber'].zero? ? nil : details['columnNumber'])
548
596
  end
549
597
  end
550
598
 
@@ -607,6 +655,20 @@ module Capybara::Apparition
607
655
  wait_for_loaded
608
656
  end
609
657
 
658
+ def accept_modal?(type, message:, manual:)
659
+ if type == :beforeunload
660
+ true
661
+ else
662
+ response = @modals.pop
663
+ if !response&.key?(type)
664
+ manual ? manual_unexpected_modal(type) : auto_unexpected_modal(type)
665
+ else
666
+ @modal_messages.push(message)
667
+ response[type].nil? ? true : response[type]
668
+ end
669
+ end
670
+ end
671
+
610
672
  def _execute_script(script, *args)
611
673
  args = args.map do |arg|
612
674
  if arg.is_a? Capybara::Apparition::Node
@@ -658,26 +720,48 @@ module Capybara::Apparition
658
720
  decode_result(result)
659
721
  end
660
722
 
661
- def handle_unexpected_modal(type)
723
+ def manual_unexpected_modal(type)
724
+ warn "An unexpected #{type} modal has opened - please close"
725
+ @modal_mutex.synchronize do
726
+ @modal_closed.wait(@modal_mutex)
727
+ end
728
+ nil
729
+ end
730
+
731
+ def auto_unexpected_modal(type)
662
732
  case type
663
733
  when :prompt
664
734
  warn 'Unexpected prompt modal - accepting with the default value.' \
665
- 'This is deprecated behavior, start using `accept_prompt`.'
666
- nil
735
+ 'You should be using `accept_prompt` or `dismiss_prompt`.'
667
736
  when :confirm
668
737
  warn 'Unexpected confirm modal - accepting.' \
669
- 'This is deprecated behavior, start using `accept_confirm`.'
670
- true
738
+ 'You should be using `accept_confirm` or `dismiss_confirm`.'
671
739
  else
672
- raise "Unexpected #{type} modal"
740
+ warn 'Unexpected alert modal - clearing.' \
741
+ 'You should be using `accept_alert`.'
673
742
  end
743
+ true
674
744
  end
675
745
 
676
- def handle_auth(interception_id)
746
+ def handle_proxy_auth(interception_id)
747
+ credentials_response = if @proxy_auth_attempts.include?(interception_id)
748
+ puts 'Cancelling proxy auth' if ENV['DEBUG']
749
+ { response: 'CancelAuth' }
750
+ else
751
+ puts 'Replying with proxy auth credentials' if ENV['DEBUG']
752
+ @proxy_auth_attempts.push(interception_id)
753
+ { response: 'ProvideCredentials' }.merge(@browser.proxy_auth || {})
754
+ end
755
+ continue_request(interception_id, authChallengeResponse: credentials_response)
756
+ end
757
+
758
+ def handle_user_auth(interception_id)
677
759
  credentials_response = if @auth_attempts.include?(interception_id)
760
+ puts 'Cancelling auth' if ENV['DEBUG']
678
761
  { response: 'CancelAuth' }
679
762
  else
680
763
  @auth_attempts.push(interception_id)
764
+ puts 'Replying with auth credentials' if ENV['DEBUG']
681
765
  { response: 'ProvideCredentials' }.merge(@credentials || {})
682
766
  end
683
767
  continue_request(interception_id, authChallengeResponse: credentials_response)
@@ -690,7 +774,7 @@ module Capybara::Apparition
690
774
  objectId: result['objectId'],
691
775
  ownProperties: true)
692
776
 
693
- properties = remote_object['result']
777
+ properties = remote_object['result'].reject { |prop| prop['name'] == 'apparitionId' }
694
778
  results = []
695
779
 
696
780
  properties.each do |property|
@@ -714,16 +798,22 @@ module Capybara::Apparition
714
798
  remote_object = command('Runtime.getProperties',
715
799
  objectId: result['objectId'],
716
800
  ownProperties: true)
717
- stable_id = remote_object['internalProperties']
718
- .find { |prop| prop['name'] == '[[StableObjectId]]' }
719
- .dig('value', 'value')
801
+
802
+ # stable_id went away in Chrome 72 - we add our own id in evaluate
803
+ # stable_id = remote_object['internalProperties']
804
+ # &.find { |prop| prop['name'] == '[[StableObjectId]]' }
805
+ # &.dig('value', 'value')
806
+
720
807
  # We could actually return cyclic objects here but Capybara would need to be updated to support
721
- # return object_cache[stable_id] if object_cache.key?(stable_id)
808
+ # return object_cache[stable_id] if object_cache.key?(stable_id) # and update the cache to be a hash
722
809
 
810
+ stable_id = remote_object['result']
811
+ &.find { |prop| prop['name'] == 'apparitionId'}
812
+ &.dig('value', 'value')
723
813
  return '(cyclic structure)' if object_cache.key?(stable_id)
724
814
 
725
815
  object_cache[stable_id] = {}
726
- properties = remote_object['result']
816
+ properties = remote_object['result'].reject { |prop| prop['name'] == "apparitionId"}
727
817
 
728
818
  return properties.each_with_object(object_cache[stable_id]) do |property, memo|
729
819
  if property['enumerable']
File without changes
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'capybara/apparition/frame'
3
+ require 'capybara/apparition/page/frame'
4
4
 
5
5
  module Capybara::Apparition
6
6
  class FrameManager
@@ -10,10 +10,12 @@ module Capybara::Apparition
10
10
 
11
11
  def click_at(x:, y:, button: 'left', count: 1, modifiers: [])
12
12
  move_to x: x, y: y
13
- @keyboard.with_keys(modifiers) do
14
- mouse_params = { x: x, y: y, button: button, count: count }
15
- down mouse_params
16
- up mouse_params
13
+ count.times do |num|
14
+ @keyboard.with_keys(modifiers) do
15
+ mouse_params = { x: x, y: y, button: button, count: num+1 }
16
+ down mouse_params
17
+ up mouse_params
18
+ end
17
19
  end
18
20
  self
19
21
  end
@@ -24,21 +26,21 @@ module Capybara::Apparition
24
26
  self
25
27
  end
26
28
 
27
- def down(**options)
28
- options = @current_pos.merge(options)
29
+ def down(button: 'left', **options)
30
+ options = @current_pos.merge(button: button).merge(options)
29
31
  mouse_event('mousePressed', options)
30
32
  self
31
33
  end
32
34
 
33
- def up(**options)
34
- options = @current_pos.merge(options)
35
+ def up(button: 'left', **options)
36
+ options = @current_pos.merge(button: button).merge(options)
35
37
  mouse_event('mouseReleased', options)
36
38
  self
37
39
  end
38
40
 
39
41
  private
40
42
 
41
- def mouse_event(type, x:, y:, button: 'left', count: 1)
43
+ def mouse_event(type, x:, y:, button: 'none', count: 1)
42
44
  @page.command('Input.dispatchMouseEvent',
43
45
  type: type,
44
46
  button: button,
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Capybara
4
4
  module Apparition
5
- VERSION = '0.0.4'
5
+ VERSION = '0.0.5'
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.4
4
+ version: 0.0.5
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-28 00:00:00.000000000 Z
11
+ date: 2019-01-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: backports
@@ -184,6 +184,20 @@ dependencies:
184
184
  - - "~>"
185
185
  - !ruby/object:Gem::Version
186
186
  version: '3.6'
187
+ - !ruby/object:Gem::Dependency
188
+ name: selenium-webdriver
189
+ requirement: !ruby/object:Gem::Requirement
190
+ requirements:
191
+ - - ">="
192
+ - !ruby/object:Gem::Version
193
+ version: '0'
194
+ type: :development
195
+ prerelease: false
196
+ version_requirements: !ruby/object:Gem::Requirement
197
+ requirements:
198
+ - - ">="
199
+ - !ruby/object:Gem::Version
200
+ version: '0'
187
201
  - !ruby/object:Gem::Dependency
188
202
  name: sinatra
189
203
  requirement: !ruby/object:Gem::Requirement
@@ -210,20 +224,19 @@ files:
210
224
  - README.md
211
225
  - lib/capybara/apparition.rb
212
226
  - lib/capybara/apparition/browser.rb
213
- - lib/capybara/apparition/chrome_client.rb
214
227
  - lib/capybara/apparition/console.rb
215
228
  - lib/capybara/apparition/cookie.rb
229
+ - lib/capybara/apparition/cookie_jar.rb
216
230
  - lib/capybara/apparition/dev_tools_protocol/session.rb
217
231
  - lib/capybara/apparition/dev_tools_protocol/target.rb
218
232
  - lib/capybara/apparition/dev_tools_protocol/target_manager.rb
219
233
  - lib/capybara/apparition/driver.rb
234
+ - lib/capybara/apparition/driver/chrome_client.rb
235
+ - lib/capybara/apparition/driver/launcher.rb
236
+ - lib/capybara/apparition/driver/response.rb
237
+ - lib/capybara/apparition/driver/web_socket_client.rb
220
238
  - lib/capybara/apparition/errors.rb
221
- - lib/capybara/apparition/frame.rb
222
- - lib/capybara/apparition/frame_manager.rb
223
239
  - lib/capybara/apparition/inspector.rb
224
- - lib/capybara/apparition/keyboard.rb
225
- - lib/capybara/apparition/launcher.rb
226
- - lib/capybara/apparition/mouse.rb
227
240
  - lib/capybara/apparition/network_traffic.rb
228
241
  - lib/capybara/apparition/network_traffic/error.rb
229
242
  - lib/capybara/apparition/network_traffic/request.rb
@@ -231,10 +244,12 @@ files:
231
244
  - lib/capybara/apparition/node.rb
232
245
  - lib/capybara/apparition/node/drag.rb
233
246
  - lib/capybara/apparition/page.rb
234
- - lib/capybara/apparition/response.rb
247
+ - lib/capybara/apparition/page/frame.rb
248
+ - lib/capybara/apparition/page/frame_manager.rb
249
+ - lib/capybara/apparition/page/keyboard.rb
250
+ - lib/capybara/apparition/page/mouse.rb
235
251
  - lib/capybara/apparition/utility.rb
236
252
  - lib/capybara/apparition/version.rb
237
- - lib/capybara/apparition/web_socket_client.rb
238
253
  homepage: https://github.com/twalpole/apparition
239
254
  licenses:
240
255
  - MIT