poltergeist 0.4.0 → 0.5.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.
- data/README.md +48 -2
- data/lib/capybara/poltergeist.rb +3 -0
- data/lib/capybara/poltergeist/browser.rb +48 -55
- data/lib/capybara/poltergeist/client.rb +42 -12
- data/lib/capybara/poltergeist/client/agent.coffee +48 -5
- data/lib/capybara/poltergeist/client/browser.coffee +64 -42
- data/lib/capybara/poltergeist/client/compiled/agent.js +71 -8
- data/lib/capybara/poltergeist/client/compiled/browser.js +68 -41
- data/lib/capybara/poltergeist/client/compiled/main.js +34 -5
- data/lib/capybara/poltergeist/client/compiled/node.js +34 -25
- data/lib/capybara/poltergeist/client/compiled/web_page.js +30 -7
- data/lib/capybara/poltergeist/client/main.coffee +26 -3
- data/lib/capybara/poltergeist/client/node.coffee +30 -23
- data/lib/capybara/poltergeist/client/web_page.coffee +31 -7
- data/lib/capybara/poltergeist/driver.rb +54 -19
- data/lib/capybara/poltergeist/errors.rb +63 -6
- data/lib/capybara/poltergeist/inspector.rb +35 -0
- data/lib/capybara/poltergeist/node.rb +15 -5
- data/lib/capybara/poltergeist/server.rb +7 -12
- data/lib/capybara/poltergeist/spawn.rb +17 -0
- data/lib/capybara/poltergeist/util.rb +12 -0
- data/lib/capybara/poltergeist/version.rb +1 -1
- data/lib/capybara/poltergeist/web_socket_server.rb +25 -6
- metadata +31 -25
@@ -7,8 +7,8 @@ Poltergeist.WebPage = (function() {
|
|
7
7
|
function WebPage() {
|
8
8
|
var callback, _i, _len, _ref;
|
9
9
|
this["native"] = require('webpage').create();
|
10
|
-
this.nodes = {};
|
11
10
|
this._source = "";
|
11
|
+
this._errors = [];
|
12
12
|
this.setViewportSize({
|
13
13
|
width: 1024,
|
14
14
|
height: 768
|
@@ -54,7 +54,8 @@ Poltergeist.WebPage = (function() {
|
|
54
54
|
if (this.evaluate(function() {
|
55
55
|
return typeof __poltergeist;
|
56
56
|
}) === "undefined") {
|
57
|
-
|
57
|
+
this["native"].injectJs('agent.js');
|
58
|
+
return this.nodes = {};
|
58
59
|
}
|
59
60
|
};
|
60
61
|
WebPage.prototype.onConsoleMessageNative = function(message) {
|
@@ -66,8 +67,12 @@ Poltergeist.WebPage = (function() {
|
|
66
67
|
WebPage.prototype.onLoadFinishedNative = function() {
|
67
68
|
return this._source || (this._source = this["native"].content);
|
68
69
|
};
|
69
|
-
WebPage.prototype.onConsoleMessage = function(message) {
|
70
|
-
|
70
|
+
WebPage.prototype.onConsoleMessage = function(message, line, file) {
|
71
|
+
if (line === 0 && file === "undefined") {
|
72
|
+
return this._errors.push(message);
|
73
|
+
} else {
|
74
|
+
return console.log(message);
|
75
|
+
}
|
71
76
|
};
|
72
77
|
WebPage.prototype.content = function() {
|
73
78
|
return this["native"].content;
|
@@ -75,6 +80,12 @@ Poltergeist.WebPage = (function() {
|
|
75
80
|
WebPage.prototype.source = function() {
|
76
81
|
return this._source;
|
77
82
|
};
|
83
|
+
WebPage.prototype.errors = function() {
|
84
|
+
return this._errors;
|
85
|
+
};
|
86
|
+
WebPage.prototype.clearErrors = function() {
|
87
|
+
return this._errors = [];
|
88
|
+
};
|
78
89
|
WebPage.prototype.viewportSize = function() {
|
79
90
|
return this["native"].viewportSize;
|
80
91
|
};
|
@@ -135,7 +146,7 @@ Poltergeist.WebPage = (function() {
|
|
135
146
|
WebPage.prototype.evaluate = function() {
|
136
147
|
var args, fn;
|
137
148
|
fn = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
|
138
|
-
return this["native"].evaluate("function() { return " + (this.stringifyCall(fn, args)) + " }");
|
149
|
+
return JSON.parse(this["native"].evaluate("function() { return JSON.stringify(" + (this.stringifyCall(fn, args)) + ") }"));
|
139
150
|
};
|
140
151
|
WebPage.prototype.execute = function() {
|
141
152
|
var args, fn;
|
@@ -163,9 +174,21 @@ Poltergeist.WebPage = (function() {
|
|
163
174
|
};
|
164
175
|
};
|
165
176
|
WebPage.prototype.runCommand = function(name, arguments) {
|
166
|
-
|
167
|
-
|
177
|
+
var result;
|
178
|
+
result = this.evaluate(function(name, arguments) {
|
179
|
+
return __poltergeist.externalCall(name, arguments);
|
168
180
|
}, name, arguments);
|
181
|
+
if (result.error) {
|
182
|
+
switch (result.error) {
|
183
|
+
case "PoltergeistAgent.ObsoleteNode":
|
184
|
+
throw new Poltergeist.ObsoleteNode;
|
185
|
+
break;
|
186
|
+
default:
|
187
|
+
throw result.error;
|
188
|
+
}
|
189
|
+
} else {
|
190
|
+
return result.value;
|
191
|
+
}
|
169
192
|
};
|
170
193
|
return WebPage;
|
171
194
|
}).call(this);
|
@@ -7,13 +7,36 @@ class Poltergeist
|
|
7
7
|
try
|
8
8
|
@browser[command.name].apply(@browser, command.args)
|
9
9
|
catch error
|
10
|
-
|
10
|
+
this.sendError(error)
|
11
11
|
|
12
12
|
sendResponse: (response) ->
|
13
|
-
@connection.send(
|
13
|
+
@connection.send(response: response)
|
14
|
+
|
15
|
+
sendError: (error) ->
|
16
|
+
@connection.send(
|
17
|
+
error:
|
18
|
+
name: error.name || 'Generic',
|
19
|
+
args: error.args && error.args() || [error.toString()]
|
20
|
+
)
|
21
|
+
|
22
|
+
# This is necessary because the remote debugger will wrap the
|
23
|
+
# script in a function, causing the Poltergeist variable to
|
24
|
+
# become local.
|
25
|
+
window.Poltergeist = Poltergeist
|
14
26
|
|
15
27
|
class Poltergeist.ObsoleteNode
|
16
|
-
|
28
|
+
name: "Poltergeist.ObsoleteNode"
|
29
|
+
args: -> []
|
30
|
+
|
31
|
+
class Poltergeist.ClickFailed
|
32
|
+
constructor: (@selector, @position) ->
|
33
|
+
name: "Poltergeist.ClickFailed"
|
34
|
+
args: -> [@selector, @position]
|
35
|
+
|
36
|
+
class Poltergeist.JavascriptError
|
37
|
+
constructor: (@errors) ->
|
38
|
+
name: "Poltergeist.JavascriptError"
|
39
|
+
args: -> [@errors]
|
17
40
|
|
18
41
|
phantom.injectJs('web_page.js')
|
19
42
|
phantom.injectJs('node.js')
|
@@ -1,26 +1,21 @@
|
|
1
1
|
# Proxy object for forwarding method calls to the node object inside the page.
|
2
2
|
|
3
3
|
class Poltergeist.Node
|
4
|
-
@DELEGATES = ['text', 'getAttribute', 'value', 'set', 'setAttribute',
|
5
|
-
'
|
4
|
+
@DELEGATES = ['text', 'getAttribute', 'value', 'set', 'setAttribute',
|
5
|
+
'removeAttribute', 'isMultiple', 'select', 'tagName', 'find',
|
6
|
+
'isVisible', 'position', 'trigger', 'parentId', 'clickTest']
|
6
7
|
|
7
8
|
constructor: (@page, @id) ->
|
8
9
|
|
9
10
|
parent: ->
|
10
11
|
new Poltergeist.Node(@page, this.parentId())
|
11
12
|
|
12
|
-
isObsolete: ->
|
13
|
-
@page.nodeCall(@id, 'isObsolete')
|
14
|
-
|
15
13
|
for name in @DELEGATES
|
16
14
|
do (name) =>
|
17
15
|
this.prototype[name] = (arguments...) ->
|
18
|
-
|
19
|
-
throw new Poltergeist.ObsoleteNode
|
20
|
-
else
|
21
|
-
@page.nodeCall(@id, name, arguments)
|
16
|
+
@page.nodeCall(@id, name, arguments)
|
22
17
|
|
23
|
-
|
18
|
+
clickPosition: (scrollIntoView = true) ->
|
24
19
|
dimensions = @page.validatedDimensions()
|
25
20
|
document = dimensions.document
|
26
21
|
viewport = dimensions.viewport
|
@@ -41,23 +36,35 @@ class Poltergeist.Node
|
|
41
36
|
scroll[coord] + pos[coord] - viewport[measurement] + (viewport[measurement] / 2)
|
42
37
|
)
|
43
38
|
|
44
|
-
|
45
|
-
|
39
|
+
if scrollIntoView
|
40
|
+
adjust('left', 'width')
|
41
|
+
adjust('top', 'height')
|
42
|
+
|
43
|
+
if scroll.left != dimensions.left || scroll.top != dimensions.top
|
44
|
+
@page.setScrollPosition(scroll)
|
45
|
+
pos = this.position()
|
46
46
|
|
47
|
-
|
48
|
-
|
49
|
-
pos = this.position()
|
47
|
+
middle = (start, end, size) ->
|
48
|
+
start + ((Math.min(end, size) - start) / 2)
|
50
49
|
|
51
|
-
|
50
|
+
{
|
51
|
+
x: middle(pos.left, pos.right, viewport.width),
|
52
|
+
y: middle(pos.top, pos.bottom, viewport.height)
|
53
|
+
}
|
52
54
|
|
53
55
|
click: ->
|
54
|
-
|
55
|
-
|
56
|
+
pos = this.clickPosition()
|
57
|
+
test = this.clickTest(pos.x, pos.y)
|
58
|
+
|
59
|
+
if test.status == 'success'
|
60
|
+
@page.sendEvent('click', pos.x, pos.y)
|
61
|
+
else
|
62
|
+
throw new Poltergeist.ClickFailed(test.selector, pos)
|
56
63
|
|
57
64
|
dragTo: (other) ->
|
58
|
-
position = this.
|
59
|
-
otherPosition = other.
|
65
|
+
position = this.clickPosition()
|
66
|
+
otherPosition = other.clickPosition(false)
|
60
67
|
|
61
|
-
@page.sendEvent('mousedown', position.
|
62
|
-
@page.sendEvent('mousemove', otherPosition.
|
63
|
-
@page.sendEvent('mouseup', otherPosition.
|
68
|
+
@page.sendEvent('mousedown', position.x, position.y)
|
69
|
+
@page.sendEvent('mousemove', otherPosition.x, otherPosition.y)
|
70
|
+
@page.sendEvent('mouseup', otherPosition.x, otherPosition.y)
|
@@ -1,13 +1,15 @@
|
|
1
1
|
class Poltergeist.WebPage
|
2
2
|
@CALLBACKS = ['onAlert', 'onConsoleMessage', 'onLoadFinished', 'onInitialized',
|
3
3
|
'onLoadStarted', 'onResourceRequested', 'onResourceReceived']
|
4
|
+
|
4
5
|
@DELEGATES = ['open', 'sendEvent', 'uploadFile', 'release', 'render']
|
6
|
+
|
5
7
|
@COMMANDS = ['currentUrl', 'find', 'nodeCall', 'pushFrame', 'popFrame', 'documentSize']
|
6
8
|
|
7
9
|
constructor: ->
|
8
10
|
@native = require('webpage').create()
|
9
|
-
@nodes = {}
|
10
11
|
@_source = ""
|
12
|
+
@_errors = []
|
11
13
|
|
12
14
|
this.setViewportSize(width: 1024, height: 768)
|
13
15
|
|
@@ -29,11 +31,12 @@ class Poltergeist.WebPage
|
|
29
31
|
onInitializedNative: ->
|
30
32
|
@_source = null
|
31
33
|
this.injectAgent()
|
32
|
-
this.setScrollPosition(
|
34
|
+
this.setScrollPosition(left: 0, top: 0)
|
33
35
|
|
34
36
|
injectAgent: ->
|
35
37
|
if this.evaluate(-> typeof __poltergeist) == "undefined"
|
36
38
|
@native.injectJs('agent.js')
|
39
|
+
@nodes = {}
|
37
40
|
|
38
41
|
onConsoleMessageNative: (message) ->
|
39
42
|
if message == '__DOMContentLoaded'
|
@@ -43,8 +46,14 @@ class Poltergeist.WebPage
|
|
43
46
|
onLoadFinishedNative: ->
|
44
47
|
@_source or= @native.content
|
45
48
|
|
46
|
-
onConsoleMessage: (message) ->
|
47
|
-
|
49
|
+
onConsoleMessage: (message, line, file) ->
|
50
|
+
if line == 0 && file == "undefined"
|
51
|
+
# file:line will always be "undefined:0" in current release of
|
52
|
+
# PhantomJS ;(
|
53
|
+
@_errors.push(message)
|
54
|
+
else
|
55
|
+
# here line == 1 && file == "". don't ask me why!
|
56
|
+
console.log(message)
|
48
57
|
|
49
58
|
content: ->
|
50
59
|
@native.content
|
@@ -52,6 +61,12 @@ class Poltergeist.WebPage
|
|
52
61
|
source: ->
|
53
62
|
@_source
|
54
63
|
|
64
|
+
errors: ->
|
65
|
+
@_errors
|
66
|
+
|
67
|
+
clearErrors: ->
|
68
|
+
@_errors = []
|
69
|
+
|
55
70
|
viewportSize: ->
|
56
71
|
@native.viewportSize
|
57
72
|
|
@@ -104,7 +119,7 @@ class Poltergeist.WebPage
|
|
104
119
|
@nodes[id] or= new Poltergeist.Node(this, id)
|
105
120
|
|
106
121
|
evaluate: (fn, args...) ->
|
107
|
-
@native.evaluate("function() { return #{this.stringifyCall(fn, args)} }")
|
122
|
+
JSON.parse @native.evaluate("function() { return JSON.stringify(#{this.stringifyCall(fn, args)}) }")
|
108
123
|
|
109
124
|
execute: (fn, args...) ->
|
110
125
|
@native.evaluate("function() { #{this.stringifyCall(fn, args)} }")
|
@@ -129,7 +144,16 @@ class Poltergeist.WebPage
|
|
129
144
|
that[name].apply(that, arguments)
|
130
145
|
|
131
146
|
runCommand: (name, arguments) ->
|
132
|
-
this.evaluate(
|
133
|
-
(name, arguments) -> __poltergeist
|
147
|
+
result = this.evaluate(
|
148
|
+
(name, arguments) -> __poltergeist.externalCall(name, arguments),
|
134
149
|
name, arguments
|
135
150
|
)
|
151
|
+
|
152
|
+
if result.error
|
153
|
+
switch result.error
|
154
|
+
when "PoltergeistAgent.ObsoleteNode"
|
155
|
+
throw new Poltergeist.ObsoleteNode
|
156
|
+
else
|
157
|
+
throw result.error
|
158
|
+
else
|
159
|
+
result.value
|
@@ -1,34 +1,65 @@
|
|
1
1
|
module Capybara::Poltergeist
|
2
2
|
class Driver < Capybara::Driver::Base
|
3
|
-
|
3
|
+
DEFAULT_TIMEOUT = 30
|
4
|
+
|
5
|
+
attr_reader :app, :app_server, :server, :client, :browser, :options
|
4
6
|
|
5
7
|
def initialize(app, options = {})
|
6
|
-
@app
|
7
|
-
@options
|
8
|
-
@
|
9
|
-
@
|
8
|
+
@app = app
|
9
|
+
@options = options
|
10
|
+
@browser = nil
|
11
|
+
@inspector = nil
|
12
|
+
@server = nil
|
13
|
+
@client = nil
|
10
14
|
|
11
|
-
@
|
15
|
+
@app_server = Capybara::Server.new(app)
|
16
|
+
@app_server.boot if Capybara.run_server
|
12
17
|
end
|
13
18
|
|
14
19
|
def browser
|
15
|
-
@browser ||= Browser.new(
|
16
|
-
|
17
|
-
|
18
|
-
|
20
|
+
@browser ||= Browser.new(server, client, logger)
|
21
|
+
end
|
22
|
+
|
23
|
+
def inspector
|
24
|
+
@inspector ||= options[:inspector] && Inspector.new(options[:inspector])
|
25
|
+
end
|
26
|
+
|
27
|
+
def server
|
28
|
+
@server ||= Server.new(options.fetch(:timeout, DEFAULT_TIMEOUT))
|
29
|
+
end
|
30
|
+
|
31
|
+
def client
|
32
|
+
@client ||= Client.start(server.port, inspector, options[:phantomjs])
|
33
|
+
end
|
34
|
+
|
35
|
+
def client_pid
|
36
|
+
client.pid
|
37
|
+
end
|
38
|
+
|
39
|
+
def timeout
|
40
|
+
server.timeout
|
41
|
+
end
|
42
|
+
|
43
|
+
def timeout=(sec)
|
44
|
+
server.timeout = sec
|
19
45
|
end
|
20
46
|
|
21
47
|
def restart
|
22
48
|
browser.restart
|
23
49
|
end
|
24
50
|
|
51
|
+
def quit
|
52
|
+
server.stop
|
53
|
+
client.stop
|
54
|
+
end
|
55
|
+
|
25
56
|
# logger should be an object that responds to puts, or nil
|
26
57
|
def logger
|
27
58
|
options[:logger] || (options[:debug] && STDERR)
|
28
59
|
end
|
29
60
|
|
30
|
-
def visit(path
|
31
|
-
browser.visit
|
61
|
+
def visit(path)
|
62
|
+
browser.visit app_server.url(path)
|
32
63
|
end
|
33
64
|
|
34
65
|
def current_url
|
@@ -44,7 +75,7 @@ module Capybara::Poltergeist
|
|
44
75
|
end
|
45
76
|
|
46
77
|
def find(selector)
|
47
|
-
browser.find(selector).map { |
|
78
|
+
browser.find(selector).map { |page_id, id| Capybara::Poltergeist::Node.new(self, page_id, id) }
|
48
79
|
end
|
49
80
|
|
50
81
|
def evaluate_script(script)
|
@@ -72,6 +103,16 @@ module Capybara::Poltergeist
|
|
72
103
|
browser.resize(width, height)
|
73
104
|
end
|
74
105
|
|
106
|
+
def debug
|
107
|
+
inspector.open
|
108
|
+
pause
|
109
|
+
end
|
110
|
+
|
111
|
+
def pause
|
112
|
+
STDERR.puts "Poltergeist execution paused. Press enter to continue."
|
113
|
+
STDIN.gets
|
114
|
+
end
|
115
|
+
|
75
116
|
def wait?
|
76
117
|
true
|
77
118
|
end
|
@@ -79,11 +120,5 @@ module Capybara::Poltergeist
|
|
79
120
|
def invalid_element_errors
|
80
121
|
[Capybara::Poltergeist::ObsoleteNode]
|
81
122
|
end
|
82
|
-
|
83
|
-
private
|
84
|
-
|
85
|
-
def url(path)
|
86
|
-
server.url(path)
|
87
|
-
end
|
88
123
|
end
|
89
124
|
end
|
@@ -3,11 +3,21 @@ module Capybara
|
|
3
3
|
class Error < StandardError
|
4
4
|
end
|
5
5
|
|
6
|
-
class
|
7
|
-
attr_reader :
|
6
|
+
class ClientError < Error
|
7
|
+
attr_reader :response
|
8
8
|
|
9
|
-
def initialize(
|
10
|
-
@
|
9
|
+
def initialize(response)
|
10
|
+
@response = response
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class BrowserError < ClientError
|
15
|
+
def name
|
16
|
+
response['name']
|
17
|
+
end
|
18
|
+
|
19
|
+
def text
|
20
|
+
response['args'].first
|
11
21
|
end
|
12
22
|
|
13
23
|
def message
|
@@ -15,11 +25,44 @@ module Capybara
|
|
15
25
|
end
|
16
26
|
end
|
17
27
|
|
18
|
-
class
|
28
|
+
class JavascriptError < ClientError
|
29
|
+
def javascript_messages
|
30
|
+
response['args'].first
|
31
|
+
end
|
32
|
+
|
33
|
+
def message
|
34
|
+
"One or more errors were raised in the Javascript code on the page: #{javascript_messages.inspect} " \
|
35
|
+
"Unfortunately, it is not currently possible to provide a stack trace, or even the line/file where " \
|
36
|
+
"the error occurred. (This is due to lack of support within QtWebKit.) Fixing this is a high " \
|
37
|
+
"priority, but we're not there yet."
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class NodeError < ClientError
|
19
42
|
attr_reader :node
|
20
43
|
|
21
|
-
def initialize(node)
|
44
|
+
def initialize(node, response)
|
22
45
|
@node = node
|
46
|
+
super(response)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class ObsoleteNode < NodeError
|
51
|
+
end
|
52
|
+
|
53
|
+
class ClickFailed < NodeError
|
54
|
+
def selector
|
55
|
+
response['args'][0]
|
56
|
+
end
|
57
|
+
|
58
|
+
def position
|
59
|
+
[response['args'][1]['x'], response['args'][1]['y']]
|
60
|
+
end
|
61
|
+
|
62
|
+
def message
|
63
|
+
"Click at co-ordinates [#{position.join(', ')}] failed. Poltergeist detected " \
|
64
|
+
"another element with CSS selector '#{selector}' at this position. " \
|
65
|
+
"It may be overlapping the element you are trying to click."
|
23
66
|
end
|
24
67
|
end
|
25
68
|
|
@@ -54,5 +97,19 @@ module Capybara
|
|
54
97
|
"PhantomJS version #{version} is too old. You must use at least version #{Client::PHANTOMJS_VERSION}"
|
55
98
|
end
|
56
99
|
end
|
100
|
+
|
101
|
+
class PhantomJSFailed < Error
|
102
|
+
attr_reader :status
|
103
|
+
|
104
|
+
def initialize(status)
|
105
|
+
@status = status
|
106
|
+
end
|
107
|
+
|
108
|
+
def message
|
109
|
+
"PhantomJS returned non-zero exit status #{status.exitstatus}. Ensure there is an X display available and " \
|
110
|
+
"that DISPLAY is set. (See the Poltergeist README for details.) Make sure 'phantomjs --version' " \
|
111
|
+
"runs successfully on your system."
|
112
|
+
end
|
113
|
+
end
|
57
114
|
end
|
58
115
|
end
|