poltergeist 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,7 @@
1
1
  class Poltergeist.Browser
2
- constructor: (@owner) ->
2
+ constructor: (@owner, width, height) ->
3
+ @width = width || 1024
4
+ @height = height || 768
3
5
  @state = 'default'
4
6
  @page_id = 0
5
7
 
@@ -7,11 +9,14 @@ class Poltergeist.Browser
7
9
 
8
10
  resetPage: ->
9
11
  @page.release() if @page?
10
- @page = new Poltergeist.WebPage
12
+ @page = new Poltergeist.WebPage(@width, @height)
11
13
 
12
14
  @page.onLoadStarted = =>
13
15
  @state = 'loading' if @state == 'clicked'
14
16
 
17
+ @page.onNavigationRequested = (url, navigation) =>
18
+ @state = 'loading' if @state == 'clicked' && navigation == 'FormSubmitted'
19
+
15
20
  @page.onLoadFinished = (status) =>
16
21
  if @state == 'loading'
17
22
  this.sendResponse(status)
@@ -25,37 +30,26 @@ class Poltergeist.Browser
25
30
 
26
31
  if errors.length > 0
27
32
  @page.clearErrors()
28
- @owner.sendError(new Poltergeist.JavascriptError(errors))
33
+ throw new Poltergeist.JavascriptError(errors)
29
34
  else
30
35
  @owner.sendResponse(response)
31
36
 
32
- getNode: (page_id, id, callback) ->
37
+ node: (page_id, id) ->
33
38
  if page_id == @page_id
34
- callback.call this, @page.get(id)
39
+ @page.get(id)
35
40
  else
36
- @owner.sendError(new Poltergeist.ObsoleteNode)
37
-
38
- nodeCall: (page_id, id, fn, args...) ->
39
- callback = args.pop()
40
-
41
- this.getNode(
42
- page_id, id,
43
- (node) ->
44
- result = node[fn](args...)
45
-
46
- if result instanceof Poltergeist.ObsoleteNode
47
- @owner.sendError(result)
48
- else
49
- callback.call(this, result, node)
50
- )
41
+ throw new Poltergeist.ObsoleteNode
51
42
 
52
- visit: (url) ->
43
+ visit: (url, headers) ->
53
44
  @state = 'loading'
54
- @page.open(url)
45
+ @page.open(url, operation: "get", headers: headers)
55
46
 
56
47
  current_url: ->
57
48
  this.sendResponse @page.currentUrl()
58
49
 
50
+ status_code: ->
51
+ this.sendResponse @page.statusCode()
52
+
59
53
  body: ->
60
54
  this.sendResponse @page.content()
61
55
 
@@ -66,53 +60,44 @@ class Poltergeist.Browser
66
60
  this.sendResponse(page_id: @page_id, ids: @page.find(selector))
67
61
 
68
62
  find_within: (page_id, id, selector) ->
69
- this.nodeCall(page_id, id, 'find', selector, this.sendResponse)
63
+ this.sendResponse this.node(page_id, id).find(selector)
70
64
 
71
65
  text: (page_id, id) ->
72
- this.nodeCall(page_id, id, 'text', this.sendResponse)
66
+ this.sendResponse this.node(page_id, id).text()
73
67
 
74
68
  attribute: (page_id, id, name) ->
75
- this.nodeCall(page_id, id, 'getAttribute', name, this.sendResponse)
69
+ this.sendResponse this.node(page_id, id).getAttribute(name)
76
70
 
77
71
  value: (page_id, id) ->
78
- this.nodeCall(page_id, id, 'value', this.sendResponse)
72
+ this.sendResponse this.node(page_id, id).value()
79
73
 
80
74
  set: (page_id, id, value) ->
81
- this.nodeCall(page_id, id, 'set', value, -> this.sendResponse(true))
75
+ this.node(page_id, id).set(value)
76
+ this.sendResponse(true)
82
77
 
83
78
  # PhantomJS only allows us to reference the element by CSS selector, not XPath,
84
79
  # so we have to add an attribute to the element to identify it, then remove it
85
80
  # afterwards.
86
- #
87
- # PhantomJS does not support multiple-file inputs, so we have to blatently cheat
88
- # by temporarily changing it to a single-file input. This obviously could break
89
- # things in various ways, which is not ideal, but it works in the simplest case.
90
81
  select_file: (page_id, id, value) ->
91
- this.nodeCall(
92
- page_id, id, 'isMultiple',
93
- (multiple, node) ->
94
- node.removeAttribute('multiple') if multiple
95
- node.setAttribute('_poltergeist_selected', '')
82
+ node = this.node(page_id, id)
96
83
 
97
- @page.uploadFile('[_poltergeist_selected]', value)
84
+ node.setAttribute('_poltergeist_selected', '')
85
+ @page.uploadFile('[_poltergeist_selected]', value)
86
+ node.removeAttribute('_poltergeist_selected')
98
87
 
99
- node.removeAttribute('_poltergeist_selected')
100
- node.setAttribute('multiple', 'multiple') if multiple
101
-
102
- this.sendResponse(true)
103
- )
88
+ this.sendResponse(true)
104
89
 
105
90
  select: (page_id, id, value) ->
106
- this.nodeCall(page_id, id, 'select', value, this.sendResponse)
91
+ this.sendResponse this.node(page_id, id).select(value)
107
92
 
108
93
  tag_name: (page_id, id) ->
109
- this.nodeCall(page_id, id, 'tagName', this.sendResponse)
94
+ this.sendResponse this.node(page_id, id).tagName()
110
95
 
111
96
  visible: (page_id, id) ->
112
- this.nodeCall(page_id, id, 'isVisible', this.sendResponse)
97
+ this.sendResponse this.node(page_id, id).isVisible()
113
98
 
114
99
  evaluate: (script) ->
115
- this.sendResponse JSON.parse(@page.evaluate("function() { return JSON.stringify(#{script}) }"))
100
+ this.sendResponse @page.evaluate("function() { return #{script} }")
116
101
 
117
102
  execute: (script) ->
118
103
  @page.execute("function() { #{script} }")
@@ -127,43 +112,29 @@ class Poltergeist.Browser
127
112
  this.sendResponse(true)
128
113
 
129
114
  click: (page_id, id) ->
130
- # We just check the node is not obsolete before proceeding. If it is,
131
- # the callback will not fire.
132
- this.nodeCall(
133
- page_id, id, 'isObsolete',
134
- (obsolete, node) ->
135
- # If the click event triggers onLoadStarted, we will transition to the 'loading'
136
- # state and wait for onLoadFinished before sending a response.
137
- @state = 'clicked'
138
-
139
- click = node.click()
140
-
141
- # Use a timeout in order to let the stack clear, so that the @page.onLoadStarted
142
- # callback can (possibly) fire, before we decide whether to send a response.
143
- setTimeout(
144
- =>
145
- if @state == 'clicked'
146
- @state = 'default'
147
-
148
- if click instanceof Poltergeist.ClickFailed
149
- @owner.sendError(click)
150
- else
151
- this.sendResponse(true)
152
- ,
153
- 10
154
- )
155
- )
115
+ # Get the node before changing state, in case there is an exception
116
+ node = this.node(page_id, id)
117
+
118
+ # If the click event triggers onNavigationRequested, we will transition to the 'loading'
119
+ # state and wait for onLoadFinished before sending a response.
120
+ @state = 'clicked'
121
+
122
+ node.click()
123
+
124
+ if @state != 'loading'
125
+ @state = 'default'
126
+ this.sendResponse(true)
156
127
 
157
128
  drag: (page_id, id, other_id) ->
158
- this.nodeCall(
159
- page_id, id, 'isObsolete'
160
- (obsolete, node) ->
161
- node.dragTo(@page.get(other_id))
162
- this.sendResponse(true)
163
- )
129
+ this.node(page_id, id).dragTo this.node(page_id, other_id)
130
+ this.sendResponse(true)
164
131
 
165
132
  trigger: (page_id, id, event) ->
166
- this.nodeCall(page_id, id, 'trigger', event, -> this.sendResponse(event))
133
+ this.node(page_id, id).trigger(event)
134
+ this.sendResponse(event)
135
+
136
+ equals: (page_id, id, other_id) ->
137
+ this.sendResponse this.node(page_id, id).isEqual(this.node(page_id, other_id))
167
138
 
168
139
  reset: ->
169
140
  this.resetPage()
@@ -189,8 +160,15 @@ class Poltergeist.Browser
189
160
  @page.setViewportSize(width: width, height: height)
190
161
  this.sendResponse(true)
191
162
 
163
+ network_traffic: ->
164
+ this.sendResponse(@page.networkTraffic())
165
+
192
166
  exit: ->
193
167
  phantom.exit()
194
168
 
195
169
  noop: ->
196
170
  # NOOOOOOP!
171
+
172
+ # This command is purely for testing error handling
173
+ browser_error: ->
174
+ throw new Error('zomg')
@@ -1,101 +1,146 @@
1
1
  var PoltergeistAgent;
2
- var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
2
+
3
3
  PoltergeistAgent = (function() {
4
+
4
5
  function PoltergeistAgent() {
5
6
  this.elements = [];
6
7
  this.nodes = {};
7
8
  this.windows = [];
8
9
  this.pushWindow(window);
9
10
  }
10
- PoltergeistAgent.prototype.externalCall = function(name, arguments) {
11
- return {
12
- value: this[name].apply(this, arguments)
13
- };
11
+
12
+ PoltergeistAgent.prototype.externalCall = function(name, args) {
13
+ try {
14
+ return {
15
+ value: this[name].apply(this, args)
16
+ };
17
+ } catch (error) {
18
+ return {
19
+ error: {
20
+ message: error.toString(),
21
+ stack: error.stack
22
+ }
23
+ };
24
+ }
25
+ };
26
+
27
+ PoltergeistAgent.stringify = function(object) {
28
+ return JSON.stringify(object, function(key, value) {
29
+ if (Array.isArray(this[key])) {
30
+ return this[key];
31
+ } else {
32
+ return value;
33
+ }
34
+ });
14
35
  };
36
+
15
37
  PoltergeistAgent.prototype.pushWindow = function(new_window) {
16
38
  this.windows.push(new_window);
17
39
  this.window = new_window;
18
40
  this.document = this.window.document;
19
41
  return null;
20
42
  };
43
+
21
44
  PoltergeistAgent.prototype.popWindow = function() {
22
45
  this.windows.pop();
23
46
  this.window = this.windows[this.windows.length - 1];
24
47
  this.document = this.window.document;
25
48
  return null;
26
49
  };
50
+
27
51
  PoltergeistAgent.prototype.pushFrame = function(id) {
28
52
  return this.pushWindow(this.document.getElementById(id).contentWindow);
29
53
  };
54
+
30
55
  PoltergeistAgent.prototype.popFrame = function() {
31
56
  return this.popWindow();
32
57
  };
58
+
33
59
  PoltergeistAgent.prototype.currentUrl = function() {
34
60
  return window.location.toString();
35
61
  };
62
+
36
63
  PoltergeistAgent.prototype.find = function(selector, within) {
37
- var i, ids, results, _ref;
64
+ var i, ids, results, _i, _ref;
38
65
  if (within == null) {
39
66
  within = this.document;
40
67
  }
41
68
  results = this.document.evaluate(selector, within, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
42
69
  ids = [];
43
- for (i = 0, _ref = results.snapshotLength; 0 <= _ref ? i < _ref : i > _ref; 0 <= _ref ? i++ : i--) {
70
+ for (i = _i = 0, _ref = results.snapshotLength; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) {
44
71
  ids.push(this.register(results.snapshotItem(i)));
45
72
  }
46
73
  return ids;
47
74
  };
75
+
48
76
  PoltergeistAgent.prototype.register = function(element) {
49
77
  this.elements.push(element);
50
78
  return this.elements.length - 1;
51
79
  };
80
+
52
81
  PoltergeistAgent.prototype.documentSize = function() {
53
82
  return {
54
83
  height: this.document.documentElement.scrollHeight,
55
84
  width: this.document.documentElement.scrollWidth
56
85
  };
57
86
  };
87
+
58
88
  PoltergeistAgent.prototype.get = function(id) {
59
89
  var _base;
60
90
  return (_base = this.nodes)[id] || (_base[id] = new PoltergeistAgent.Node(this, this.elements[id]));
61
91
  };
62
- PoltergeistAgent.prototype.nodeCall = function(id, name, arguments) {
92
+
93
+ PoltergeistAgent.prototype.nodeCall = function(id, name, args) {
63
94
  var node;
64
95
  node = this.get(id);
65
96
  if (node.isObsolete()) {
66
97
  throw new PoltergeistAgent.ObsoleteNode;
67
98
  }
68
- return node[name].apply(node, arguments);
99
+ return node[name].apply(node, args);
69
100
  };
101
+
70
102
  return PoltergeistAgent;
103
+
71
104
  })();
105
+
72
106
  PoltergeistAgent.ObsoleteNode = (function() {
107
+
73
108
  function ObsoleteNode() {}
109
+
74
110
  ObsoleteNode.prototype.toString = function() {
75
111
  return "PoltergeistAgent.ObsoleteNode";
76
112
  };
113
+
77
114
  return ObsoleteNode;
115
+
78
116
  })();
117
+
79
118
  PoltergeistAgent.Node = (function() {
119
+
80
120
  Node.EVENTS = {
81
121
  FOCUS: ['blur', 'focus', 'focusin', 'focusout'],
82
122
  MOUSE: ['click', 'dblclick', 'mousedown', 'mouseenter', 'mouseleave', 'mousemove', 'mouseover', 'mouseout', 'mouseup']
83
123
  };
124
+
84
125
  function Node(agent, element) {
85
126
  this.agent = agent;
86
127
  this.element = element;
87
128
  }
129
+
88
130
  Node.prototype.parentId = function() {
89
131
  return this.agent.register(this.element.parentNode);
90
132
  };
133
+
91
134
  Node.prototype.find = function(selector) {
92
135
  return this.agent.find(selector, this.element);
93
136
  };
137
+
94
138
  Node.prototype.isObsolete = function() {
95
- var obsolete;
96
- obsolete = __bind(function(element) {
139
+ var obsolete,
140
+ _this = this;
141
+ obsolete = function(element) {
97
142
  if (element.parentNode != null) {
98
- if (element.parentNode === this.agent.document) {
143
+ if (element.parentNode === _this.agent.document) {
99
144
  return false;
100
145
  } else {
101
146
  return obsolete(element.parentNode);
@@ -103,20 +148,55 @@ PoltergeistAgent.Node = (function() {
103
148
  } else {
104
149
  return true;
105
150
  }
106
- }, this);
151
+ };
107
152
  return obsolete(this.element);
108
153
  };
154
+
109
155
  Node.prototype.changed = function() {
110
156
  var event;
111
157
  event = document.createEvent('HTMLEvents');
112
- event.initEvent("change", true, false);
158
+ event.initEvent('change', true, false);
159
+ return this.element.dispatchEvent(event);
160
+ };
161
+
162
+ Node.prototype.input = function() {
163
+ var event;
164
+ event = document.createEvent('HTMLEvents');
165
+ event.initEvent('input', true, false);
113
166
  return this.element.dispatchEvent(event);
114
167
  };
168
+
169
+ Node.prototype.keyupdowned = function(eventName, keyCode) {
170
+ var event;
171
+ event = document.createEvent('UIEvents');
172
+ event.initEvent(eventName, true, true);
173
+ event.keyCode = keyCode;
174
+ event.which = keyCode;
175
+ event.charCode = 0;
176
+ return this.element.dispatchEvent(event);
177
+ };
178
+
179
+ Node.prototype.keypressed = function(altKey, ctrlKey, shiftKey, metaKey, keyCode, charCode) {
180
+ var event;
181
+ event = document.createEvent('UIEvents');
182
+ event.initEvent('keypress', true, true);
183
+ event.window = this.agent.window;
184
+ event.altKey = altKey;
185
+ event.ctrlKey = ctrlKey;
186
+ event.shiftKey = shiftKey;
187
+ event.metaKey = metaKey;
188
+ event.keyCode = keyCode;
189
+ event.charCode = charCode;
190
+ event.which = keyCode;
191
+ return this.element.dispatchEvent(event);
192
+ };
193
+
115
194
  Node.prototype.insideBody = function() {
116
195
  return this.element === this.agent.document.body || this.agent.document.evaluate('ancestor::body', this.element, null, XPathResult.BOOLEAN_TYPE, null).booleanValue;
117
196
  };
197
+
118
198
  Node.prototype.text = function() {
119
- var el, i, node, results, text, _ref;
199
+ var el, i, node, results, text, _i, _ref;
120
200
  if (!this.isVisible()) {
121
201
  return '';
122
202
  }
@@ -127,7 +207,7 @@ PoltergeistAgent.Node = (function() {
127
207
  }
128
208
  results = this.agent.document.evaluate('.//text()[not(ancestor::script)]', el, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
129
209
  text = '';
130
- for (i = 0, _ref = results.snapshotLength; 0 <= _ref ? i < _ref : i > _ref; 0 <= _ref ? i++ : i--) {
210
+ for (i = _i = 0, _ref = results.snapshotLength; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) {
131
211
  node = results.snapshotItem(i);
132
212
  if (this.isVisible(node.parentNode)) {
133
213
  text += node.textContent;
@@ -135,6 +215,7 @@ PoltergeistAgent.Node = (function() {
135
215
  }
136
216
  return text;
137
217
  };
218
+
138
219
  Node.prototype.getAttribute = function(name) {
139
220
  if (name === 'checked' || name === 'selected') {
140
221
  return this.element[name];
@@ -142,6 +223,11 @@ PoltergeistAgent.Node = (function() {
142
223
  return this.element.getAttribute(name);
143
224
  }
144
225
  };
226
+
227
+ Node.prototype.scrollIntoView = function() {
228
+ return this.element.scrollIntoViewIfNeeded();
229
+ };
230
+
145
231
  Node.prototype.value = function() {
146
232
  var option, _i, _len, _ref, _results;
147
233
  if (this.element.tagName === 'SELECT' && this.element.multiple) {
@@ -158,22 +244,39 @@ PoltergeistAgent.Node = (function() {
158
244
  return this.element.value;
159
245
  }
160
246
  };
247
+
161
248
  Node.prototype.set = function(value) {
249
+ var char, keyCode, _i, _len;
162
250
  if (this.element.maxLength >= 0) {
163
251
  value = value.substr(0, this.element.maxLength);
164
252
  }
165
- this.element.value = value;
166
- return this.changed();
253
+ this.element.value = '';
254
+ this.trigger('focus');
255
+ for (_i = 0, _len = value.length; _i < _len; _i++) {
256
+ char = value[_i];
257
+ this.element.value += char;
258
+ keyCode = this.characterToKeyCode(char);
259
+ this.keyupdowned('keydown', keyCode);
260
+ this.keypressed(false, false, false, false, char.charCodeAt(0), char.charCodeAt(0));
261
+ this.keyupdowned('keyup', keyCode);
262
+ }
263
+ this.changed();
264
+ this.input();
265
+ return this.trigger('blur');
167
266
  };
267
+
168
268
  Node.prototype.isMultiple = function() {
169
269
  return this.element.multiple;
170
270
  };
271
+
171
272
  Node.prototype.setAttribute = function(name, value) {
172
273
  return this.element.setAttribute(name, value);
173
274
  };
275
+
174
276
  Node.prototype.removeAttribute = function(name) {
175
277
  return this.element.removeAttribute(name);
176
278
  };
279
+
177
280
  Node.prototype.select = function(value) {
178
281
  if (value === false && !this.element.parentNode.multiple) {
179
282
  return false;
@@ -183,9 +286,11 @@ PoltergeistAgent.Node = (function() {
183
286
  return true;
184
287
  }
185
288
  };
289
+
186
290
  Node.prototype.tagName = function() {
187
291
  return this.element.tagName;
188
292
  };
293
+
189
294
  Node.prototype.isVisible = function(element) {
190
295
  if (!element) {
191
296
  element = this.element;
@@ -198,6 +303,7 @@ PoltergeistAgent.Node = (function() {
198
303
  return true;
199
304
  }
200
305
  };
306
+
201
307
  Node.prototype.position = function() {
202
308
  var rect;
203
309
  rect = this.element.getClientRects()[0];
@@ -210,6 +316,7 @@ PoltergeistAgent.Node = (function() {
210
316
  height: rect.height
211
317
  };
212
318
  };
319
+
213
320
  Node.prototype.trigger = function(name) {
214
321
  var event;
215
322
  if (Node.EVENTS.MOUSE.indexOf(name) !== -1) {
@@ -223,6 +330,7 @@ PoltergeistAgent.Node = (function() {
223
330
  }
224
331
  return this.element.dispatchEvent(event);
225
332
  };
333
+
226
334
  Node.prototype.clickTest = function(x, y) {
227
335
  var el, origEl;
228
336
  el = origEl = document.elementFromPoint(x, y);
@@ -240,6 +348,7 @@ PoltergeistAgent.Node = (function() {
240
348
  selector: origEl && this.getSelector(origEl)
241
349
  };
242
350
  };
351
+
243
352
  Node.prototype.getSelector = function(el) {
244
353
  var className, selector, _i, _len, _ref;
245
354
  selector = el.tagName !== 'HTML' ? this.getSelector(el.parentNode) + ' ' : '';
@@ -254,15 +363,66 @@ PoltergeistAgent.Node = (function() {
254
363
  }
255
364
  return selector;
256
365
  };
366
+
367
+ Node.prototype.characterToKeyCode = function(character) {
368
+ var code, specialKeys;
369
+ code = character.toUpperCase().charCodeAt(0);
370
+ specialKeys = {
371
+ 96: 192,
372
+ 45: 189,
373
+ 61: 187,
374
+ 91: 219,
375
+ 93: 221,
376
+ 92: 220,
377
+ 59: 186,
378
+ 39: 222,
379
+ 44: 188,
380
+ 46: 190,
381
+ 47: 191,
382
+ 127: 46,
383
+ 126: 192,
384
+ 33: 49,
385
+ 64: 50,
386
+ 35: 51,
387
+ 36: 52,
388
+ 37: 53,
389
+ 94: 54,
390
+ 38: 55,
391
+ 42: 56,
392
+ 40: 57,
393
+ 41: 48,
394
+ 95: 189,
395
+ 43: 187,
396
+ 123: 219,
397
+ 125: 221,
398
+ 124: 220,
399
+ 58: 186,
400
+ 34: 222,
401
+ 60: 188,
402
+ 62: 190,
403
+ 63: 191
404
+ };
405
+ return specialKeys[code] || code;
406
+ };
407
+
408
+ Node.prototype.isDOMEqual = function(other_id) {
409
+ return this.element === this.agent.get(other_id).element;
410
+ };
411
+
257
412
  return Node;
413
+
258
414
  })();
415
+
259
416
  window.__poltergeist = new PoltergeistAgent;
417
+
260
418
  document.addEventListener('DOMContentLoaded', function() {
261
419
  return console.log('__DOMContentLoaded');
262
420
  });
421
+
263
422
  window.confirm = function(message) {
264
423
  return true;
265
424
  };
425
+
266
426
  window.prompt = function(message, _default) {
267
427
  return _default || null;
268
- };
428
+ };