poltergeist 0.6.0 → 0.7.0

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.
@@ -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
+ };