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