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,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())
|