apparition 0.1.0 → 0.6.0

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