poltergeist 1.0.3 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,13 +1,16 @@
1
1
  # Poltergeist - A PhantomJS driver for Capybara #
2
2
 
3
- Version: 1.0.3
4
-
5
3
  [![Build Status](https://secure.travis-ci.org/jonleighton/poltergeist.png)](http://travis-ci.org/jonleighton/poltergeist)
6
4
 
7
5
  Poltergeist is a driver for [Capybara](https://github.com/jnicklas/capybara). It allows you to
8
6
  run your Capybara tests on a headless [WebKit](http://webkit.org) browser,
9
7
  provided by [PhantomJS](http://www.phantomjs.org/).
10
8
 
9
+ **If you're viewing this at https://github.com/jonleighton/poltergeist,
10
+ you're reading the documentation for the master branch.
11
+ [View documentation for the latest release
12
+ (1.1.0).](https://github.com/jonleighton/poltergeist/tree/v1.1.0)**
13
+
11
14
  ## Installation ##
12
15
 
13
16
  Add `poltergeist` to your Gemfile, and in your test setup add:
@@ -24,20 +27,21 @@ detail](https://github.com/jnicklas/capybara/blob/master/README.md#transactions-
24
27
 
25
28
  ## Installing PhantomJS ##
26
29
 
27
- You need at least PhantomJS 1.7.0. There are *no other external
30
+ You need at least PhantomJS 1.8.1. There are *no other external
28
31
  dependencies* (you don't need Qt, or a running X server, etc.)
29
32
 
30
33
  ### Mac ###
31
34
 
32
35
  * *Homebrew*: `brew install phantomjs`
33
- * *Manual install*: [Download this](http://code.google.com/p/phantomjs/downloads/detail?name=phantomjs-1.7.0-macosx.zip&can=2&q=)
36
+ * *MacPorts*: `sudo port install phantomjs`
37
+ * *Manual install*: [Download this](http://code.google.com/p/phantomjs/downloads/detail?name=phantomjs-1.8.1-macosx.zip&can=2&q=)
34
38
 
35
39
  ### Linux ###
36
40
 
37
41
  * Download the [32
38
- bit](http://code.google.com/p/phantomjs/downloads/detail?name=phantomjs-1.7.0-linux-i686.tar.bz2&can=2&q=)
42
+ bit](http://code.google.com/p/phantomjs/downloads/detail?name=phantomjs-1.8.1-linux-i686.tar.bz2&can=2&q=)
39
43
  or [64
40
- bit](http://code.google.com/p/phantomjs/downloads/detail?name=phantomjs-1.7.0-linux-x86_64.tar.bz2&can=2&q=)
44
+ bit](http://code.google.com/p/phantomjs/downloads/detail?name=phantomjs-1.8.1-linux-x86_64.tar.bz2&can=2&q=)
41
45
  binary.
42
46
  * Extract the tarball and copy `bin/phantomjs` into your `PATH`
43
47
 
@@ -46,7 +50,7 @@ binary.
46
50
  Do this as a last resort if the binaries don't work for you. It will
47
51
  take quite a long time as it has to build WebKit.
48
52
 
49
- * Download [the source tarball](http://code.google.com/p/phantomjs/downloads/detail?name=phantomjs-1.7.0-source.zip&can=2&q=)
53
+ * Download [the source tarball](http://code.google.com/p/phantomjs/downloads/detail?name=phantomjs-1.8.1-source.zip&can=2&q=)
50
54
  * Extract and cd in
51
55
  * `./build.sh`
52
56
 
@@ -55,13 +59,12 @@ guide](http://phantomjs.org/build.html).)
55
59
 
56
60
  ## Compatibility ##
57
61
 
58
- Supported: MRI 1.8.7, MRI 1.9.2, MRI 1.9.3, JRuby 1.8, JRuby 1.9,
59
- Rubinius 1.8 on UNIX platforms.
62
+ Poltergeist runs on MRI 1.9, JRuby 1.9 and Rubinius 1.9.
60
63
 
61
- Not supported: Rubinius 1.9, Windows.
64
+ Ruby 1.8 is no longer supported. The last release to support Ruby 1.8
65
+ was 1.0.2, so you should use that if you still need Ruby 1.8 support.
62
66
 
63
- Contributions are welcome in order to move 'unsupported'
64
- items into the 'supported' list.
67
+ Poltergeist does not currently support the Windows operating system.
65
68
 
66
69
  ## Running on a CI ##
67
70
 
@@ -108,6 +111,11 @@ the entire page, use `page.driver.render('/path/to/file.png', :full => true)`.
108
111
  Sometimes the window size is important to how things are rendered. Poltergeist sets the window
109
112
  size to 1024x768 by default, but you can set this yourself with `page.driver.resize(width, height)`.
110
113
 
114
+ ### Clicking precise coordinates ###
115
+
116
+ Sometimes its desirable to click a very specific area of the screen. You can accomplish this with
117
+ `page.driver.click(x, y)`, where x and y are the screen coordinates.
118
+
111
119
  ### Remote debugging (experimental) ###
112
120
 
113
121
  If you use the `:inspector => true` option (see below), remote debugging
@@ -167,17 +175,22 @@ end
167
175
  `options` is a hash of options. The following options are supported:
168
176
 
169
177
  * `:phantomjs` (String) - A custom path to the phantomjs executable
170
- * `:debug` (Boolean) - When true, debug output is logged to `STDERR`
178
+ * `:debug` (Boolean) - When true, debug output is logged to `STDERR`.
179
+ Some debug info from the PhantomJS portion of Poltergeist is also
180
+ output, but this goes to `STDOUT` due to technical limitations.
171
181
  * `:logger` (Object responding to `puts`) - When present, debug output is written to this object
182
+ * `:phantomjs_logger` (`IO` object) - Where the `STDOUT` from PhantomJS is written to. This is
183
+ where you `console.log` statements will show up. Default: `STDOUT`
172
184
  * `:timeout` (Numeric) - The number of seconds we'll wait for a response
173
- when communicating with PhantomJS. `nil` means wait forever. Default
174
- is 30.
185
+ when communicating with PhantomJS. Default is 30.
175
186
  * `:inspector` (Boolean, String) - See 'Remote Debugging', above.
176
187
  * `:js_errors` (Boolean) - When false, Javascript errors do not get re-raised in Ruby.
177
188
  * `:window_size` (Array) - The dimensions of the browser window in which to test, expressed
178
189
  as a 2-element array, e.g. [1024, 768]. Default: [1024, 768]
179
- * `:phantomjs_options` (Array) - Additional [command line options](http://code.google.com/p/phantomjs/wiki/Interface#Command-line_Options)
190
+ * `:phantomjs_options` (Array) - Additional [command line options](https://github.com/ariya/phantomjs/wiki/API-Reference)
180
191
  to be passed to PhantomJS, e.g. `['--load-images=no', '--ignore-ssl-errors=yes']`
192
+ * `:extensions` (Array) - An array of JS files to be preloaded into
193
+ the phantomjs browser. Useful for faking unsupported APIs.
181
194
  * `:port` (Fixnum) - The port which should be used to communicate
182
195
  with the PhantomJS process. Default: 44678.
183
196
 
@@ -286,11 +299,50 @@ Include as much information as possible. For example:
286
299
 
287
300
  ## Changes ##
288
301
 
289
- ### 1.0.3 ###
302
+ ### 1.1.0 ###
303
+
304
+ #### Features ####
305
+
306
+ * Add support for custom phantomjs loggers via `:phantomjs_logger` option.
307
+ (Gabe Bell)
308
+ * Add `page.driver.click(x, y)` to click precise coordinates.
309
+ (Micah Geisel)
310
+ * Add Capybara 2.0 support. Capybara 1.1 and Ruby 1.8 are *no
311
+ longer supported*. (Mauro Asprea) [Issue #163]
312
+ * Add `node.base.double_click` to double click the node.
313
+ (Andy Shen)
314
+ * The `:debug` option now causes the PhantomJS portion of Poltergeist
315
+ to output some additional debug info, which may be useful in
316
+ figuring out timeout errors.
290
317
 
291
318
  #### Bug fixes ####
292
319
 
293
- * Tied to faye-websocket 0.4, as 0.5 introduces incompatibilities.
320
+ * Fix timing issue when using `within_frame` that could cause errors.
321
+ [Issue #183, #211] (@errm, @motemen)
322
+ * Fix bug with `within_frame` not properly switching the context back
323
+ after the block has executed. [Issue #242]
324
+ * Fix calculation of click position when clicking within a frame.
325
+ [Issue #222, #225]
326
+ * Fix error raising when calling `expires` if not set on cookie.
327
+ [Issue #203] (@arnvald)
328
+ * Fix the `:js_errors` option. Previously errors were not being
329
+ reported, but would still cause commands to fail. [Issue #229]
330
+ * Fix incorrect time zone handling when setting cookie expiry time
331
+ [Issue #228]
332
+ * Send SIGKILL to PhantomJS if it doesn't exit within 2 seconds
333
+ [Issue #196]
334
+ * Provide a more informative message for the `ObsoleteNode` error.
335
+ [Issue #192]
336
+ * Fix `ObsoleteNode` error when using `attach_file` with the `jQuery
337
+ File Upload` plugin. [Issue #115]
338
+ * Add the ability to extend the phantomjs environment via browser
339
+ options. e.g.
340
+ `Capybara::Poltergeist::Driver.new( app, :extensions => ['file.js', 'another.js'])`
341
+ (@JonRowe)
342
+ * Ensure that a `String` is passed over-the-wire to PhantomJS for
343
+ file input paths, allowing `attach_file` to be called with arbitry
344
+ objects such as a Pathname. (@mjtko) [Issue #215]
345
+ * Cookies can now be set before the first request. [Issue #193]
294
346
 
295
347
  ### 1.0.2 ###
296
348
 
@@ -1,3 +1,8 @@
1
+ if RUBY_VERSION < "1.9.2"
2
+ raise "This version of Capybara/Poltergeist does not support Ruby versions " \
3
+ "less than 1.9.2."
4
+ end
5
+
1
6
  require 'capybara'
2
7
 
3
8
  module Capybara
@@ -9,12 +14,9 @@ module Capybara
9
14
  require 'capybara/poltergeist/web_socket_server'
10
15
  require 'capybara/poltergeist/client'
11
16
  require 'capybara/poltergeist/inspector'
12
- require 'capybara/poltergeist/spawn'
13
- require 'capybara/poltergeist/json'
14
17
  require 'capybara/poltergeist/network_traffic'
15
18
  require 'capybara/poltergeist/errors'
16
19
  require 'capybara/poltergeist/cookie'
17
- require 'capybara/poltergeist/util'
18
20
  end
19
21
  end
20
22
 
@@ -1,20 +1,21 @@
1
- require 'multi_json'
1
+ require 'json'
2
2
  require 'time'
3
3
 
4
4
  module Capybara::Poltergeist
5
5
  class Browser
6
- attr_reader :server, :client, :logger, :js_errors
6
+ attr_reader :server, :client, :logger
7
7
 
8
- def initialize(server, client, logger = nil, js_errors = true)
9
- @server = server
10
- @client = client
11
- @logger = logger
12
- @js_errors = js_errors
8
+ def initialize(server, client, logger = nil)
9
+ @server = server
10
+ @client = client
11
+ @logger = logger
13
12
  end
14
13
 
15
14
  def restart
16
15
  server.restart
17
16
  client.restart
17
+
18
+ self.debug = @debug if @debug
18
19
  end
19
20
 
20
21
  def visit(url)
@@ -74,6 +75,10 @@ module Capybara::Poltergeist
74
75
  command 'visible', page_id, id
75
76
  end
76
77
 
78
+ def click_coordinates(x, y)
79
+ command 'click_coordinates', x, y
80
+ end
81
+
77
82
  def evaluate(script)
78
83
  command 'evaluate', script
79
84
  end
@@ -100,6 +105,10 @@ module Capybara::Poltergeist
100
105
  command 'click', page_id, id
101
106
  end
102
107
 
108
+ def double_click(page_id, id)
109
+ command 'double_click', page_id, id
110
+ end
111
+
103
112
  def drag(page_id, id, other_id)
104
113
  command 'drag', page_id, id, other_id
105
114
  end
@@ -150,8 +159,8 @@ module Capybara::Poltergeist
150
159
  end
151
160
 
152
161
  def set_cookie(cookie)
153
- if cookie[:expires].respond_to?(:httpdate)
154
- cookie[:expires] = cookie[:expires].httpdate
162
+ if cookie[:expires]
163
+ cookie[:expires] = cookie[:expires].to_i * 1000
155
164
  end
156
165
 
157
166
  command 'set_cookie', cookie
@@ -161,21 +170,31 @@ module Capybara::Poltergeist
161
170
  command 'remove_cookie', name
162
171
  end
163
172
 
173
+ def js_errors=(val)
174
+ command 'set_js_errors', !!val
175
+ end
176
+
177
+ def extensions=(names)
178
+ Array(names).each do |name|
179
+ command 'add_extension', name
180
+ end
181
+ end
182
+
183
+ def debug=(val)
184
+ @debug = val
185
+ command 'set_debug', !!val
186
+ end
187
+
164
188
  def command(name, *args)
165
189
  message = { 'name' => name, 'args' => args }
166
190
  log message.inspect
167
191
 
168
- json = JSON.load(server.send(JSON.dump(message)))
192
+ json = JSON.load(server.send(JSON.generate(message)))
169
193
  log json.inspect
170
194
 
171
195
  if json['error']
172
196
  if json['error']['name'] == 'Poltergeist.JavascriptError'
173
- error = JavascriptError.new(json['error'])
174
- if js_errors
175
- raise error
176
- else
177
- log error
178
- end
197
+ raise JavascriptError.new(json['error'])
179
198
  else
180
199
  raise BrowserError.new(json['error'])
181
200
  end
@@ -1,22 +1,27 @@
1
+ require "timeout"
2
+
1
3
  module Capybara::Poltergeist
2
4
  class Client
3
5
  PHANTOMJS_SCRIPT = File.expand_path('../client/compiled/main.js', __FILE__)
4
- PHANTOMJS_VERSION = '1.7.0'
6
+ PHANTOMJS_VERSION = '1.8.1'
5
7
  PHANTOMJS_NAME = 'phantomjs'
6
8
 
9
+ KILL_TIMEOUT = 2 # seconds
10
+
7
11
  def self.start(*args)
8
12
  client = new(*args)
9
13
  client.start
10
14
  client
11
15
  end
12
16
 
13
- attr_reader :pid, :port, :path, :window_size, :phantomjs_options
17
+ attr_reader :pid, :server, :path, :window_size, :phantomjs_options
14
18
 
15
- def initialize(port, options = {})
16
- @port = port
19
+ def initialize(server, options = {})
20
+ @server = server
17
21
  @path = options[:path] || PHANTOMJS_NAME
18
22
  @window_size = options[:window_size] || [1024, 768]
19
23
  @phantomjs_options = options[:phantomjs_options] || []
24
+ @phantomjs_logger = options[:phantomjs_logger] || $stdout
20
25
 
21
26
  pid = Process.pid
22
27
  at_exit { stop if Process.pid == pid }
@@ -24,18 +29,34 @@ module Capybara::Poltergeist
24
29
 
25
30
  def start
26
31
  check_phantomjs_version
27
- @pid = Spawn.spawn(*command)
32
+ read, write = IO.pipe
33
+ @out_thread = Thread.new {
34
+ while !read.eof? && data = read.readpartial(1024)
35
+ @phantomjs_logger.write(data)
36
+ end
37
+ }
38
+
39
+ redirect_stdout(write) do
40
+ @pid = Process.spawn(*command.map(&:to_s))
41
+ end
28
42
  end
29
43
 
30
44
  def stop
31
45
  if pid
32
46
  begin
33
47
  Process.kill('TERM', pid)
34
- Process.wait(pid)
48
+
49
+ begin
50
+ Timeout.timeout(KILL_TIMEOUT) { Process.wait(pid) }
51
+ rescue Timeout::Error
52
+ Process.kill('KILL', pid)
53
+ Process.wait(pid)
54
+ end
35
55
  rescue Errno::ESRCH, Errno::ECHILD
36
56
  # Zed's dead, baby
37
57
  end
38
58
 
59
+ @out_thread.kill
39
60
  @pid = nil
40
61
  end
41
62
  end
@@ -46,14 +67,12 @@ module Capybara::Poltergeist
46
67
  end
47
68
 
48
69
  def command
49
- @command ||= begin
50
- parts = [path]
51
- parts.concat phantomjs_options
52
- parts << PHANTOMJS_SCRIPT
53
- parts << port
54
- parts.concat window_size
55
- parts
56
- end
70
+ parts = [path]
71
+ parts.concat phantomjs_options
72
+ parts << PHANTOMJS_SCRIPT
73
+ parts << server.port
74
+ parts.concat window_size
75
+ parts
57
76
  end
58
77
 
59
78
  private
@@ -71,5 +90,18 @@ module Capybara::Poltergeist
71
90
 
72
91
  @phantomjs_version_checked = true
73
92
  end
93
+
94
+ # This abomination is because JRuby doesn't support the :out option of
95
+ # Process.spawn
96
+ def redirect_stdout(to)
97
+ prev = STDOUT.dup
98
+ prev.autoclose = false
99
+ $stdout = to
100
+ STDOUT.reopen(to)
101
+ yield
102
+ ensure
103
+ STDOUT.reopen(prev)
104
+ $stdout = STDOUT
105
+ end
74
106
  end
75
107
  end
@@ -52,6 +52,12 @@ class PoltergeistAgent
52
52
  throw new PoltergeistAgent.ObsoleteNode if node.isObsolete()
53
53
  node[name].apply(node, args)
54
54
 
55
+ beforeUpload: (id) ->
56
+ this.get(id).setAttribute('_poltergeist_selected', '')
57
+
58
+ afterUpload: (id) ->
59
+ this.get(id).removeAttribute('_poltergeist_selected')
60
+
55
61
  class PoltergeistAgent.ObsoleteNode
56
62
  toString: -> "PoltergeistAgent.ObsoleteNode"
57
63
 
@@ -141,19 +147,35 @@ class PoltergeistAgent.Node
141
147
  else
142
148
  true
143
149
 
150
+ frameOffset: ->
151
+ win = window
152
+ offset = { top: 0, left: 0 }
153
+
154
+ while win.frameElement
155
+ rect = window.frameElement.getClientRects()[0]
156
+ win = win.parent
157
+
158
+ offset.top += rect.top
159
+ offset.left += rect.left
160
+
161
+ offset
162
+
144
163
  position: ->
145
164
  rect = @element.getClientRects()[0]
146
165
  throw new PoltergeistAgent.ObsoleteNode unless rect
166
+ frameOffset = this.frameOffset()
147
167
 
148
- {
149
- top: rect.top,
150
- right: rect.right,
151
- left: rect.left,
152
- bottom: rect.bottom,
168
+ pos = {
169
+ top: rect.top + frameOffset.top,
170
+ right: rect.right + frameOffset.left,
171
+ left: rect.left + frameOffset.left,
172
+ bottom: rect.bottom + frameOffset.top,
153
173
  width: rect.width,
154
174
  height: rect.height
155
175
  }
156
176
 
177
+ pos
178
+
157
179
  trigger: (name) ->
158
180
  if Node.EVENTS.MOUSE.indexOf(name) != -1
159
181
  event = document.createEvent('MouseEvent')
@@ -177,6 +199,11 @@ class PoltergeistAgent.Node
177
199
  @element.blur()
178
200
 
179
201
  clickTest: (x, y) ->
202
+ frameOffset = this.frameOffset()
203
+
204
+ x -= frameOffset.left
205
+ y -= frameOffset.top
206
+
180
207
  el = origEl = document.elementFromPoint(x, y)
181
208
 
182
209
  while el