poltergeistFork 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +22 -0
  3. data/README.md +425 -0
  4. data/lib/capybara/poltergeist/browser.rb +426 -0
  5. data/lib/capybara/poltergeist/client.rb +151 -0
  6. data/lib/capybara/poltergeist/client/agent.coffee +423 -0
  7. data/lib/capybara/poltergeist/client/browser.coffee +497 -0
  8. data/lib/capybara/poltergeist/client/cmd.coffee +17 -0
  9. data/lib/capybara/poltergeist/client/compiled/agent.js +587 -0
  10. data/lib/capybara/poltergeist/client/compiled/browser.js +687 -0
  11. data/lib/capybara/poltergeist/client/compiled/cmd.js +31 -0
  12. data/lib/capybara/poltergeist/client/compiled/connection.js +25 -0
  13. data/lib/capybara/poltergeist/client/compiled/main.js +228 -0
  14. data/lib/capybara/poltergeist/client/compiled/node.js +88 -0
  15. data/lib/capybara/poltergeist/client/compiled/web_page.js +539 -0
  16. data/lib/capybara/poltergeist/client/connection.coffee +11 -0
  17. data/lib/capybara/poltergeist/client/main.coffee +99 -0
  18. data/lib/capybara/poltergeist/client/node.coffee +70 -0
  19. data/lib/capybara/poltergeist/client/pre/agent.js +587 -0
  20. data/lib/capybara/poltergeist/client/pre/browser.js +688 -0
  21. data/lib/capybara/poltergeist/client/pre/cmd.js +31 -0
  22. data/lib/capybara/poltergeist/client/pre/connection.js +25 -0
  23. data/lib/capybara/poltergeist/client/pre/main.js +228 -0
  24. data/lib/capybara/poltergeist/client/pre/node.js +88 -0
  25. data/lib/capybara/poltergeist/client/pre/web_page.js +540 -0
  26. data/lib/capybara/poltergeist/client/web_page.coffee +372 -0
  27. data/lib/capybara/poltergeist/command.rb +17 -0
  28. data/lib/capybara/poltergeist/cookie.rb +35 -0
  29. data/lib/capybara/poltergeist/driver.rb +394 -0
  30. data/lib/capybara/poltergeist/errors.rb +183 -0
  31. data/lib/capybara/poltergeist/inspector.rb +46 -0
  32. data/lib/capybara/poltergeist/json.rb +25 -0
  33. data/lib/capybara/poltergeist/network_traffic.rb +7 -0
  34. data/lib/capybara/poltergeist/network_traffic/error.rb +19 -0
  35. data/lib/capybara/poltergeist/network_traffic/request.rb +27 -0
  36. data/lib/capybara/poltergeist/network_traffic/response.rb +40 -0
  37. data/lib/capybara/poltergeist/node.rb +177 -0
  38. data/lib/capybara/poltergeist/server.rb +36 -0
  39. data/lib/capybara/poltergeist/utility.rb +9 -0
  40. data/lib/capybara/poltergeist/version.rb +5 -0
  41. data/lib/capybara/poltergeist/web_socket_server.rb +107 -0
  42. data/lib/capybara/poltergeistFork.rb +27 -0
  43. 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())