apparition 0.1.0 → 0.6.0

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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +40 -4
  3. data/lib/capybara/apparition.rb +0 -2
  4. data/lib/capybara/apparition/browser.rb +75 -133
  5. data/lib/capybara/apparition/browser/cookie.rb +4 -16
  6. data/lib/capybara/apparition/browser/header.rb +2 -2
  7. data/lib/capybara/apparition/browser/launcher.rb +25 -0
  8. data/lib/capybara/apparition/browser/launcher/local.rb +213 -0
  9. data/lib/capybara/apparition/browser/launcher/remote.rb +55 -0
  10. data/lib/capybara/apparition/browser/page_manager.rb +90 -0
  11. data/lib/capybara/apparition/browser/window.rb +29 -29
  12. data/lib/capybara/apparition/configuration.rb +100 -0
  13. data/lib/capybara/apparition/console.rb +8 -1
  14. data/lib/capybara/apparition/dev_tools_protocol/remote_object.rb +23 -7
  15. data/lib/capybara/apparition/dev_tools_protocol/session.rb +3 -4
  16. data/lib/capybara/apparition/driver.rb +107 -35
  17. data/lib/capybara/apparition/driver/chrome_client.rb +13 -8
  18. data/lib/capybara/apparition/driver/response.rb +1 -1
  19. data/lib/capybara/apparition/driver/web_socket_client.rb +1 -0
  20. data/lib/capybara/apparition/errors.rb +3 -3
  21. data/lib/capybara/apparition/network_traffic/error.rb +1 -0
  22. data/lib/capybara/apparition/network_traffic/request.rb +5 -5
  23. data/lib/capybara/apparition/node.rb +142 -50
  24. data/lib/capybara/apparition/node/drag.rb +165 -65
  25. data/lib/capybara/apparition/page.rb +180 -142
  26. data/lib/capybara/apparition/page/frame.rb +3 -0
  27. data/lib/capybara/apparition/page/frame_manager.rb +2 -1
  28. data/lib/capybara/apparition/page/keyboard.rb +29 -7
  29. data/lib/capybara/apparition/page/mouse.rb +20 -6
  30. data/lib/capybara/apparition/utility.rb +1 -1
  31. data/lib/capybara/apparition/version.rb +1 -1
  32. metadata +53 -23
  33. data/lib/capybara/apparition/dev_tools_protocol/target.rb +0 -64
  34. data/lib/capybara/apparition/dev_tools_protocol/target_manager.rb +0 -48
  35. data/lib/capybara/apparition/driver/launcher.rb +0 -217
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara::Apparition
4
+ class Configuration
5
+ class << self
6
+ private
7
+
8
+ def instance
9
+ @instance ||= new
10
+ end
11
+ end
12
+
13
+ def self.to_hash
14
+ instance.freeze.to_hash
15
+ end
16
+
17
+ def self.modify
18
+ raise 'All configuration must take place before the driver starts' if instance.frozen?
19
+ end
20
+
21
+ attr_accessor :allowed_urls
22
+ attr_writer :block_unknown_urls
23
+ attr_accessor :blocked_urls
24
+ attr_accessor :debug
25
+ attr_writer :ignore_ssl_errors
26
+ attr_accessor :proxy
27
+ attr_accessor :stderr
28
+ attr_accessor :timeout
29
+ attr_writer :skip_image_loading
30
+ attr_accessor :raise_javascript_errors
31
+
32
+ def initialize
33
+ @allowed_urls = []
34
+ @blocked_urls = []
35
+ @block_unknown_urls = false
36
+ @debug = false
37
+ @ignore_ssl_errors = false
38
+ @proxy = nil
39
+ @skip_image_loading = false
40
+ @stderr = $stderr
41
+ @timeout = -1
42
+ @raise_javascript_errors = false
43
+ end
44
+
45
+ def allow_url(url)
46
+ @allowed_urls << url
47
+ end
48
+
49
+ def block_url(url)
50
+ @blocked_urls << url
51
+ end
52
+
53
+ def block_unknown_urls
54
+ @block_unknown_urls = true
55
+ end
56
+
57
+ def block_unknown_urls?
58
+ @block_unknown_urls
59
+ end
60
+
61
+ def allow_unknown_urls
62
+ allow_url('*')
63
+ end
64
+
65
+ def ignore_ssl_errors
66
+ @ignore_ssl_errors = true
67
+ end
68
+
69
+ def ignore_ssl_errors?
70
+ @ignore_ssl_errors
71
+ end
72
+
73
+ def skip_image_loading
74
+ @skip_image_loading = true
75
+ end
76
+
77
+ def skip_image_loading?
78
+ @skip_image_loading
79
+ end
80
+
81
+ def use_proxy(proxy)
82
+ @proxy = proxy
83
+ end
84
+
85
+ def to_hash
86
+ {
87
+ url_whitelist: allowed_urls,
88
+ block_unknown_urls: block_unknown_urls?,
89
+ url_blacklist: blocked_urls,
90
+ debug: debug,
91
+ ignore_ssl_errors: ignore_ssl_errors?,
92
+ proxy: proxy,
93
+ skip_image_loading: skip_image_loading?,
94
+ stderr: stderr,
95
+ timeout: timeout,
96
+ js_errors: raise_javascript_errors
97
+ }
98
+ end
99
+ end
100
+ end
@@ -8,8 +8,15 @@ module Capybara::Apparition
8
8
  end
9
9
 
10
10
  def log(type, message, **options)
11
+ return unless @logger
12
+
11
13
  @messages << OpenStruct.new(type: type, message: message, **options)
12
- @logger&.puts "#{type}: #{message}"
14
+ message_to_log = "#{type}: #{message}"
15
+ if @logger.respond_to?(:puts)
16
+ @logger.puts(message_to_log)
17
+ else
18
+ @logger.info(message_to_log)
19
+ end
13
20
  end
14
21
 
15
22
  def clear
@@ -19,13 +19,18 @@ module Capybara::Apparition
19
19
  def cyclic_checked_value(object_cache)
20
20
  if object?
21
21
  if array?
22
- extract_properties_array(get_remote_object(object_id), object_cache)
22
+ extract_properties_array(get_remote_object(id), object_cache)
23
23
  elsif node?
24
24
  params
25
- elsif object_class?
26
- extract_properties_object(get_remote_object(object_id), object_cache)
25
+ elsif date?
26
+ res = get_date_string(id)
27
+ DateTime.parse(res)
27
28
  elsif window_class?
28
- { object_id: object_id }
29
+ { object_id: id }
30
+ elsif validity_state?
31
+ extract_properties_object(get_remote_object(id, false), object_cache)
32
+ elsif object_class? || css_style? || classname?
33
+ extract_properties_object(get_remote_object(id), object_cache)
29
34
  else
30
35
  params['value']
31
36
  end
@@ -38,13 +43,17 @@ module Capybara::Apparition
38
43
 
39
44
  def object?; type == 'object' end
40
45
  def array?; subtype == 'array' end
46
+ def date?; subtype == 'date' end
41
47
  def node?; subtype == 'node' end
42
48
  def object_class?; classname == 'Object' end
49
+ def css_style?; classname == 'CSSStyleDeclaration' end
43
50
  def window_class?; classname == 'Window' end
51
+ def validity_state?; classname == 'ValidityState' end
52
+ def classname?; !classname.nil? end
44
53
 
45
54
  def type; params['type'] end
46
55
  def subtype; params['subtype'] end
47
- def object_id; params['objectId'] end
56
+ def id; params['objectId'] end
48
57
  def classname; params['className'] end
49
58
 
50
59
  def extract_properties_array(properties, object_cache)
@@ -80,8 +89,15 @@ module Capybara::Apparition
80
89
  end
81
90
  end
82
91
 
83
- def get_remote_object(id)
84
- @page.command('Runtime.getProperties', objectId: id, ownProperties: true)['result']
92
+ def get_remote_object(id, own_props = true)
93
+ @page.command('Runtime.getProperties', objectId: id, ownProperties: own_props)['result']
94
+ end
95
+
96
+ def get_date_string(id)
97
+ @page.command('Runtime.callFunctionOn',
98
+ functionDeclaration: 'function(){ return this.toUTCString() }',
99
+ objectId: id,
100
+ returnByValue: true).dig('result', 'value')
85
101
  end
86
102
  end
87
103
  end
@@ -3,12 +3,11 @@
3
3
  module Capybara::Apparition
4
4
  module DevToolsProtocol
5
5
  class Session
6
- attr_reader :browser, :connection, :target_id, :session_id
6
+ attr_reader :browser, :connection, :session_id
7
7
 
8
- def initialize(browser, connection, target_id, session_id)
8
+ def initialize(browser, connection, session_id)
9
9
  @browser = browser
10
10
  @connection = connection
11
- @target_id = target_id
12
11
  @session_id = session_id
13
12
  @handlers = []
14
13
  end
@@ -23,7 +22,7 @@ module Capybara::Apparition
23
22
  end
24
23
 
25
24
  def async_command(name, **params)
26
- send_cmd(name, params).discard_result
25
+ send_cmd(name, **params).discard_result
27
26
  end
28
27
 
29
28
  def async_commands(*names)
@@ -3,7 +3,8 @@
3
3
  require 'uri'
4
4
  require 'forwardable'
5
5
  require 'capybara/apparition/driver/chrome_client'
6
- require 'capybara/apparition/driver/launcher'
6
+ require 'capybara/apparition/configuration'
7
+ require 'capybara/apparition/browser/launcher'
7
8
 
8
9
  module Capybara::Apparition
9
10
  class Driver < Capybara::Driver::Base
@@ -15,7 +16,7 @@ module Capybara::Apparition
15
16
 
16
17
  delegate %i[restart current_url status_code body
17
18
  title frame_title frame_url switch_to_frame
18
- window_handles close_window open_new_window switch_to_window within_window
19
+ window_handles close_window switch_to_window
19
20
  paper_size= zoom_factor=
20
21
  scroll_to
21
22
  network_traffic clear_network_traffic
@@ -32,6 +33,7 @@ module Capybara::Apparition
32
33
  @browser = nil
33
34
  @inspector = nil
34
35
  @client = nil
36
+ @launcher = nil
35
37
  @started = false
36
38
  end
37
39
 
@@ -45,14 +47,14 @@ module Capybara::Apparition
45
47
 
46
48
  def browser
47
49
  @browser ||= begin
48
- browser = Browser.new(client, browser_logger)
49
- browser.js_errors = options.fetch(:js_errors, true)
50
- browser.ignore_https_errors = options.fetch(:ignore_https_errors, false)
51
- browser.extensions = options.fetch(:extensions, [])
52
- browser.debug = options.fetch(:debug, false)
53
- browser.url_blacklist = options[:url_blacklist] || []
54
- browser.url_whitelist = options[:url_whitelist] || []
55
- browser
50
+ Browser.new(client, browser_logger) do |browser|
51
+ browser.js_errors = options.fetch(:js_errors, true)
52
+ browser.ignore_https_errors = options.fetch(:ignore_https_errors, false)
53
+ browser.extensions = options.fetch(:extensions, [])
54
+ browser.debug = options.fetch(:debug, false)
55
+ browser.url_blacklist = options[:url_blacklist] || []
56
+ browser.url_whitelist = options[:url_whitelist] || []
57
+ end
56
58
  end
57
59
  end
58
60
 
@@ -62,10 +64,7 @@ module Capybara::Apparition
62
64
 
63
65
  def client
64
66
  @client ||= begin
65
- @launcher ||= Browser::Launcher.start(
66
- headless: options.fetch(:headless, true),
67
- browser_options: browser_options
68
- )
67
+ @launcher ||= Browser::Launcher.start(options)
69
68
  ws_url = @launcher.ws_url
70
69
  ::Capybara::Apparition::ChromeClient.client(ws_url.to_s)
71
70
  end
@@ -98,7 +97,9 @@ module Capybara::Apparition
98
97
  end
99
98
 
100
99
  def find(method, selector)
101
- browser.find(method, selector).map { |page_id, id| Capybara::Apparition::Node.new(self, page_id, id) }
100
+ browser.find(method, selector).map do |page_id, id, attrs|
101
+ Capybara::Apparition::Node.new(self, page_id, id, attrs)
102
+ end
102
103
  end
103
104
 
104
105
  def find_xpath(selector)
@@ -114,20 +115,26 @@ module Capybara::Apparition
114
115
  end
115
116
 
116
117
  def evaluate_script(script, *args)
117
- unwrap_script_result(browser.evaluate(script, *native_args(args)))
118
+ retry_if_wrong_world do
119
+ unwrap_script_result(browser.evaluate(script, *native_args(args)))
120
+ end
118
121
  end
119
122
 
120
123
  def evaluate_async_script(script, *args)
121
- unwrap_script_result(browser.evaluate_async(script, session_wait_time, *native_args(args)))
124
+ retry_if_wrong_world do
125
+ unwrap_script_result(browser.evaluate_async(script, session_wait_time, *native_args(args)))
126
+ end
122
127
  end
123
128
 
124
129
  def execute_script(script, *args)
125
- browser.execute(script, *native_args(args))
130
+ retry_if_wrong_world do
131
+ browser.execute(script, *native_args(args))
132
+ end
126
133
  nil
127
134
  end
128
135
 
129
136
  def current_window_handle
130
- browser.window_handle
137
+ browser.current_window_handle
131
138
  end
132
139
 
133
140
  def no_such_window_error
@@ -158,28 +165,33 @@ module Capybara::Apparition
158
165
  def resize(width, height)
159
166
  browser.resize(width, height, screen: options[:screen_size])
160
167
  end
161
- alias resize_window resize
168
+
169
+ def resize_window(width, height)
170
+ warn '[DEPRECATION] Capybara::Apparition::Driver#resize_window ' \
171
+ 'is deprecated. Please use Capybara::Window#resize_to instead.'
172
+ resize(width, height)
173
+ end
162
174
 
163
175
  def resize_window_to(handle, width, height)
164
- within_window(handle) do
176
+ _within_window(handle) do
165
177
  resize(width, height)
166
178
  end
167
179
  end
168
180
 
169
181
  def maximize_window(handle)
170
- within_window(handle) do
182
+ _within_window(handle) do
171
183
  browser.maximize
172
184
  end
173
185
  end
174
186
 
175
187
  def fullscreen_window(handle)
176
- within_window(handle) do
188
+ _within_window(handle) do
177
189
  browser.fullscreen
178
190
  end
179
191
  end
180
192
 
181
193
  def window_size(handle)
182
- within_window(handle) do
194
+ _within_window(handle) do
183
195
  evaluate_script('[window.innerWidth, window.innerHeight]')
184
196
  end
185
197
  end
@@ -201,14 +213,12 @@ module Capybara::Apparition
201
213
  end
202
214
 
203
215
  def add_header(name, value, options = {})
204
- browser.add_header({ name => value }, { permanent: true }.merge(options))
216
+ browser.add_header({ name => value }, **{ permanent: true }.merge(options))
205
217
  end
206
218
  alias_method :header, :add_header
207
219
 
208
220
  def response_headers
209
- browser.response_headers.each_with_object({}) do |(key, value), hsh|
210
- hsh[key.split('-').map(&:capitalize).join('-')] = value
211
- end
221
+ browser.response_headers.transform_keys { |key| key.split('-').map(&:capitalize).join('-') }
212
222
  end
213
223
 
214
224
  def set_cookie(name, value = nil, options = {})
@@ -228,7 +238,7 @@ module Capybara::Apparition
228
238
  end
229
239
 
230
240
  def proxy_authorize(user = nil, password = nil)
231
- browser.set_proxy_aauth(user, password)
241
+ browser.set_proxy_auth(user, password)
232
242
  end
233
243
 
234
244
  def basic_authorize(user = nil, password = nil)
@@ -266,12 +276,12 @@ module Capybara::Apparition
266
276
  write.close
267
277
  end
268
278
 
269
- STDERR.puts "Apparition execution paused. Press enter (or run 'kill -CONT #{Process.pid}') to continue."
279
+ STDERR.puts "Apparition execution paused. Press enter (or run 'kill -CONT #{Process.pid}') to continue." # rubocop:disable Style/StderrPuts
270
280
 
271
281
  signal = false
272
282
  old_trap = trap('SIGCONT') do
273
283
  signal = true
274
- STDERR.puts "\nSignal SIGCONT received"
284
+ STDERR.puts "\nSignal SIGCONT received" # rubocop:disable Style/StderrPuts
275
285
  end
276
286
  # wait for data on STDIN or signal SIGCONT received
277
287
  keyboard = IO.select([read], nil, nil, 1) until keyboard || signal
@@ -280,13 +290,13 @@ module Capybara::Apparition
280
290
  begin
281
291
  input = read.read_nonblock(80) # clear out the read buffer
282
292
  puts unless input&.end_with?("\n")
283
- rescue EOFError, IO::WaitReadable # rubocop:disable Lint/HandleExceptions
293
+ rescue EOFError, IO::WaitReadable
284
294
  # Ignore problems reading from STDIN.
285
295
  end
286
296
  end
287
297
  ensure
288
298
  trap('SIGCONT', old_trap) # Restore the previous signal handler, if there was one.
289
- STDERR.puts 'Continuing'
299
+ STDERR.puts 'Continuing' # rubocop:disable Style/StderrPuts
290
300
  end
291
301
 
292
302
  def wait?
@@ -336,8 +346,53 @@ module Capybara::Apparition
336
346
  console_messages('error')
337
347
  end
338
348
 
349
+ def within_window(selector, &block)
350
+ warn 'Driver#within_window is deprecated, please switch to using Session#within_window instead.'
351
+ _within_window(selector, &block)
352
+ orig_window = current_window_handle
353
+ switch_to_window(selector)
354
+ begin
355
+ yield
356
+ ensure
357
+ switch_to_window(orig_window)
358
+ end
359
+ end
360
+
361
+ def version
362
+ chrome_version = browser.command('Browser.getVersion')
363
+ format(VERSION_STRING,
364
+ capybara: Capybara::VERSION,
365
+ apparition: Capybara::Apparition::VERSION,
366
+ chrome: chrome_version['product'])
367
+ end
368
+
369
+ def open_new_window
370
+ # needed because Capybara does arity detection on this method
371
+ browser.open_new_window
372
+ end
373
+
339
374
  private
340
375
 
376
+ def retry_if_wrong_world
377
+ timer = Capybara::Helpers.timer(expire_in: session_wait_time)
378
+ begin
379
+ yield
380
+ rescue WrongWorld
381
+ retry unless timer.expired?
382
+ raise
383
+ end
384
+ end
385
+
386
+ def _within_window(selector)
387
+ orig_window = current_window_handle
388
+ switch_to_window(selector)
389
+ begin
390
+ yield
391
+ ensure
392
+ switch_to_window(orig_window)
393
+ end
394
+ end
395
+
341
396
  def browser_options
342
397
  @options[:browser_options]
343
398
  end
@@ -363,7 +418,16 @@ module Capybara::Apparition
363
418
  if @options[:skip_image_loading]
364
419
  browser_options['blink-settings'] = [browser_options['blink-settings'], 'imagesEnabled=false'].compact.join(',')
365
420
  end
421
+
366
422
  @options[:browser_options] = browser_options
423
+ process_cw_options(@options[:cw_options])
424
+ end
425
+
426
+ def process_cw_options(cw_options)
427
+ return if cw_options.nil?
428
+
429
+ (options[:url_blacklist] ||= []).concat cw_options[:url_blacklist]
430
+ options[:js_errors] ||= cw_options[:js_errors]
367
431
  end
368
432
 
369
433
  def process_browser_options(options)
@@ -377,7 +441,7 @@ module Capybara::Apparition
377
441
  end
378
442
  end
379
443
  when Hash
380
- options.each_with_object({}) { |(option, val), hsh| hsh[option.to_s.tr('_', '-')] = val }
444
+ options.transform_keys { |option| option.to_s.tr('_', '-') }
381
445
  else
382
446
  raise ArgumentError, 'browser_options must be an Array or a Hash'
383
447
  end
@@ -454,7 +518,8 @@ module Capybara::Apparition
454
518
  object_cache[arg]
455
519
  when Hash
456
520
  if (arg['subtype'] == 'node') && arg['objectId']
457
- Capybara::Apparition::Node.new(self, browser.current_page, arg['objectId'])
521
+ tag_name = arg['description'].split(/[.#]/, 2)[0]
522
+ Capybara::Apparition::Node.new(self, browser.current_page, arg['objectId'], tag_name: tag_name)
458
523
  else
459
524
  object_cache[arg] = {}
460
525
  arg.each { |k, v| object_cache[arg][k] = unwrap_script_result(v, object_cache) }
@@ -464,5 +529,12 @@ module Capybara::Apparition
464
529
  arg
465
530
  end
466
531
  end
532
+
533
+ VERSION_STRING = <<~VERSION
534
+ Versions in use:
535
+ Capybara: %<capybara>s
536
+ Apparition: %<apparition>s
537
+ Chrome: %<chrome>s
538
+ VERSION
467
539
  end
468
540
  end