poltergeist 0.6.0 → 0.7.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 +77 -40
- data/lib/capybara/poltergeist.rb +11 -11
- data/lib/capybara/poltergeist/browser.rb +35 -12
- data/lib/capybara/poltergeist/client.rb +9 -12
- data/lib/capybara/poltergeist/client/agent.coffee +99 -6
- data/lib/capybara/poltergeist/client/browser.coffee +57 -79
- data/lib/capybara/poltergeist/client/compiled/agent.js +179 -19
- data/lib/capybara/poltergeist/client/compiled/browser.js +109 -81
- data/lib/capybara/poltergeist/client/compiled/connection.js +8 -1
- data/lib/capybara/poltergeist/client/compiled/main.js +75 -34
- data/lib/capybara/poltergeist/client/compiled/node.js +32 -39
- data/lib/capybara/poltergeist/client/compiled/web_page.js +118 -41
- data/lib/capybara/poltergeist/client/main.coffee +11 -23
- data/lib/capybara/poltergeist/client/node.coffee +16 -33
- data/lib/capybara/poltergeist/client/web_page.coffee +57 -26
- data/lib/capybara/poltergeist/driver.rb +36 -6
- data/lib/capybara/poltergeist/errors.rb +4 -15
- data/lib/capybara/poltergeist/json.rb +25 -0
- data/lib/capybara/poltergeist/network_traffic.rb +6 -0
- data/lib/capybara/poltergeist/network_traffic/request.rb +26 -0
- data/lib/capybara/poltergeist/network_traffic/response.rb +40 -0
- data/lib/capybara/poltergeist/node.rb +10 -6
- data/lib/capybara/poltergeist/version.rb +1 -1
- data/lib/capybara/poltergeist/web_socket_server.rb +3 -0
- metadata +112 -23
@@ -3,7 +3,8 @@
|
|
3
3
|
class Poltergeist.Node
|
4
4
|
@DELEGATES = ['text', 'getAttribute', 'value', 'set', 'setAttribute', 'isObsolete',
|
5
5
|
'removeAttribute', 'isMultiple', 'select', 'tagName', 'find',
|
6
|
-
'isVisible', 'position', 'trigger', 'parentId', 'clickTest'
|
6
|
+
'isVisible', 'position', 'trigger', 'parentId', 'clickTest',
|
7
|
+
'scrollIntoView', 'isDOMEqual']
|
7
8
|
|
8
9
|
constructor: (@page, @id) ->
|
9
10
|
|
@@ -12,37 +13,12 @@ class Poltergeist.Node
|
|
12
13
|
|
13
14
|
for name in @DELEGATES
|
14
15
|
do (name) =>
|
15
|
-
this.prototype[name] = (
|
16
|
-
@page.nodeCall(@id, name,
|
16
|
+
this.prototype[name] = (args...) ->
|
17
|
+
@page.nodeCall(@id, name, args)
|
17
18
|
|
18
|
-
clickPosition:
|
19
|
-
|
20
|
-
|
21
|
-
viewport = dimensions.viewport
|
22
|
-
pos = this.position()
|
23
|
-
|
24
|
-
scroll = { left: dimensions.left, top: dimensions.top }
|
25
|
-
|
26
|
-
adjust = (coord, measurement) ->
|
27
|
-
if pos[coord] < 0
|
28
|
-
scroll[coord] = Math.max(
|
29
|
-
0,
|
30
|
-
scroll[coord] + pos[coord] - (viewport[measurement] / 2)
|
31
|
-
)
|
32
|
-
|
33
|
-
else if pos[coord] >= viewport[measurement]
|
34
|
-
scroll[coord] = Math.min(
|
35
|
-
document[measurement] - viewport[measurement],
|
36
|
-
scroll[coord] + pos[coord] - viewport[measurement] + (viewport[measurement] / 2)
|
37
|
-
)
|
38
|
-
|
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()
|
19
|
+
clickPosition: ->
|
20
|
+
viewport = @page.viewportSize()
|
21
|
+
pos = this.position()
|
46
22
|
|
47
23
|
middle = (start, end, size) ->
|
48
24
|
start + ((Math.min(end, size) - start) / 2)
|
@@ -53,18 +29,25 @@ class Poltergeist.Node
|
|
53
29
|
}
|
54
30
|
|
55
31
|
click: ->
|
32
|
+
this.scrollIntoView()
|
33
|
+
|
56
34
|
pos = this.clickPosition()
|
57
35
|
test = this.clickTest(pos.x, pos.y)
|
58
36
|
|
59
37
|
if test.status == 'success'
|
60
38
|
@page.sendEvent('click', pos.x, pos.y)
|
61
39
|
else
|
62
|
-
new Poltergeist.ClickFailed(test.selector, pos)
|
40
|
+
throw new Poltergeist.ClickFailed(test.selector, pos)
|
63
41
|
|
64
42
|
dragTo: (other) ->
|
43
|
+
this.scrollIntoView()
|
44
|
+
|
65
45
|
position = this.clickPosition()
|
66
|
-
otherPosition = other.clickPosition(
|
46
|
+
otherPosition = other.clickPosition()
|
67
47
|
|
68
48
|
@page.sendEvent('mousedown', position.x, position.y)
|
69
49
|
@page.sendEvent('mousemove', otherPosition.x, otherPosition.y)
|
70
50
|
@page.sendEvent('mouseup', otherPosition.x, otherPosition.y)
|
51
|
+
|
52
|
+
isEqual: (other) ->
|
53
|
+
@page == other.page && this.isDOMEqual(other.id)
|
@@ -1,18 +1,19 @@
|
|
1
1
|
class Poltergeist.WebPage
|
2
2
|
@CALLBACKS = ['onAlert', 'onConsoleMessage', 'onLoadFinished', 'onInitialized',
|
3
3
|
'onLoadStarted', 'onResourceRequested', 'onResourceReceived',
|
4
|
-
'onError']
|
4
|
+
'onError', 'onNavigationRequested', 'onUrlChanged']
|
5
5
|
|
6
6
|
@DELEGATES = ['open', 'sendEvent', 'uploadFile', 'release', 'render']
|
7
7
|
|
8
8
|
@COMMANDS = ['currentUrl', 'find', 'nodeCall', 'pushFrame', 'popFrame', 'documentSize']
|
9
9
|
|
10
|
-
constructor: ->
|
11
|
-
@native
|
12
|
-
@_source
|
13
|
-
@_errors
|
10
|
+
constructor: (width, height) ->
|
11
|
+
@native = require('webpage').create()
|
12
|
+
@_source = ""
|
13
|
+
@_errors = []
|
14
|
+
@_networkTraffic = {}
|
14
15
|
|
15
|
-
this.setViewportSize(width:
|
16
|
+
this.setViewportSize(width: width, height: height)
|
16
17
|
|
17
18
|
for callback in WebPage.CALLBACKS
|
18
19
|
this.bindCallback(callback)
|
@@ -22,7 +23,7 @@ class Poltergeist.WebPage
|
|
22
23
|
for command in @COMMANDS
|
23
24
|
do (command) =>
|
24
25
|
this.prototype[command] =
|
25
|
-
(
|
26
|
+
(args...) -> this.runCommand(command, args)
|
26
27
|
|
27
28
|
for delegate in @DELEGATES
|
28
29
|
do (delegate) =>
|
@@ -35,7 +36,7 @@ class Poltergeist.WebPage
|
|
35
36
|
this.setScrollPosition(left: 0, top: 0)
|
36
37
|
|
37
38
|
injectAgent: ->
|
38
|
-
if
|
39
|
+
if @native.evaluate(-> typeof __poltergeist) == "undefined"
|
39
40
|
@native.injectJs("#{phantom.libraryPath}/agent.js")
|
40
41
|
@nodes = {}
|
41
42
|
|
@@ -44,19 +45,44 @@ class Poltergeist.WebPage
|
|
44
45
|
@_source = @native.content
|
45
46
|
false
|
46
47
|
|
48
|
+
onLoadStartedNative: ->
|
49
|
+
@requestId = @lastRequestId
|
50
|
+
|
47
51
|
onLoadFinishedNative: ->
|
48
52
|
@_source or= @native.content
|
49
53
|
|
50
|
-
onConsoleMessage: (message
|
51
|
-
|
52
|
-
# get wrongly reported to be onError and onConsoleMessage:
|
53
|
-
#
|
54
|
-
# http://code.google.com/p/phantomjs/issues/detail?id=166#c18
|
55
|
-
unless @_errors.length && @_errors[@_errors.length - 1].message == message
|
56
|
-
console.log(message)
|
54
|
+
onConsoleMessage: (message) ->
|
55
|
+
console.log(message)
|
57
56
|
|
58
57
|
onErrorNative: (message, stack) ->
|
59
|
-
|
58
|
+
stackString = message
|
59
|
+
|
60
|
+
stack.forEach (frame) ->
|
61
|
+
stackString += "\n"
|
62
|
+
stackString += " at #{frame.file}:#{frame.line}"
|
63
|
+
stackString += " in #{frame.function}" if frame.function && frame.function != ''
|
64
|
+
|
65
|
+
@_errors.push(message: message, stack: stackString)
|
66
|
+
|
67
|
+
onResourceRequestedNative: (request) ->
|
68
|
+
@lastRequestId = request.id
|
69
|
+
|
70
|
+
@_networkTraffic[request.id] = {
|
71
|
+
request: request,
|
72
|
+
responseParts: []
|
73
|
+
}
|
74
|
+
|
75
|
+
onResourceReceivedNative: (response) ->
|
76
|
+
@_networkTraffic[response.id].responseParts.push(response)
|
77
|
+
|
78
|
+
if @requestId == response.id
|
79
|
+
if response.redirectURL
|
80
|
+
@requestId = response.id
|
81
|
+
else
|
82
|
+
@_statusCode = response.status
|
83
|
+
|
84
|
+
networkTraffic: ->
|
85
|
+
@_networkTraffic
|
60
86
|
|
61
87
|
content: ->
|
62
88
|
@native.content
|
@@ -70,6 +96,9 @@ class Poltergeist.WebPage
|
|
70
96
|
clearErrors: ->
|
71
97
|
@_errors = []
|
72
98
|
|
99
|
+
statusCode: ->
|
100
|
+
@_statusCode
|
101
|
+
|
73
102
|
viewportSize: ->
|
74
103
|
@native.viewportSize
|
75
104
|
|
@@ -102,9 +131,6 @@ class Poltergeist.WebPage
|
|
102
131
|
dimensions = this.dimensions()
|
103
132
|
document = dimensions.document
|
104
133
|
|
105
|
-
orig_left = dimensions.left
|
106
|
-
orig_top = dimensions.top
|
107
|
-
|
108
134
|
if dimensions.right > document.width
|
109
135
|
dimensions.left = Math.max(0, dimensions.left - (dimensions.right - document.width))
|
110
136
|
dimensions.right = document.width
|
@@ -113,8 +139,7 @@ class Poltergeist.WebPage
|
|
113
139
|
dimensions.top = Math.max(0, dimensions.top - (dimensions.bottom - document.height))
|
114
140
|
dimensions.bottom = document.height
|
115
141
|
|
116
|
-
|
117
|
-
this.setScrollPosition(left: dimensions.left, top: dimensions.top)
|
142
|
+
this.setScrollPosition(left: dimensions.left, top: dimensions.top)
|
118
143
|
|
119
144
|
dimensions
|
120
145
|
|
@@ -122,7 +147,7 @@ class Poltergeist.WebPage
|
|
122
147
|
@nodes[id] or= new Poltergeist.Node(this, id)
|
123
148
|
|
124
149
|
evaluate: (fn, args...) ->
|
125
|
-
JSON.parse @native.evaluate("function() { return
|
150
|
+
JSON.parse @native.evaluate("function() { return PoltergeistAgent.stringify(#{this.stringifyCall(fn, args)}) }")
|
126
151
|
|
127
152
|
execute: (fn, args...) ->
|
128
153
|
@native.evaluate("function() { #{this.stringifyCall(fn, args)} }")
|
@@ -149,10 +174,16 @@ class Poltergeist.WebPage
|
|
149
174
|
# Any error raised here or inside the evaluate will get reported to
|
150
175
|
# phantom.onError. If result is null, that means there was an error
|
151
176
|
# inside the agent.
|
152
|
-
runCommand: (name,
|
177
|
+
runCommand: (name, args) ->
|
153
178
|
result = this.evaluate(
|
154
|
-
(name,
|
155
|
-
name,
|
179
|
+
(name, args) -> __poltergeist.externalCall(name, args),
|
180
|
+
name, args
|
156
181
|
)
|
157
182
|
|
158
|
-
|
183
|
+
if result.error?
|
184
|
+
if result.error.message == 'PoltergeistAgent.ObsoleteNode'
|
185
|
+
throw new Poltergeist.ObsoleteNode
|
186
|
+
else
|
187
|
+
throw new Poltergeist.JavascriptError([result.error])
|
188
|
+
else
|
189
|
+
result.value
|
@@ -3,6 +3,7 @@ module Capybara::Poltergeist
|
|
3
3
|
DEFAULT_TIMEOUT = 30
|
4
4
|
|
5
5
|
attr_reader :app, :app_server, :server, :client, :browser, :options
|
6
|
+
attr_accessor :headers
|
6
7
|
|
7
8
|
def initialize(app, options = {})
|
8
9
|
@app = app
|
@@ -11,13 +12,14 @@ module Capybara::Poltergeist
|
|
11
12
|
@inspector = nil
|
12
13
|
@server = nil
|
13
14
|
@client = nil
|
15
|
+
@headers = {}
|
14
16
|
|
15
17
|
@app_server = Capybara::Server.new(app)
|
16
18
|
@app_server.boot if Capybara.run_server
|
17
19
|
end
|
18
20
|
|
19
21
|
def browser
|
20
|
-
@browser ||= Browser.new(server, client, logger)
|
22
|
+
@browser ||= Browser.new(server, client, logger, js_errors)
|
21
23
|
end
|
22
24
|
|
23
25
|
def inspector
|
@@ -29,7 +31,17 @@ module Capybara::Poltergeist
|
|
29
31
|
end
|
30
32
|
|
31
33
|
def client
|
32
|
-
@client ||= Client.start(server.port,
|
34
|
+
@client ||= Client.start(server.port,
|
35
|
+
:path => options[:phantomjs],
|
36
|
+
:window_size => options[:window_size],
|
37
|
+
:phantomjs_options => phantomjs_options
|
38
|
+
)
|
39
|
+
end
|
40
|
+
|
41
|
+
def phantomjs_options
|
42
|
+
list = options[:phantomjs_options] || []
|
43
|
+
list += ["--remote-debugger-port=#{inspector.port}", "--remote-debugger-autorun=yes"] if inspector
|
44
|
+
list
|
33
45
|
end
|
34
46
|
|
35
47
|
def client_pid
|
@@ -58,14 +70,22 @@ module Capybara::Poltergeist
|
|
58
70
|
options[:logger] || (options[:debug] && STDERR)
|
59
71
|
end
|
60
72
|
|
73
|
+
def js_errors
|
74
|
+
options.fetch(:js_errors, true)
|
75
|
+
end
|
76
|
+
|
61
77
|
def visit(path)
|
62
|
-
browser.visit app_server.url(path)
|
78
|
+
browser.visit app_server.url(path), @headers
|
63
79
|
end
|
64
80
|
|
65
81
|
def current_url
|
66
82
|
browser.current_url
|
67
83
|
end
|
68
84
|
|
85
|
+
def status_code
|
86
|
+
browser.status_code
|
87
|
+
end
|
88
|
+
|
69
89
|
def body
|
70
90
|
browser.body
|
71
91
|
end
|
@@ -93,6 +113,7 @@ module Capybara::Poltergeist
|
|
93
113
|
|
94
114
|
def reset!
|
95
115
|
browser.reset
|
116
|
+
@headers = {}
|
96
117
|
end
|
97
118
|
|
98
119
|
def render(path, options = {})
|
@@ -103,9 +124,18 @@ module Capybara::Poltergeist
|
|
103
124
|
browser.resize(width, height)
|
104
125
|
end
|
105
126
|
|
127
|
+
def network_traffic
|
128
|
+
browser.network_traffic
|
129
|
+
end
|
130
|
+
|
106
131
|
def debug
|
107
|
-
inspector
|
108
|
-
|
132
|
+
if @options[:inspector]
|
133
|
+
inspector.open
|
134
|
+
pause
|
135
|
+
else
|
136
|
+
raise Error, "To use the remote debugging, you have to launch the driver " \
|
137
|
+
"with `:inspector => true` configuration option"
|
138
|
+
end
|
109
139
|
end
|
110
140
|
|
111
141
|
def pause
|
@@ -118,7 +148,7 @@ module Capybara::Poltergeist
|
|
118
148
|
end
|
119
149
|
|
120
150
|
def invalid_element_errors
|
121
|
-
[Capybara::Poltergeist::ObsoleteNode]
|
151
|
+
[Capybara::Poltergeist::ObsoleteNode, Capybara::Poltergeist::ClickFailed]
|
122
152
|
end
|
123
153
|
end
|
124
154
|
end
|
@@ -20,18 +20,7 @@ module Capybara
|
|
20
20
|
end
|
21
21
|
|
22
22
|
def to_s
|
23
|
-
|
24
|
-
end
|
25
|
-
|
26
|
-
private
|
27
|
-
|
28
|
-
def formatted_stack
|
29
|
-
stack = self.stack.map do |item|
|
30
|
-
s = " #{item['file']}:#{item['line']}"
|
31
|
-
s << " in #{item['function']}" if item['function'] && !item['function'].empty?
|
32
|
-
s
|
33
|
-
end
|
34
|
-
stack.join("\n")
|
23
|
+
stack
|
35
24
|
end
|
36
25
|
end
|
37
26
|
|
@@ -128,9 +117,9 @@ module Capybara
|
|
128
117
|
end
|
129
118
|
|
130
119
|
def message
|
131
|
-
"PhantomJS returned non-zero exit status #{status.exitstatus}.
|
132
|
-
"
|
133
|
-
"
|
120
|
+
"PhantomJS returned non-zero exit status #{status.exitstatus}. Make sure phantomjs runs successfully " \
|
121
|
+
"on your system. You can test this by just running the `phantomjs` command which should give you " \
|
122
|
+
"a Javascript REPL."
|
134
123
|
end
|
135
124
|
end
|
136
125
|
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Capybara::Poltergeist
|
2
|
+
module JSON
|
3
|
+
def self.load(message)
|
4
|
+
if dumpy_multi_json?
|
5
|
+
MultiJson.load(message)
|
6
|
+
else
|
7
|
+
MultiJson.decode(message)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.dump(message)
|
12
|
+
if dumpy_multi_json?
|
13
|
+
MultiJson.dump(message)
|
14
|
+
else
|
15
|
+
MultiJson.encode(message)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def self.dumpy_multi_json?
|
22
|
+
MultiJson.respond_to?(:dump) && MultiJson.respond_to?(:load)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Capybara::Poltergeist::NetworkTraffic
|
2
|
+
class Request
|
3
|
+
attr_reader :response_parts
|
4
|
+
|
5
|
+
def initialize(data, response_parts = [])
|
6
|
+
@data = data
|
7
|
+
@response_parts = response_parts
|
8
|
+
end
|
9
|
+
|
10
|
+
def url
|
11
|
+
@data['url']
|
12
|
+
end
|
13
|
+
|
14
|
+
def method
|
15
|
+
@data['method']
|
16
|
+
end
|
17
|
+
|
18
|
+
def headers
|
19
|
+
@data['headers']
|
20
|
+
end
|
21
|
+
|
22
|
+
def time
|
23
|
+
@data['time'] && Time.parse(@data['time'])
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Capybara::Poltergeist::NetworkTraffic
|
2
|
+
class Response
|
3
|
+
def initialize(data)
|
4
|
+
@data = data
|
5
|
+
end
|
6
|
+
|
7
|
+
def url
|
8
|
+
@data['url']
|
9
|
+
end
|
10
|
+
|
11
|
+
def status
|
12
|
+
@data['status']
|
13
|
+
end
|
14
|
+
|
15
|
+
def status_text
|
16
|
+
@data['statusText']
|
17
|
+
end
|
18
|
+
|
19
|
+
def headers
|
20
|
+
@data['headers']
|
21
|
+
end
|
22
|
+
|
23
|
+
def redirect_url
|
24
|
+
@data['redirectUrl']
|
25
|
+
end
|
26
|
+
|
27
|
+
def body_size
|
28
|
+
@data['bodySize']
|
29
|
+
end
|
30
|
+
|
31
|
+
def content_type
|
32
|
+
@data['contentType']
|
33
|
+
end
|
34
|
+
|
35
|
+
def time
|
36
|
+
@data['time'] && Time.parse(@data['time'])
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|