poltergeist 1.3.0 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
  };