poltergeistFork 0.0.1
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.
- checksums.yaml +7 -0
- data/LICENSE +22 -0
- data/README.md +425 -0
- data/lib/capybara/poltergeist/browser.rb +426 -0
- data/lib/capybara/poltergeist/client.rb +151 -0
- data/lib/capybara/poltergeist/client/agent.coffee +423 -0
- data/lib/capybara/poltergeist/client/browser.coffee +497 -0
- data/lib/capybara/poltergeist/client/cmd.coffee +17 -0
- data/lib/capybara/poltergeist/client/compiled/agent.js +587 -0
- data/lib/capybara/poltergeist/client/compiled/browser.js +687 -0
- data/lib/capybara/poltergeist/client/compiled/cmd.js +31 -0
- data/lib/capybara/poltergeist/client/compiled/connection.js +25 -0
- data/lib/capybara/poltergeist/client/compiled/main.js +228 -0
- data/lib/capybara/poltergeist/client/compiled/node.js +88 -0
- data/lib/capybara/poltergeist/client/compiled/web_page.js +539 -0
- data/lib/capybara/poltergeist/client/connection.coffee +11 -0
- data/lib/capybara/poltergeist/client/main.coffee +99 -0
- data/lib/capybara/poltergeist/client/node.coffee +70 -0
- data/lib/capybara/poltergeist/client/pre/agent.js +587 -0
- data/lib/capybara/poltergeist/client/pre/browser.js +688 -0
- data/lib/capybara/poltergeist/client/pre/cmd.js +31 -0
- data/lib/capybara/poltergeist/client/pre/connection.js +25 -0
- data/lib/capybara/poltergeist/client/pre/main.js +228 -0
- data/lib/capybara/poltergeist/client/pre/node.js +88 -0
- data/lib/capybara/poltergeist/client/pre/web_page.js +540 -0
- data/lib/capybara/poltergeist/client/web_page.coffee +372 -0
- data/lib/capybara/poltergeist/command.rb +17 -0
- data/lib/capybara/poltergeist/cookie.rb +35 -0
- data/lib/capybara/poltergeist/driver.rb +394 -0
- data/lib/capybara/poltergeist/errors.rb +183 -0
- data/lib/capybara/poltergeist/inspector.rb +46 -0
- data/lib/capybara/poltergeist/json.rb +25 -0
- data/lib/capybara/poltergeist/network_traffic.rb +7 -0
- data/lib/capybara/poltergeist/network_traffic/error.rb +19 -0
- data/lib/capybara/poltergeist/network_traffic/request.rb +27 -0
- data/lib/capybara/poltergeist/network_traffic/response.rb +40 -0
- data/lib/capybara/poltergeist/node.rb +177 -0
- data/lib/capybara/poltergeist/server.rb +36 -0
- data/lib/capybara/poltergeist/utility.rb +9 -0
- data/lib/capybara/poltergeist/version.rb +5 -0
- data/lib/capybara/poltergeist/web_socket_server.rb +107 -0
- data/lib/capybara/poltergeistFork.rb +27 -0
- metadata +268 -0
@@ -0,0 +1,372 @@
|
|
1
|
+
class Poltergeist.WebPage
|
2
|
+
@CALLBACKS = ['onConsoleMessage','onError',
|
3
|
+
'onLoadFinished', 'onInitialized', 'onLoadStarted',
|
4
|
+
'onResourceRequested', 'onResourceReceived', 'onResourceError',
|
5
|
+
'onNavigationRequested', 'onUrlChanged', 'onPageCreated',
|
6
|
+
'onClosing']
|
7
|
+
|
8
|
+
@DELEGATES = ['open', 'sendEvent', 'uploadFile', 'release', 'render',
|
9
|
+
'renderBase64', 'goBack', 'goForward']
|
10
|
+
|
11
|
+
@COMMANDS = ['currentUrl', 'find', 'nodeCall', 'documentSize',
|
12
|
+
'beforeUpload', 'afterUpload', 'clearLocalStorage']
|
13
|
+
|
14
|
+
@EXTENSIONS = []
|
15
|
+
|
16
|
+
constructor: (@_native) ->
|
17
|
+
@_native or= require('webpage').create()
|
18
|
+
|
19
|
+
@id = 0
|
20
|
+
@source = null
|
21
|
+
@closed = false
|
22
|
+
@state = 'default'
|
23
|
+
@urlBlacklist = []
|
24
|
+
@frames = []
|
25
|
+
@errors = []
|
26
|
+
@_networkTraffic = {}
|
27
|
+
@_tempHeaders = {}
|
28
|
+
@_blockedUrls = []
|
29
|
+
|
30
|
+
for callback in WebPage.CALLBACKS
|
31
|
+
this.bindCallback(callback)
|
32
|
+
|
33
|
+
for command in @COMMANDS
|
34
|
+
do (command) =>
|
35
|
+
this.prototype[command] =
|
36
|
+
(args...) -> this.runCommand(command, args)
|
37
|
+
|
38
|
+
for delegate in @DELEGATES
|
39
|
+
do (delegate) =>
|
40
|
+
this.prototype[delegate] =
|
41
|
+
-> @_native[delegate].apply(@_native, arguments)
|
42
|
+
|
43
|
+
onInitializedNative: ->
|
44
|
+
@id += 1
|
45
|
+
@source = null
|
46
|
+
@injectAgent()
|
47
|
+
this.removeTempHeaders()
|
48
|
+
this.setScrollPosition(left: 0, top: 0)
|
49
|
+
|
50
|
+
onClosingNative: ->
|
51
|
+
@handle = null
|
52
|
+
@closed = true
|
53
|
+
|
54
|
+
onConsoleMessageNative: (message) ->
|
55
|
+
if message == '__DOMContentLoaded'
|
56
|
+
@source = @_native.content
|
57
|
+
false
|
58
|
+
else
|
59
|
+
console.log(message)
|
60
|
+
|
61
|
+
onLoadStartedNative: ->
|
62
|
+
@state = 'loading'
|
63
|
+
@requestId = @lastRequestId
|
64
|
+
|
65
|
+
onLoadFinishedNative: (@status) ->
|
66
|
+
@state = 'default'
|
67
|
+
@source or= @_native.content
|
68
|
+
|
69
|
+
onErrorNative: (message, stack) ->
|
70
|
+
stackString = message
|
71
|
+
|
72
|
+
stack.forEach (frame) ->
|
73
|
+
stackString += "\n"
|
74
|
+
stackString += " at #{frame.file}:#{frame.line}"
|
75
|
+
stackString += " in #{frame.function}" if frame.function && frame.function != ''
|
76
|
+
|
77
|
+
@errors.push(message: message, stack: stackString)
|
78
|
+
return true
|
79
|
+
|
80
|
+
onResourceRequestedNative: (request, net) ->
|
81
|
+
abort = @urlBlacklist.some (blacklisted_url) ->
|
82
|
+
request.url.indexOf(blacklisted_url) != -1
|
83
|
+
|
84
|
+
if abort
|
85
|
+
@_blockedUrls.push request.url unless request.url in @_blockedUrls
|
86
|
+
net.abort()
|
87
|
+
else
|
88
|
+
@lastRequestId = request.id
|
89
|
+
|
90
|
+
if @normalizeURL(request.url) == @redirectURL
|
91
|
+
@redirectURL = null
|
92
|
+
@requestId = request.id
|
93
|
+
|
94
|
+
@_networkTraffic[request.id] = {
|
95
|
+
request: request,
|
96
|
+
responseParts: []
|
97
|
+
error: null
|
98
|
+
}
|
99
|
+
return true
|
100
|
+
|
101
|
+
onResourceReceivedNative: (response) ->
|
102
|
+
@_networkTraffic[response.id]?.responseParts.push(response)
|
103
|
+
|
104
|
+
if @requestId == response.id
|
105
|
+
if response.redirectURL
|
106
|
+
@redirectURL = @normalizeURL(response.redirectURL)
|
107
|
+
else
|
108
|
+
@statusCode = response.status
|
109
|
+
@_responseHeaders = response.headers
|
110
|
+
return true
|
111
|
+
|
112
|
+
onResourceErrorNative: (errorResponse) ->
|
113
|
+
@_networkTraffic[errorResponse.id]?.error = errorResponse
|
114
|
+
return true
|
115
|
+
|
116
|
+
injectAgent: ->
|
117
|
+
if this.native().evaluate(-> typeof __poltergeist) == "undefined"
|
118
|
+
this.native().injectJs "#{phantom.libraryPath}/agent.js"
|
119
|
+
for extension in WebPage.EXTENSIONS
|
120
|
+
this.native().injectJs extension
|
121
|
+
return true
|
122
|
+
return false
|
123
|
+
|
124
|
+
injectExtension: (file) ->
|
125
|
+
WebPage.EXTENSIONS.push file
|
126
|
+
this.native().injectJs file
|
127
|
+
|
128
|
+
native: ->
|
129
|
+
if @closed
|
130
|
+
throw new Poltergeist.NoSuchWindowError
|
131
|
+
else
|
132
|
+
@_native
|
133
|
+
|
134
|
+
windowName: ->
|
135
|
+
this.native().windowName
|
136
|
+
|
137
|
+
keyCode: (name) ->
|
138
|
+
this.native().event.key[name]
|
139
|
+
|
140
|
+
keyModifierCode: (names) ->
|
141
|
+
modifiers = this.native().event.modifier
|
142
|
+
names = names.split(',').map ((name) -> modifiers[name])
|
143
|
+
names[0] | names[1] # return codes for 1 or 2 modifiers
|
144
|
+
|
145
|
+
keyModifierKeys: (names) ->
|
146
|
+
names.split(',').map (name) =>
|
147
|
+
this.keyCode(name.charAt(0).toUpperCase() + name.substring(1))
|
148
|
+
|
149
|
+
waitState: (state, callback) ->
|
150
|
+
if @state == state
|
151
|
+
callback.call()
|
152
|
+
else
|
153
|
+
setTimeout (=> @waitState(state, callback)), 100
|
154
|
+
|
155
|
+
setHttpAuth: (user, password) ->
|
156
|
+
this.native().settings.userName = user
|
157
|
+
this.native().settings.password = password
|
158
|
+
return true
|
159
|
+
|
160
|
+
networkTraffic: ->
|
161
|
+
@_networkTraffic
|
162
|
+
|
163
|
+
clearNetworkTraffic: ->
|
164
|
+
@_networkTraffic = {}
|
165
|
+
return true
|
166
|
+
|
167
|
+
blockedUrls: ->
|
168
|
+
@_blockedUrls
|
169
|
+
|
170
|
+
clearBlockedUrls: ->
|
171
|
+
@_blockedUrls = []
|
172
|
+
return true
|
173
|
+
|
174
|
+
content: ->
|
175
|
+
this.native().frameContent
|
176
|
+
|
177
|
+
title: ->
|
178
|
+
this.native().frameTitle
|
179
|
+
|
180
|
+
frameUrl: (frameNameOrId) ->
|
181
|
+
query = (frameNameOrId) ->
|
182
|
+
document.querySelector("iframe[name='#{frameNameOrId}'], iframe[id='#{frameNameOrId}']")?.src
|
183
|
+
this.evaluate(query, frameNameOrId)
|
184
|
+
|
185
|
+
clearErrors: ->
|
186
|
+
@errors = []
|
187
|
+
return true
|
188
|
+
|
189
|
+
responseHeaders: ->
|
190
|
+
headers = {}
|
191
|
+
@_responseHeaders.forEach (item) ->
|
192
|
+
headers[item.name] = item.value
|
193
|
+
headers
|
194
|
+
|
195
|
+
cookies: ->
|
196
|
+
this.native().cookies
|
197
|
+
|
198
|
+
deleteCookie: (name) ->
|
199
|
+
this.native().deleteCookie(name)
|
200
|
+
|
201
|
+
viewportSize: ->
|
202
|
+
this.native().viewportSize
|
203
|
+
|
204
|
+
setViewportSize: (size) ->
|
205
|
+
this.native().viewportSize = size
|
206
|
+
|
207
|
+
setZoomFactor: (zoom_factor) ->
|
208
|
+
this.native().zoomFactor = zoom_factor
|
209
|
+
|
210
|
+
setPaperSize: (size) ->
|
211
|
+
this.native().paperSize = size
|
212
|
+
|
213
|
+
scrollPosition: ->
|
214
|
+
this.native().scrollPosition
|
215
|
+
|
216
|
+
setScrollPosition: (pos) ->
|
217
|
+
this.native().scrollPosition = pos
|
218
|
+
|
219
|
+
clipRect: ->
|
220
|
+
this.native().clipRect
|
221
|
+
|
222
|
+
setClipRect: (rect) ->
|
223
|
+
this.native().clipRect = rect
|
224
|
+
|
225
|
+
elementBounds: (selector) ->
|
226
|
+
this.native().evaluate(
|
227
|
+
(selector) ->
|
228
|
+
document.querySelector(selector).getBoundingClientRect()
|
229
|
+
, selector
|
230
|
+
)
|
231
|
+
|
232
|
+
setUserAgent: (userAgent) ->
|
233
|
+
this.native().settings.userAgent = userAgent
|
234
|
+
|
235
|
+
getCustomHeaders: ->
|
236
|
+
this.native().customHeaders
|
237
|
+
|
238
|
+
setCustomHeaders: (headers) ->
|
239
|
+
this.native().customHeaders = headers
|
240
|
+
|
241
|
+
addTempHeader: (header) ->
|
242
|
+
for name, value of header
|
243
|
+
@_tempHeaders[name] = value
|
244
|
+
@_tempHeaders
|
245
|
+
|
246
|
+
removeTempHeaders: ->
|
247
|
+
allHeaders = this.getCustomHeaders()
|
248
|
+
for name, value of @_tempHeaders
|
249
|
+
delete allHeaders[name]
|
250
|
+
this.setCustomHeaders(allHeaders)
|
251
|
+
|
252
|
+
pushFrame: (name) ->
|
253
|
+
if this.native().switchToFrame(name)
|
254
|
+
@frames.push(name)
|
255
|
+
return true
|
256
|
+
else
|
257
|
+
frame_no = this.native().evaluate(
|
258
|
+
(frame_name) ->
|
259
|
+
frames = document.querySelectorAll("iframe, frame")
|
260
|
+
(idx for f, idx in frames when f?['name'] == frame_name or f?['id'] == frame_name)[0]
|
261
|
+
, name)
|
262
|
+
if frame_no? and this.native().switchToFrame(frame_no)
|
263
|
+
@frames.push(name)
|
264
|
+
return true
|
265
|
+
else
|
266
|
+
return false
|
267
|
+
|
268
|
+
popFrame: ->
|
269
|
+
@frames.pop()
|
270
|
+
this.native().switchToParentFrame()
|
271
|
+
|
272
|
+
dimensions: ->
|
273
|
+
scroll = this.scrollPosition()
|
274
|
+
viewport = this.viewportSize()
|
275
|
+
|
276
|
+
top: scroll.top, bottom: scroll.top + viewport.height,
|
277
|
+
left: scroll.left, right: scroll.left + viewport.width,
|
278
|
+
viewport: viewport
|
279
|
+
document: this.documentSize()
|
280
|
+
|
281
|
+
# A work around for http://code.google.com/p/phantomjs/issues/detail?id=277
|
282
|
+
validatedDimensions: ->
|
283
|
+
dimensions = this.dimensions()
|
284
|
+
document = dimensions.document
|
285
|
+
|
286
|
+
if dimensions.right > document.width
|
287
|
+
dimensions.left = Math.max(0, dimensions.left - (dimensions.right - document.width))
|
288
|
+
dimensions.right = document.width
|
289
|
+
|
290
|
+
if dimensions.bottom > document.height
|
291
|
+
dimensions.top = Math.max(0, dimensions.top - (dimensions.bottom - document.height))
|
292
|
+
dimensions.bottom = document.height
|
293
|
+
|
294
|
+
this.setScrollPosition(left: dimensions.left, top: dimensions.top)
|
295
|
+
|
296
|
+
dimensions
|
297
|
+
|
298
|
+
get: (id) ->
|
299
|
+
new Poltergeist.Node(this, id)
|
300
|
+
|
301
|
+
# Before each mouse event we make sure that the mouse is moved to where the
|
302
|
+
# event will take place. This deals with e.g. :hover changes.
|
303
|
+
mouseEvent: (name, x, y, button = 'left') ->
|
304
|
+
this.sendEvent('mousemove', x, y)
|
305
|
+
this.sendEvent(name, x, y, button)
|
306
|
+
|
307
|
+
evaluate: (fn, args...) ->
|
308
|
+
this.injectAgent()
|
309
|
+
JSON.parse this.sanitize(this.native().evaluate("function() { return PoltergeistAgent.stringify(#{this.stringifyCall(fn, args)}) }"))
|
310
|
+
|
311
|
+
sanitize: (potential_string) ->
|
312
|
+
if typeof(potential_string) == "string"
|
313
|
+
# JSON doesn't like \r or \n in strings unless escaped
|
314
|
+
potential_string.replace("\n","\\n").replace("\r","\\r")
|
315
|
+
else
|
316
|
+
potential_string
|
317
|
+
|
318
|
+
execute: (fn, args...) ->
|
319
|
+
this.native().evaluate("function() { #{this.stringifyCall(fn, args)} }")
|
320
|
+
|
321
|
+
stringifyCall: (fn, args) ->
|
322
|
+
if args.length == 0
|
323
|
+
"(#{fn.toString()})()"
|
324
|
+
else
|
325
|
+
# The JSON.stringify happens twice because the second time we are essentially
|
326
|
+
# escaping the string.
|
327
|
+
"(#{fn.toString()}).apply(this, PoltergeistAgent.JSON.parse(#{JSON.stringify(JSON.stringify(args))}))"
|
328
|
+
|
329
|
+
# For some reason phantomjs seems to have trouble with doing 'fat arrow' binding here,
|
330
|
+
# hence the 'that' closure.
|
331
|
+
bindCallback: (name) ->
|
332
|
+
that = this
|
333
|
+
this.native()[name] = ->
|
334
|
+
if that[name + 'Native']? # For internal callbacks
|
335
|
+
result = that[name + 'Native'].apply(that, arguments)
|
336
|
+
|
337
|
+
if result != false && that[name]? # For externally set callbacks
|
338
|
+
that[name].apply(that, arguments)
|
339
|
+
return true
|
340
|
+
|
341
|
+
# Any error raised here or inside the evaluate will get reported to
|
342
|
+
# phantom.onError. If result is null, that means there was an error
|
343
|
+
# inside the agent.
|
344
|
+
runCommand: (name, args) ->
|
345
|
+
result = this.evaluate(
|
346
|
+
(name, args) -> __poltergeist.externalCall(name, args),
|
347
|
+
name, args
|
348
|
+
)
|
349
|
+
|
350
|
+
if result != null
|
351
|
+
if result.error?
|
352
|
+
switch result.error.message
|
353
|
+
when 'PoltergeistAgent.ObsoleteNode'
|
354
|
+
throw new Poltergeist.ObsoleteNode
|
355
|
+
when 'PoltergeistAgent.InvalidSelector'
|
356
|
+
[method, selector] = args
|
357
|
+
throw new Poltergeist.InvalidSelector(method, selector)
|
358
|
+
else
|
359
|
+
throw new Poltergeist.BrowserError(result.error.message, result.error.stack)
|
360
|
+
else
|
361
|
+
result.value
|
362
|
+
|
363
|
+
canGoBack: ->
|
364
|
+
this.native().canGoBack
|
365
|
+
|
366
|
+
canGoForward: ->
|
367
|
+
this.native().canGoForward
|
368
|
+
|
369
|
+
normalizeURL: (url) ->
|
370
|
+
parser = document.createElement('a')
|
371
|
+
parser.href = url
|
372
|
+
return parser.href
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
|
3
|
+
module Capybara::Poltergeist
|
4
|
+
class Command
|
5
|
+
attr_reader :id
|
6
|
+
|
7
|
+
def initialize(name, *args)
|
8
|
+
@id = SecureRandom.uuid
|
9
|
+
@name = name
|
10
|
+
@args = args
|
11
|
+
end
|
12
|
+
|
13
|
+
def message
|
14
|
+
JSON.dump({ 'id' => @id, 'name' => @name, 'args' => @args })
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Capybara::Poltergeist
|
2
|
+
class Cookie
|
3
|
+
def initialize(attributes)
|
4
|
+
@attributes = attributes
|
5
|
+
end
|
6
|
+
|
7
|
+
def name
|
8
|
+
@attributes['name']
|
9
|
+
end
|
10
|
+
|
11
|
+
def value
|
12
|
+
@attributes['value']
|
13
|
+
end
|
14
|
+
|
15
|
+
def domain
|
16
|
+
@attributes['domain']
|
17
|
+
end
|
18
|
+
|
19
|
+
def path
|
20
|
+
@attributes['path']
|
21
|
+
end
|
22
|
+
|
23
|
+
def secure?
|
24
|
+
@attributes['secure']
|
25
|
+
end
|
26
|
+
|
27
|
+
def httponly?
|
28
|
+
@attributes['httponly']
|
29
|
+
end
|
30
|
+
|
31
|
+
def expires
|
32
|
+
Time.at @attributes['expiry'] if @attributes['expiry']
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,394 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
3
|
+
module Capybara::Poltergeist
|
4
|
+
class Driver < Capybara::Driver::Base
|
5
|
+
DEFAULT_TIMEOUT = 30
|
6
|
+
|
7
|
+
attr_reader :app, :options
|
8
|
+
|
9
|
+
def initialize(app, options = {})
|
10
|
+
@app = app
|
11
|
+
@options = options
|
12
|
+
@browser = nil
|
13
|
+
@inspector = nil
|
14
|
+
@server = nil
|
15
|
+
@client = nil
|
16
|
+
@started = false
|
17
|
+
end
|
18
|
+
|
19
|
+
def needs_server?
|
20
|
+
true
|
21
|
+
end
|
22
|
+
|
23
|
+
def browser
|
24
|
+
@browser ||= begin
|
25
|
+
browser = Browser.new(server, client, logger)
|
26
|
+
browser.js_errors = options[:js_errors] if options.key?(:js_errors)
|
27
|
+
browser.extensions = options.fetch(:extensions, [])
|
28
|
+
browser.debug = true if options[:debug]
|
29
|
+
browser
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def inspector
|
34
|
+
@inspector ||= options[:inspector] && Inspector.new(options[:inspector])
|
35
|
+
end
|
36
|
+
|
37
|
+
def server
|
38
|
+
@server ||= Server.new(options[:port], options.fetch(:timeout) { DEFAULT_TIMEOUT })
|
39
|
+
end
|
40
|
+
|
41
|
+
def client
|
42
|
+
@client ||= Client.start(server,
|
43
|
+
:path => options[:phantomjs],
|
44
|
+
:window_size => options[:window_size],
|
45
|
+
:phantomjs_options => phantomjs_options,
|
46
|
+
:phantomjs_logger => phantomjs_logger
|
47
|
+
)
|
48
|
+
end
|
49
|
+
|
50
|
+
def phantomjs_options
|
51
|
+
list = options[:phantomjs_options] || []
|
52
|
+
|
53
|
+
# PhantomJS defaults to only using SSLv3, which since POODLE (Oct 2014)
|
54
|
+
# many sites have dropped from their supported protocols (eg PayPal,
|
55
|
+
# Braintree).
|
56
|
+
list += ["--ssl-protocol=any"] unless list.grep(/ssl-protocol/).any?
|
57
|
+
|
58
|
+
list += ["--remote-debugger-port=#{inspector.port}", "--remote-debugger-autorun=yes"] if inspector
|
59
|
+
list
|
60
|
+
end
|
61
|
+
|
62
|
+
def client_pid
|
63
|
+
client.pid
|
64
|
+
end
|
65
|
+
|
66
|
+
def timeout
|
67
|
+
server.timeout
|
68
|
+
end
|
69
|
+
|
70
|
+
def timeout=(sec)
|
71
|
+
server.timeout = sec
|
72
|
+
end
|
73
|
+
|
74
|
+
def restart
|
75
|
+
browser.restart
|
76
|
+
end
|
77
|
+
|
78
|
+
def quit
|
79
|
+
server.stop
|
80
|
+
client.stop
|
81
|
+
end
|
82
|
+
|
83
|
+
# logger should be an object that responds to puts, or nil
|
84
|
+
def logger
|
85
|
+
options[:logger] || (options[:debug] && STDERR)
|
86
|
+
end
|
87
|
+
|
88
|
+
# logger should be an object that behaves like IO or nil
|
89
|
+
def phantomjs_logger
|
90
|
+
options.fetch(:phantomjs_logger, nil)
|
91
|
+
end
|
92
|
+
|
93
|
+
def visit(url)
|
94
|
+
@started = true
|
95
|
+
browser.visit(url)
|
96
|
+
end
|
97
|
+
|
98
|
+
def current_url
|
99
|
+
browser.current_url
|
100
|
+
end
|
101
|
+
|
102
|
+
def status_code
|
103
|
+
browser.status_code
|
104
|
+
end
|
105
|
+
|
106
|
+
def html
|
107
|
+
browser.body
|
108
|
+
end
|
109
|
+
alias_method :body, :html
|
110
|
+
|
111
|
+
def source
|
112
|
+
browser.source.to_s
|
113
|
+
end
|
114
|
+
|
115
|
+
def title
|
116
|
+
browser.title
|
117
|
+
end
|
118
|
+
|
119
|
+
def find(method, selector)
|
120
|
+
browser.find(method, selector).map { |page_id, id| Capybara::Poltergeist::Node.new(self, page_id, id) }
|
121
|
+
end
|
122
|
+
|
123
|
+
def find_xpath(selector)
|
124
|
+
find :xpath, selector
|
125
|
+
end
|
126
|
+
|
127
|
+
def find_css(selector)
|
128
|
+
find :css, selector
|
129
|
+
end
|
130
|
+
|
131
|
+
def click(x, y)
|
132
|
+
browser.click_coordinates(x, y)
|
133
|
+
end
|
134
|
+
|
135
|
+
def evaluate_script(script)
|
136
|
+
browser.evaluate(script)
|
137
|
+
end
|
138
|
+
|
139
|
+
def execute_script(script)
|
140
|
+
browser.execute(script)
|
141
|
+
nil
|
142
|
+
end
|
143
|
+
|
144
|
+
def within_frame(name, &block)
|
145
|
+
browser.within_frame(name, &block)
|
146
|
+
end
|
147
|
+
|
148
|
+
def current_window_handle
|
149
|
+
browser.window_handle
|
150
|
+
end
|
151
|
+
|
152
|
+
def window_handles
|
153
|
+
browser.window_handles
|
154
|
+
end
|
155
|
+
|
156
|
+
def close_window(handle)
|
157
|
+
browser.close_window(handle)
|
158
|
+
end
|
159
|
+
|
160
|
+
def open_new_window
|
161
|
+
browser.open_new_window
|
162
|
+
end
|
163
|
+
|
164
|
+
def switch_to_window(handle)
|
165
|
+
browser.switch_to_window(handle)
|
166
|
+
end
|
167
|
+
|
168
|
+
def within_window(name, &block)
|
169
|
+
browser.within_window(name, &block)
|
170
|
+
end
|
171
|
+
|
172
|
+
def no_such_window_error
|
173
|
+
NoSuchWindowError
|
174
|
+
end
|
175
|
+
|
176
|
+
def reset!
|
177
|
+
browser.reset
|
178
|
+
@started = false
|
179
|
+
end
|
180
|
+
|
181
|
+
def save_screenshot(path, options = {})
|
182
|
+
browser.render(path, options)
|
183
|
+
end
|
184
|
+
alias_method :render, :save_screenshot
|
185
|
+
|
186
|
+
def render_base64(format = :png, options = {})
|
187
|
+
browser.render_base64(format, options)
|
188
|
+
end
|
189
|
+
|
190
|
+
def set_screen_size(s_width,s_height)
|
191
|
+
browser.set_screen_size(s_width, s_width)
|
192
|
+
end
|
193
|
+
|
194
|
+
def paper_size=(size = {})
|
195
|
+
browser.set_paper_size(size)
|
196
|
+
end
|
197
|
+
|
198
|
+
def zoom_factor=(zoom_factor)
|
199
|
+
browser.set_zoom_factor(zoom_factor)
|
200
|
+
end
|
201
|
+
|
202
|
+
def resize(width, height)
|
203
|
+
browser.resize(width, height)
|
204
|
+
end
|
205
|
+
alias_method :resize_window, :resize
|
206
|
+
|
207
|
+
def resize_window_to(handle, width, height)
|
208
|
+
within_window(handle) do
|
209
|
+
resize(width, height)
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
def window_size(handle)
|
214
|
+
within_window(handle) do
|
215
|
+
evaluate_script('[window.innerWidth, window.innerHeight]')
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
def scroll_to(left, top)
|
220
|
+
browser.scroll_to(left, top)
|
221
|
+
end
|
222
|
+
|
223
|
+
def network_traffic
|
224
|
+
browser.network_traffic
|
225
|
+
end
|
226
|
+
|
227
|
+
def clear_network_traffic
|
228
|
+
browser.clear_network_traffic
|
229
|
+
end
|
230
|
+
|
231
|
+
def headers
|
232
|
+
browser.get_headers
|
233
|
+
end
|
234
|
+
|
235
|
+
def headers=(headers)
|
236
|
+
browser.set_headers(headers)
|
237
|
+
end
|
238
|
+
|
239
|
+
def add_headers(headers)
|
240
|
+
browser.add_headers(headers)
|
241
|
+
end
|
242
|
+
|
243
|
+
def add_header(name, value, options = {})
|
244
|
+
permanent = options.fetch(:permanent, true)
|
245
|
+
browser.add_header({ name => value }, permanent)
|
246
|
+
end
|
247
|
+
|
248
|
+
def response_headers
|
249
|
+
browser.response_headers
|
250
|
+
end
|
251
|
+
|
252
|
+
def cookies
|
253
|
+
browser.cookies
|
254
|
+
end
|
255
|
+
|
256
|
+
def set_cookie(name, value, options = {})
|
257
|
+
options[:name] ||= name
|
258
|
+
options[:value] ||= value
|
259
|
+
options[:domain] ||= begin
|
260
|
+
if @started
|
261
|
+
URI.parse(browser.current_url).host
|
262
|
+
else
|
263
|
+
URI.parse(Capybara.app_host || '').host || "127.0.0.1"
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
browser.set_cookie(options)
|
268
|
+
end
|
269
|
+
|
270
|
+
def remove_cookie(name)
|
271
|
+
browser.remove_cookie(name)
|
272
|
+
end
|
273
|
+
|
274
|
+
def clear_cookies
|
275
|
+
browser.clear_cookies
|
276
|
+
end
|
277
|
+
|
278
|
+
def cookies_enabled=(flag)
|
279
|
+
browser.cookies_enabled = flag
|
280
|
+
end
|
281
|
+
|
282
|
+
# * PhantomJS with set settings doesn't send `Authorize` on POST request
|
283
|
+
# * With manually set header PhantomJS makes next request with
|
284
|
+
# `Authorization: Basic Og==` header when settings are empty and the
|
285
|
+
# response was `401 Unauthorized` (which means Base64.encode64(':')).
|
286
|
+
# Combining both methods to reach proper behavior.
|
287
|
+
def basic_authorize(user, password)
|
288
|
+
browser.set_http_auth(user, password)
|
289
|
+
credentials = ["#{user}:#{password}"].pack('m*').strip
|
290
|
+
add_header('Authorization', "Basic #{credentials}")
|
291
|
+
end
|
292
|
+
|
293
|
+
def debug
|
294
|
+
if @options[:inspector]
|
295
|
+
# Fall back to default scheme
|
296
|
+
scheme = URI.parse(browser.current_url).scheme rescue nil
|
297
|
+
scheme = 'http' if scheme != 'https'
|
298
|
+
inspector.open(scheme)
|
299
|
+
pause
|
300
|
+
else
|
301
|
+
raise Error, "To use the remote debugging, you have to launch the driver " \
|
302
|
+
"with `:inspector => true` configuration option"
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
def pause
|
307
|
+
# STDIN is not necessarily connected to a keyboard. It might even be closed.
|
308
|
+
# So we need a method other than keypress to continue.
|
309
|
+
|
310
|
+
# In jRuby - STDIN returns immediately from select
|
311
|
+
# see https://github.com/jruby/jruby/issues/1783
|
312
|
+
read, write = IO.pipe
|
313
|
+
Thread.new { IO.copy_stream(STDIN, write); write.close }
|
314
|
+
|
315
|
+
STDERR.puts "Poltergeist execution paused. Press enter (or run 'kill -CONT #{Process.pid}') to continue."
|
316
|
+
|
317
|
+
signal = false
|
318
|
+
old_trap = trap('SIGCONT') { signal = true; STDERR.puts "\nSignal SIGCONT received" }
|
319
|
+
keyboard = IO.select([read], nil, nil, 1) until keyboard || signal # wait for data on STDIN or signal SIGCONT received
|
320
|
+
|
321
|
+
begin
|
322
|
+
input = read.read_nonblock(80) # clear out the read buffer
|
323
|
+
puts unless input && input =~ /\n\z/
|
324
|
+
rescue EOFError, IO::WaitReadable # Ignore problems reading from STDIN.
|
325
|
+
end unless signal
|
326
|
+
|
327
|
+
trap('SIGCONT', old_trap) # Restore the previuos signal handler, if there was one.
|
328
|
+
|
329
|
+
STDERR.puts 'Continuing'
|
330
|
+
end
|
331
|
+
|
332
|
+
def wait?
|
333
|
+
true
|
334
|
+
end
|
335
|
+
|
336
|
+
def invalid_element_errors
|
337
|
+
[Capybara::Poltergeist::ObsoleteNode, Capybara::Poltergeist::MouseEventFailed]
|
338
|
+
end
|
339
|
+
|
340
|
+
def go_back
|
341
|
+
browser.go_back
|
342
|
+
end
|
343
|
+
|
344
|
+
def go_forward
|
345
|
+
browser.go_forward
|
346
|
+
end
|
347
|
+
|
348
|
+
def accept_modal(type, options = {})
|
349
|
+
case type
|
350
|
+
when :confirm
|
351
|
+
browser.accept_confirm
|
352
|
+
when :prompt
|
353
|
+
browser.accept_prompt options[:with]
|
354
|
+
end
|
355
|
+
|
356
|
+
yield if block_given?
|
357
|
+
|
358
|
+
find_modal(options)
|
359
|
+
end
|
360
|
+
|
361
|
+
def dismiss_modal(type, options = {})
|
362
|
+
case type
|
363
|
+
when :confirm
|
364
|
+
browser.dismiss_confirm
|
365
|
+
when :prompt
|
366
|
+
browser.dismiss_prompt
|
367
|
+
end
|
368
|
+
|
369
|
+
yield if block_given?
|
370
|
+
find_modal(options)
|
371
|
+
end
|
372
|
+
|
373
|
+
private
|
374
|
+
|
375
|
+
def find_modal(options)
|
376
|
+
start_time = Time.now
|
377
|
+
timeout_sec = options[:wait] || begin Capybara.default_max_wait_time rescue Capybara.default_wait_time end
|
378
|
+
expect_text = options[:text]
|
379
|
+
not_found_msg = 'Unable to find modal dialog'
|
380
|
+
not_found_msg += " with #{expect_text}" if expect_text
|
381
|
+
|
382
|
+
begin
|
383
|
+
modal_text = browser.modal_message
|
384
|
+
raise Capybara::ModalNotFound if modal_text.nil?
|
385
|
+
raise Capybara::ModalNotFound if (expect_text && (modal_text != expect_text))
|
386
|
+
rescue Capybara::ModalNotFound => e
|
387
|
+
raise e, not_found_msg if (Time.now - start_time) >= timeout_sec
|
388
|
+
sleep(0.05)
|
389
|
+
retry
|
390
|
+
end
|
391
|
+
modal_text
|
392
|
+
end
|
393
|
+
end
|
394
|
+
end
|