poltergeist 1.1.2 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -9,7 +9,16 @@ provided by [PhantomJS](http://www.phantomjs.org/).
9
9
  **If you're viewing this at https://github.com/jonleighton/poltergeist,
10
10
  you're reading the documentation for the master branch.
11
11
  [View documentation for the latest release
12
- (1.1.2).](https://github.com/jonleighton/poltergeist/tree/v1.1.2)**
12
+ (1.2.0).](https://github.com/jonleighton/poltergeist/tree/v1.2.0)**
13
+
14
+ ## Getting help ##
15
+
16
+ Questions should be posted [on Stack
17
+ Overflow, using the 'poltergeist' tag](http://stackoverflow.com/questions/tagged/poltergeist).
18
+
19
+ Bug reports should be posted [on
20
+ GitHub](https://github.com/jonleighton/poltergeist/issues) (and be sure
21
+ to read the bug reporting guidance below).
13
22
 
14
23
  ## Installation ##
15
24
 
@@ -45,6 +54,9 @@ bit](http://code.google.com/p/phantomjs/downloads/detail?name=phantomjs-1.8.1-li
45
54
  binary.
46
55
  * Extract the tarball and copy `bin/phantomjs` into your `PATH`
47
56
 
57
+ ### Windows ###
58
+ * Download the [precompiled binary](http://phantomjs.org/download.html) for Windows
59
+
48
60
  ### Manual compilation ###
49
61
 
50
62
  Do this as a last resort if the binaries don't work for you. It will
@@ -59,13 +71,13 @@ guide](http://phantomjs.org/build.html).)
59
71
 
60
72
  ## Compatibility ##
61
73
 
62
- Poltergeist runs on MRI 1.9, JRuby 1.9 and Rubinius 1.9.
74
+ Poltergeist runs on MRI 1.9, JRuby 1.9 and Rubinius 1.9. Poltergeist
75
+ and PhantomJS are currently supported on Mac OS X, Linux, and Windows
76
+ platforms.
63
77
 
64
78
  Ruby 1.8 is no longer supported. The last release to support Ruby 1.8
65
79
  was 1.0.2, so you should use that if you still need Ruby 1.8 support.
66
80
 
67
- Poltergeist does not currently support the Windows operating system.
68
-
69
81
  ## Running on a CI ##
70
82
 
71
83
  There are no special steps to take. You don't need Xvfb or any running X
@@ -92,19 +104,20 @@ and the following optional features:
92
104
  * `page.within_window`
93
105
  * `page.status_code`
94
106
  * `page.response_headers`
107
+ * `page.save_screenshot`
95
108
  * cookie handling
96
109
  * drag-and-drop
97
110
 
98
111
  There are some additional features:
99
112
 
100
- ### Taking screenshots ###
113
+ ### Taking screenshots with some extensions ###
101
114
 
102
115
  You can grab screenshots of the page at any point by calling
103
- `page.driver.render('/path/to/file.png')` (this works the same way as the PhantomJS
116
+ `save_screenshot('/path/to/file.png')` (this works the same way as the PhantomJS
104
117
  render feature, so you can specify other extensions like `.pdf`, `.gif`, etc.)
105
118
 
106
119
  By default, only the viewport will be rendered (the part of the page that is in view). To render
107
- the entire page, use `page.driver.render('/path/to/file.png', :full => true)`.
120
+ the entire page, use `save_screenshot('/path/to/file.png', :full => true)`.
108
121
 
109
122
  ### Resizing the window ###
110
123
 
@@ -125,6 +138,18 @@ When this option is enabled, you can insert `page.driver.debug` into
125
138
  your tests to pause the test and launch a browser which gives you the
126
139
  WebKit inspector to view your test run with.
127
140
 
141
+ You can register this debugger driver with a different name and set it
142
+ as the current javascript driver. By example, in your helper file:
143
+
144
+ ```ruby
145
+ Capybara.register_driver :poltergeist_debug do |app|
146
+ Capybara::Poltergeist::Driver.new(app, :inspector => true)
147
+ end
148
+
149
+ # Capybara.javascript_driver = :poltergeist
150
+ Capybara.javascript_driver = :poltergeist_debug
151
+ ```
152
+
128
153
  [Read more
129
154
  here](http://jonathanleighton.com/articles/2012/poltergeist-0-6-0/)
130
155
 
@@ -225,7 +250,7 @@ If you experience sporadic crashes a lot, it may be worth configuring
225
250
  your CI to automatically re-run failing tests before reporting a failed
226
251
  build.
227
252
 
228
- ### ClickFailed errors ###
253
+ ### MouseEventFailed errors ###
229
254
 
230
255
  When Poltergeist clicks on an element, rather than generating a DOM
231
256
  click event, it actually generates a "proper" click. This is much closer
@@ -238,7 +263,7 @@ your user won't be able to click a covered up element either).
238
263
  Sometimes there can be issues with this behavior. If you have problems,
239
264
  it's worth taking screenshots of the page and trying to work out what's
240
265
  going on. If your click is failing, but you're not getting a
241
- `ClickFailed` error, then you can turn on the `:debug` option and look
266
+ `MouseEventFailed` error, then you can turn on the `:debug` option and look
242
267
  in the output to see what co-ordinates Poltergeist is using for the
243
268
  click. You can then cross-reference this with a screenshot to see if
244
269
  something is obviously wrong.
@@ -299,18 +324,22 @@ Include as much information as possible. For example:
299
324
 
300
325
  ## Changes ##
301
326
 
302
- ### 1.1.2 ###
327
+ ### 1.2.0 ###
303
328
 
304
- #### Bug fixes #####
305
-
306
- * Tie to faye-websocket 0.4 as 0.5 introduces incompatibilities.
329
+ #### Features ####
307
330
 
308
- ### 1.1.1 ###
331
+ * Support for Windows hosted Poltergeist (Aaron Tull).
332
+ * Capybara 2.1 support
309
333
 
310
- #### Features ####
334
+ #### Bug fixes ####
311
335
 
312
- * Changed Capybara dependency to `~> 2.0.1` because Poltergeist 1.1 is
313
- not compatible with Capybara 2.1.
336
+ * Reverted the "native" implementation for filling in form fields,
337
+ which was introduced in 1.0. This implementation caused various bugs
338
+ and in general doesn't seem to be worth the trouble at the moment.
339
+ It can be reconsidered in the future when PhantomJS has upgraded its
340
+ WebKit version. [Issue #176, #223]
341
+ * Run phantomjs in a new process group so ^C doesn't trigger a
342
+ DeadClient error [Issue #252]
314
343
 
315
344
  ### 1.1.0 ###
316
345
 
@@ -327,6 +356,10 @@ Include as much information as possible. For example:
327
356
  * The `:debug` option now causes the PhantomJS portion of Poltergeist
328
357
  to output some additional debug info, which may be useful in
329
358
  figuring out timeout errors.
359
+ * Add the ability to extend the phantomjs environment via browser
360
+ options. e.g.
361
+ `Capybara::Poltergeist::Driver.new( app, :extensions => ['file.js', 'another.js'])`
362
+ (Jon Rowe)
330
363
 
331
364
  #### Bug fixes ####
332
365
 
@@ -348,10 +381,6 @@ Include as much information as possible. For example:
348
381
  [Issue #192]
349
382
  * Fix `ObsoleteNode` error when using `attach_file` with the `jQuery
350
383
  File Upload` plugin. [Issue #115]
351
- * Add the ability to extend the phantomjs environment via browser
352
- options. e.g.
353
- `Capybara::Poltergeist::Driver.new( app, :extensions => ['file.js', 'another.js'])`
354
- (@JonRowe)
355
384
  * Ensure that a `String` is passed over-the-wire to PhantomJS for
356
385
  file input paths, allowing `attach_file` to be called with arbitry
357
386
  objects such as a Pathname. (@mjtko) [Issue #215]
@@ -7,6 +7,7 @@ require 'capybara'
7
7
 
8
8
  module Capybara
9
9
  module Poltergeist
10
+ require 'capybara/poltergeist/utility'
10
11
  require 'capybara/poltergeist/driver'
11
12
  require 'capybara/poltergeist/browser'
12
13
  require 'capybara/poltergeist/node'
@@ -1,8 +1,14 @@
1
+ require "capybara/poltergeist/errors"
1
2
  require 'json'
2
3
  require 'time'
3
4
 
4
5
  module Capybara::Poltergeist
5
6
  class Browser
7
+ ERROR_MAPPINGS = {
8
+ "Poltergeist.JavascriptError" => JavascriptError,
9
+ "Poltergeist.FrameNotFound" => FrameNotFound
10
+ }
11
+
6
12
  attr_reader :server, :client, :logger
7
13
 
8
14
  def initialize(server, client, logger = nil)
@@ -38,17 +44,25 @@ module Capybara::Poltergeist
38
44
  command 'source'
39
45
  end
40
46
 
41
- def find(selector)
42
- result = command('find', selector)
47
+ def title
48
+ command 'title'
49
+ end
50
+
51
+ def find(method, selector)
52
+ result = command('find', method, selector)
43
53
  result['ids'].map { |id| [result['page_id'], id] }
44
54
  end
45
55
 
46
- def find_within(page_id, id, selector)
47
- command 'find_within', page_id, id, selector
56
+ def find_within(page_id, id, method, selector)
57
+ command 'find_within', page_id, id, method, selector
48
58
  end
49
59
 
50
- def text(page_id, id)
51
- command 'text', page_id, id
60
+ def all_text(page_id, id)
61
+ command 'all_text', page_id, id
62
+ end
63
+
64
+ def visible_text(page_id, id)
65
+ command 'visible_text', page_id, id
52
66
  end
53
67
 
54
68
  def attribute(page_id, id, name)
@@ -75,6 +89,10 @@ module Capybara::Poltergeist
75
89
  command 'visible', page_id, id
76
90
  end
77
91
 
92
+ def disabled?(page_id, id)
93
+ command 'disabled', page_id, id
94
+ end
95
+
78
96
  def click_coordinates(x, y)
79
97
  command 'click_coordinates', x, y
80
98
  end
@@ -87,8 +105,13 @@ module Capybara::Poltergeist
87
105
  command 'execute', script
88
106
  end
89
107
 
90
- def within_frame(name, &block)
91
- command 'push_frame', name
108
+ def within_frame(handle, &block)
109
+ if handle.is_a?(Capybara::Node::Base)
110
+ command 'push_frame', handle['id']
111
+ else
112
+ command 'push_frame', handle
113
+ end
114
+
92
115
  yield
93
116
  ensure
94
117
  command 'pop_frame'
@@ -109,6 +132,10 @@ module Capybara::Poltergeist
109
132
  command 'double_click', page_id, id
110
133
  end
111
134
 
135
+ def hover(page_id, id)
136
+ command 'hover', page_id, id
137
+ end
138
+
112
139
  def drag(page_id, id, other_id)
113
140
  command 'drag', page_id, id, other_id
114
141
  end
@@ -193,14 +220,11 @@ module Capybara::Poltergeist
193
220
  log json.inspect
194
221
 
195
222
  if json['error']
196
- if json['error']['name'] == 'Poltergeist.JavascriptError'
197
- raise JavascriptError.new(json['error'])
198
- else
199
- raise BrowserError.new(json['error'])
200
- end
223
+ klass = ERROR_MAPPINGS[json['error']['name']] || BrowserError
224
+ raise klass.new(json['error'])
225
+ else
226
+ json['response']
201
227
  end
202
- json['response']
203
-
204
228
  rescue DeadClient
205
229
  restart
206
230
  raise
@@ -1,4 +1,5 @@
1
1
  require "timeout"
2
+ require "capybara/poltergeist/utility"
2
3
 
3
4
  module Capybara::Poltergeist
4
5
  class Client
@@ -37,20 +38,23 @@ module Capybara::Poltergeist
37
38
  }
38
39
 
39
40
  redirect_stdout(write) do
40
- @pid = Process.spawn(*command.map(&:to_s))
41
+ @pid = Process.spawn(*command.map(&:to_s), pgroup: true)
41
42
  end
42
43
  end
43
44
 
44
45
  def stop
45
46
  if pid
46
47
  begin
47
- Process.kill('TERM', pid)
48
-
49
- begin
50
- Timeout.timeout(KILL_TIMEOUT) { Process.wait(pid) }
51
- rescue Timeout::Error
48
+ if Capybara::Poltergeist.windows?
52
49
  Process.kill('KILL', pid)
53
- Process.wait(pid)
50
+ else
51
+ Process.kill('TERM', pid)
52
+ begin
53
+ Timeout.timeout(KILL_TIMEOUT) { Process.wait(pid) }
54
+ rescue Timeout::Error
55
+ Process.kill('KILL', pid)
56
+ Process.wait(pid)
57
+ end
54
58
  end
55
59
  rescue Errno::ESRCH, Errno::ECHILD
56
60
  # Zed's dead, baby
@@ -27,14 +27,14 @@ class PoltergeistAgent
27
27
  currentUrl: ->
28
28
  window.location.toString()
29
29
 
30
- find: (selector, within = document) ->
31
- results = document.evaluate(selector, within, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null)
32
- ids = []
33
-
34
- for i in [0...results.snapshotLength]
35
- ids.push(this.register(results.snapshotItem(i)))
30
+ find: (method, selector, within = document) ->
31
+ if method == "xpath"
32
+ xpath = document.evaluate(selector, within, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null)
33
+ results = (xpath.snapshotItem(i) for i in [0...xpath.snapshotLength])
34
+ else
35
+ results = within.querySelectorAll(selector)
36
36
 
37
- ids
37
+ this.register(el) for el in results
38
38
 
39
39
  register: (element) ->
40
40
  @elements.push(element)
@@ -73,8 +73,8 @@ class PoltergeistAgent.Node
73
73
  parentId: ->
74
74
  @agent.register(@element.parentNode)
75
75
 
76
- find: (selector) ->
77
- @agent.find(selector, @element)
76
+ find: (method, selector) ->
77
+ @agent.find(method, selector, @element)
78
78
 
79
79
  isObsolete: ->
80
80
  obsolete = (element) =>
@@ -92,12 +92,41 @@ class PoltergeistAgent.Node
92
92
  event.initEvent('change', true, false)
93
93
  @element.dispatchEvent(event)
94
94
 
95
+ input: ->
96
+ event = document.createEvent('HTMLEvents')
97
+ event.initEvent('input', true, false)
98
+ @element.dispatchEvent(event)
99
+
100
+ keyupdowned: (eventName, keyCode) ->
101
+ event = document.createEvent('UIEvents')
102
+ event.initEvent(eventName, true, true)
103
+ event.keyCode = keyCode
104
+ event.which = keyCode
105
+ event.charCode = 0
106
+ @element.dispatchEvent(event)
107
+
108
+ keypressed: (altKey, ctrlKey, shiftKey, metaKey, keyCode, charCode) ->
109
+ event = document.createEvent('UIEvents')
110
+ event.initEvent('keypress', true, true)
111
+ event.window = @agent.window
112
+ event.altKey = altKey
113
+ event.ctrlKey = ctrlKey
114
+ event.shiftKey = shiftKey
115
+ event.metaKey = metaKey
116
+ event.keyCode = keyCode
117
+ event.charCode = charCode
118
+ event.which = keyCode
119
+ @element.dispatchEvent(event)
120
+
95
121
  insideBody: ->
96
122
  @element == document.body ||
97
123
  document.evaluate('ancestor::body', @element, null, XPathResult.BOOLEAN_TYPE, null).booleanValue
98
124
 
99
- text: ->
100
- if @element.tagName == 'TEXTAREA'
125
+ allText: ->
126
+ @element.textContent
127
+
128
+ visibleText: ->
129
+ if @element.nodeName == "TEXTAREA"
101
130
  @element.textContent
102
131
  else
103
132
  @element.innerText
@@ -117,6 +146,27 @@ class PoltergeistAgent.Node
117
146
  else
118
147
  @element.value
119
148
 
149
+ set: (value) ->
150
+ return if @element.readOnly
151
+
152
+ if (@element.maxLength >= 0)
153
+ value = value.substr(0, @element.maxLength)
154
+
155
+ @element.value = ''
156
+ this.trigger('focus')
157
+
158
+ for char in value
159
+ keyCode = this.characterToKeyCode(char)
160
+ this.keyupdowned('keydown', keyCode)
161
+ @element.value += char
162
+
163
+ this.keypressed(false, false, false, false, char.charCodeAt(0), char.charCodeAt(0))
164
+ this.keyupdowned('keyup', keyCode)
165
+
166
+ this.changed()
167
+ this.input()
168
+ this.trigger('blur')
169
+
120
170
  isMultiple: ->
121
171
  @element.multiple
122
172
 
@@ -147,6 +197,9 @@ class PoltergeistAgent.Node
147
197
  else
148
198
  true
149
199
 
200
+ isDisabled: ->
201
+ @element.disabled || @element.tagName == 'OPTION' && @element.parentNode.disabled
202
+
150
203
  frameOffset: ->
151
204
  win = window
152
205
  offset = { top: 0, left: 0 }
@@ -191,14 +244,7 @@ class PoltergeistAgent.Node
191
244
 
192
245
  @element.dispatchEvent(event)
193
246
 
194
- focusAndHighlight: ->
195
- @element.focus()
196
- @element.select()
197
-
198
- blur: ->
199
- @element.blur()
200
-
201
- clickTest: (x, y) ->
247
+ mouseEventTest: (x, y) ->
202
248
  frameOffset = this.frameOffset()
203
249
 
204
250
  x -= frameOffset.left
@@ -222,6 +268,45 @@ class PoltergeistAgent.Node
222
268
  selector += ".#{className}"
223
269
  selector
224
270
 
271
+ characterToKeyCode: (character) ->
272
+ code = character.toUpperCase().charCodeAt(0)
273
+ specialKeys =
274
+ 96: 192 #`
275
+ 45: 189 #-
276
+ 61: 187 #=
277
+ 91: 219 #[
278
+ 93: 221 #]
279
+ 92: 220 #\
280
+ 59: 186 #;
281
+ 39: 222 #'
282
+ 44: 188 #,
283
+ 46: 190 #.
284
+ 47: 191 #/
285
+ 127: 46 #delete
286
+ 126: 192 #~
287
+ 33: 49 #!
288
+ 64: 50 #@
289
+ 35: 51 ##
290
+ 36: 52 #$
291
+ 37: 53 #%
292
+ 94: 54 #^
293
+ 38: 55 #&
294
+ 42: 56 #*
295
+ 40: 57 #(
296
+ 41: 48 #)
297
+ 95: 189 #_
298
+ 43: 187 #+
299
+ 123: 219 #{
300
+ 125: 221 #}
301
+ 124: 220 #|
302
+ 58: 186 #:
303
+ 34: 222 #"
304
+ 60: 188 #<
305
+ 62: 190 #>
306
+ 63: 191 #?
307
+
308
+ specialKeys[code] || code
309
+
225
310
  isDOMEqual: (other_id) ->
226
311
  @element == @agent.get(other_id).element
227
312