poltergeist-cj 1.5.2

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.
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