poltergeist-cj 1.5.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (31) hide show
  1. data/LICENSE +20 -0
  2. data/README.md +434 -0
  3. data/lib/capybara/poltergeist.rb +27 -0
  4. data/lib/capybara/poltergeist/browser.rb +339 -0
  5. data/lib/capybara/poltergeist/client.rb +151 -0
  6. data/lib/capybara/poltergeist/client/agent.coffee +374 -0
  7. data/lib/capybara/poltergeist/client/browser.coffee +393 -0
  8. data/lib/capybara/poltergeist/client/compiled/agent.js +535 -0
  9. data/lib/capybara/poltergeist/client/compiled/browser.js +526 -0
  10. data/lib/capybara/poltergeist/client/compiled/connection.js +25 -0
  11. data/lib/capybara/poltergeist/client/compiled/main.js +204 -0
  12. data/lib/capybara/poltergeist/client/compiled/node.js +77 -0
  13. data/lib/capybara/poltergeist/client/compiled/web_page.js +421 -0
  14. data/lib/capybara/poltergeist/client/connection.coffee +11 -0
  15. data/lib/capybara/poltergeist/client/main.coffee +89 -0
  16. data/lib/capybara/poltergeist/client/node.coffee +57 -0
  17. data/lib/capybara/poltergeist/client/web_page.coffee +297 -0
  18. data/lib/capybara/poltergeist/cookie.rb +35 -0
  19. data/lib/capybara/poltergeist/driver.rb +278 -0
  20. data/lib/capybara/poltergeist/errors.rb +178 -0
  21. data/lib/capybara/poltergeist/inspector.rb +46 -0
  22. data/lib/capybara/poltergeist/json.rb +25 -0
  23. data/lib/capybara/poltergeist/network_traffic.rb +6 -0
  24. data/lib/capybara/poltergeist/network_traffic/request.rb +26 -0
  25. data/lib/capybara/poltergeist/network_traffic/response.rb +40 -0
  26. data/lib/capybara/poltergeist/node.rb +154 -0
  27. data/lib/capybara/poltergeist/server.rb +36 -0
  28. data/lib/capybara/poltergeist/utility.rb +9 -0
  29. data/lib/capybara/poltergeist/version.rb +5 -0
  30. data/lib/capybara/poltergeist/web_socket_server.rb +96 -0
  31. metadata +285 -0
@@ -0,0 +1,11 @@
1
+ class Poltergeist.Connection
2
+ constructor: (@owner, @port) ->
3
+ @socket = new WebSocket "ws://127.0.0.1:#{@port}/"
4
+ @socket.onmessage = this.commandReceived
5
+ @socket.onclose = -> phantom.exit()
6
+
7
+ commandReceived: (message) =>
8
+ @owner.runCommand(JSON.parse(message.data))
9
+
10
+ send: (message) ->
11
+ @socket.send(JSON.stringify(message))
@@ -0,0 +1,89 @@
1
+ class Poltergeist
2
+ constructor: (port, width, height) ->
3
+ @browser = new Poltergeist.Browser(this, width, height)
4
+ @connection = new Poltergeist.Connection(this, port)
5
+
6
+ # The QtWebKit bridge doesn't seem to like Function.prototype.bind
7
+ that = this
8
+ phantom.onError = (message, stack) -> that.onError(message, stack)
9
+
10
+ @running = false
11
+
12
+ runCommand: (command) ->
13
+ @running = true
14
+
15
+ try
16
+ @browser.runCommand(command.name, command.args)
17
+ catch error
18
+ if error instanceof Poltergeist.Error
19
+ this.sendError(error)
20
+ else
21
+ this.sendError(new Poltergeist.BrowserError(error.toString(), error.stack))
22
+
23
+ sendResponse: (response) ->
24
+ this.send(response: response)
25
+
26
+ sendError: (error) ->
27
+ this.send(
28
+ error:
29
+ name: error.name || 'Generic',
30
+ args: error.args && error.args() || [error.toString()]
31
+ )
32
+
33
+ send: (data) ->
34
+ # Prevents more than one response being sent for a single
35
+ # command. This can happen in some scenarios where an error
36
+ # is raised but the script can still continue.
37
+ if @running
38
+ @connection.send(data)
39
+ @running = false
40
+
41
+ # This is necessary because the remote debugger will wrap the
42
+ # script in a function, causing the Poltergeist variable to
43
+ # become local.
44
+ window.Poltergeist = Poltergeist
45
+
46
+ class Poltergeist.Error
47
+
48
+ class Poltergeist.ObsoleteNode extends Poltergeist.Error
49
+ name: "Poltergeist.ObsoleteNode"
50
+ args: -> []
51
+ toString: -> this.name
52
+
53
+ class Poltergeist.InvalidSelector extends Poltergeist.Error
54
+ constructor: (@method, @selector) ->
55
+ name: "Poltergeist.InvalidSelector"
56
+ args: -> [@method, @selector]
57
+
58
+ class Poltergeist.FrameNotFound extends Poltergeist.Error
59
+ constructor: (@frameName) ->
60
+ name: "Poltergeist.FrameNotFound"
61
+ args: -> [@frameName]
62
+
63
+ class Poltergeist.MouseEventFailed extends Poltergeist.Error
64
+ constructor: (@eventName, @selector, @position) ->
65
+ name: "Poltergeist.MouseEventFailed"
66
+ args: -> [@eventName, @selector, @position]
67
+
68
+ class Poltergeist.JavascriptError extends Poltergeist.Error
69
+ constructor: (@errors) ->
70
+ name: "Poltergeist.JavascriptError"
71
+ args: -> [@errors]
72
+
73
+ class Poltergeist.BrowserError extends Poltergeist.Error
74
+ constructor: (@message, @stack) ->
75
+ name: "Poltergeist.BrowserError"
76
+ args: -> [@message, @stack]
77
+
78
+ class Poltergeist.StatusFailError extends Poltergeist.Error
79
+ name: "Poltergeist.StatusFailError"
80
+ args: -> []
81
+
82
+ # We're using phantom.libraryPath so that any stack traces
83
+ # report the full path.
84
+ phantom.injectJs("#{phantom.libraryPath}/web_page.js")
85
+ phantom.injectJs("#{phantom.libraryPath}/node.js")
86
+ phantom.injectJs("#{phantom.libraryPath}/connection.js")
87
+ phantom.injectJs("#{phantom.libraryPath}/browser.js")
88
+
89
+ new Poltergeist(phantom.args[0], phantom.args[1], phantom.args[2])
@@ -0,0 +1,57 @@
1
+ # Proxy object for forwarding method calls to the node object inside the page.
2
+
3
+ class Poltergeist.Node
4
+ @DELEGATES = ['allText', 'visibleText', 'getAttribute', 'value', 'set', 'setAttribute', 'isObsolete',
5
+ 'removeAttribute', 'isMultiple', 'select', 'tagName', 'find', 'getAttributes',
6
+ 'isVisible', 'position', 'trigger', 'parentId', 'parentIds', 'mouseEventTest',
7
+ 'scrollIntoView', 'isDOMEqual', 'isDisabled', 'deleteText', 'containsSelection']
8
+
9
+ constructor: (@page, @id) ->
10
+
11
+ parent: ->
12
+ new Poltergeist.Node(@page, this.parentId())
13
+
14
+ for name in @DELEGATES
15
+ do (name) =>
16
+ this.prototype[name] = (args...) ->
17
+ @page.nodeCall(@id, name, args)
18
+
19
+ mouseEventPosition: ->
20
+ viewport = @page.viewportSize()
21
+ pos = this.position()
22
+
23
+ middle = (start, end, size) ->
24
+ start + ((Math.min(end, size) - start) / 2)
25
+
26
+ {
27
+ x: middle(pos.left, pos.right, viewport.width),
28
+ y: middle(pos.top, pos.bottom, viewport.height)
29
+ }
30
+
31
+ mouseEvent: (name) ->
32
+ this.scrollIntoView()
33
+
34
+ pos = this.mouseEventPosition()
35
+ test = this.mouseEventTest(pos.x, pos.y)
36
+
37
+ if test.status == 'success'
38
+ if name == 'rightclick'
39
+ @page.mouseEvent('click', pos.x, pos.y, 'right')
40
+ this.trigger('contextmenu')
41
+ else
42
+ @page.mouseEvent(name, pos.x, pos.y)
43
+ pos
44
+ else
45
+ throw new Poltergeist.MouseEventFailed(name, test.selector, pos)
46
+
47
+ dragTo: (other) ->
48
+ this.scrollIntoView()
49
+
50
+ position = this.mouseEventPosition()
51
+ otherPosition = other.mouseEventPosition()
52
+
53
+ @page.mouseEvent('mousedown', position.x, position.y)
54
+ @page.mouseEvent('mouseup', otherPosition.x, otherPosition.y)
55
+
56
+ isEqual: (other) ->
57
+ @page == other.page && this.isDOMEqual(other.id)
@@ -0,0 +1,297 @@
1
+ class Poltergeist.WebPage
2
+ @CALLBACKS = ['onAlert', 'onConsoleMessage', 'onLoadFinished', 'onInitialized',
3
+ 'onLoadStarted', 'onResourceRequested', 'onResourceReceived',
4
+ 'onError', 'onNavigationRequested', 'onUrlChanged', 'onPageCreated']
5
+
6
+ @DELEGATES = ['open', 'sendEvent', 'uploadFile', 'release', 'render', 'renderBase64', 'goBack', 'goForward']
7
+
8
+ @COMMANDS = ['currentUrl', 'find', 'nodeCall', 'documentSize', 'beforeUpload', 'afterUpload']
9
+
10
+ @EXTENSIONS = []
11
+
12
+ constructor: (@native) ->
13
+ @native or= require('webpage').create()
14
+
15
+ @_source = null
16
+ @_errors = []
17
+ @_networkTraffic = {}
18
+ @_temp_headers = {}
19
+ @frames = []
20
+
21
+ for callback in WebPage.CALLBACKS
22
+ this.bindCallback(callback)
23
+
24
+ for command in @COMMANDS
25
+ do (command) =>
26
+ this.prototype[command] =
27
+ (args...) -> this.runCommand(command, args)
28
+
29
+ for delegate in @DELEGATES
30
+ do (delegate) =>
31
+ this.prototype[delegate] =
32
+ -> @native[delegate].apply(@native, arguments)
33
+
34
+ onInitializedNative: ->
35
+ @_source = null
36
+ @injectAgent()
37
+ this.removeTempHeaders()
38
+ this.setScrollPosition(left: 0, top: 0)
39
+
40
+ injectAgent: ->
41
+ if @native.evaluate(-> typeof __poltergeist) == "undefined"
42
+ @native.injectJs "#{phantom.libraryPath}/agent.js"
43
+ for extension in WebPage.EXTENSIONS
44
+ @native.injectJs extension
45
+
46
+ injectExtension: (file) ->
47
+ WebPage.EXTENSIONS.push file
48
+ @native.injectJs file
49
+
50
+ onConsoleMessageNative: (message) ->
51
+ if message == '__DOMContentLoaded'
52
+ @_source = @native.content
53
+ false
54
+
55
+ onLoadStartedNative: ->
56
+ @requestId = @lastRequestId
57
+
58
+ onLoadFinishedNative: ->
59
+ @_source or= @native.content
60
+
61
+ onConsoleMessage: (message) ->
62
+ console.log(message)
63
+
64
+ onErrorNative: (message, stack) ->
65
+ stackString = message
66
+
67
+ stack.forEach (frame) ->
68
+ stackString += "\n"
69
+ stackString += " at #{frame.file}:#{frame.line}"
70
+ stackString += " in #{frame.function}" if frame.function && frame.function != ''
71
+
72
+ @_errors.push(message: message, stack: stackString)
73
+
74
+ onResourceRequestedNative: (request) ->
75
+ @lastRequestId = request.id
76
+
77
+ if request.url == @redirectURL
78
+ @redirectURL = null
79
+ @requestId = request.id
80
+
81
+ @_networkTraffic[request.id] = {
82
+ request: request,
83
+ responseParts: []
84
+ }
85
+
86
+ onResourceReceivedNative: (response) ->
87
+ @_networkTraffic[response.id]?.responseParts.push(response)
88
+
89
+ if @requestId == response.id
90
+ if response.redirectURL
91
+ @redirectURL = response.redirectURL
92
+ else
93
+ @_statusCode = response.status
94
+ @_responseHeaders = response.headers
95
+
96
+ setHttpAuth: (user, password) ->
97
+ @native.settings.userName = user
98
+ @native.settings.password = password
99
+
100
+ networkTraffic: ->
101
+ @_networkTraffic
102
+
103
+ clearNetworkTraffic: ->
104
+ @_networkTraffic = {}
105
+
106
+ content: ->
107
+ @native.frameContent
108
+
109
+ source: ->
110
+ @_source
111
+
112
+ title: ->
113
+ @native.frameTitle
114
+
115
+ errors: ->
116
+ @_errors
117
+
118
+ clearErrors: ->
119
+ @_errors = []
120
+
121
+ statusCode: ->
122
+ @_statusCode
123
+
124
+ responseHeaders: ->
125
+ headers = {}
126
+ @_responseHeaders.forEach (item) ->
127
+ headers[item.name] = item.value
128
+ headers
129
+
130
+ cookies: ->
131
+ @native.cookies
132
+
133
+ deleteCookie: (name) ->
134
+ @native.deleteCookie(name)
135
+
136
+ viewportSize: ->
137
+ @native.viewportSize
138
+
139
+ setViewportSize: (size) ->
140
+ @native.viewportSize = size
141
+
142
+ setZoomFactor: (zoom_factor) ->
143
+ @native.zoomFactor = zoom_factor
144
+
145
+ setPaperSize: (size) ->
146
+ @native.paperSize = size
147
+
148
+ scrollPosition: ->
149
+ @native.scrollPosition
150
+
151
+ setScrollPosition: (pos) ->
152
+ @native.scrollPosition = pos
153
+
154
+ clipRect: ->
155
+ @native.clipRect
156
+
157
+ setClipRect: (rect) ->
158
+ @native.clipRect = rect
159
+
160
+ elementBounds: (selector) ->
161
+ @native.evaluate(
162
+ (selector) -> document.querySelector(selector).getBoundingClientRect(),
163
+ selector
164
+ )
165
+
166
+ setUserAgent: (userAgent) ->
167
+ @native.settings.userAgent = userAgent
168
+
169
+ getCustomHeaders: ->
170
+ @native.customHeaders
171
+
172
+ setCustomHeaders: (headers) ->
173
+ @native.customHeaders = headers
174
+
175
+ addTempHeader: (header) ->
176
+ for name, value of header
177
+ @_temp_headers[name] = value
178
+
179
+ removeTempHeaders: ->
180
+ allHeaders = this.getCustomHeaders()
181
+ for name, value of @_temp_headers
182
+ delete allHeaders[name]
183
+ this.setCustomHeaders(allHeaders)
184
+
185
+ pushFrame: (name) ->
186
+ if @native.switchToFrame(name)
187
+ @frames.push(name)
188
+ true
189
+ else
190
+ false
191
+
192
+ pages: ->
193
+ @native.pagesWindowName
194
+
195
+ popFrame: ->
196
+ @frames.pop()
197
+ @native.switchToParentFrame()
198
+
199
+ getPage: (name) ->
200
+ page = @native.getPage(name)
201
+ new Poltergeist.WebPage(page) if page
202
+
203
+ dimensions: ->
204
+ scroll = this.scrollPosition()
205
+ viewport = this.viewportSize()
206
+
207
+ top: scroll.top, bottom: scroll.top + viewport.height,
208
+ left: scroll.left, right: scroll.left + viewport.width,
209
+ viewport: viewport
210
+ document: this.documentSize()
211
+
212
+ # A work around for http://code.google.com/p/phantomjs/issues/detail?id=277
213
+ validatedDimensions: ->
214
+ dimensions = this.dimensions()
215
+ document = dimensions.document
216
+
217
+ if dimensions.right > document.width
218
+ dimensions.left = Math.max(0, dimensions.left - (dimensions.right - document.width))
219
+ dimensions.right = document.width
220
+
221
+ if dimensions.bottom > document.height
222
+ dimensions.top = Math.max(0, dimensions.top - (dimensions.bottom - document.height))
223
+ dimensions.bottom = document.height
224
+
225
+ this.setScrollPosition(left: dimensions.left, top: dimensions.top)
226
+
227
+ dimensions
228
+
229
+ get: (id) ->
230
+ new Poltergeist.Node(this, id)
231
+
232
+ # Before each mouse event we make sure that the mouse is moved to where the
233
+ # event will take place. This deals with e.g. :hover changes.
234
+ mouseEvent: (name, x, y, button = 'left') ->
235
+ this.sendEvent('mousemove', x, y)
236
+ this.sendEvent(name, x, y, button)
237
+
238
+ evaluate: (fn, args...) ->
239
+ this.injectAgent()
240
+ JSON.parse this.sanitize(@native.evaluate("function() { return PoltergeistAgent.stringify(#{this.stringifyCall(fn, args)}) }"))
241
+
242
+ sanitize: (potential_string) ->
243
+ if typeof(potential_string) == "string"
244
+ # JSON doesn't like \r or \n in strings unless escaped
245
+ potential_string.replace("\n","\\n").replace("\r","\\r")
246
+ else
247
+ potential_string
248
+
249
+ execute: (fn, args...) ->
250
+ @native.evaluate("function() { #{this.stringifyCall(fn, args)} }")
251
+
252
+ stringifyCall: (fn, args) ->
253
+ if args.length == 0
254
+ "(#{fn.toString()})()"
255
+ else
256
+ # The JSON.stringify happens twice because the second time we are essentially
257
+ # escaping the string.
258
+ "(#{fn.toString()}).apply(this, JSON.parse(#{JSON.stringify(JSON.stringify(args))}))"
259
+
260
+ # For some reason phantomjs seems to have trouble with doing 'fat arrow' binding here,
261
+ # hence the 'that' closure.
262
+ bindCallback: (name) ->
263
+ that = this
264
+ @native[name] = ->
265
+ if that[name + 'Native']? # For internal callbacks
266
+ result = that[name + 'Native'].apply(that, arguments)
267
+
268
+ if result != false && that[name]? # For externally set callbacks
269
+ that[name].apply(that, arguments)
270
+
271
+ # Any error raised here or inside the evaluate will get reported to
272
+ # phantom.onError. If result is null, that means there was an error
273
+ # inside the agent.
274
+ runCommand: (name, args) ->
275
+ result = this.evaluate(
276
+ (name, args) -> __poltergeist.externalCall(name, args),
277
+ name, args
278
+ )
279
+
280
+ if result != null
281
+ if result.error?
282
+ switch result.error.message
283
+ when 'PoltergeistAgent.ObsoleteNode'
284
+ throw new Poltergeist.ObsoleteNode
285
+ when 'PoltergeistAgent.InvalidSelector'
286
+ [method, selector] = args
287
+ throw new Poltergeist.InvalidSelector(method, selector)
288
+ else
289
+ throw new Poltergeist.BrowserError(result.error.message, result.error.stack)
290
+ else
291
+ result.value
292
+
293
+ canGoBack: ->
294
+ @native.canGoBack
295
+
296
+ canGoForward: ->
297
+ @native.canGoForward