poltergeist 1.3.0 → 1.4.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,10 +1,11 @@
1
1
  require "timeout"
2
2
  require "capybara/poltergeist/utility"
3
+ require 'cliver'
3
4
 
4
5
  module Capybara::Poltergeist
5
6
  class Client
6
7
  PHANTOMJS_SCRIPT = File.expand_path('../client/compiled/main.js', __FILE__)
7
- PHANTOMJS_VERSION = '1.8.1'
8
+ PHANTOMJS_VERSION = ['~> 1.8','>= 1.8.1']
8
9
  PHANTOMJS_NAME = 'phantomjs'
9
10
 
10
11
  KILL_TIMEOUT = 2 # seconds
@@ -15,11 +16,26 @@ module Capybara::Poltergeist
15
16
  client
16
17
  end
17
18
 
19
+ # Returns a proc, that when called will attempt to kill the given process.
20
+ # This is because implementing ObjectSpace.define_finalizer is tricky.
21
+ # Hat-Tip to @mperham for describing in detail:
22
+ # http://www.mikeperham.com/2010/02/24/the-trouble-with-ruby-finalizers/
23
+ def self.process_killer(pid)
24
+ proc do
25
+ begin
26
+ Process.kill('KILL', pid)
27
+ rescue Errno::ESRCH, Errno::ECHILD
28
+ end
29
+ end
30
+ end
31
+
18
32
  attr_reader :pid, :server, :path, :window_size, :phantomjs_options
19
33
 
20
34
  def initialize(server, options = {})
21
35
  @server = server
22
- @path = options[:path] || PHANTOMJS_NAME
36
+ @path = Cliver::detect!((options[:path] || PHANTOMJS_NAME),
37
+ *PHANTOMJS_VERSION)
38
+
23
39
  @window_size = options[:window_size] || [1024, 768]
24
40
  @phantomjs_options = options[:phantomjs_options] || []
25
41
  @phantomjs_logger = options[:phantomjs_logger] || $stdout
@@ -29,16 +45,19 @@ module Capybara::Poltergeist
29
45
  end
30
46
 
31
47
  def start
32
- check_phantomjs_version
33
- read, write = IO.pipe
48
+ @read_io, @write_io = IO.pipe
34
49
  @out_thread = Thread.new {
35
- while !read.eof? && data = read.readpartial(1024)
50
+ while !@read_io.eof? && data = @read_io.readpartial(1024)
36
51
  @phantomjs_logger.write(data)
37
52
  end
38
53
  }
39
54
 
40
- redirect_stdout(write) do
41
- @pid = Process.spawn(*command.map(&:to_s), pgroup: true)
55
+ process_options = {}
56
+ process_options[:pgroup] = true unless Capybara::Poltergeist.windows?
57
+
58
+ redirect_stdout do
59
+ @pid = Process.spawn(*command.map(&:to_s), process_options)
60
+ ObjectSpace.define_finalizer(self, self.class.process_killer(@pid) )
42
61
  end
43
62
  end
44
63
 
@@ -59,7 +78,9 @@ module Capybara::Poltergeist
59
78
  rescue Errno::ESRCH, Errno::ECHILD
60
79
  # Zed's dead, baby
61
80
  end
62
-
81
+ ObjectSpace.undefine_finalizer(self)
82
+ @write_io.close
83
+ @read_io.close
63
84
  @out_thread.kill
64
85
  @pid = nil
65
86
  end
@@ -81,33 +102,13 @@ module Capybara::Poltergeist
81
102
 
82
103
  private
83
104
 
84
- def check_phantomjs_version
85
- return if @phantomjs_version_checked
86
-
87
- version = `#{path} --version` rescue nil
88
-
89
- if version.nil? || $? != 0
90
- raise PhantomJSFailed.new($?)
91
- else
92
- major, minor, build = version.chomp.split('.').map(&:to_i)
93
- min_major, min_minor, min_build = PHANTOMJS_VERSION.split('.').map(&:to_i)
94
- if major < min_major ||
95
- major == min_major && minor < min_minor ||
96
- major == min_major && minor == min_minor && build < min_build
97
- raise PhantomJSTooOld.new(version)
98
- end
99
- end
100
-
101
- @phantomjs_version_checked = true
102
- end
103
-
104
105
  # This abomination is because JRuby doesn't support the :out option of
105
106
  # Process.spawn
106
- def redirect_stdout(to)
107
+ def redirect_stdout
107
108
  prev = STDOUT.dup
108
109
  prev.autoclose = false
109
- $stdout = to
110
- STDOUT.reopen(to)
110
+ $stdout = @write_io
111
+ STDOUT.reopen(@write_io)
111
112
  yield
112
113
  ensure
113
114
  STDOUT.reopen(prev)
@@ -25,7 +25,7 @@ class PoltergeistAgent
25
25
  throw error
26
26
 
27
27
  currentUrl: ->
28
- window.location.toString()
28
+ encodeURI(window.location.href)
29
29
 
30
30
  find: (method, selector, within = document) ->
31
31
  try
@@ -214,11 +214,12 @@ class PoltergeistAgent.Node
214
214
  offset = { top: 0, left: 0 }
215
215
 
216
216
  while win.frameElement
217
- rect = window.frameElement.getClientRects()[0]
218
- win = win.parent
219
-
220
- offset.top += rect.top
221
- offset.left += rect.left
217
+ rect = win.frameElement.getClientRects()[0]
218
+ style = win.getComputedStyle(win.frameElement)
219
+ win = win.parent
220
+
221
+ offset.top += rect.top + parseInt(style.getPropertyValue("padding-top"), 10)
222
+ offset.left += rect.left + parseInt(style.getPropertyValue("padding-left"), 10)
222
223
 
223
224
  offset
224
225
 
@@ -82,7 +82,10 @@ class Poltergeist.Browser
82
82
  visit: (url) ->
83
83
  this.setState 'loading'
84
84
 
85
- prev_url = @page.currentUrl()
85
+ # Prevent firing `page.onInitialized` event twice. Calling currentUrl
86
+ # method before page is actually opened fires this event for the first time.
87
+ # The second time will be in the right place after `page.open`
88
+ prev_url = if @page.source() is null then 'about:blank' else @page.currentUrl()
86
89
 
87
90
  @page.open(url)
88
91
 
@@ -171,6 +174,9 @@ class Poltergeist.Browser
171
174
  else
172
175
  @owner.sendError(new Poltergeist.FrameNotFound(name))
173
176
 
177
+ pages: ->
178
+ this.sendResponse(@page.pages())
179
+
174
180
  pop_frame: ->
175
181
  this.sendResponse(@page.popFrame())
176
182
 
@@ -240,21 +246,36 @@ class Poltergeist.Browser
240
246
  this.resetPage()
241
247
  this.sendResponse(true)
242
248
 
243
- render: (path, full) ->
249
+ scroll_to: (left, top) ->
250
+ @page.setScrollPosition(left: left, top: top)
251
+ this.sendResponse(true)
252
+
253
+ render_base64: (format, full, selector = null)->
254
+ this.set_clip_rect(full, selector)
255
+ encoded_image = @page.renderBase64(format)
256
+ this.sendResponse(encoded_image)
257
+
258
+ render: (path, full, selector = null) ->
259
+ dimensions = this.set_clip_rect(full, selector)
260
+ @page.setScrollPosition(left: 0, top: 0)
261
+ @page.render(path)
262
+ @page.setScrollPosition(left: dimensions.left, top: dimensions.top)
263
+ this.sendResponse(true)
264
+
265
+ set_clip_rect: (full, selector) ->
244
266
  dimensions = @page.validatedDimensions()
245
- document = dimensions.document
246
- viewport = dimensions.viewport
247
-
248
- if full
249
- @page.setScrollPosition(left: 0, top: 0)
250
- @page.setClipRect(left: 0, top: 0, width: document.width, height: document.height)
251
- @page.render(path)
252
- @page.setScrollPosition(left: dimensions.left, top: dimensions.top)
267
+ [document, viewport] = [dimensions.document, dimensions.viewport]
268
+
269
+ rect = if full
270
+ left: 0, top: 0, width: document.width, height: document.height
253
271
  else
254
- @page.setClipRect(left: 0, top: 0, width: viewport.width, height: viewport.height)
255
- @page.render(path)
272
+ if selector?
273
+ @page.elementBounds(selector)
274
+ else
275
+ left: 0, top: 0, width: viewport.width, height: viewport.height
256
276
 
257
- this.sendResponse(true)
277
+ @page.setClipRect(rect)
278
+ dimensions
258
279
 
259
280
  resize: (width, height) ->
260
281
  @page.setViewportSize(width: width, height: height)
@@ -263,12 +284,25 @@ class Poltergeist.Browser
263
284
  network_traffic: ->
264
285
  this.sendResponse(@page.networkTraffic())
265
286
 
287
+ get_headers: ->
288
+ this.sendResponse(@page.getCustomHeaders())
289
+
266
290
  set_headers: (headers) ->
267
291
  # Workaround for https://code.google.com/p/phantomjs/issues/detail?id=745
268
292
  @page.setUserAgent(headers['User-Agent']) if headers['User-Agent']
269
293
  @page.setCustomHeaders(headers)
270
294
  this.sendResponse(true)
271
295
 
296
+ add_headers: (headers) ->
297
+ allHeaders = @page.getCustomHeaders()
298
+ for name, value of headers
299
+ allHeaders[name] = value
300
+ this.set_headers(allHeaders)
301
+
302
+ add_header: (header, permanent) ->
303
+ @page.addTempHeader(header) unless permanent
304
+ this.add_headers(header)
305
+
272
306
  response_headers: ->
273
307
  this.sendResponse(@page.responseHeaders())
274
308
 
@@ -8,7 +8,6 @@ PoltergeistAgent = (function() {
8
8
 
9
9
  PoltergeistAgent.prototype.externalCall = function(name, args) {
10
10
  var error;
11
-
12
11
  try {
13
12
  return {
14
13
  value: this[name].apply(this, args)
@@ -26,7 +25,6 @@ PoltergeistAgent = (function() {
26
25
 
27
26
  PoltergeistAgent.stringify = function(object) {
28
27
  var error;
29
-
30
28
  try {
31
29
  return JSON.stringify(object, function(key, value) {
32
30
  if (Array.isArray(this[key])) {
@@ -46,12 +44,11 @@ PoltergeistAgent = (function() {
46
44
  };
47
45
 
48
46
  PoltergeistAgent.prototype.currentUrl = function() {
49
- return window.location.toString();
47
+ return encodeURI(window.location.href);
50
48
  };
51
49
 
52
50
  PoltergeistAgent.prototype.find = function(method, selector, within) {
53
51
  var el, error, i, results, xpath, _i, _len, _results;
54
-
55
52
  if (within == null) {
56
53
  within = document;
57
54
  }
@@ -60,7 +57,6 @@ PoltergeistAgent = (function() {
60
57
  xpath = document.evaluate(selector, within, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
61
58
  results = (function() {
62
59
  var _i, _ref, _results;
63
-
64
60
  _results = [];
65
61
  for (i = _i = 0, _ref = xpath.snapshotLength; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) {
66
62
  _results.push(xpath.snapshotItem(i));
@@ -100,13 +96,11 @@ PoltergeistAgent = (function() {
100
96
 
101
97
  PoltergeistAgent.prototype.get = function(id) {
102
98
  var _base;
103
-
104
99
  return (_base = this.nodes)[id] || (_base[id] = new PoltergeistAgent.Node(this, this.elements[id]));
105
100
  };
106
101
 
107
102
  PoltergeistAgent.prototype.nodeCall = function(id, name, args) {
108
103
  var node;
109
-
110
104
  node = this.get(id);
111
105
  if (node.isObsolete()) {
112
106
  throw new PoltergeistAgent.ObsoleteNode;
@@ -170,7 +164,6 @@ PoltergeistAgent.Node = (function() {
170
164
  Node.prototype.isObsolete = function() {
171
165
  var obsolete,
172
166
  _this = this;
173
-
174
167
  obsolete = function(element) {
175
168
  if (element.parentNode != null) {
176
169
  if (element.parentNode === document) {
@@ -187,7 +180,6 @@ PoltergeistAgent.Node = (function() {
187
180
 
188
181
  Node.prototype.changed = function() {
189
182
  var event;
190
-
191
183
  event = document.createEvent('HTMLEvents');
192
184
  event.initEvent('change', true, false);
193
185
  return this.element.dispatchEvent(event);
@@ -195,7 +187,6 @@ PoltergeistAgent.Node = (function() {
195
187
 
196
188
  Node.prototype.input = function() {
197
189
  var event;
198
-
199
190
  event = document.createEvent('HTMLEvents');
200
191
  event.initEvent('input', true, false);
201
192
  return this.element.dispatchEvent(event);
@@ -203,7 +194,6 @@ PoltergeistAgent.Node = (function() {
203
194
 
204
195
  Node.prototype.keyupdowned = function(eventName, keyCode) {
205
196
  var event;
206
-
207
197
  event = document.createEvent('UIEvents');
208
198
  event.initEvent(eventName, true, true);
209
199
  event.keyCode = keyCode;
@@ -214,7 +204,6 @@ PoltergeistAgent.Node = (function() {
214
204
 
215
205
  Node.prototype.keypressed = function(altKey, ctrlKey, shiftKey, metaKey, keyCode, charCode) {
216
206
  var event;
217
-
218
207
  event = document.createEvent('UIEvents');
219
208
  event.initEvent('keypress', true, true);
220
209
  event.window = this.agent.window;
@@ -258,7 +247,6 @@ PoltergeistAgent.Node = (function() {
258
247
 
259
248
  Node.prototype.value = function() {
260
249
  var option, _i, _len, _ref, _results;
261
-
262
250
  if (this.element.tagName === 'SELECT' && this.element.multiple) {
263
251
  _ref = this.element.children;
264
252
  _results = [];
@@ -276,7 +264,6 @@ PoltergeistAgent.Node = (function() {
276
264
 
277
265
  Node.prototype.set = function(value) {
278
266
  var char, keyCode, _i, _len;
279
-
280
267
  if (this.element.readOnly) {
281
268
  return;
282
269
  }
@@ -342,25 +329,24 @@ PoltergeistAgent.Node = (function() {
342
329
  };
343
330
 
344
331
  Node.prototype.frameOffset = function() {
345
- var offset, rect, win;
346
-
332
+ var offset, rect, style, win;
347
333
  win = window;
348
334
  offset = {
349
335
  top: 0,
350
336
  left: 0
351
337
  };
352
338
  while (win.frameElement) {
353
- rect = window.frameElement.getClientRects()[0];
339
+ rect = win.frameElement.getClientRects()[0];
340
+ style = win.getComputedStyle(win.frameElement);
354
341
  win = win.parent;
355
- offset.top += rect.top;
356
- offset.left += rect.left;
342
+ offset.top += rect.top + parseInt(style.getPropertyValue("padding-top"), 10);
343
+ offset.left += rect.left + parseInt(style.getPropertyValue("padding-left"), 10);
357
344
  }
358
345
  return offset;
359
346
  };
360
347
 
361
348
  Node.prototype.position = function() {
362
349
  var frameOffset, pos, rect;
363
-
364
350
  rect = this.element.getClientRects()[0];
365
351
  if (!rect) {
366
352
  throw new PoltergeistAgent.ObsoleteNode;
@@ -379,7 +365,6 @@ PoltergeistAgent.Node = (function() {
379
365
 
380
366
  Node.prototype.trigger = function(name) {
381
367
  var event;
382
-
383
368
  if (Node.EVENTS.MOUSE.indexOf(name) !== -1) {
384
369
  event = document.createEvent('MouseEvent');
385
370
  event.initMouseEvent(name, true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
@@ -394,7 +379,6 @@ PoltergeistAgent.Node = (function() {
394
379
 
395
380
  Node.prototype.mouseEventTest = function(x, y) {
396
381
  var el, frameOffset, origEl;
397
-
398
382
  frameOffset = this.frameOffset();
399
383
  x -= frameOffset.left;
400
384
  y -= frameOffset.top;
@@ -416,7 +400,6 @@ PoltergeistAgent.Node = (function() {
416
400
 
417
401
  Node.prototype.getSelector = function(el) {
418
402
  var className, selector, _i, _len, _ref;
419
-
420
403
  selector = el.tagName !== 'HTML' ? this.getSelector(el.parentNode) + ' ' : '';
421
404
  selector += el.tagName.toLowerCase();
422
405
  if (el.id) {
@@ -432,7 +415,6 @@ PoltergeistAgent.Node = (function() {
432
415
 
433
416
  Node.prototype.characterToKeyCode = function(character) {
434
417
  var code, specialKeys;
435
-
436
418
  code = character.toUpperCase().charCodeAt(0);
437
419
  specialKeys = {
438
420
  96: 192,
@@ -13,7 +13,6 @@ Poltergeist.Browser = (function() {
13
13
 
14
14
  Browser.prototype.resetPage = function() {
15
15
  var _this = this;
16
-
17
16
  if (this.page != null) {
18
17
  this.page.release();
19
18
  phantom.clearCookies();
@@ -50,7 +49,6 @@ Poltergeist.Browser = (function() {
50
49
  };
51
50
  return this.page.onPageCreated = function(sub_page) {
52
51
  var name;
53
-
54
52
  if (_this.state === 'awaiting_sub_page') {
55
53
  name = _this.page_name;
56
54
  _this.page_name = null;
@@ -83,7 +81,6 @@ Poltergeist.Browser = (function() {
83
81
 
84
82
  Browser.prototype.sendResponse = function(response) {
85
83
  var errors;
86
-
87
84
  errors = this.page.errors();
88
85
  this.page.clearErrors();
89
86
  if (errors.length > 0 && this.js_errors) {
@@ -108,9 +105,8 @@ Poltergeist.Browser = (function() {
108
105
 
109
106
  Browser.prototype.visit = function(url) {
110
107
  var prev_url;
111
-
112
108
  this.setState('loading');
113
- prev_url = this.page.currentUrl();
109
+ prev_url = this.page.source() === null ? 'about:blank' : this.page.currentUrl();
114
110
  this.page.open(url);
115
111
  if (/#/.test(url) && prev_url.split('#')[0] === url.split('#')[0]) {
116
112
  this.setState('default');
@@ -172,7 +168,6 @@ Poltergeist.Browser = (function() {
172
168
 
173
169
  Browser.prototype.select_file = function(page_id, id, value) {
174
170
  var node;
175
-
176
171
  node = this.node(page_id, id);
177
172
  this.page.beforeUpload(node.id);
178
173
  this.page.uploadFile('[_poltergeist_selected]', value);
@@ -207,7 +202,6 @@ Poltergeist.Browser = (function() {
207
202
 
208
203
  Browser.prototype.push_frame = function(name, timeout) {
209
204
  var _this = this;
210
-
211
205
  if (timeout == null) {
212
206
  timeout = new Date().getTime() + 2000;
213
207
  }
@@ -228,6 +222,10 @@ Poltergeist.Browser = (function() {
228
222
  }
229
223
  };
230
224
 
225
+ Browser.prototype.pages = function() {
226
+ return this.sendResponse(this.page.pages());
227
+ };
228
+
231
229
  Browser.prototype.pop_frame = function() {
232
230
  return this.sendResponse(this.page.popFrame());
233
231
  };
@@ -235,7 +233,6 @@ Poltergeist.Browser = (function() {
235
233
  Browser.prototype.push_window = function(name) {
236
234
  var sub_page,
237
235
  _this = this;
238
-
239
236
  sub_page = this.page.getPage(name);
240
237
  if (sub_page) {
241
238
  if (sub_page.currentUrl() === 'about:blank') {
@@ -257,7 +254,6 @@ Poltergeist.Browser = (function() {
257
254
 
258
255
  Browser.prototype.pop_window = function() {
259
256
  var prev_page;
260
-
261
257
  prev_page = this.page_stack.pop();
262
258
  if (prev_page) {
263
259
  this.page = prev_page;
@@ -268,7 +264,6 @@ Poltergeist.Browser = (function() {
268
264
  Browser.prototype.mouse_event = function(page_id, id, name) {
269
265
  var node,
270
266
  _this = this;
271
-
272
267
  node = this.node(page_id, id);
273
268
  this.setState('mouse_event');
274
269
  this.last_mouse_event = node.mouseEvent(name);
@@ -321,40 +316,61 @@ Poltergeist.Browser = (function() {
321
316
  return this.sendResponse(true);
322
317
  };
323
318
 
324
- Browser.prototype.render = function(path, full) {
325
- var dimensions, document, viewport;
319
+ Browser.prototype.scroll_to = function(left, top) {
320
+ this.page.setScrollPosition({
321
+ left: left,
322
+ top: top
323
+ });
324
+ return this.sendResponse(true);
325
+ };
326
326
 
327
- dimensions = this.page.validatedDimensions();
328
- document = dimensions.document;
329
- viewport = dimensions.viewport;
330
- if (full) {
331
- this.page.setScrollPosition({
332
- left: 0,
333
- top: 0
334
- });
335
- this.page.setClipRect({
336
- left: 0,
337
- top: 0,
338
- width: document.width,
339
- height: document.height
340
- });
341
- this.page.render(path);
342
- this.page.setScrollPosition({
343
- left: dimensions.left,
344
- top: dimensions.top
345
- });
346
- } else {
347
- this.page.setClipRect({
348
- left: 0,
349
- top: 0,
350
- width: viewport.width,
351
- height: viewport.height
352
- });
353
- this.page.render(path);
327
+ Browser.prototype.render_base64 = function(format, full, selector) {
328
+ var encoded_image;
329
+ if (selector == null) {
330
+ selector = null;
354
331
  }
332
+ this.set_clip_rect(full, selector);
333
+ encoded_image = this.page.renderBase64(format);
334
+ return this.sendResponse(encoded_image);
335
+ };
336
+
337
+ Browser.prototype.render = function(path, full, selector) {
338
+ var dimensions;
339
+ if (selector == null) {
340
+ selector = null;
341
+ }
342
+ dimensions = this.set_clip_rect(full, selector);
343
+ this.page.setScrollPosition({
344
+ left: 0,
345
+ top: 0
346
+ });
347
+ this.page.render(path);
348
+ this.page.setScrollPosition({
349
+ left: dimensions.left,
350
+ top: dimensions.top
351
+ });
355
352
  return this.sendResponse(true);
356
353
  };
357
354
 
355
+ Browser.prototype.set_clip_rect = function(full, selector) {
356
+ var dimensions, document, rect, viewport, _ref;
357
+ dimensions = this.page.validatedDimensions();
358
+ _ref = [dimensions.document, dimensions.viewport], document = _ref[0], viewport = _ref[1];
359
+ rect = full ? {
360
+ left: 0,
361
+ top: 0,
362
+ width: document.width,
363
+ height: document.height
364
+ } : selector != null ? this.page.elementBounds(selector) : {
365
+ left: 0,
366
+ top: 0,
367
+ width: viewport.width,
368
+ height: viewport.height
369
+ };
370
+ this.page.setClipRect(rect);
371
+ return dimensions;
372
+ };
373
+
358
374
  Browser.prototype.resize = function(width, height) {
359
375
  this.page.setViewportSize({
360
376
  width: width,
@@ -367,6 +383,10 @@ Poltergeist.Browser = (function() {
367
383
  return this.sendResponse(this.page.networkTraffic());
368
384
  };
369
385
 
386
+ Browser.prototype.get_headers = function() {
387
+ return this.sendResponse(this.page.getCustomHeaders());
388
+ };
389
+
370
390
  Browser.prototype.set_headers = function(headers) {
371
391
  if (headers['User-Agent']) {
372
392
  this.page.setUserAgent(headers['User-Agent']);
@@ -375,6 +395,23 @@ Poltergeist.Browser = (function() {
375
395
  return this.sendResponse(true);
376
396
  };
377
397
 
398
+ Browser.prototype.add_headers = function(headers) {
399
+ var allHeaders, name, value;
400
+ allHeaders = this.page.getCustomHeaders();
401
+ for (name in headers) {
402
+ value = headers[name];
403
+ allHeaders[name] = value;
404
+ }
405
+ return this.set_headers(allHeaders);
406
+ };
407
+
408
+ Browser.prototype.add_header = function(header, permanent) {
409
+ if (!permanent) {
410
+ this.page.addTempHeader(header);
411
+ }
412
+ return this.add_headers(header);
413
+ };
414
+
378
415
  Browser.prototype.response_headers = function() {
379
416
  return this.sendResponse(this.page.responseHeaders());
380
417
  };