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,423 @@
|
|
1
|
+
# This is injected into each page that is loaded
|
2
|
+
|
3
|
+
class PoltergeistAgent
|
4
|
+
# Since this code executes in the sites browser space - copy needed JSON functions
|
5
|
+
# in case user code messes with JSON (early mootools for instance)
|
6
|
+
@.JSON ||= { parse: JSON.parse, stringify: JSON.stringify }
|
7
|
+
|
8
|
+
constructor: ->
|
9
|
+
@elements = []
|
10
|
+
@nodes = {}
|
11
|
+
|
12
|
+
externalCall: (name, args) ->
|
13
|
+
try
|
14
|
+
{ value: this[name].apply(this, args) }
|
15
|
+
catch error
|
16
|
+
{ error: { message: error.toString(), stack: error.stack } }
|
17
|
+
|
18
|
+
@stringify: (object) ->
|
19
|
+
try
|
20
|
+
PoltergeistAgent.JSON.stringify object, (key, value) ->
|
21
|
+
if Array.isArray(this[key])
|
22
|
+
return this[key]
|
23
|
+
else
|
24
|
+
return value
|
25
|
+
catch error
|
26
|
+
if error instanceof TypeError
|
27
|
+
'"(cyclic structure)"'
|
28
|
+
else
|
29
|
+
throw error
|
30
|
+
|
31
|
+
# Somehow PhantomJS returns all characters(brackets, etc) properly encoded
|
32
|
+
# except whitespace character in pathname part of the location. This hack
|
33
|
+
# is intended to fix this up.
|
34
|
+
currentUrl: ->
|
35
|
+
window.location.href.replace(/\ /g, '%20')
|
36
|
+
|
37
|
+
find: (method, selector, within = document) ->
|
38
|
+
try
|
39
|
+
if method == "xpath"
|
40
|
+
xpath = document.evaluate(selector, within, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null)
|
41
|
+
results = (xpath.snapshotItem(i) for i in [0...xpath.snapshotLength])
|
42
|
+
else
|
43
|
+
results = within.querySelectorAll(selector)
|
44
|
+
|
45
|
+
this.register(el) for el in results
|
46
|
+
catch error
|
47
|
+
# DOMException.INVALID_EXPRESSION_ERR is undefined, using pure code
|
48
|
+
if error.code == DOMException.SYNTAX_ERR || error.code == 51
|
49
|
+
throw new PoltergeistAgent.InvalidSelector
|
50
|
+
else
|
51
|
+
throw error
|
52
|
+
|
53
|
+
register: (element) ->
|
54
|
+
@elements.push(element)
|
55
|
+
@elements.length - 1
|
56
|
+
|
57
|
+
documentSize: ->
|
58
|
+
height: document.documentElement.scrollHeight || document.documentElement.clientHeight,
|
59
|
+
width: document.documentElement.scrollWidth || document.documentElement.clientWidth
|
60
|
+
|
61
|
+
get: (id) ->
|
62
|
+
@nodes[id] or= new PoltergeistAgent.Node(this, @elements[id])
|
63
|
+
|
64
|
+
nodeCall: (id, name, args) ->
|
65
|
+
node = this.get(id)
|
66
|
+
throw new PoltergeistAgent.ObsoleteNode if node.isObsolete()
|
67
|
+
node[name].apply(node, args)
|
68
|
+
|
69
|
+
beforeUpload: (id) ->
|
70
|
+
this.get(id).setAttribute('_poltergeist_selected', '')
|
71
|
+
|
72
|
+
afterUpload: (id) ->
|
73
|
+
this.get(id).removeAttribute('_poltergeist_selected')
|
74
|
+
|
75
|
+
clearLocalStorage: ->
|
76
|
+
localStorage.clear()
|
77
|
+
|
78
|
+
class PoltergeistAgent.ObsoleteNode
|
79
|
+
toString: -> "PoltergeistAgent.ObsoleteNode"
|
80
|
+
|
81
|
+
class PoltergeistAgent.InvalidSelector
|
82
|
+
toString: -> "PoltergeistAgent.InvalidSelector"
|
83
|
+
|
84
|
+
class PoltergeistAgent.Node
|
85
|
+
@EVENTS = {
|
86
|
+
FOCUS: ['blur', 'focus', 'focusin', 'focusout'],
|
87
|
+
MOUSE: ['click', 'dblclick', 'mousedown', 'mouseenter', 'mouseleave', 'mousemove',
|
88
|
+
'mouseover', 'mouseout', 'mouseup', 'contextmenu'],
|
89
|
+
FORM: ['submit']
|
90
|
+
}
|
91
|
+
|
92
|
+
constructor: (@agent, @element) ->
|
93
|
+
|
94
|
+
parentId: ->
|
95
|
+
@agent.register(@element.parentNode)
|
96
|
+
|
97
|
+
parentIds: ->
|
98
|
+
ids = []
|
99
|
+
parent = @element.parentNode
|
100
|
+
while parent != document
|
101
|
+
ids.push @agent.register(parent)
|
102
|
+
parent = parent.parentNode
|
103
|
+
ids
|
104
|
+
|
105
|
+
find: (method, selector) ->
|
106
|
+
@agent.find(method, selector, @element)
|
107
|
+
|
108
|
+
isObsolete: ->
|
109
|
+
obsolete = (element) =>
|
110
|
+
if (parent = element.parentNode)?
|
111
|
+
if parent == document
|
112
|
+
return false
|
113
|
+
else
|
114
|
+
obsolete parent
|
115
|
+
else
|
116
|
+
return true
|
117
|
+
obsolete @element
|
118
|
+
|
119
|
+
changed: ->
|
120
|
+
event = document.createEvent('HTMLEvents')
|
121
|
+
event.initEvent('change', true, false)
|
122
|
+
|
123
|
+
# In the case of an OPTION tag, the change event should come
|
124
|
+
# from the parent SELECT
|
125
|
+
if @element.nodeName == 'OPTION'
|
126
|
+
element = @element.parentNode
|
127
|
+
element = element.parentNode if element.nodeName == 'OPTGROUP'
|
128
|
+
element
|
129
|
+
else
|
130
|
+
element = @element
|
131
|
+
|
132
|
+
element.dispatchEvent(event)
|
133
|
+
|
134
|
+
input: ->
|
135
|
+
event = document.createEvent('HTMLEvents')
|
136
|
+
event.initEvent('input', true, false)
|
137
|
+
@element.dispatchEvent(event)
|
138
|
+
|
139
|
+
keyupdowned: (eventName, keyCode) ->
|
140
|
+
event = document.createEvent('UIEvents')
|
141
|
+
event.initEvent(eventName, true, true)
|
142
|
+
event.keyCode = keyCode
|
143
|
+
event.which = keyCode
|
144
|
+
event.charCode = 0
|
145
|
+
@element.dispatchEvent(event)
|
146
|
+
|
147
|
+
keypressed: (altKey, ctrlKey, shiftKey, metaKey, keyCode, charCode) ->
|
148
|
+
event = document.createEvent('UIEvents')
|
149
|
+
event.initEvent('keypress', true, true)
|
150
|
+
event.window = @agent.window
|
151
|
+
event.altKey = altKey
|
152
|
+
event.ctrlKey = ctrlKey
|
153
|
+
event.shiftKey = shiftKey
|
154
|
+
event.metaKey = metaKey
|
155
|
+
event.keyCode = keyCode
|
156
|
+
event.charCode = charCode
|
157
|
+
event.which = keyCode
|
158
|
+
@element.dispatchEvent(event)
|
159
|
+
|
160
|
+
insideBody: ->
|
161
|
+
@element == document.body ||
|
162
|
+
document.evaluate('ancestor::body', @element, null, XPathResult.BOOLEAN_TYPE, null).booleanValue
|
163
|
+
|
164
|
+
allText: ->
|
165
|
+
@element.textContent
|
166
|
+
|
167
|
+
visibleText: ->
|
168
|
+
if this.isVisible()
|
169
|
+
if @element.nodeName == "TEXTAREA"
|
170
|
+
@element.textContent
|
171
|
+
else
|
172
|
+
@element.innerText || @element.textContent
|
173
|
+
|
174
|
+
deleteText: ->
|
175
|
+
range = document.createRange()
|
176
|
+
range.selectNodeContents(@element)
|
177
|
+
window.getSelection().removeAllRanges()
|
178
|
+
window.getSelection().addRange(range)
|
179
|
+
window.getSelection().deleteFromDocument()
|
180
|
+
|
181
|
+
getProperty: (name) ->
|
182
|
+
@element[name]
|
183
|
+
|
184
|
+
getAttributes: ->
|
185
|
+
attrs = {}
|
186
|
+
for attr in @element.attributes
|
187
|
+
attrs[attr.name] = attr.value.replace("\n","\\n");
|
188
|
+
attrs
|
189
|
+
|
190
|
+
getAttribute: (name) ->
|
191
|
+
if name == 'checked' || name == 'selected'
|
192
|
+
@element[name]
|
193
|
+
else
|
194
|
+
@element.getAttribute(name)
|
195
|
+
|
196
|
+
scrollIntoView: ->
|
197
|
+
@element.scrollIntoViewIfNeeded()
|
198
|
+
#Sometimes scrollIntoViewIfNeeded doesn't seem to work, not really sure why.
|
199
|
+
#Just calling scrollIntoView doesnt work either, however calling scrollIntoView
|
200
|
+
#after scrollIntoViewIfNeeded when element is not in the viewport does appear to work
|
201
|
+
@element.scrollIntoView() unless this.isInViewport()
|
202
|
+
|
203
|
+
value: ->
|
204
|
+
if @element.tagName == 'SELECT' && @element.multiple
|
205
|
+
option.value for option in @element.children when option.selected
|
206
|
+
else
|
207
|
+
@element.value
|
208
|
+
|
209
|
+
set: (value) ->
|
210
|
+
return if @element.readOnly
|
211
|
+
|
212
|
+
if (@element.maxLength >= 0)
|
213
|
+
value = value.substr(0, @element.maxLength)
|
214
|
+
|
215
|
+
this.trigger('focus')
|
216
|
+
@element.value = ''
|
217
|
+
|
218
|
+
if @element.type == 'number'
|
219
|
+
@element.value = value
|
220
|
+
else
|
221
|
+
for char in value
|
222
|
+
keyCode = this.characterToKeyCode(char)
|
223
|
+
this.keyupdowned('keydown', keyCode)
|
224
|
+
@element.value += char
|
225
|
+
|
226
|
+
this.keypressed(false, false, false, false, char.charCodeAt(0), char.charCodeAt(0))
|
227
|
+
this.keyupdowned('keyup', keyCode)
|
228
|
+
|
229
|
+
this.changed()
|
230
|
+
this.input()
|
231
|
+
this.trigger('blur')
|
232
|
+
|
233
|
+
isMultiple: ->
|
234
|
+
@element.multiple
|
235
|
+
|
236
|
+
setAttribute: (name, value) ->
|
237
|
+
@element.setAttribute(name, value)
|
238
|
+
|
239
|
+
removeAttribute: (name) ->
|
240
|
+
@element.removeAttribute(name)
|
241
|
+
|
242
|
+
select: (value) ->
|
243
|
+
if @isDisabled()
|
244
|
+
false
|
245
|
+
else if value == false && !@element.parentNode.multiple
|
246
|
+
false
|
247
|
+
else
|
248
|
+
this.trigger('focus', @element.parentNode)
|
249
|
+
|
250
|
+
@element.selected = value
|
251
|
+
this.changed()
|
252
|
+
|
253
|
+
this.trigger('blur', @element.parentNode)
|
254
|
+
true
|
255
|
+
|
256
|
+
tagName: ->
|
257
|
+
@element.tagName
|
258
|
+
|
259
|
+
isVisible: (element = @element) ->
|
260
|
+
while (element)
|
261
|
+
style = window.getComputedStyle(element)
|
262
|
+
return false if style.display == 'none' or
|
263
|
+
style.visibility == 'hidden' or
|
264
|
+
parseFloat(style.opacity) == 0
|
265
|
+
element = element.parentElement
|
266
|
+
|
267
|
+
return true
|
268
|
+
|
269
|
+
isInViewport: ->
|
270
|
+
rect = @element.getBoundingClientRect();
|
271
|
+
|
272
|
+
rect.top >= 0 &&
|
273
|
+
rect.left >= 0 &&
|
274
|
+
rect.bottom <= window.innerHeight &&
|
275
|
+
rect.right <= window.innerWidth
|
276
|
+
|
277
|
+
isDisabled: ->
|
278
|
+
@element.disabled || @element.tagName == 'OPTION' && @element.parentNode.disabled
|
279
|
+
|
280
|
+
path: ->
|
281
|
+
elements = @parentIds().reverse().map((id) => @agent.get(id))
|
282
|
+
elements.push(this)
|
283
|
+
selectors = elements.map (el)->
|
284
|
+
prev_siblings = el.find('xpath', "./preceding-sibling::#{el.tagName()}")
|
285
|
+
"#{el.tagName()}[#{prev_siblings.length + 1}]"
|
286
|
+
"//" + selectors.join('/')
|
287
|
+
|
288
|
+
containsSelection: ->
|
289
|
+
selectedNode = document.getSelection().focusNode
|
290
|
+
|
291
|
+
return false if !selectedNode
|
292
|
+
|
293
|
+
if selectedNode.nodeType == 3
|
294
|
+
selectedNode = selectedNode.parentNode
|
295
|
+
|
296
|
+
@element.contains(selectedNode)
|
297
|
+
|
298
|
+
frameOffset: ->
|
299
|
+
win = window
|
300
|
+
offset = { top: 0, left: 0 }
|
301
|
+
|
302
|
+
while win.frameElement
|
303
|
+
rect = win.frameElement.getClientRects()[0]
|
304
|
+
style = win.getComputedStyle(win.frameElement)
|
305
|
+
win = win.parent
|
306
|
+
|
307
|
+
offset.top += rect.top + parseInt(style.getPropertyValue("padding-top"), 10)
|
308
|
+
offset.left += rect.left + parseInt(style.getPropertyValue("padding-left"), 10)
|
309
|
+
|
310
|
+
offset
|
311
|
+
|
312
|
+
position: ->
|
313
|
+
# Elements inside an SVG return underfined for getClientRects???
|
314
|
+
rect = @element.getClientRects()[0] || @element.getBoundingClientRect()
|
315
|
+
throw new PoltergeistAgent.ObsoleteNode unless rect
|
316
|
+
frameOffset = this.frameOffset()
|
317
|
+
|
318
|
+
pos = {
|
319
|
+
top: rect.top + frameOffset.top,
|
320
|
+
right: rect.right + frameOffset.left,
|
321
|
+
left: rect.left + frameOffset.left,
|
322
|
+
bottom: rect.bottom + frameOffset.top,
|
323
|
+
width: rect.width,
|
324
|
+
height: rect.height
|
325
|
+
}
|
326
|
+
|
327
|
+
pos
|
328
|
+
|
329
|
+
trigger: (name, element = @element) ->
|
330
|
+
if Node.EVENTS.MOUSE.indexOf(name) != -1
|
331
|
+
event = document.createEvent('MouseEvent')
|
332
|
+
event.initMouseEvent(
|
333
|
+
name, true, true, window, 0, 0, 0, 0, 0,
|
334
|
+
false, false, false, false, 0, null
|
335
|
+
)
|
336
|
+
else if Node.EVENTS.FOCUS.indexOf(name) != -1
|
337
|
+
event = this.obtainEvent(name)
|
338
|
+
else if Node.EVENTS.FORM.indexOf(name) != -1
|
339
|
+
event = this.obtainEvent(name)
|
340
|
+
else
|
341
|
+
throw "Unknown event"
|
342
|
+
|
343
|
+
element.dispatchEvent(event)
|
344
|
+
|
345
|
+
obtainEvent: (name) ->
|
346
|
+
event = document.createEvent('HTMLEvents')
|
347
|
+
event.initEvent(name, true, true)
|
348
|
+
event
|
349
|
+
|
350
|
+
mouseEventTest: (x, y) ->
|
351
|
+
frameOffset = this.frameOffset()
|
352
|
+
|
353
|
+
x -= frameOffset.left
|
354
|
+
y -= frameOffset.top
|
355
|
+
|
356
|
+
el = origEl = document.elementFromPoint(x, y)
|
357
|
+
|
358
|
+
while el
|
359
|
+
if el == @element
|
360
|
+
return { status: 'success' }
|
361
|
+
else
|
362
|
+
el = el.parentNode
|
363
|
+
|
364
|
+
{ status: 'failure', selector: origEl && this.getSelector(origEl) }
|
365
|
+
getSelector: (el) ->
|
366
|
+
selector = if el.tagName != 'HTML' then this.getSelector(el.parentNode) + ' ' else ''
|
367
|
+
selector += el.tagName.toLowerCase()
|
368
|
+
selector += "##{el.id}" if el.id
|
369
|
+
|
370
|
+
#PhantomJS < 2.0 doesn't support classList for SVG elements - so get classes manually
|
371
|
+
classes = el.classList || (el.getAttribute('class')?.trim()?.split(/\s+/)) || []
|
372
|
+
for className in classes when className != ''
|
373
|
+
selector += ".#{className}"
|
374
|
+
selector
|
375
|
+
|
376
|
+
characterToKeyCode: (character) ->
|
377
|
+
code = character.toUpperCase().charCodeAt(0)
|
378
|
+
specialKeys =
|
379
|
+
96: 192 #`
|
380
|
+
45: 189 #-
|
381
|
+
61: 187 #=
|
382
|
+
91: 219 #[
|
383
|
+
93: 221 #]
|
384
|
+
92: 220 #\
|
385
|
+
59: 186 #;
|
386
|
+
39: 222 #'
|
387
|
+
44: 188 #,
|
388
|
+
46: 190 #.
|
389
|
+
47: 191 #/
|
390
|
+
127: 46 #delete
|
391
|
+
126: 192 #~
|
392
|
+
33: 49 #!
|
393
|
+
64: 50 #@
|
394
|
+
35: 51 ##
|
395
|
+
36: 52 #$
|
396
|
+
37: 53 #%
|
397
|
+
94: 54 #^
|
398
|
+
38: 55 #&
|
399
|
+
42: 56 #*
|
400
|
+
40: 57 #(
|
401
|
+
41: 48 #)
|
402
|
+
95: 189 #_
|
403
|
+
43: 187 #+
|
404
|
+
123: 219 #{
|
405
|
+
125: 221 #}
|
406
|
+
124: 220 #|
|
407
|
+
58: 186 #:
|
408
|
+
34: 222 #"
|
409
|
+
60: 188 #<
|
410
|
+
62: 190 #>
|
411
|
+
63: 191 #?
|
412
|
+
|
413
|
+
specialKeys[code] || code
|
414
|
+
|
415
|
+
isDOMEqual: (other_id) ->
|
416
|
+
@element == @agent.get(other_id).element
|
417
|
+
|
418
|
+
window.__poltergeist = new PoltergeistAgent
|
419
|
+
|
420
|
+
document.addEventListener(
|
421
|
+
'DOMContentLoaded',
|
422
|
+
-> console.log('__DOMContentLoaded')
|
423
|
+
)
|
@@ -0,0 +1,497 @@
|
|
1
|
+
class Poltergeist.Browser
|
2
|
+
constructor: (width, height) ->
|
3
|
+
@width = width || 1024
|
4
|
+
@height = height || 768
|
5
|
+
@pages = []
|
6
|
+
@js_errors = true
|
7
|
+
@_debug = false
|
8
|
+
@_counter = 0
|
9
|
+
|
10
|
+
@processed_modal_messages = []
|
11
|
+
@confirm_processes = []
|
12
|
+
@prompt_responses = []
|
13
|
+
|
14
|
+
this.resetPage()
|
15
|
+
|
16
|
+
resetPage: ->
|
17
|
+
[@_counter, @pages] = [0, []]
|
18
|
+
|
19
|
+
if @page?
|
20
|
+
unless @page.closed
|
21
|
+
@page.clearLocalStorage() if @page.currentUrl() != 'about:blank'
|
22
|
+
@page.release()
|
23
|
+
phantom.clearCookies()
|
24
|
+
|
25
|
+
@page = @currentPage = new Poltergeist.WebPage
|
26
|
+
@page.setViewportSize(width: @width, height: @height)
|
27
|
+
@page.handle = "#{@_counter++}"
|
28
|
+
@pages.push(@page)
|
29
|
+
|
30
|
+
@processed_modal_messages = []
|
31
|
+
@confirm_processes = []
|
32
|
+
@prompt_responses = []
|
33
|
+
|
34
|
+
|
35
|
+
@page.native().onAlert = (msg) =>
|
36
|
+
@setModalMessage msg
|
37
|
+
return
|
38
|
+
|
39
|
+
@page.native().onConfirm = (msg) =>
|
40
|
+
process = @confirm_processes.pop()
|
41
|
+
process = true if process == undefined
|
42
|
+
@setModalMessage msg
|
43
|
+
return process
|
44
|
+
|
45
|
+
@page.native().onPrompt = (msg, defaultVal) =>
|
46
|
+
response = @prompt_responses.pop()
|
47
|
+
response = defaultVal if (response == undefined || response == false)
|
48
|
+
|
49
|
+
@setModalMessage msg
|
50
|
+
return response
|
51
|
+
|
52
|
+
@page.onPageCreated = (newPage) =>
|
53
|
+
page = new Poltergeist.WebPage(newPage)
|
54
|
+
page.handle = "#{@_counter++}"
|
55
|
+
@pages.push(page)
|
56
|
+
|
57
|
+
return
|
58
|
+
|
59
|
+
getPageByHandle: (handle) ->
|
60
|
+
@pages.filter((p) -> !p.closed && p.handle == handle)[0]
|
61
|
+
|
62
|
+
runCommand: (command) ->
|
63
|
+
@current_command = command
|
64
|
+
@currentPage.state = 'default'
|
65
|
+
this[command.name].apply(this, command.args)
|
66
|
+
|
67
|
+
debug: (message) ->
|
68
|
+
if @_debug
|
69
|
+
console.log "poltergeist [#{new Date().getTime()}] #{message}"
|
70
|
+
|
71
|
+
setModalMessage: (msg) ->
|
72
|
+
@processed_modal_messages.push(msg)
|
73
|
+
return
|
74
|
+
|
75
|
+
add_extension: (extension) ->
|
76
|
+
@currentPage.injectExtension extension
|
77
|
+
@current_command.sendResponse 'success'
|
78
|
+
|
79
|
+
node: (page_id, id) ->
|
80
|
+
if @currentPage.id == page_id
|
81
|
+
@currentPage.get(id)
|
82
|
+
else
|
83
|
+
throw new Poltergeist.ObsoleteNode
|
84
|
+
|
85
|
+
visit: (url) ->
|
86
|
+
|
87
|
+
@currentPage.state = 'loading'
|
88
|
+
#reset modal processing state when changing page
|
89
|
+
@processed_modal_messages = []
|
90
|
+
@confirm_processes = []
|
91
|
+
@prompt_responses = []
|
92
|
+
|
93
|
+
|
94
|
+
# Prevent firing `page.onInitialized` event twice. Calling currentUrl
|
95
|
+
# method before page is actually opened fires this event for the first time.
|
96
|
+
# The second time will be in the right place after `page.open`
|
97
|
+
prevUrl = if @currentPage.source is null then 'about:blank' else @currentPage.currentUrl()
|
98
|
+
|
99
|
+
@currentPage.open(url)
|
100
|
+
|
101
|
+
if /#/.test(url) && prevUrl.split('#')[0] == url.split('#')[0]
|
102
|
+
# Hash change occurred, so there will be no onLoadFinished
|
103
|
+
@currentPage.state = 'default'
|
104
|
+
@current_command.sendResponse(status: 'success')
|
105
|
+
else
|
106
|
+
command = @current_command
|
107
|
+
@currentPage.waitState 'default', =>
|
108
|
+
if @currentPage.statusCode == null && @currentPage.status == 'fail'
|
109
|
+
command.sendError(new Poltergeist.StatusFailError(url))
|
110
|
+
else
|
111
|
+
command.sendResponse(status: @currentPage.status)
|
112
|
+
return
|
113
|
+
|
114
|
+
current_url: ->
|
115
|
+
@current_command.sendResponse @currentPage.currentUrl()
|
116
|
+
|
117
|
+
status_code: ->
|
118
|
+
@current_command.sendResponse @currentPage.statusCode
|
119
|
+
|
120
|
+
body: ->
|
121
|
+
@current_command.sendResponse @currentPage.content()
|
122
|
+
|
123
|
+
source: ->
|
124
|
+
@current_command.sendResponse @currentPage.source
|
125
|
+
|
126
|
+
title: ->
|
127
|
+
@current_command.sendResponse @currentPage.title()
|
128
|
+
|
129
|
+
find: (method, selector) ->
|
130
|
+
@current_command.sendResponse(page_id: @currentPage.id, ids: @currentPage.find(method, selector))
|
131
|
+
|
132
|
+
find_within: (page_id, id, method, selector) ->
|
133
|
+
@current_command.sendResponse this.node(page_id, id).find(method, selector)
|
134
|
+
|
135
|
+
all_text: (page_id, id) ->
|
136
|
+
@current_command.sendResponse this.node(page_id, id).allText()
|
137
|
+
|
138
|
+
visible_text: (page_id, id) ->
|
139
|
+
@current_command.sendResponse this.node(page_id, id).visibleText()
|
140
|
+
|
141
|
+
delete_text: (page_id, id) ->
|
142
|
+
@current_command.sendResponse this.node(page_id, id).deleteText()
|
143
|
+
|
144
|
+
property: (page_id, id, name) ->
|
145
|
+
@current_command.sendResponse this.node(page_id, id).getProperty(name)
|
146
|
+
|
147
|
+
attribute: (page_id, id, name) ->
|
148
|
+
@current_command.sendResponse this.node(page_id, id).getAttribute(name)
|
149
|
+
|
150
|
+
attributes: (page_id, id, name) ->
|
151
|
+
@current_command.sendResponse this.node(page_id, id).getAttributes()
|
152
|
+
|
153
|
+
parents: (page_id, id) ->
|
154
|
+
@current_command.sendResponse this.node(page_id, id).parentIds()
|
155
|
+
|
156
|
+
value: (page_id, id) ->
|
157
|
+
@current_command.sendResponse this.node(page_id, id).value()
|
158
|
+
|
159
|
+
set: (page_id, id, value) ->
|
160
|
+
this.node(page_id, id).set(value)
|
161
|
+
@current_command.sendResponse(true)
|
162
|
+
|
163
|
+
# PhantomJS only allows us to reference the element by CSS selector, not XPath,
|
164
|
+
# so we have to add an attribute to the element to identify it, then remove it
|
165
|
+
# afterwards.
|
166
|
+
select_file: (page_id, id, value) ->
|
167
|
+
node = this.node(page_id, id)
|
168
|
+
|
169
|
+
@currentPage.beforeUpload(node.id)
|
170
|
+
@currentPage.uploadFile('[_poltergeist_selected]', value)
|
171
|
+
@currentPage.afterUpload(node.id)
|
172
|
+
if phantom.version.major == 2
|
173
|
+
# In phantomjs 2 - uploadFile only fully works if executed within a user action
|
174
|
+
# It does however setup the filenames to be uploaded, so if we then click on the
|
175
|
+
# file input element the filenames will get set
|
176
|
+
@click(page_id, id)
|
177
|
+
else
|
178
|
+
@current_command.sendResponse(true)
|
179
|
+
|
180
|
+
select: (page_id, id, value) ->
|
181
|
+
@current_command.sendResponse this.node(page_id, id).select(value)
|
182
|
+
|
183
|
+
tag_name: (page_id, id) ->
|
184
|
+
@current_command.sendResponse this.node(page_id, id).tagName()
|
185
|
+
|
186
|
+
visible: (page_id, id) ->
|
187
|
+
@current_command.sendResponse this.node(page_id, id).isVisible()
|
188
|
+
|
189
|
+
disabled: (page_id, id) ->
|
190
|
+
@current_command.sendResponse this.node(page_id, id).isDisabled()
|
191
|
+
|
192
|
+
path: (page_id, id) ->
|
193
|
+
@current_command.sendResponse this.node(page_id, id).path()
|
194
|
+
|
195
|
+
evaluate: (script) ->
|
196
|
+
@current_command.sendResponse @currentPage.evaluate("function() { return #{script} }")
|
197
|
+
|
198
|
+
execute: (script) ->
|
199
|
+
@currentPage.execute("function() { #{script} }")
|
200
|
+
@current_command.sendResponse(true)
|
201
|
+
|
202
|
+
frameUrl: (frame_name) ->
|
203
|
+
@currentPage.frameUrl(frame_name)
|
204
|
+
|
205
|
+
pushFrame: (command, name, timeout) ->
|
206
|
+
if Array.isArray(name)
|
207
|
+
frame = this.node(name...)
|
208
|
+
name = frame.getAttribute('name') || frame.getAttribute('id')
|
209
|
+
unless name
|
210
|
+
frame.setAttribute('name', "_random_name_#{new Date().getTime()}")
|
211
|
+
name = frame.getAttribute('name')
|
212
|
+
|
213
|
+
if @frameUrl(name) in @currentPage.blockedUrls()
|
214
|
+
command.sendResponse(true)
|
215
|
+
else if @currentPage.pushFrame(name)
|
216
|
+
if @currentPage.currentUrl() == 'about:blank'
|
217
|
+
@currentPage.state = 'awaiting_frame_load'
|
218
|
+
@currentPage.waitState 'default', =>
|
219
|
+
command.sendResponse(true)
|
220
|
+
else
|
221
|
+
command.sendResponse(true)
|
222
|
+
else
|
223
|
+
if new Date().getTime() < timeout
|
224
|
+
setTimeout((=> @pushFrame(command, name, timeout)), 50)
|
225
|
+
else
|
226
|
+
command.sendError(new Poltergeist.FrameNotFound(name))
|
227
|
+
|
228
|
+
push_frame: (name, timeout = (new Date().getTime()) + 2000) ->
|
229
|
+
@pushFrame(@current_command, name, timeout)
|
230
|
+
|
231
|
+
pop_frame: ->
|
232
|
+
@current_command.sendResponse(@currentPage.popFrame())
|
233
|
+
|
234
|
+
window_handles: ->
|
235
|
+
handles = @pages.filter((p) -> !p.closed).map((p) -> p.handle)
|
236
|
+
@current_command.sendResponse(handles)
|
237
|
+
|
238
|
+
window_handle: (name = null) ->
|
239
|
+
handle = if name
|
240
|
+
page = @pages.filter((p) -> !p.closed && p.windowName() == name)[0]
|
241
|
+
if page then page.handle else null
|
242
|
+
else
|
243
|
+
@currentPage.handle
|
244
|
+
|
245
|
+
@current_command.sendResponse(handle)
|
246
|
+
|
247
|
+
switch_to_window: (handle) ->
|
248
|
+
command = @current_command
|
249
|
+
page = @getPageByHandle(handle)
|
250
|
+
if page
|
251
|
+
if page != @currentPage
|
252
|
+
page.waitState 'default', =>
|
253
|
+
@currentPage = page
|
254
|
+
command.sendResponse(true)
|
255
|
+
else
|
256
|
+
command.sendResponse(true)
|
257
|
+
else
|
258
|
+
throw new Poltergeist.NoSuchWindowError
|
259
|
+
|
260
|
+
open_new_window: ->
|
261
|
+
this.execute 'window.open()'
|
262
|
+
@current_command.sendResponse(true)
|
263
|
+
|
264
|
+
close_window: (handle) ->
|
265
|
+
page = @getPageByHandle(handle)
|
266
|
+
if page
|
267
|
+
page.release()
|
268
|
+
@current_command.sendResponse(true)
|
269
|
+
else
|
270
|
+
@current_command.sendResponse(false)
|
271
|
+
|
272
|
+
mouse_event: (page_id, id, name) ->
|
273
|
+
# Get the node before changing state, in case there is an exception
|
274
|
+
node = this.node(page_id, id)
|
275
|
+
# If the event triggers onNavigationRequested, we will transition to the 'loading'
|
276
|
+
# state and wait for onLoadFinished before sending a response.
|
277
|
+
@currentPage.state = 'mouse_event'
|
278
|
+
|
279
|
+
@last_mouse_event = node.mouseEvent(name)
|
280
|
+
|
281
|
+
command = @current_command
|
282
|
+
|
283
|
+
setTimeout =>
|
284
|
+
# If the state is still the same then navigation event won't happen
|
285
|
+
if @currentPage.state == 'mouse_event'
|
286
|
+
@currentPage.state = 'default'
|
287
|
+
command.sendResponse(position: @last_mouse_event)
|
288
|
+
else
|
289
|
+
@currentPage.waitState 'default', =>
|
290
|
+
command.sendResponse(position: @last_mouse_event)
|
291
|
+
, 5
|
292
|
+
|
293
|
+
click: (page_id, id) ->
|
294
|
+
this.mouse_event page_id, id, 'click'
|
295
|
+
|
296
|
+
right_click: (page_id, id) ->
|
297
|
+
this.mouse_event page_id, id, 'rightclick'
|
298
|
+
|
299
|
+
double_click: (page_id, id) ->
|
300
|
+
this.mouse_event page_id, id, 'doubleclick'
|
301
|
+
|
302
|
+
hover: (page_id, id) ->
|
303
|
+
this.mouse_event page_id, id, 'mousemove'
|
304
|
+
|
305
|
+
click_coordinates: (x, y) ->
|
306
|
+
@currentPage.sendEvent('click', x, y)
|
307
|
+
@current_command.sendResponse(click: { x: x, y: y })
|
308
|
+
|
309
|
+
drag: (page_id, id, other_id) ->
|
310
|
+
this.node(page_id, id).dragTo this.node(page_id, other_id)
|
311
|
+
@current_command.sendResponse(true)
|
312
|
+
|
313
|
+
drag_by: (page_id, id, x, y) ->
|
314
|
+
this.node(page_id, id).dragBy(x, y)
|
315
|
+
@current_command.sendResponse(true)
|
316
|
+
|
317
|
+
trigger: (page_id, id, event) ->
|
318
|
+
this.node(page_id, id).trigger(event)
|
319
|
+
@current_command.sendResponse(event)
|
320
|
+
|
321
|
+
equals: (page_id, id, other_id) ->
|
322
|
+
@current_command.sendResponse this.node(page_id, id).isEqual(this.node(page_id, other_id))
|
323
|
+
|
324
|
+
reset: ->
|
325
|
+
this.resetPage()
|
326
|
+
@current_command.sendResponse(true)
|
327
|
+
|
328
|
+
scroll_to: (left, top) ->
|
329
|
+
@currentPage.setScrollPosition(left: left, top: top)
|
330
|
+
@current_command.sendResponse(true)
|
331
|
+
|
332
|
+
send_keys: (page_id, id, keys) ->
|
333
|
+
target = this.node(page_id, id)
|
334
|
+
|
335
|
+
# Programmatically generated focus doesn't work for `sendKeys`.
|
336
|
+
# That's why we need something more realistic like user behavior.
|
337
|
+
if !target.containsSelection()
|
338
|
+
target.mouseEvent('click')
|
339
|
+
|
340
|
+
for sequence in keys
|
341
|
+
key = if sequence.key? then @currentPage.keyCode(sequence.key) else sequence
|
342
|
+
if sequence.modifier?
|
343
|
+
modifier_keys = @currentPage.keyModifierKeys(sequence.modifier)
|
344
|
+
modifier_code = @currentPage.keyModifierCode(sequence.modifier)
|
345
|
+
@currentPage.sendEvent('keydown', modifier_key) for modifier_key in modifier_keys
|
346
|
+
@currentPage.sendEvent('keypress', key, null, null, modifier_code)
|
347
|
+
@currentPage.sendEvent('keyup', modifier_key) for modifier_key in modifier_keys
|
348
|
+
else
|
349
|
+
@currentPage.sendEvent('keypress', key)
|
350
|
+
|
351
|
+
@current_command.sendResponse(true)
|
352
|
+
|
353
|
+
render_base64: (format, full, selector = null)->
|
354
|
+
this.set_clip_rect(full, selector)
|
355
|
+
encoded_image = @currentPage.renderBase64(format)
|
356
|
+
@current_command.sendResponse(encoded_image)
|
357
|
+
|
358
|
+
render: (path, full, selector = null) ->
|
359
|
+
dimensions = this.set_clip_rect(full, selector)
|
360
|
+
@currentPage.setScrollPosition(left: 0, top: 0)
|
361
|
+
@currentPage.render(path)
|
362
|
+
@currentPage.setScrollPosition(left: dimensions.left, top: dimensions.top)
|
363
|
+
@current_command.sendResponse(true)
|
364
|
+
|
365
|
+
set_clip_rect: (full, selector) ->
|
366
|
+
dimensions = @currentPage.validatedDimensions()
|
367
|
+
[document, viewport] = [dimensions.document, dimensions.viewport]
|
368
|
+
|
369
|
+
rect = if full
|
370
|
+
left: 0, top: 0, width: document.width, height: document.height
|
371
|
+
else
|
372
|
+
if selector?
|
373
|
+
@currentPage.elementBounds(selector)
|
374
|
+
else
|
375
|
+
left: 0, top: 0, width: viewport.width, height: viewport.height
|
376
|
+
|
377
|
+
@currentPage.setClipRect(rect)
|
378
|
+
dimensions
|
379
|
+
|
380
|
+
set_paper_size: (size) ->
|
381
|
+
@currentPage.setPaperSize(size)
|
382
|
+
@current_command.sendResponse(true)
|
383
|
+
|
384
|
+
set_zoom_factor: (zoom_factor) ->
|
385
|
+
@currentPage.setZoomFactor(zoom_factor)
|
386
|
+
@current_command.sendResponse(true)
|
387
|
+
|
388
|
+
resize: (width, height) ->
|
389
|
+
@currentPage.setViewportSize(width: width, height: height)
|
390
|
+
@current_command.sendResponse(true)
|
391
|
+
|
392
|
+
network_traffic: ->
|
393
|
+
@current_command.sendResponse(@currentPage.networkTraffic())
|
394
|
+
|
395
|
+
clear_network_traffic: ->
|
396
|
+
@currentPage.clearNetworkTraffic()
|
397
|
+
@current_command.sendResponse(true)
|
398
|
+
|
399
|
+
get_headers: ->
|
400
|
+
@current_command.sendResponse(@currentPage.getCustomHeaders())
|
401
|
+
|
402
|
+
set_headers: (headers) ->
|
403
|
+
# Workaround for https://code.google.com/p/phantomjs/issues/detail?id=745
|
404
|
+
@currentPage.setUserAgent(headers['User-Agent']) if headers['User-Agent']
|
405
|
+
@currentPage.setCustomHeaders(headers)
|
406
|
+
@current_command.sendResponse(true)
|
407
|
+
|
408
|
+
add_headers: (headers) ->
|
409
|
+
allHeaders = @currentPage.getCustomHeaders()
|
410
|
+
for name, value of headers
|
411
|
+
allHeaders[name] = value
|
412
|
+
this.set_headers(allHeaders)
|
413
|
+
|
414
|
+
add_header: (header, permanent) ->
|
415
|
+
@currentPage.addTempHeader(header) unless permanent
|
416
|
+
this.add_headers(header)
|
417
|
+
|
418
|
+
response_headers: ->
|
419
|
+
@current_command.sendResponse(@currentPage.responseHeaders())
|
420
|
+
|
421
|
+
cookies: ->
|
422
|
+
@current_command.sendResponse(@currentPage.cookies())
|
423
|
+
|
424
|
+
# We're using phantom.addCookie so that cookies can be set
|
425
|
+
# before the first page load has taken place.
|
426
|
+
set_cookie: (cookie) ->
|
427
|
+
phantom.addCookie(cookie)
|
428
|
+
@current_command.sendResponse(true)
|
429
|
+
|
430
|
+
remove_cookie: (name) ->
|
431
|
+
@currentPage.deleteCookie(name)
|
432
|
+
@current_command.sendResponse(true)
|
433
|
+
|
434
|
+
clear_cookies: () ->
|
435
|
+
phantom.clearCookies()
|
436
|
+
@current_command.sendResponse(true)
|
437
|
+
|
438
|
+
cookies_enabled: (flag) ->
|
439
|
+
phantom.cookiesEnabled = flag
|
440
|
+
@current_command.sendResponse(true)
|
441
|
+
|
442
|
+
set_http_auth: (user, password) ->
|
443
|
+
@currentPage.setHttpAuth(user, password)
|
444
|
+
@current_command.sendResponse(true)
|
445
|
+
|
446
|
+
set_js_errors: (value) ->
|
447
|
+
@js_errors = value
|
448
|
+
@current_command.sendResponse(true)
|
449
|
+
|
450
|
+
set_debug: (value) ->
|
451
|
+
@_debug = value
|
452
|
+
@current_command.sendResponse(true)
|
453
|
+
|
454
|
+
exit: ->
|
455
|
+
phantom.exit()
|
456
|
+
|
457
|
+
noop: ->
|
458
|
+
# NOOOOOOP!
|
459
|
+
|
460
|
+
# This command is purely for testing error handling
|
461
|
+
browser_error: ->
|
462
|
+
throw new Error('zomg')
|
463
|
+
|
464
|
+
go_back: ->
|
465
|
+
command = @current_command
|
466
|
+
if @currentPage.canGoBack
|
467
|
+
@currentPage.state = 'loading'
|
468
|
+
@currentPage.goBack()
|
469
|
+
@currentPage.waitState 'default', =>
|
470
|
+
command.sendResponse(true)
|
471
|
+
else
|
472
|
+
command.sendResponse(false)
|
473
|
+
|
474
|
+
go_forward: ->
|
475
|
+
command = @current_command
|
476
|
+
if @currentPage.canGoForward
|
477
|
+
@currentPage.state = 'loading'
|
478
|
+
@currentPage.goForward()
|
479
|
+
@currentPage.waitState 'default', =>
|
480
|
+
command.sendResponse(true)
|
481
|
+
else
|
482
|
+
command.sendResponse(false)
|
483
|
+
|
484
|
+
set_url_blacklist: ->
|
485
|
+
@currentPage.urlBlacklist = Array.prototype.slice.call(arguments)
|
486
|
+
@current_command.sendResponse(true)
|
487
|
+
|
488
|
+
set_confirm_process: (process) ->
|
489
|
+
@confirm_processes.push process
|
490
|
+
@current_command.sendResponse(true)
|
491
|
+
|
492
|
+
set_prompt_response: (response) ->
|
493
|
+
@prompt_responses.push response
|
494
|
+
@current_command.sendResponse(true)
|
495
|
+
|
496
|
+
modal_message: ->
|
497
|
+
@current_command.sendResponse(@processed_modal_messages.shift())
|