poltergeistFork 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|