apparition 0.1.0 → 0.2.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.
@@ -19,13 +19,16 @@ 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)
28
+ elsif object_class? || css_style?
29
+ extract_properties_object(get_remote_object(id), object_cache)
27
30
  elsif window_class?
28
- { object_id: object_id }
31
+ { object_id: id }
29
32
  else
30
33
  params['value']
31
34
  end
@@ -38,13 +41,15 @@ module Capybara::Apparition
38
41
 
39
42
  def object?; type == 'object' end
40
43
  def array?; subtype == 'array' end
44
+ def date?; subtype == 'date' end
41
45
  def node?; subtype == 'node' end
42
46
  def object_class?; classname == 'Object' end
47
+ def css_style?; classname == 'CSSStyleDeclaration' end
43
48
  def window_class?; classname == 'Window' end
44
49
 
45
50
  def type; params['type'] end
46
51
  def subtype; params['subtype'] end
47
- def object_id; params['objectId'] end
52
+ def id; params['objectId'] end
48
53
  def classname; params['className'] end
49
54
 
50
55
  def extract_properties_array(properties, object_cache)
@@ -80,8 +85,15 @@ module Capybara::Apparition
80
85
  end
81
86
  end
82
87
 
83
- def get_remote_object(id)
84
- @page.command('Runtime.getProperties', objectId: id, ownProperties: true)['result']
88
+ def get_remote_object(id, own_props = true)
89
+ @page.command('Runtime.getProperties', objectId: id, ownProperties: own_props)['result']
90
+ end
91
+
92
+ def get_date_string(id)
93
+ @page.command('Runtime.callFunctionOn',
94
+ functionDeclaration: 'function(){ return this.toUTCString() }',
95
+ objectId: id,
96
+ returnByValue: true).dig('result', 'value')
85
97
  end
86
98
  end
87
99
  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
@@ -4,6 +4,7 @@ require 'uri'
4
4
  require 'forwardable'
5
5
  require 'capybara/apparition/driver/chrome_client'
6
6
  require 'capybara/apparition/driver/launcher'
7
+ require 'capybara/apparition/configuration'
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
 
@@ -114,20 +116,26 @@ module Capybara::Apparition
114
116
  end
115
117
 
116
118
  def evaluate_script(script, *args)
117
- unwrap_script_result(browser.evaluate(script, *native_args(args)))
119
+ retry_if_wrong_world do
120
+ unwrap_script_result(browser.evaluate(script, *native_args(args)))
121
+ end
118
122
  end
119
123
 
120
124
  def evaluate_async_script(script, *args)
121
- unwrap_script_result(browser.evaluate_async(script, session_wait_time, *native_args(args)))
125
+ retry_if_wrong_world do
126
+ unwrap_script_result(browser.evaluate_async(script, session_wait_time, *native_args(args)))
127
+ end
122
128
  end
123
129
 
124
130
  def execute_script(script, *args)
125
- browser.execute(script, *native_args(args))
131
+ retry_if_wrong_world do
132
+ browser.execute(script, *native_args(args))
133
+ end
126
134
  nil
127
135
  end
128
136
 
129
137
  def current_window_handle
130
- browser.window_handle
138
+ browser.current_window_handle
131
139
  end
132
140
 
133
141
  def no_such_window_error
@@ -158,28 +166,33 @@ module Capybara::Apparition
158
166
  def resize(width, height)
159
167
  browser.resize(width, height, screen: options[:screen_size])
160
168
  end
161
- alias resize_window resize
169
+
170
+ def resize_window(width, height)
171
+ warn '[DEPRECATION] Capybara::Apparition::Driver#resize_window ' \
172
+ 'is deprecated. Please use Capybara::Window#resize_to instead.'
173
+ resize(width, height)
174
+ end
162
175
 
163
176
  def resize_window_to(handle, width, height)
164
- within_window(handle) do
177
+ _within_window(handle) do
165
178
  resize(width, height)
166
179
  end
167
180
  end
168
181
 
169
182
  def maximize_window(handle)
170
- within_window(handle) do
183
+ _within_window(handle) do
171
184
  browser.maximize
172
185
  end
173
186
  end
174
187
 
175
188
  def fullscreen_window(handle)
176
- within_window(handle) do
189
+ _within_window(handle) do
177
190
  browser.fullscreen
178
191
  end
179
192
  end
180
193
 
181
194
  def window_size(handle)
182
- within_window(handle) do
195
+ _within_window(handle) do
183
196
  evaluate_script('[window.innerWidth, window.innerHeight]')
184
197
  end
185
198
  end
@@ -266,12 +279,12 @@ module Capybara::Apparition
266
279
  write.close
267
280
  end
268
281
 
269
- STDERR.puts "Apparition execution paused. Press enter (or run 'kill -CONT #{Process.pid}') to continue."
282
+ STDERR.puts "Apparition execution paused. Press enter (or run 'kill -CONT #{Process.pid}') to continue." # rubocop:disable Style/StderrPuts
270
283
 
271
284
  signal = false
272
285
  old_trap = trap('SIGCONT') do
273
286
  signal = true
274
- STDERR.puts "\nSignal SIGCONT received"
287
+ STDERR.puts "\nSignal SIGCONT received" # rubocop:disable Style/StderrPuts
275
288
  end
276
289
  # wait for data on STDIN or signal SIGCONT received
277
290
  keyboard = IO.select([read], nil, nil, 1) until keyboard || signal
@@ -286,7 +299,7 @@ module Capybara::Apparition
286
299
  end
287
300
  ensure
288
301
  trap('SIGCONT', old_trap) # Restore the previous signal handler, if there was one.
289
- STDERR.puts 'Continuing'
302
+ STDERR.puts 'Continuing' # rubocop:disable Style/StderrPuts
290
303
  end
291
304
 
292
305
  def wait?
@@ -336,8 +349,53 @@ module Capybara::Apparition
336
349
  console_messages('error')
337
350
  end
338
351
 
352
+ def within_window(selector, &block)
353
+ warn 'Driver#within_window is deprecated, please switch to using Session#within_window instead.'
354
+ _within_window(selector, &block)
355
+ orig_window = current_window_handle
356
+ switch_to_window(selector)
357
+ begin
358
+ yield
359
+ ensure
360
+ switch_to_window(orig_window)
361
+ end
362
+ end
363
+
364
+ def version
365
+ chrome_version = browser.command('Browser.getVersion')
366
+ format(VERSION_STRING,
367
+ capybara: Capybara::VERSION,
368
+ apparition: Capybara::Apparition::VERSION,
369
+ chrome: chrome_version['product'])
370
+ end
371
+
372
+ def open_new_window
373
+ # needed because Capybara does arity detection on this method
374
+ browser.open_new_window
375
+ end
376
+
339
377
  private
340
378
 
379
+ def retry_if_wrong_world
380
+ timer = Capybara::Helpers.timer(expire_in: session_wait_time)
381
+ begin
382
+ yield
383
+ rescue WrongWorld
384
+ retry unless timer.expired?
385
+ raise
386
+ end
387
+ end
388
+
389
+ def _within_window(selector)
390
+ orig_window = current_window_handle
391
+ switch_to_window(selector)
392
+ begin
393
+ yield
394
+ ensure
395
+ switch_to_window(orig_window)
396
+ end
397
+ end
398
+
341
399
  def browser_options
342
400
  @options[:browser_options]
343
401
  end
@@ -363,7 +421,16 @@ module Capybara::Apparition
363
421
  if @options[:skip_image_loading]
364
422
  browser_options['blink-settings'] = [browser_options['blink-settings'], 'imagesEnabled=false'].compact.join(',')
365
423
  end
424
+
366
425
  @options[:browser_options] = browser_options
426
+ process_cw_options(@options[:cw_options])
427
+ end
428
+
429
+ def process_cw_options(cw_options)
430
+ return if cw_options.nil?
431
+
432
+ (options[:url_blacklist] ||= []).concat cw_options[:url_blacklist]
433
+ options[:js_errors] ||= cw_options[:js_errors]
367
434
  end
368
435
 
369
436
  def process_browser_options(options)
@@ -464,5 +531,12 @@ module Capybara::Apparition
464
531
  arg
465
532
  end
466
533
  end
534
+
535
+ VERSION_STRING = <<~VERSION
536
+ Versions in use:
537
+ Capybara: %<capybara>s
538
+ Apparition: %<apparition>s
539
+ Chrome: %<chrome>s
540
+ VERSION
467
541
  end
468
542
  end
@@ -92,7 +92,7 @@ module Capybara::Apparition
92
92
  def send_msg(command, params)
93
93
  msg_id, msg = generate_msg(command, params)
94
94
  @send_mutex.synchronize do
95
- puts "#{Time.now.to_i}: sending msg: #{msg}" if ENV['DEBUG']
95
+ puts "#{Time.now.to_i}: sending msg: #{msg}" if ENV['DEBUG'] == 'V'
96
96
  @ws.send_msg(msg)
97
97
  end
98
98
  msg_id
@@ -141,7 +141,7 @@ module Capybara::Apparition
141
141
 
142
142
  def read_msg
143
143
  msg = JSON.parse(@ws.read_msg)
144
- puts "#{Time.now.to_i}: got msg: #{msg}" if ENV['DEBUG']
144
+ puts "#{Time.now.to_i}: got msg: #{msg}" if ENV['DEBUG'] == 'V'
145
145
  # Check if it's an event and push on event queue
146
146
  @events.push msg.dup if msg['method']
147
147
 
@@ -162,7 +162,7 @@ module Capybara::Apparition
162
162
  @msg_mutex.synchronize do
163
163
  @message_available.wait(@msg_mutex, 0.1)
164
164
  (@responses.keys & @async_ids).each do |msg_id|
165
- puts "Cleaning up response for #{msg_id}" if ENV['DEBUG'] == 'v'
165
+ puts "Cleaning up response for #{msg_id}" if ENV['DEBUG'] == 'V'
166
166
  @responses.delete(msg_id)
167
167
  @async_ids.delete(msg_id)
168
168
  end
@@ -3,7 +3,7 @@
3
3
  module Capybara::Apparition
4
4
  class Browser
5
5
  class Launcher
6
- KILL_TIMEOUT = 2
6
+ KILL_TIMEOUT = 5
7
7
 
8
8
  def self.start(*args)
9
9
  new(*args).tap(&:start)
@@ -12,10 +12,11 @@ module Capybara::Apparition
12
12
  def self.process_killer(pid)
13
13
  proc do
14
14
  begin
15
+ sleep 1
15
16
  if Capybara::Apparition.windows?
16
17
  ::Process.kill('KILL', pid)
17
18
  else
18
- ::Process.kill('TERM', pid)
19
+ ::Process.kill('USR1', pid)
19
20
  timer = Capybara::Helpers.timer(expire_in: KILL_TIMEOUT)
20
21
  while ::Process.wait(pid, ::Process::WNOHANG).nil?
21
22
  sleep 0.05
@@ -137,7 +138,7 @@ module Capybara::Apparition
137
138
  when /darwin|mac os/
138
139
  macosx_path
139
140
  when /linux|solaris|bsd/
140
- find_first_binary('google-chrome', 'chrome') || '/usr/bin/chrome'
141
+ linux_path
141
142
  else
142
143
  raise ArgumentError, "unknown os: #{host_os.inspect}"
143
144
  end
@@ -148,26 +149,36 @@ module Capybara::Apparition
148
149
  end
149
150
 
150
151
  def windows_path
151
- raise ArgumentError, 'Not yet Implemented'
152
+ envs = %w[LOCALAPPDATA PROGRAMFILES PROGRAMFILES(X86)]
153
+ directories = %w[\\Google\\Chrome\\Application \\Chromium\\Application]
154
+ files = %w[chrome.exe]
155
+
156
+ directories.product(envs, files).lazy.map { |(dir, env, file)| "#{ENV[env]}\\#{dir}\\#{file}" }
157
+ .find { |f| File.exist?(f) } || find_first_binary(*files)
152
158
  end
153
159
 
154
160
  def macosx_path
155
- path = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'
156
- path = File.expand_path("~#{path}") unless File.exist?(path)
157
- path = find_first_binary('Google Chrome') unless File.exist?(path)
158
- path
161
+ directories = ['', File.expand_path('~')]
162
+ files = ['/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
163
+ '/Applications/Chromium.app/Contents/MacOS/Chromium']
164
+ directories.product(files).map(&:join).find { |f| File.exist?(f) } ||
165
+ find_first_binary('Google Chrome', 'Chromium')
166
+ end
167
+
168
+ def linux_path
169
+ directories = %w[/usr/local/sbin /usr/local/bin /usr/sbin /usr/bin /sbin /bin /opt/google/chrome]
170
+ files = %w[google-chrome chrome chromium chromium-browser]
171
+
172
+ directories.product(files).map { |p| p.join('/') }.find { |f| File.exist?(f) } ||
173
+ find_first_binary(*files)
159
174
  end
160
175
 
161
176
  def find_first_binary(*binaries)
162
177
  paths = ENV['PATH'].split(File::PATH_SEPARATOR)
163
178
 
164
- binaries.each do |binary|
165
- paths.each do |path|
166
- full_path = File.join(path, binary)
167
- exe = Dir.glob(full_path).find { |f| File.executable?(f) }
168
- return exe if exe
169
- end
170
- end
179
+ binaries.product(paths).lazy.map do |(binary, path)|
180
+ Dir.glob(File.join(path, binary)).find { |f| File.executable?(f) }
181
+ end.reject(&:nil?).first
171
182
  end
172
183
 
173
184
  # Chromium command line options
@@ -187,6 +198,7 @@ module Capybara::Apparition
187
198
  disable-prompt-on-repost
188
199
  disable-sync
189
200
  disable-translate
201
+ disable-session-crashed-bubble
190
202
  metrics-recording-only
191
203
  no-first-run
192
204
  safebrowsing-disable-auto-update
@@ -15,7 +15,7 @@ module Capybara::Apparition
15
15
  handle_error(resp['error']) if resp['error']
16
16
  resp
17
17
  end.last
18
- puts "Processed msg: #{@msg_ids.last} in #{Time.now - @send_time} seconds" if ENV['DEBUG']
18
+ puts "Processed msg: #{@msg_ids.last} in #{Time.now - @send_time} seconds" if ENV['DEBUG'] == 'V'
19
19
 
20
20
  response['result']
21
21
  end
@@ -45,7 +45,7 @@ module Capybara
45
45
  end
46
46
 
47
47
  def message
48
- 'There was an error inside the Puppeteer portion of Apparition. ' \
48
+ 'There was an error inside Apparition. ' \
49
49
  'If this is the error returned, and not the cause of a more detailed error response, ' \
50
50
  'this is probably a bug, so please report it. ' \
51
51
  "\n\n#{name}: #{error_parameters}"
@@ -57,7 +57,7 @@ module Capybara
57
57
  # response['args'].first.map { |data| JSErrorItem.new(data['message'], data['stack']) }
58
58
  # end
59
59
  def javascript_errors
60
- [response]
60
+ [message: response]
61
61
  end
62
62
 
63
63
  def message
@@ -65,7 +65,7 @@ module Capybara
65
65
  "If you don't care about these errors, you can ignore them by " \
66
66
  'setting js_errors: false in your Apparition configuration (see ' \
67
67
  'documentation for details).' \
68
- "\n\n#{javascript_errors.map(&:to_s).join("\n")}"
68
+ "\n\n#{javascript_errors.map { |err| err[:message] }.join("\n")}"
69
69
  end
70
70
  end
71
71
 
@@ -33,9 +33,9 @@ module Capybara::Apparition
33
33
  Capybara::Apparition::Node.new(driver, @page, r_o['objectId'])
34
34
  end
35
35
  rescue ::Capybara::Apparition::BrowserError => e
36
- raise unless e.name =~ /is not a valid (XPath expression|selector)/
36
+ raise unless /is not a valid (XPath expression|selector)/.match? e.name
37
37
 
38
- raise Capybara::Apparition::InvalidSelector, [method, selector]
38
+ raise Capybara::Apparition::InvalidSelector, 'args' => [method, selector]
39
39
  end
40
40
 
41
41
  def find_xpath(selector)
@@ -71,6 +71,18 @@ module Capybara::Apparition
71
71
  visible_text
72
72
  end
73
73
 
74
+ # capybara-webkit method
75
+ def inner_html
76
+ self[:innerHTML]
77
+ end
78
+
79
+ # capybara-webkit method
80
+ def inner_html=(value)
81
+ driver.execute_script <<~JS, self, value
82
+ arguments[0].innerHTML = arguments[1]
83
+ JS
84
+ end
85
+
74
86
  def property(name)
75
87
  evaluate_on('name => this[name]', value: name)
76
88
  end
@@ -118,10 +130,13 @@ module Capybara::Apparition
118
130
  when 'datetime-local'
119
131
  set_datetime_local(value)
120
132
  else
121
- set_text(value.to_s, delay: options.fetch(:delay, 0))
133
+ set_text(value.to_s, { delay: 0 }.merge(options))
122
134
  end
123
135
  elsif tag_name == 'textarea'
124
136
  set_text(value.to_s)
137
+ elsif tag_name == 'select'
138
+ warn "Setting the value of a select element via 'set' is deprecated, please use 'select' or 'select_option'."
139
+ evaluate_on '()=>{ this.value = arguments[0] }', value: value.to_s
125
140
  elsif self[:isContentEditable]
126
141
  delete_text
127
142
  send_keys(value.to_s, delay: options.fetch(:delay, 0))
@@ -150,6 +165,10 @@ module Capybara::Apparition
150
165
  evaluate_on VISIBLE_JS
151
166
  end
152
167
 
168
+ def obscured?(x: nil, y: nil)
169
+ evaluate_on(OBSCURED_JS) == true
170
+ end
171
+
153
172
  def checked?
154
173
  self[:checked]
155
174
  end
@@ -227,6 +246,10 @@ module Capybara::Apparition
227
246
  evaluate_on DISPATCH_EVENT_JS, { value: event_type }, { value: name }, value: opts.merge(options)
228
247
  end
229
248
 
249
+ def submit
250
+ evaluate_on '()=>{ this.submit() }'
251
+ end
252
+
230
253
  def ==(other)
231
254
  evaluate_on('el => this == el', objectId: other.id)
232
255
  rescue ObsoleteNode
@@ -710,6 +733,24 @@ module Capybara::Apparition
710
733
  }
711
734
  JS
712
735
 
736
+ OBSCURED_JS = <<~JS
737
+ function(x, y) {
738
+ var box = this.getBoundingClientRect();
739
+ if (x == null) x = box.width/2;
740
+ if (y == null) y = box.height/2 ;
741
+
742
+ var px = box.left + x,
743
+ py = box.top + y,
744
+ e = document.elementFromPoint(px, py);
745
+
746
+ if (!this.contains(e))
747
+ return true;
748
+
749
+ return { x: px, y: py };
750
+ }
751
+ JS
752
+
753
+
713
754
  DELETE_TEXT_JS = <<~JS
714
755
  function(){
715
756
  range = document.createRange();