apparition 0.0.4 → 0.0.5

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.
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