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 +50 -21
- data/lib/capybara/poltergeist.rb +1 -0
- data/lib/capybara/poltergeist/browser.rb +39 -15
- data/lib/capybara/poltergeist/client.rb +11 -7
- data/lib/capybara/poltergeist/client/agent.coffee +104 -19
- data/lib/capybara/poltergeist/client/browser.coffee +34 -18
- data/lib/capybara/poltergeist/client/compiled/agent.js +154 -26
- data/lib/capybara/poltergeist/client/compiled/browser.js +54 -22
- data/lib/capybara/poltergeist/client/compiled/connection.js +0 -2
- data/lib/capybara/poltergeist/client/compiled/main.js +32 -15
- data/lib/capybara/poltergeist/client/compiled/node.js +13 -19
- data/lib/capybara/poltergeist/client/compiled/web_page.js +19 -5
- data/lib/capybara/poltergeist/client/main.coffee +9 -4
- data/lib/capybara/poltergeist/client/node.coffee +11 -19
- data/lib/capybara/poltergeist/client/web_page.coffee +4 -5
- data/lib/capybara/poltergeist/driver.rb +15 -3
- data/lib/capybara/poltergeist/errors.rb +20 -5
- data/lib/capybara/poltergeist/node.rb +32 -6
- data/lib/capybara/poltergeist/utility.rb +9 -0
- data/lib/capybara/poltergeist/version.rb +1 -1
- metadata +14 -7
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.
|
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
|
-
`
|
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 `
|
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
|
-
###
|
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
|
-
`
|
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.
|
327
|
+
### 1.2.0 ###
|
303
328
|
|
304
|
-
####
|
305
|
-
|
306
|
-
* Tie to faye-websocket 0.4 as 0.5 introduces incompatibilities.
|
329
|
+
#### Features ####
|
307
330
|
|
308
|
-
|
331
|
+
* Support for Windows hosted Poltergeist (Aaron Tull).
|
332
|
+
* Capybara 2.1 support
|
309
333
|
|
310
|
-
####
|
334
|
+
#### Bug fixes ####
|
311
335
|
|
312
|
-
*
|
313
|
-
|
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]
|
data/lib/capybara/poltergeist.rb
CHANGED
@@ -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
|
42
|
-
|
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
|
51
|
-
command '
|
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(
|
91
|
-
|
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
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
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
|
-
|
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
|
-
|
100
|
-
|
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
|
-
|
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
|
|