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
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