poltergeist 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Poltergeist - A PhantomJS driver for Capybara #
2
2
 
3
- Version: 0.6.0
3
+ Version: 0.7.0
4
4
 
5
5
  [![Build Status](https://secure.travis-ci.org/jonleighton/poltergeist.png)](http://travis-ci.org/jonleighton/poltergeist)
6
6
  [![Dependency Status](https://gemnasium.com/jonleighton/poltergeist.png)](https://gemnasium.com/jonleighton/poltergeist)
@@ -18,47 +18,31 @@ require 'capybara/poltergeist'
18
18
  Capybara.javascript_driver = :poltergeist
19
19
  ```
20
20
 
21
- ## Important note about Rack versions < 1.3.0 ##
22
-
23
- Prior to version 1.3.0, the Rack handlers for Mongrel and Thin wrap your
24
- app in the `Rack::Chunked` middleware so that it uses
25
- `Transfer-Encoding: chunked`
26
- ([commit](https://github.com/rack/rack/commit/50cdd0bf000a9ffb3eb3760fda2ff3e1ad18f3a7)).
27
- This has been observed to cause problems,
28
- probably due to race conditions in Qt's HTTP handling code, so you are
29
- recommended to avoid this by specifying your own server setup for
30
- Capybara:
31
-
32
- ``` ruby
33
- Capybara.server do |app, port|
34
- require 'rack/handler/thin'
35
- Thin::Logging.silent = true
36
- Thin::Server.new('0.0.0.0', port, app).start
37
- end
38
- ```
39
-
40
- If you're using Rails 3.0, this affects you. If you're using Rails 3.1+,
41
- this doesn't affect you.
21
+ If you were previously using the `:rack_test` driver, be aware that
22
+ your app will now run in a separate thread and this can have
23
+ consequences for transactional tests. [See the Capybara README for more
24
+ detail](https://github.com/jnicklas/capybara/blob/master/README.md#transactions-and-database-setup).
42
25
 
43
26
  ## Installing PhantomJS ##
44
27
 
45
- You need PhantomJS 1.5.0. There are no other dependencies (you don't
46
- need Qt, or Xvfb, etc.)
28
+ You need at least PhantomJS 1.6.0, but 1.6.1 is recommended as there some issues with the former.
29
+ There are *no other external dependencies* (you don't need Qt, or a running X
30
+ server, etc.)
47
31
 
48
32
  ### Mac ###
49
33
 
50
- * *With homebrew*: `brew install phantomjs`
51
- * *Without homebrew*: [Download this](http://code.google.com/p/phantomjs/downloads/detail?name=phantomjs-1.5.0-macosx-static.zip&can=2&q=)
34
+ * *Manual install*: [Download this](http://code.google.com/p/phantomjs/downloads/detail?name=phantomjs-1.6.1-macosx-static.zip&can=2&q=)
35
+ * *Homebrew*: `brew install phantomjs`
52
36
 
53
37
  ### Linux ###
54
38
 
55
39
  * Download the [32
56
- bit](http://code.google.com/p/phantomjs/downloads/detail?name=phantomjs-1.5.0-linux-x86-dynamic.tar.gz&can=2&q=)
40
+ bit](http://code.google.com/p/phantomjs/downloads/detail?name=phantomjs-1.6.1-linux-i686-dynamic.tar.bz2&can=2&q=)
57
41
  or [64
58
- bit](http://code.google.com/p/phantomjs/downloads/detail?name=phantomjs-1.5.0-linux-x86_64-dynamic.tar.gz&can=2&q=)
42
+ bit](http://code.google.com/p/phantomjs/downloads/detail?name=phantomjs-1.6.1-linux-x86_64-dynamic.tar.bz2&can=2&q=)
59
43
  binary.
60
- * Extract it: `sudo tar xvzf phantomjs-1.5.0-linux-*-dynamic.tar.gz -C /usr/local`
61
- * Link it: `sudo ln -s /usr/local/phantomjs/bin/phantomjs /usr/local/bin/phantomjs`
44
+ * Extract it: `sudo tar xvjf phantomjs-1.6.1-linux-*-dynamic.tar.gz -C /usr/local`
45
+ * Link it: `sudo ln -s /usr/local/phantomjs-1.6.1-linux*/bin/phantomjs /usr/local/bin/phantomjs`
62
46
 
63
47
  (Note that you cannot copy the `/usr/local/phantomjs/bin/phantomjs`
64
48
  binary elsewhere on its own as it dynamically links with other files in
@@ -69,7 +53,7 @@ binary elsewhere on its own as it dynamically links with other files in
69
53
  Do this as a last resort if the binaries don't work for you. It will
70
54
  take quite a long time as it has to build WebKit.
71
55
 
72
- * Download [the source tarball](http://code.google.com/p/phantomjs/downloads/detail?name=phantomjs-1.5.0-source.tar.gz&can=2&q=)
56
+ * Download [the source tarball](http://code.google.com/p/phantomjs/downloads/detail?name=phantomjs-1.6.1-source.zip&can=2&q=)
73
57
  * Extract and cd in
74
58
  * `./build.sh`
75
59
 
@@ -79,7 +63,7 @@ Supported: MRI 1.8.7, MRI 1.9.2, MRI 1.9.3, JRuby 1.8, JRuby 1.9.
79
63
 
80
64
  Not supported:
81
65
 
82
- * Rubinius (due to some unknown socket related issues)
66
+ * Rubinius
83
67
  * Windows
84
68
 
85
69
  Contributions are welcome in order to move 'unsupported'
@@ -90,11 +74,11 @@ items into the 'supported' list.
90
74
  There are no special steps to take. You don't need Xvfb or any running X
91
75
  server at all.
92
76
 
93
- [Travis CI](http://travis-ci.org/) has PhantomJS 1.5.0 installed.
94
-
95
- You may like to use their [chef
96
- cookbook](https://github.com/travis-ci/travis-cookbooks/tree/master/ci_environment/phantomjs)
97
- on your own servers.
77
+ Depending on your tests, one thing that you may need is some fonts. If
78
+ you're getting errors on a CI that don't occur during development then
79
+ try taking some screenshots - it may well be missing fonts throwing
80
+ things off kilter. Your distro will have various font packages available
81
+ to install.
98
82
 
99
83
  ## What's supported? ##
100
84
 
@@ -126,9 +110,28 @@ When this option is enabled, you can insert `page.driver.debug` into
126
110
  your tests to pause the test and launch a browser which gives you the
127
111
  WebKit inspector to view your test run with.
128
112
 
129
- (This feature is considered experimental - it needs more polish
130
- and [apparently will only work on
131
- Linux](http://code.google.com/p/phantomjs/issues/detail?id=430).)
113
+ [Read more
114
+ here](http://jonathanleighton.com/articles/2012/poltergeist-0-6-0/)
115
+
116
+ ### Setting request headers ###
117
+
118
+ Additional HTTP request headers can be set like so:
119
+
120
+ ``` ruby
121
+ page.driver.headers = {
122
+ "Cookie" => "foo=bar",
123
+ "Host" => "foo.com"
124
+ }
125
+ ```
126
+
127
+ They will be cleared between tests, so you do not have to do this manually.
128
+
129
+ ### Inspecting network traffic ###
130
+
131
+ You can inspect the network traffic (i.e. what resources have been
132
+ loaded) on the current page by calling `page.driver.network_traffic`.
133
+ This returns an array of request objects. A request object has a
134
+ `response_parts` method containing data about the response chunks.
132
135
 
133
136
  ## Customization ##
134
137
 
@@ -150,6 +153,11 @@ end
150
153
  when communicating with PhantomJS. `nil` means wait forever. Default
151
154
  is 30.
152
155
  * `:inspector` (Boolean, String) - See 'Remote Debugging', above.
156
+ * `:js_errors` (Boolean) - When false, Javascript errors do not get re-raised in Ruby.
157
+ * `:window_size` (Array) - The dimensions of the browser window in which to test, expressed
158
+ as a 2-element array, e.g. [1024, 768]. Default: [1024, 768]
159
+ * `:phantomjs_options` (Array) - Additional [command line options](http://code.google.com/p/phantomjs/wiki/Interface#Command-line_Options)
160
+ to be passed to PhantomJS, e.g. `['--load-images=no', '--ignore-ssl-errors=yes']`
153
161
 
154
162
  ## Bugs ##
155
163
 
@@ -173,6 +181,35 @@ makes debugging easier). Running `rake autocompile` will watch the
173
181
 
174
182
  ## Changes ##
175
183
 
184
+ ### 0.7.0 ###
185
+
186
+ #### Features ####
187
+
188
+ * Added an option `:js_errors`, allowing poltergeist to continue
189
+ running after JS errors. (John Griffin & Tom Stuart) [Issue #62] [Issue #69]
190
+ * Added an option `:window_size`, allowing users to specify
191
+ dimensions to which the browser window will be resized.
192
+ (Tom Stuart) [Issue #53]
193
+ * Capybara 1.0 is no longer supported. Capybara ~> 1.1 is required.
194
+ * Added ability to set arbitrary http request headers
195
+ * Inspect network traffic on the page via
196
+ `page.driver.network_traffic` (Doug McInnes) [Issue #77]
197
+ * Added an option `:phantomjs_options`, allowing users to specify
198
+ additional command-line options passed to phantomjs executable.
199
+ (wynst) [Issue #97]
200
+ * Scroll element into viewport if needed on click (Gabriel Sobrinho)
201
+ [Issue #83]
202
+ * Added status code support. (Dmitriy Nesteryuk and Jon Leighton) [Issue #37]
203
+
204
+ #### Bug fixes ###
205
+
206
+ * Fix issue with `ClickFailed` exception happening with a negative
207
+ co-ordinate (which should be impossible). (Jon Leighton, Gabriel
208
+ Sobrinho, Tom Stuart) [Issue #60]
209
+ * Fix issue with `undefined method map for "[]":String`, which
210
+ happened when dealing with pages that include JS rewriting
211
+ Array.prototype.toJSON. (Tom Stuart) [Issue #63]
212
+
176
213
  ### 0.6.0 ###
177
214
 
178
215
  #### Features ####
@@ -2,17 +2,17 @@ require 'capybara'
2
2
 
3
3
  module Capybara
4
4
  module Poltergeist
5
- autoload :Driver, 'capybara/poltergeist/driver'
6
- autoload :Browser, 'capybara/poltergeist/browser'
7
- autoload :Node, 'capybara/poltergeist/node'
8
- autoload :ServerManager, 'capybara/poltergeist/server_manager'
9
- autoload :Server, 'capybara/poltergeist/server'
10
- autoload :WebSocketServer, 'capybara/poltergeist/web_socket_server'
11
- autoload :Client, 'capybara/poltergeist/client'
12
- autoload :Util, 'capybara/poltergeist/util'
13
- autoload :Inspector, 'capybara/poltergeist/inspector'
14
- autoload :Spawn, 'capybara/poltergeist/spawn'
15
-
5
+ require 'capybara/poltergeist/driver'
6
+ require 'capybara/poltergeist/browser'
7
+ require 'capybara/poltergeist/node'
8
+ require 'capybara/poltergeist/server'
9
+ require 'capybara/poltergeist/web_socket_server'
10
+ require 'capybara/poltergeist/client'
11
+ require 'capybara/poltergeist/util'
12
+ require 'capybara/poltergeist/inspector'
13
+ require 'capybara/poltergeist/spawn'
14
+ require 'capybara/poltergeist/json'
15
+ require 'capybara/poltergeist/network_traffic'
16
16
  require 'capybara/poltergeist/errors'
17
17
  end
18
18
  end
@@ -2,12 +2,13 @@ require 'multi_json'
2
2
 
3
3
  module Capybara::Poltergeist
4
4
  class Browser
5
- attr_reader :server, :client, :logger
5
+ attr_reader :server, :client, :logger, :js_errors
6
6
 
7
- def initialize(server, client, logger = nil)
8
- @server = server
9
- @client = client
10
- @logger = logger
7
+ def initialize(server, client, logger = nil, js_errors = true)
8
+ @server = server
9
+ @client = client
10
+ @logger = logger
11
+ @js_errors = js_errors
11
12
  end
12
13
 
13
14
  def restart
@@ -15,14 +16,18 @@ module Capybara::Poltergeist
15
16
  client.restart
16
17
  end
17
18
 
18
- def visit(url)
19
- command 'visit', url
19
+ def visit(url, headers)
20
+ command 'visit', url, headers
20
21
  end
21
22
 
22
23
  def current_url
23
24
  command 'current_url'
24
25
  end
25
26
 
27
+ def status_code
28
+ command 'status_code'
29
+ end
30
+
26
31
  def body
27
32
  command 'body'
28
33
  end
@@ -79,6 +84,7 @@ module Capybara::Poltergeist
79
84
  def within_frame(id, &block)
80
85
  command 'push_frame', id
81
86
  yield
87
+ ensure
82
88
  command 'pop_frame'
83
89
  end
84
90
 
@@ -103,29 +109,46 @@ module Capybara::Poltergeist
103
109
  end
104
110
 
105
111
  def render(path, options = {})
106
- command 'render', path, !!options[:full]
112
+ command 'render', path.to_s, !!options[:full]
107
113
  end
108
114
 
109
115
  def resize(width, height)
110
116
  command 'resize', width, height
111
117
  end
112
118
 
119
+ def network_traffic
120
+ command('network_traffic').values.map do |event|
121
+ NetworkTraffic::Request.new(
122
+ event['request'],
123
+ event['responseParts'].map { |response| NetworkTraffic::Response.new(response) }
124
+ )
125
+ end
126
+ end
127
+
128
+ def equals(page_id, id, other_id)
129
+ command('equals', page_id, id, other_id)
130
+ end
131
+
113
132
  def command(name, *args)
114
133
  message = { 'name' => name, 'args' => args }
115
134
  log message.inspect
116
135
 
117
- json = MultiJson.decode(server.send(MultiJson.encode(message)))
136
+ json = JSON.load(server.send(JSON.dump(message)))
118
137
  log json.inspect
119
138
 
120
139
  if json['error']
121
140
  if json['error']['name'] == 'Poltergeist.JavascriptError'
122
- raise JavascriptError.new(json['error'])
141
+ error = JavascriptError.new(json['error'])
142
+ if js_errors
143
+ raise error
144
+ else
145
+ log error
146
+ end
123
147
  else
124
148
  raise BrowserError.new(json['error'])
125
149
  end
126
- else
127
- json['response']
128
150
  end
151
+ json['response']
129
152
 
130
153
  rescue DeadClient
131
154
  restart
@@ -1,7 +1,7 @@
1
1
  module Capybara::Poltergeist
2
2
  class Client
3
3
  PHANTOMJS_SCRIPT = File.expand_path('../client/compiled/main.js', __FILE__)
4
- PHANTOMJS_VERSION = '1.5.0'
4
+ PHANTOMJS_VERSION = '1.6.0'
5
5
  PHANTOMJS_NAME = 'phantomjs'
6
6
 
7
7
  def self.start(*args)
@@ -10,12 +10,13 @@ module Capybara::Poltergeist
10
10
  client
11
11
  end
12
12
 
13
- attr_reader :pid, :port, :path, :inspector
13
+ attr_reader :pid, :port, :path, :window_size, :phantomjs_options
14
14
 
15
- def initialize(port, inspector = nil, path = nil)
16
- @port = port
17
- @inspector = inspector
18
- @path = path || PHANTOMJS_NAME
15
+ def initialize(port, options = {})
16
+ @port = port
17
+ @path = options[:path] || PHANTOMJS_NAME
18
+ @window_size = options[:window_size] || [1024, 768]
19
+ @phantomjs_options = options[:phantomjs_options] || []
19
20
 
20
21
  pid = Process.pid
21
22
  at_exit { stop if Process.pid == pid }
@@ -47,14 +48,10 @@ module Capybara::Poltergeist
47
48
  def command
48
49
  @command ||= begin
49
50
  parts = [path]
50
-
51
- if inspector
52
- parts << "--remote-debugger-port=#{inspector.port}"
53
- parts << "--remote-debugger-autorun=yes"
54
- end
55
-
51
+ parts.concat phantomjs_options
56
52
  parts << PHANTOMJS_SCRIPT
57
53
  parts << port
54
+ parts.concat window_size
58
55
  parts
59
56
  end
60
57
  end
@@ -7,8 +7,18 @@ class PoltergeistAgent
7
7
  @windows = []
8
8
  this.pushWindow(window)
9
9
 
10
- externalCall: (name, arguments) ->
11
- { value: this[name].apply(this, arguments) }
10
+ externalCall: (name, args) ->
11
+ try
12
+ { value: this[name].apply(this, args) }
13
+ catch error
14
+ { error: { message: error.toString(), stack: error.stack } }
15
+
16
+ @stringify: (object) ->
17
+ JSON.stringify object, (key, value) ->
18
+ if Array.isArray(this[key])
19
+ return this[key]
20
+ else
21
+ return value
12
22
 
13
23
  pushWindow: (new_window) ->
14
24
  @windows.push(new_window)
@@ -55,10 +65,10 @@ class PoltergeistAgent
55
65
  get: (id) ->
56
66
  @nodes[id] or= new PoltergeistAgent.Node(this, @elements[id])
57
67
 
58
- nodeCall: (id, name, arguments) ->
68
+ nodeCall: (id, name, args) ->
59
69
  node = this.get(id)
60
70
  throw new PoltergeistAgent.ObsoleteNode if node.isObsolete()
61
- node[name].apply(node, arguments)
71
+ node[name].apply(node, args)
62
72
 
63
73
  class PoltergeistAgent.ObsoleteNode
64
74
  toString: -> "PoltergeistAgent.ObsoleteNode"
@@ -91,7 +101,33 @@ class PoltergeistAgent.Node
91
101
 
92
102
  changed: ->
93
103
  event = document.createEvent('HTMLEvents')
94
- event.initEvent("change", true, false)
104
+ event.initEvent('change', true, false)
105
+ @element.dispatchEvent(event)
106
+
107
+ input: ->
108
+ event = document.createEvent('HTMLEvents')
109
+ event.initEvent('input', true, false)
110
+ @element.dispatchEvent(event)
111
+
112
+ keyupdowned: (eventName, keyCode) ->
113
+ event = document.createEvent('UIEvents')
114
+ event.initEvent(eventName, true, true)
115
+ event.keyCode = keyCode
116
+ event.which = keyCode
117
+ event.charCode = 0
118
+ @element.dispatchEvent(event)
119
+
120
+ keypressed: (altKey, ctrlKey, shiftKey, metaKey, keyCode, charCode) ->
121
+ event = document.createEvent('UIEvents')
122
+ event.initEvent('keypress', true, true)
123
+ event.window = @agent.window
124
+ event.altKey = altKey
125
+ event.ctrlKey = ctrlKey
126
+ event.shiftKey = shiftKey
127
+ event.metaKey = metaKey
128
+ event.keyCode = keyCode
129
+ event.charCode = charCode
130
+ event.which = keyCode
95
131
  @element.dispatchEvent(event)
96
132
 
97
133
  insideBody: ->
@@ -120,6 +156,9 @@ class PoltergeistAgent.Node
120
156
  else
121
157
  @element.getAttribute(name)
122
158
 
159
+ scrollIntoView: ->
160
+ @element.scrollIntoViewIfNeeded()
161
+
123
162
  value: ->
124
163
  if @element.tagName == 'SELECT' && @element.multiple
125
164
  option.value for option in @element.children when option.selected
@@ -130,8 +169,20 @@ class PoltergeistAgent.Node
130
169
  if (@element.maxLength >= 0)
131
170
  value = value.substr(0, @element.maxLength)
132
171
 
133
- @element.value = value
172
+ @element.value = ''
173
+ this.trigger('focus')
174
+
175
+ for char in value
176
+ @element.value += char
177
+
178
+ keyCode = this.characterToKeyCode(char)
179
+ this.keyupdowned('keydown', keyCode)
180
+ this.keypressed(false, false, false, false, char.charCodeAt(0), char.charCodeAt(0))
181
+ this.keyupdowned('keyup', keyCode)
182
+
134
183
  this.changed()
184
+ this.input()
185
+ this.trigger('blur')
135
186
 
136
187
  isMultiple: ->
137
188
  @element.multiple
@@ -209,6 +260,48 @@ class PoltergeistAgent.Node
209
260
  selector += ".#{className}"
210
261
  selector
211
262
 
263
+ characterToKeyCode: (character) ->
264
+ code = character.toUpperCase().charCodeAt(0)
265
+ specialKeys =
266
+ 96: 192 #`
267
+ 45: 189 #-
268
+ 61: 187 #=
269
+ 91: 219 #[
270
+ 93: 221 #]
271
+ 92: 220 #\
272
+ 59: 186 #;
273
+ 39: 222 #'
274
+ 44: 188 #,
275
+ 46: 190 #.
276
+ 47: 191 #/
277
+ 127: 46 #delete
278
+ 126: 192 #~
279
+ 33: 49 #!
280
+ 64: 50 #@
281
+ 35: 51 ##
282
+ 36: 52 #$
283
+ 37: 53 #%
284
+ 94: 54 #^
285
+ 38: 55 #&
286
+ 42: 56 #*
287
+ 40: 57 #(
288
+ 41: 48 #)
289
+ 95: 189 #_
290
+ 43: 187 #+
291
+ 123: 219 #{
292
+ 125: 221 #}
293
+ 124: 220 #|
294
+ 58: 186 #:
295
+ 34: 222 #"
296
+ 60: 188 #<
297
+ 62: 190 #>
298
+ 63: 191 #?
299
+
300
+ specialKeys[code] || code
301
+
302
+ isDOMEqual: (other_id) ->
303
+ @element == @agent.get(other_id).element
304
+
212
305
  window.__poltergeist = new PoltergeistAgent
213
306
 
214
307
  document.addEventListener(