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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b51cb57f961f9825c56ce38377bbf4fd5477a5cdad6d75a2dac1aab827b81561
4
- data.tar.gz: 29e9b0c81cdcd3be2820a8d9a2719708750d3099f56a6c403c474a77bb254bce
3
+ metadata.gz: 3a8ccd4ce950f7c17f8ab0ac0c5ddcbbc13a336f428011d66f4b3cb028ced150
4
+ data.tar.gz: c524d1e955e3e4e958d0bd64350190eec6f53c7f612a6bd1b41867c05ab11427
5
5
  SHA512:
6
- metadata.gz: 95be5f025a29554e4909b5e4500d2694f580b20ee92b9cf555c7fcf2a9bc1962871d95c81730d9800a55d8cc38f99656713f88cd10f0f7bd8e880083819c0edc
7
- data.tar.gz: 34c0d53702b1b0be001cd65ebf0bfcb17ab6d3e86505851a23bbe39bd9bd80d4c0cb6d3d95a990804bd3ee123090ebce155892ddadd2ab4484782219ad0535b3
6
+ metadata.gz: 1d772071df2776e86a611a07b718eb3c411cf9048e466d66515920261f6aa8b6705ddd1559d3fa8c8728fe3510a58b37b82986d698ad17f5add7f7399a9d4fc8
7
+ data.tar.gz: bdd2bdfba3591b9a798cc820ff4dd8ce7d90af853f79c716828e3bbb2a51810c0e2f4705c16181b9284533569bd4f050fa94d95f1b62d3c5fc2880d29d817a72
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![Build Status](https://secure.travis-ci.org/twalpole/apparition.svg)](http://travis-ci.org/twalpole/apparition)
4
4
 
5
- Apparition is a driver for [Capybara](https://github.com/jnicklas/capybara). It allows you to
5
+ Apparition is a driver for [Capybara](https://github.com/teamcapybara/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
8
  with the Poltergeist API as possible. Implementing the `capybara-webkit` specific driver methods has also begun.
@@ -27,7 +27,7 @@ gem 'apparition'
27
27
  or
28
28
 
29
29
  ``` ruby
30
- gem apparition', github: 'twalpole/apparition'
30
+ gem 'apparition', github: 'twalpole/apparition'
31
31
  ```
32
32
 
33
33
  to your Gemfile and run `bundle install`.
@@ -82,6 +82,29 @@ same as for `save_screenshot`.
82
82
  Sometimes its desirable to click a very specific area of the screen. You can accomplish this with
83
83
  `page.driver.click(x, y)`, where x and y are the screen coordinates.
84
84
 
85
+ ### Remote debugging (not yet implemented) ###
86
+
87
+ If you use the `:inspector => true` option (see below), remote debugging
88
+ will be enabled.
89
+
90
+ When this option is enabled, you can insert `page.driver.debug` into
91
+ your tests to pause the test and launch a browser which gives you the
92
+ WebKit inspector to view your test run with.
93
+
94
+ You can register this debugger driver with a different name and set it
95
+ as the current javascript driver. By example, in your helper file:
96
+
97
+ ```ruby
98
+ Capybara.register_driver :apparition_debug do |app|
99
+ Capybara::Apparition::Driver.new(app, :inspector => true)
100
+ end
101
+ # Capybara.javascript_driver = :apparition
102
+ Capybara.javascript_driver = :apparition_debug
103
+ ```
104
+
105
+ [Read more
106
+ here](https://www.jonathanleighton.com/articles/2012/poltergeist-0-6-0/)
107
+
85
108
  ### Manipulating request headers ###
86
109
 
87
110
  You can manipulate HTTP request headers with these methods:
@@ -156,8 +179,9 @@ end
156
179
  `options` is a hash of options. The following options are supported:
157
180
 
158
181
  * `:headless` (Boolean) - When false, run the browser visibly
182
+ * `:remote` (Boolean) - When true, connect to remote browser instead of starting locally (see [below](#Remote Chrome Driver))
159
183
  * `:debug` (Boolean) - When true, debug output is logged to `STDERR`.
160
- * `:logger` (Object responding to `puts`) - When present, debug output is written to this object
184
+ * `:logger` (Ruby logger object or any object responding to `puts`) - When present, debug output is written to this object
161
185
  * `:browser_logger` (`IO` object) - This is where your `console.log` statements will show up. Default: `STDOUT`
162
186
  * `:timeout` (Numeric) - The number of seconds we'll wait for a response
163
187
  when communicating with Chrome. Default is 30.
@@ -175,6 +199,18 @@ end
175
199
  * `:browser_options` (Hash) - Extra command line options to pass to Chrome when starting
176
200
  * `:skip_image_loading` (Boolean) - Don't load images
177
201
 
202
+ ### Remote Chrome Driver ###
203
+ Apparition can connect to already running instance of chrome.
204
+ Remote mode is useful when running tests in CI and chrome is available as separate docker container.
205
+
206
+ In order to use remote browser - set up apparition in the following way:
207
+ ```ruby
208
+ Capybara.register_driver :apparition do |app|
209
+ browser_options = { 'remote-debugging-address' => '127.0.0.1', 'remote-debugging-port' => 9999 }
210
+ Capybara::Apparition::Driver.new(app, remote: true, browser_options: browser_options)
211
+ end
212
+ ```
213
+
178
214
  ### URL Blacklisting & Whitelisting ###
179
215
  Apparition supports URL blacklisting, which allows you
180
216
  to prevent scripts from running on designated domains:
@@ -205,7 +241,7 @@ test to allow sufficient time for the page to settle.
205
241
 
206
242
  If you have these types of problems, read through the [Capybara
207
243
  documentation on asynchronous
208
- JavaScript](https://github.com/jnicklas/capybara#asynchronous-javascript-ajax-and-friends)
244
+ JavaScript](https://github.com/teamcapybara/capybara#asynchronous-javascript-ajax-and-friends)
209
245
  which explains the tools that Capybara provides for dealing with this.
210
246
 
211
247
  ### Filing a bug ###
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'backports/2.4.0/enumerable/sum'
4
- require 'backports/2.4.0/string/match'
5
3
  require 'capybara'
6
4
 
7
5
  module Capybara
@@ -1,14 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'capybara/apparition/errors'
4
- require 'capybara/apparition/dev_tools_protocol/target_manager'
5
4
  require 'capybara/apparition/page'
6
5
  require 'capybara/apparition/console'
6
+ require 'capybara/apparition/dev_tools_protocol/session'
7
7
  require 'capybara/apparition/browser/header'
8
8
  require 'capybara/apparition/browser/window'
9
9
  require 'capybara/apparition/browser/render'
10
10
  require 'capybara/apparition/browser/cookie'
11
11
  require 'capybara/apparition/browser/modal'
12
+ require 'capybara/apparition/browser/page_manager'
12
13
  require 'capybara/apparition/browser/frame'
13
14
  require 'capybara/apparition/browser/auth'
14
15
  require 'json'
@@ -17,6 +18,7 @@ require 'time'
17
18
  module Capybara::Apparition
18
19
  class Browser
19
20
  attr_reader :client, :paper_size, :zoom_factor, :console, :proxy_auth
21
+
20
22
  extend Forwardable
21
23
 
22
24
  delegate %i[visit current_url status_code
@@ -30,7 +32,7 @@ module Capybara::Apparition
30
32
  def initialize(client, logger = nil)
31
33
  @client = client
32
34
  @current_page_handle = nil
33
- @targets = Capybara::Apparition::DevToolsProtocol::TargetManager.new(self)
35
+ @pages = PageManager.new(self)
34
36
  @context_id = nil
35
37
  @js_errors = true
36
38
  @ignore_https_errors = false
@@ -41,15 +43,12 @@ module Capybara::Apparition
41
43
  initialize_handlers
42
44
 
43
45
  command('Target.setDiscoverTargets', discover: true)
44
- while @current_page_handle.nil?
45
- puts 'waiting for target...'
46
- sleep 0.1
47
- end
48
- @context_id = current_target.context_id
46
+ yield self if block_given?
47
+ reset
49
48
  end
50
49
 
51
50
  def restart
52
- puts 'handle client restart'
51
+ # puts 'handle client restart'
53
52
  # client.restart
54
53
 
55
54
  self.debug = @debug if defined?(@debug)
@@ -81,34 +80,43 @@ module Capybara::Apparition
81
80
  include Auth
82
81
 
83
82
  def reset
84
- current_page_targets = @targets.of_type('page').values
85
-
86
83
  new_context_id = command('Target.createBrowserContext')['browserContextId']
87
84
  new_target_response = client.send_cmd('Target.createTarget', url: 'about:blank', browserContextId: new_context_id)
88
85
 
89
- current_page_targets.each do |target|
90
- begin
91
- client.send_cmd('Target.disposeBrowserContext', browserContextId: target.context_id).discard_result
92
- rescue WrongWorld
93
- puts 'Unknown browserContextId'
94
- end
95
- @targets.delete(target.id)
96
- end
86
+ @pages.reset
97
87
 
98
88
  new_target_id = new_target_response['targetId']
99
89
 
90
+ session_id = command('Target.attachToTarget', targetId: new_target_id)['sessionId']
91
+ session = Capybara::Apparition::DevToolsProtocol::Session.new(self, client, session_id)
92
+
93
+ @pages.create(new_target_id, session, new_context_id,
94
+ ignore_https_errors: ignore_https_errors,
95
+ js_errors: js_errors, extensions: @extensions,
96
+ url_blacklist: @url_blacklist,
97
+ url_whitelist: @url_whitelist).send(:main_frame).loaded!
98
+
100
99
  timer = Capybara::Helpers.timer(expire_in: 10)
101
- until @targets.get(new_target_id)&.page&.usable?
100
+ until @pages[new_target_id].usable?
102
101
  if timer.expired?
103
102
  puts 'Timedout waiting for reset'
104
- raise TimeoutError.new('reset')
103
+ raise TimeoutError, 'reset'
105
104
  end
106
105
  sleep 0.01
107
106
  end
107
+ console.clear
108
108
  @current_page_handle = new_target_id
109
109
  true
110
110
  end
111
111
 
112
+ def refresh_pages(opener:)
113
+ @pages.refresh(opener: opener,
114
+ ignore_https_errors: ignore_https_errors,
115
+ js_errors: js_errors,
116
+ url_blacklist: @url_blacklist,
117
+ url_whitelist: @url_whitelist)
118
+ end
119
+
112
120
  def resize(width, height, screen: nil)
113
121
  current_page.set_viewport width: width, height: height, screen: screen
114
122
  end
@@ -127,20 +135,16 @@ module Capybara::Apparition
127
135
  def extensions=(filenames)
128
136
  @extensions = filenames
129
137
  Array(filenames).each do |name|
130
- begin
131
- current_page.command('Page.addScriptToEvaluateOnNewDocument', source: File.read(name))
132
- rescue Errno::ENOENT
133
- raise ::Capybara::Apparition::BrowserError.new('name' => "Unable to load extension: #{name}", 'args' => nil)
134
- end
138
+ current_page(allow_nil: true)&.add_extension(name)
135
139
  end
136
140
  end
137
141
 
138
142
  def url_whitelist=(whitelist)
139
- current_page&.url_whitelist = whitelist
143
+ @url_whitelist = @pages.whitelist = whitelist
140
144
  end
141
145
 
142
146
  def url_blacklist=(blacklist)
143
- current_page&.url_blacklist = blacklist
147
+ @url_blacklist = @pages.blacklist = blacklist
144
148
  end
145
149
 
146
150
  attr_writer :debug
@@ -149,7 +153,7 @@ module Capybara::Apparition
149
153
  current_page.command('Network.clearBrowserCache')
150
154
  end
151
155
 
152
- def command(name, params = {})
156
+ def command(name, **params)
153
157
  result = client.send_cmd(name, params).result
154
158
  log result
155
159
 
@@ -166,8 +170,14 @@ module Capybara::Apparition
166
170
  raise
167
171
  end
168
172
 
169
- def current_page
170
- current_target.page
173
+ def current_page(allow_nil: false)
174
+ @pages[@current_page_handle] || begin
175
+ puts "No current page: #{@current_page_handle} : #{caller}" if ENV['DEBUG']
176
+ @current_page_handle = nil
177
+ raise NoSuchWindowError unless allow_nil
178
+
179
+ @current_page_handle
180
+ end
171
181
  end
172
182
 
173
183
  def console_messages(type = nil)
@@ -176,115 +186,47 @@ module Capybara::Apparition
176
186
 
177
187
  private
178
188
 
179
- def current_target
180
- @targets.get(@current_page_handle) || begin
181
- puts "No current page: #{@current_page_handle}"
182
- puts caller
183
- @current_page_handle = nil
184
- raise NoSuchWindowError
185
- end
186
- end
187
-
188
189
  def log(message)
189
- @logger&.puts message if ENV['DEBUG']
190
- end
191
-
192
- # KEY_ALIASES = {
193
- # command: :Meta,
194
- # equals: :Equal,
195
- # control: :Control,
196
- # ctrl: :Control,
197
- # multiply: 'numpad*',
198
- # add: 'numpad+',
199
- # divide: 'numpad/',
200
- # subtract: 'numpad-',
201
- # decimal: 'numpad.',
202
- # left: 'ArrowLeft',
203
- # right: 'ArrowRight',
204
- # down: 'ArrowDown',
205
- # up: 'ArrowUp'
206
- # }.freeze
207
- #
208
- # def normalize_keys(keys)
209
- # keys.map do |key_desc|
210
- # case key_desc
211
- # when Array
212
- # # [:Shift, "s"] => { modifier: "shift", keys: "S" }
213
- # # [:Shift, "string"] => { modifier: "shift", keys: "STRING" }
214
- # # [:Ctrl, :Left] => { modifier: "ctrl", key: 'Left' }
215
- # # [:Ctrl, :Shift, :Left] => { modifier: "ctrl,shift", key: 'Left' }
216
- # # [:Ctrl, :Left, :Left] => { modifier: "ctrl", key: [:Left, :Left] }
217
- # keys_chunks = key_desc.chunk do |k|
218
- # k.is_a?(Symbol) && %w[shift ctrl control alt meta command].include?(k.to_s.downcase)
219
- # end
220
- # modifiers = modifiers_from_chunks(keys_chunks)
221
- # letters = normalize_keys(_keys.next[1].map { |k| k.is_a?(String) ? k.upcase : k })
222
- # { modifier: modifiers, keys: letters }
223
- # when Symbol
224
- # symbol_to_desc(key_desc)
225
- # when String
226
- # key_desc # Plain string, nothing to do
227
- # end
228
- # end
229
- # end
230
- #
231
- # def modifiers_from_chunks(chunks)
232
- # if chunks.peek[0]
233
- # chunks.next[1].map do |k|
234
- # k = k.to_s.downcase
235
- # k = 'control' if k == 'ctrl'
236
- # k = 'meta' if k == 'command'
237
- # k
238
- # end.join(',')
239
- # else
240
- # ''
241
- # end
242
- # end
243
- #
244
- # def symbol_to_desc(symbol)
245
- # if symbol == :space
246
- # res = ' '
247
- # else
248
- # key = KEY_ALIASES.fetch(symbol.downcase, symbol)
249
- # if (match = key.to_s.match(/numpad(.)/))
250
- # res = { keys: match[1], modifier: 'keypad' }
251
- # elsif !/^[A-Z]/.match?(key)
252
- # key = key.to_s.split('_').map(&:capitalize).join
253
- # end
254
- # end
255
- # res || { key: key }
256
- # end
190
+ return unless @logger && ENV['DEBUG']
257
191
 
258
- def initialize_handlers
259
- @client.on 'Target.targetCreated' do |info|
260
- puts "Target Created Info: #{info}" if ENV['DEBUG']
261
- target_info = info['targetInfo']
262
- if !@targets.target?(target_info['targetId'])
263
- # @targets.add(target_info['targetId'], DevToolsProtocol::Target.new(self, target_info))
264
- @targets.add(target_info['targetId'], target_info)
265
- puts "**** Target Added #{info}" if ENV['DEBUG']
266
- elsif ENV['DEBUG']
267
- puts "Target already existed #{info}"
268
- end
269
- @current_page_handle ||= target_info['targetId'] if target_info['type'] == 'page'
192
+ if @logger.respond_to?(:puts)
193
+ @logger.puts(message)
194
+ else
195
+ @logger.debug(message)
270
196
  end
197
+ end
271
198
 
272
- @client.on 'Target.targetDestroyed' do |info|
273
- puts "**** Target Destroyed Info: #{info}" if ENV['DEBUG']
274
- @targets.delete(info['targetId'])
199
+ def initialize_handlers
200
+ # @client.on 'Target.targetCreated' do |info|
201
+ # byebug
202
+ # puts "Target Created Info: #{info}" if ENV['DEBUG']
203
+ # target_info = info['targetInfo']
204
+ # if !@pages.key?(target_info['targetId'])
205
+ # @pages.add(target_info['targetId'], target_info)
206
+ # puts "**** Target Added #{info}" if ENV['DEBUG']
207
+ # elsif ENV['DEBUG']
208
+ # puts "Target already existed #{info}"
209
+ # end
210
+ # @current_page_handle ||= target_info['targetId'] if target_info['type'] == 'page'
211
+ # end
212
+
213
+ @client.on 'Target.targetDestroyed' do |target_id:, **info|
214
+ puts "**** Target Destroyed Info: #{target_id} - #{info}" if ENV['DEBUG']
215
+ @pages.delete(target_id)
275
216
  end
276
217
 
277
- @client.on 'Target.targetInfoChanged' do |info|
278
- puts "**** Target Info Changed: #{info}" if ENV['DEBUG']
279
- target_info = info['targetInfo']
280
- target = @targets.get(target_info['targetId'])
281
- if target
282
- target.update(target_info)
283
- else
284
- puts '****No target for the info change- creating****' if ENV['DEBUG']
285
- @targets.add(target_info['targetId'], target_info)
286
- end
287
- end
218
+ # @client.on 'Target.targetInfoChanged' do |info|
219
+ # byebug
220
+ # puts "**** Target Info Changed: #{info}" if ENV['DEBUG']
221
+ # target_info = info['targetInfo']
222
+ # page = @pages[target_info['targetId']]
223
+ # if page
224
+ # page.update(target_info)
225
+ # else
226
+ # puts '****No target for the info change- creating****' if ENV['DEBUG']
227
+ # @pages.add(target_info['targetId'], target_info)
228
+ # end
229
+ # end
288
230
  end
289
231
  end
290
232
  end
@@ -6,18 +6,9 @@ module Capybara::Apparition
6
6
  class Browser
7
7
  module Cookie
8
8
  def cookies
9
- CookieJar.new(
10
- # current_page.command('Network.getCookies')['cookies'].map { |c| Cookie.new(c) }
11
- self
12
- )
13
- end
14
-
15
- def all_cookies
16
- CookieJar.new(
17
- # current_page.command('Network.getAllCookies')['cookies'].map { |c| Cookie.new(c) }
18
- self
19
- )
9
+ CookieJar.new(self)
20
10
  end
11
+ alias :all_cookies :cookies
21
12
 
22
13
  def get_raw_cookies
23
14
  current_page.command('Network.getAllCookies')['cookies'].map do |c|
@@ -26,12 +17,9 @@ module Capybara::Apparition
26
17
  end
27
18
 
28
19
  def set_cookie(cookie)
29
- if cookie[:expires]
30
- # cookie[:expires] = cookie[:expires].to_i * 1000
31
- cookie[:expires] = cookie[:expires].to_i
32
- end
20
+ cookie[:expires] = cookie[:expires].to_i if cookie[:expires]
33
21
 
34
- current_page.command('Network.setCookie', cookie)
22
+ current_page.command('Network.setCookie', **cookie)
35
23
  end
36
24
 
37
25
  def remove_cookie(name)
@@ -8,7 +8,7 @@ module Capybara::Apparition
8
8
  end
9
9
 
10
10
  def headers=(headers)
11
- @targets.pages.each do |page|
11
+ @pages.each do |page|
12
12
  page.perm_headers = headers.dup
13
13
  page.temp_headers = {}
14
14
  page.temp_no_redirect_headers = {}
@@ -23,7 +23,7 @@ module Capybara::Apparition
23
23
 
24
24
  def add_header(header, permanent: true, **_options)
25
25
  if permanent == true
26
- @targets.pages.each do |page|
26
+ @pages.each do |page|
27
27
  page.perm_headers.merge! header
28
28
  page.update_headers
29
29
  end