apparition 0.1.0 → 0.2.0

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