poltergeist 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.md CHANGED
@@ -0,0 +1,7 @@
1
+ ## 0.2 ##
2
+
3
+ * First version considered 'ready', hopefully fewer problems.
4
+
5
+ ## 0.1 ##
6
+
7
+ * First version, various problems.
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Poltergeist - A PhantomJS driver for Capybara #
2
2
 
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
 
5
5
  Poltergeist is a driver for [Capybara](https://github.com/jnicklas/capybara). It allows you to
6
6
  run your Capybara tests on a headless [WebKit](http://webkit.org) browser,
@@ -8,29 +8,84 @@ provided by [PhantomJS](http://www.phantomjs.org/).
8
8
 
9
9
  ## Installation ##
10
10
 
11
- Add `poltergeist` to your Gemfile, and add `Capybara.javascript_driver = :poltergeist`
12
- in your test setup.
11
+ Add `poltergeist` to your Gemfile, and add in your test setup add:
13
12
 
14
- You will also need PhantomJS 1.3+ on your system.
15
- [Here's how to do that](http://code.google.com/p/phantomjs/wiki/BuildInstructions).
13
+ require 'capybara/poltergeist'
14
+ Capybara.javascript_driver = :poltergeist
16
15
 
17
16
  Currently PhantomJS is not 'truly headless', so to run it on a continuous integration
18
17
  server you will need to use [Xvfb](http://en.wikipedia.org/wiki/Xvfb). You can either use the
19
18
  [headless gem](https://github.com/leonid-shevtsov/headless) for this,
20
19
  or make sure that Xvfb is running and the `DISPLAY` environment variable is set.
21
20
 
21
+ ## Installing PhantomJS ##
22
+
23
+ You need PhantomJS 1.4.1+, built against Qt 4.8, on your system.
24
+
25
+ ### Mac users ##
26
+
27
+ By far the easiest, most reliable thing to do is to [install the
28
+ pre-built static binary](http://code.google.com/p/phantomjs/downloads/detail?name=phantomjs-1.4.1-macosx-static-x86.zip&can=2&q=).
29
+ Try this first.
30
+
31
+ ### Linux users, or if the pre-built Mac binary doesn't work ###
32
+
33
+ You need to build PhantomJS manually. Unfortunately, this not
34
+ currently straightforward, for two reasons:
35
+
36
+ 1. Using Poltergeist with PhantomJS built against Qt 4.7 causes
37
+ segfaults in WebKit's Javascript engine. Fortunately, this problem
38
+ doesn't occur under the recently released Qt 4.8. But if you don't
39
+ have Qt 4.8 on your system (check with `qmake --version`), you'll
40
+ need to build it.
41
+
42
+ 2. A change in the version of WebKit bundled with Qt 4.8 means that in order
43
+ to be able to attach files to file `<input>` elements, we must apply
44
+ a patch to the Qt source tree that PhantomJS is built against.
45
+
46
+ So, you basically have two options:
47
+
48
+ 1. **If you have Qt 4.8 on your system, and don't need to use file
49
+ inputs**, [follow the standard PhantomJS build instructions](http://code.google.com/p/phantomjs/wiki/BuildInstructions).
50
+
51
+ 2. **Otherwise**, [download the PhantomJS tarball](http://code.google.com/p/phantomjs/downloads/detail?name=phantomjs-1.4.1-source.tar.gz&can=2&q=),
52
+ `cd deploy/` and run either `./build-linux.sh --qt-4.8` or `./build-mac.sh`.
53
+ The script will
54
+ download Qt, apply some patches, build it, and then build PhantomJS
55
+ against the patched build of Qt. It takes quite a while, around 30
56
+ minutes on a modern computer with two hyperthreaded cores. Afterwards,
57
+ you should copy the `bin/phantomjs` binary into your `PATH`.
58
+
59
+ PhantomJS 1.5 plans to bundle a stripped-down version of Qt, which will
60
+ reduce the build time a bit (although most of the time is spent building
61
+ WebKit) and make it easier to apply patches. When it is possible to make
62
+ static builds for Linux, those may be provided too, so most users will
63
+ avoid having to build it themselves.
64
+
22
65
  ## What's supported? ##
23
66
 
24
67
  Poltergeist supports basically everything that is supported by the stock Selenium driver,
25
68
  including Javascript, drag-and-drop, etc.
26
69
 
27
- Additionally, you can grab screenshots of the page at any point by calling
70
+ There are some additional features:
71
+
72
+ ### Taking screenshots ###
73
+
74
+ You can grab screenshots of the page at any point by calling
28
75
  `page.driver.render('/path/to/file.png')` (this works the same way as the PhantomJS
29
76
  render feature, so you can specify other extensions like `.pdf`, `.gif`, etc.)
30
77
 
31
- ## Customisation ##
78
+ By default, only the viewport will be rendered (the part of the page that is in view). To render
79
+ the entire page, use `page.driver.render('/path/to/file.png', :full => true)`.
80
+
81
+ ### Resizing the window ###
32
82
 
33
- You can customise the way that Capybara sets up Poltegeist via the following code in your
83
+ Sometimes the window size is important to how things are rendered. Poltergeist sets the window
84
+ size to 1024x768 by default, but you can set this yourself with `page.driver.resize(width, height)`.
85
+
86
+ ## Customization ##
87
+
88
+ You can customize the way that Capybara sets up Poltegeist via the following code in your
34
89
  test setup:
35
90
 
36
91
  Capybara.register_driver :poltergeist do |app|
@@ -46,7 +101,8 @@ test setup:
46
101
  ## Bugs ##
47
102
 
48
103
  Please file bug reports on Github and include example code to reproduce the problem wherever
49
- possible. (Tests are even better.)
104
+ possible. (Tests are even better.) Please also provide the output with
105
+ `:debug` turned on, and screenshots if you think it's relevant.
50
106
 
51
107
  ## Why not use [capybara-webkit](https://github.com/thoughtbot/capybara-webkit)? ##
52
108
 
@@ -54,13 +110,36 @@ If capybara-webkit works for you, then by all means carry on using it.
54
110
 
55
111
  However, I have had some trouble with it, and Poltergeist basically started
56
112
  as an experiment to see whether a PhantomJS driver was possible. (It turned out it
57
- was, but only thanks to some new features in the recent 1.3.0 release.)
113
+ was, but only thanks to some new features since the 1.3 release.)
58
114
 
59
115
  In the long term, I think having a PhantomJS driver makes sense, because that allows
60
116
  PhantomJS to concentrate on being an awesome headless browser, while the capybara driver
61
117
  (Poltergeist) is able to be the minimal amount of glue code necessary to drive the
62
118
  browser.
63
119
 
120
+ I also find it more pleasant to hack in CoffeeScript than C++,
121
+ particularly as my C++ experience only goes as far as trying to make
122
+ PhantomJS/Qt/WebKit work with Poltergeist :)
123
+
124
+ ## Hacking ##
125
+
126
+ Contributions are very welcome and I will happily give commit access to
127
+ anyone who does a few good pull requests.
128
+
129
+ To get setup, run `bundle install`. You can run the full test suite with
130
+ `rspec spec/` or `rake`.
131
+
132
+ I previously set up the repository on [Travis CI](http://travis-ci.org/)
133
+ but unfortunately given they need a custom-built Qt+PhantomJS in order
134
+ to pass, it can't be used for now. When static Linux PhantomJS builds
135
+ are working this can be revisited.
136
+
137
+ While PhantomJS is capable of compiling and running CoffeeScript code
138
+ directly, I prefer to compile the code myself and distribute that (it
139
+ makes debugging easier). Running `rake autocompile` will watch the
140
+ `.coffee` files for changes, and compile them into
141
+ `lib/capybara/client/compiled`.
142
+
64
143
  ## License ##
65
144
 
66
145
  Copyright (c) 2011 Jonathan Leighton
@@ -9,9 +9,7 @@ module Capybara
9
9
  autoload :Server, 'capybara/poltergeist/server'
10
10
  autoload :Client, 'capybara/poltergeist/client'
11
11
 
12
- autoload :Error, 'capybara/poltergeist/errors'
13
- autoload :BrowserError, 'capybara/poltergeist/errors'
14
- autoload :ObsoleteNode, 'capybara/poltergeist/errors'
12
+ require 'capybara/poltergeist/errors'
15
13
  end
16
14
  end
17
15
 
@@ -101,8 +101,12 @@ module Capybara::Poltergeist
101
101
  command 'reset'
102
102
  end
103
103
 
104
- def render(path)
105
- command 'render', path
104
+ def render(path, options = {})
105
+ command 'render', path, !!options[:full]
106
+ end
107
+
108
+ def resize(width, height)
109
+ command 'resize', width, height
106
110
  end
107
111
 
108
112
  def logger
@@ -125,6 +129,10 @@ module Capybara::Poltergeist
125
129
  else
126
130
  json['response']
127
131
  end
132
+
133
+ rescue DeadClient
134
+ restart
135
+ raise
128
136
  end
129
137
  end
130
138
  end
@@ -1,10 +1,10 @@
1
- require 'open3'
1
+ require 'sfl'
2
2
 
3
3
  module Capybara::Poltergeist
4
4
  class Client
5
5
  PHANTOM_SCRIPT = File.expand_path('../client/compiled/main.js', __FILE__)
6
6
 
7
- attr_reader :pid, :port, :path
7
+ attr_reader :thread, :pid, :err, :port, :path
8
8
 
9
9
  def initialize(port, path = nil)
10
10
  @port = port
@@ -15,38 +15,11 @@ module Capybara::Poltergeist
15
15
  end
16
16
 
17
17
  def start
18
- @pid = Process.fork do
19
- Open3.popen3("#{path} #{PHANTOM_SCRIPT} #{port}") do |stdin, stdout, stderr|
20
- loop do
21
- select = IO.select([stdout, stderr])
22
- stream = select.first.first
23
-
24
- break if stream.eof?
25
-
26
- if stream == stdout
27
- STDOUT.puts stdout.readline
28
- elsif stream == stderr
29
- line = stderr.readline
30
-
31
- # QtWebkit seems to throw this error all the time when using WebSockets, but
32
- # it doesn't appear to actually stop anything working, so filter it out.
33
- #
34
- # This isn't the nicest solution I know :( Hopefully it will be fixed in
35
- # QtWebkit (if you search for this string, you'll see it's been reported in
36
- # various places).
37
- unless line.include?('WebCore::SocketStreamHandlePrivate::socketSentData()')
38
- STDERR.puts line
39
- end
40
- end
41
- end
42
- end
43
- end
18
+ @pid = Kernel.spawn("#{path} #{PHANTOM_SCRIPT} #{port}")
44
19
  end
45
20
 
46
21
  def stop
47
22
  Process.kill('TERM', pid)
48
- rescue Errno::ESRCH
49
- # Bovvered, I ain't
50
23
  end
51
24
 
52
25
  def restart
@@ -76,7 +76,7 @@ class Poltergeist.Browser
76
76
  @owner.sendResponse @page.evaluate("function() { return #{script} }")
77
77
 
78
78
  execute: (script) ->
79
- @page.execute("function() { return #{script} }")
79
+ @page.execute("function() { #{script} }")
80
80
  @owner.sendResponse(true)
81
81
 
82
82
  push_frame: (id) ->
@@ -88,9 +88,13 @@ class Poltergeist.Browser
88
88
  @owner.sendResponse(true)
89
89
 
90
90
  click: (id) ->
91
+ load_detected = false
92
+
91
93
  # Detect if the click event triggers a page load. If it does, don't send
92
94
  # a response here, because the response will be sent once the page has loaded.
93
- @page.onLoadStarted = => @awaiting_response = true
95
+ @page.onLoadStarted = =>
96
+ @awaiting_response = true
97
+ load_detected = true
94
98
 
95
99
  @page.get(id).click()
96
100
 
@@ -99,9 +103,9 @@ class Poltergeist.Browser
99
103
  setTimeout(
100
104
  =>
101
105
  @page.onLoadStarted = null
102
- @owner.sendResponse(true) unless @awaiting_response
106
+ @owner.sendResponse(true) unless load_detected
103
107
  ,
104
- 0
108
+ 10
105
109
  )
106
110
 
107
111
  drag: (id, other_id) ->
@@ -116,6 +120,25 @@ class Poltergeist.Browser
116
120
  this.resetPage()
117
121
  @owner.sendResponse(true)
118
122
 
119
- render: (path) ->
120
- @page.render(path)
123
+ render: (path, full) ->
124
+ dimensions = @page.validatedDimensions()
125
+ document = dimensions.document
126
+ viewport = dimensions.viewport
127
+
128
+ if full
129
+ @page.setScrollPosition(left: 0, top: 0)
130
+ @page.setClipRect(left: 0, top: 0, width: document.width, height: document.height)
131
+ @page.render(path)
132
+ @page.setScrollPosition(left: dimensions.left, top: dimensions.top)
133
+ else
134
+ @page.setClipRect(left: 0, top: 0, width: viewport.width, height: viewport.height)
135
+ @page.render(path)
136
+
137
+ @owner.sendResponse(true)
138
+
139
+ resize: (width, height) ->
140
+ @page.setViewportSize(width: width, height: height)
121
141
  @owner.sendResponse(true)
142
+
143
+ exit: ->
144
+ phantom.exit()
@@ -74,7 +74,7 @@ Poltergeist.Browser = (function() {
74
74
  return this.owner.sendResponse(this.page.evaluate("function() { return " + script + " }"));
75
75
  };
76
76
  Browser.prototype.execute = function(script) {
77
- this.page.execute("function() { return " + script + " }");
77
+ this.page.execute("function() { " + script + " }");
78
78
  return this.owner.sendResponse(true);
79
79
  };
80
80
  Browser.prototype.push_frame = function(id) {
@@ -86,16 +86,19 @@ Poltergeist.Browser = (function() {
86
86
  return this.owner.sendResponse(true);
87
87
  };
88
88
  Browser.prototype.click = function(id) {
89
+ var load_detected;
90
+ load_detected = false;
89
91
  this.page.onLoadStarted = __bind(function() {
90
- return this.awaiting_response = true;
92
+ this.awaiting_response = true;
93
+ return load_detected = true;
91
94
  }, this);
92
95
  this.page.get(id).click();
93
96
  return setTimeout(__bind(function() {
94
97
  this.page.onLoadStarted = null;
95
- if (!this.awaiting_response) {
98
+ if (!load_detected) {
96
99
  return this.owner.sendResponse(true);
97
100
  }
98
- }, this), 0);
101
+ }, this), 10);
99
102
  };
100
103
  Browser.prototype.drag = function(id, other_id) {
101
104
  this.page.get(id).dragTo(this.page.get(other_id));
@@ -109,9 +112,47 @@ Poltergeist.Browser = (function() {
109
112
  this.resetPage();
110
113
  return this.owner.sendResponse(true);
111
114
  };
112
- Browser.prototype.render = function(path) {
113
- this.page.render(path);
115
+ Browser.prototype.render = function(path, full) {
116
+ var dimensions, document, viewport;
117
+ dimensions = this.page.validatedDimensions();
118
+ document = dimensions.document;
119
+ viewport = dimensions.viewport;
120
+ if (full) {
121
+ this.page.setScrollPosition({
122
+ left: 0,
123
+ top: 0
124
+ });
125
+ this.page.setClipRect({
126
+ left: 0,
127
+ top: 0,
128
+ width: document.width,
129
+ height: document.height
130
+ });
131
+ this.page.render(path);
132
+ this.page.setScrollPosition({
133
+ left: dimensions.left,
134
+ top: dimensions.top
135
+ });
136
+ } else {
137
+ this.page.setClipRect({
138
+ left: 0,
139
+ top: 0,
140
+ width: viewport.width,
141
+ height: viewport.height
142
+ });
143
+ this.page.render(path);
144
+ }
145
+ return this.owner.sendResponse(true);
146
+ };
147
+ Browser.prototype.resize = function(width, height) {
148
+ this.page.setViewportSize({
149
+ width: width,
150
+ height: height
151
+ });
114
152
  return this.owner.sendResponse(true);
115
153
  };
154
+ Browser.prototype.exit = function() {
155
+ return phantom.exit();
156
+ };
116
157
  return Browser;
117
158
  })();
@@ -1,6 +1,6 @@
1
1
  var Poltergeist;
2
- if (phantom.version.major < 1 || phantom.version.minor < 3) {
3
- console.log("Poltergeist requires a PhantomJS version of at least 1.3");
2
+ if (phantom.version.major < 1 || phantom.version.minor < 4 || phantom.version.patch < 1) {
3
+ console.log("Poltergeist requires a PhantomJS version of at least 1.4.1");
4
4
  phantom.exit(1);
5
5
  }
6
6
  Poltergeist = (function() {
@@ -35,4 +35,4 @@ phantom.injectJs('web_page.js');
35
35
  phantom.injectJs('node.js');
36
36
  phantom.injectJs('connection.js');
37
37
  phantom.injectJs('browser.js');
38
- new Poltergeist(phantom.args[0]);
38
+ new Poltergeist(phantom.args[0]);
@@ -29,21 +29,22 @@ Poltergeist.Node = (function() {
29
29
  _fn(name);
30
30
  }
31
31
  Node.prototype.scrollIntoView = function() {
32
- var pos, scroll, size, viewport, _ref2, _ref3;
33
- viewport = this.page.viewport();
34
- size = this.page.documentSize();
32
+ var dimensions, document, pos, scroll, viewport, _ref2, _ref3;
33
+ dimensions = this.page.validatedDimensions();
34
+ document = dimensions.document;
35
+ viewport = dimensions.viewport;
35
36
  pos = this.position();
36
37
  scroll = {
37
- left: viewport.left,
38
- top: viewport.top
38
+ left: dimensions.left,
39
+ top: dimensions.top
39
40
  };
40
- if (!((viewport.left <= (_ref2 = pos.x) && _ref2 < viewport.right))) {
41
- scroll.left = Math.min(pos.x, size.width - viewport.width);
41
+ if (!((dimensions.left <= (_ref2 = pos.x) && _ref2 < dimensions.right))) {
42
+ scroll.left = Math.min(pos.x, document.width - viewport.width);
42
43
  }
43
- if (!((viewport.top <= (_ref3 = pos.y) && _ref3 < viewport.bottom))) {
44
- scroll.top = Math.min(pos.y, size.height - viewport.height);
44
+ if (!((dimensions.top <= (_ref3 = pos.y) && _ref3 < dimensions.bottom))) {
45
+ scroll.top = Math.min(pos.y, document.height - viewport.height);
45
46
  }
46
- if (scroll.left !== viewport.left || scroll.top !== viewport.top) {
47
+ if (scroll.left !== dimensions.left || scroll.top !== dimensions.top) {
47
48
  this.page.setScrollPosition(scroll);
48
49
  }
49
50
  return {
@@ -9,6 +9,10 @@ Poltergeist.WebPage = (function() {
9
9
  this["native"] = require('webpage').create();
10
10
  this.nodes = {};
11
11
  this._source = "";
12
+ this.setViewportSize({
13
+ width: 1024,
14
+ height: 768
15
+ });
12
16
  _ref = WebPage.CALLBACKS;
13
17
  for (_i = 0, _len = _ref.length; _i < _len; _i++) {
14
18
  callback = _ref[_i];
@@ -74,25 +78,57 @@ Poltergeist.WebPage = (function() {
74
78
  WebPage.prototype.viewportSize = function() {
75
79
  return this["native"].viewportSize;
76
80
  };
81
+ WebPage.prototype.setViewportSize = function(size) {
82
+ return this["native"].viewportSize = size;
83
+ };
77
84
  WebPage.prototype.scrollPosition = function() {
78
85
  return this["native"].scrollPosition;
79
86
  };
80
87
  WebPage.prototype.setScrollPosition = function(pos) {
81
88
  return this["native"].scrollPosition = pos;
82
89
  };
83
- WebPage.prototype.viewport = function() {
84
- var scroll, size;
90
+ WebPage.prototype.clipRect = function() {
91
+ return this["native"].clipRect;
92
+ };
93
+ WebPage.prototype.setClipRect = function(rect) {
94
+ return this["native"].clipRect = rect;
95
+ };
96
+ WebPage.prototype.dimensions = function() {
97
+ var scroll, viewport;
85
98
  scroll = this.scrollPosition();
86
- size = this.viewportSize();
99
+ viewport = this.viewportSize();
87
100
  return {
88
101
  top: scroll.top,
89
- bottom: scroll.top + size.height,
102
+ bottom: scroll.top + viewport.height,
90
103
  left: scroll.left,
91
- right: scroll.left + size.width,
92
- width: size.width,
93
- height: size.height
104
+ right: scroll.left + viewport.width,
105
+ viewport: viewport,
106
+ document: this.documentSize()
94
107
  };
95
108
  };
109
+ WebPage.prototype.validatedDimensions = function() {
110
+ var changed, dimensions, document;
111
+ dimensions = this.dimensions();
112
+ document = dimensions.document;
113
+ changed = false;
114
+ if (dimensions.right > document.width) {
115
+ dimensions.left -= dimensions.right - document.width;
116
+ dimensions.right = document.width;
117
+ changed = true;
118
+ }
119
+ if (dimensions.bottom > document.height) {
120
+ dimensions.top -= dimensions.bottom - document.height;
121
+ dimensions.bottom = document.height;
122
+ changed = true;
123
+ }
124
+ if (changed) {
125
+ this.setScrollPosition({
126
+ left: dimensions.left,
127
+ top: dimensions.top
128
+ });
129
+ }
130
+ return dimensions;
131
+ };
96
132
  WebPage.prototype.get = function(id) {
97
133
  var _base;
98
134
  return (_base = this.nodes)[id] || (_base[id] = new Poltergeist.Node(this, id));
@@ -1,5 +1,7 @@
1
- if phantom.version.major < 1 || phantom.version.minor < 3
2
- console.log "Poltergeist requires a PhantomJS version of at least 1.3"
1
+ if phantom.version.major < 1 ||
2
+ phantom.version.minor < 4 ||
3
+ phantom.version.patch < 1
4
+ console.log "Poltergeist requires a PhantomJS version of at least 1.4.1"
3
5
  phantom.exit(1)
4
6
 
5
7
  class Poltergeist
@@ -21,19 +21,20 @@ class Poltergeist.Node
21
21
  @page.nodeCall(@id, name, arguments)
22
22
 
23
23
  scrollIntoView: ->
24
- viewport = @page.viewport()
25
- size = @page.documentSize()
26
- pos = this.position()
24
+ dimensions = @page.validatedDimensions()
25
+ document = dimensions.document
26
+ viewport = dimensions.viewport
27
+ pos = this.position()
27
28
 
28
- scroll = { left: viewport.left, top: viewport.top }
29
+ scroll = { left: dimensions.left, top: dimensions.top }
29
30
 
30
- unless viewport.left <= pos.x < viewport.right
31
- scroll.left = Math.min(pos.x, size.width - viewport.width)
31
+ unless dimensions.left <= pos.x < dimensions.right
32
+ scroll.left = Math.min(pos.x, document.width - viewport.width)
32
33
 
33
- unless viewport.top <= pos.y < viewport.bottom
34
- scroll.top = Math.min(pos.y, size.height - viewport.height)
34
+ unless dimensions.top <= pos.y < dimensions.bottom
35
+ scroll.top = Math.min(pos.y, document.height - viewport.height)
35
36
 
36
- if scroll.left != viewport.left || scroll.top != viewport.top
37
+ if scroll.left != dimensions.left || scroll.top != dimensions.top
37
38
  @page.setScrollPosition(scroll)
38
39
 
39
40
  position: this.relativePosition(pos, scroll),
@@ -9,6 +9,8 @@ class Poltergeist.WebPage
9
9
  @nodes = {}
10
10
  @_source = ""
11
11
 
12
+ this.setViewportSize(width: 1024, height: 768)
13
+
12
14
  for callback in WebPage.CALLBACKS
13
15
  this.bindCallback(callback)
14
16
 
@@ -27,7 +29,7 @@ class Poltergeist.WebPage
27
29
  onInitializedNative: ->
28
30
  @_source = null
29
31
  this.injectAgent()
30
- this.setScrollPosition({ left: 0, top: 0})
32
+ this.setScrollPosition({ left: 0, top: 0 })
31
33
 
32
34
  injectAgent: ->
33
35
  if this.evaluate(-> typeof __poltergeist) == "undefined"
@@ -53,19 +55,50 @@ class Poltergeist.WebPage
53
55
  viewportSize: ->
54
56
  @native.viewportSize
55
57
 
58
+ setViewportSize: (size) ->
59
+ @native.viewportSize = size
60
+
56
61
  scrollPosition: ->
57
62
  @native.scrollPosition
58
63
 
59
64
  setScrollPosition: (pos) ->
60
65
  @native.scrollPosition = pos
61
66
 
62
- viewport: ->
63
- scroll = this.scrollPosition()
64
- size = this.viewportSize()
67
+ clipRect: ->
68
+ @native.clipRect
69
+
70
+ setClipRect: (rect) ->
71
+ @native.clipRect = rect
72
+
73
+ dimensions: ->
74
+ scroll = this.scrollPosition()
75
+ viewport = this.viewportSize()
76
+
77
+ top: scroll.top, bottom: scroll.top + viewport.height,
78
+ left: scroll.left, right: scroll.left + viewport.width,
79
+ viewport: viewport
80
+ document: this.documentSize()
81
+
82
+ # A work around for http://code.google.com/p/phantomjs/issues/detail?id=277
83
+ validatedDimensions: ->
84
+ dimensions = this.dimensions()
85
+ document = dimensions.document
86
+ changed = false
87
+
88
+ if dimensions.right > document.width
89
+ dimensions.left -= dimensions.right - document.width
90
+ dimensions.right = document.width
91
+ changed = true
92
+
93
+ if dimensions.bottom > document.height
94
+ dimensions.top -= dimensions.bottom - document.height
95
+ dimensions.bottom = document.height
96
+ changed = true
97
+
98
+ if changed
99
+ this.setScrollPosition(left: dimensions.left, top: dimensions.top)
65
100
 
66
- top: scroll.top, bottom: scroll.top + size.height,
67
- left: scroll.left, right: scroll.left + size.width,
68
- width: size.width, height: size.height
101
+ dimensions
69
102
 
70
103
  get: (id) ->
71
104
  @nodes[id] or= new Poltergeist.Node(this, id)
@@ -64,8 +64,12 @@ module Capybara::Poltergeist
64
64
  browser.reset
65
65
  end
66
66
 
67
- def render(path)
68
- browser.render(path)
67
+ def render(path, options = {})
68
+ browser.render(path, options)
69
+ end
70
+
71
+ def resize(width, height)
72
+ browser.resize(width, height)
69
73
  end
70
74
 
71
75
  def wait?
@@ -22,5 +22,25 @@ module Capybara
22
22
  @node = node
23
23
  end
24
24
  end
25
+
26
+ class TimeoutError < Error
27
+ def initialize(message)
28
+ @message = message
29
+ end
30
+
31
+ def message
32
+ "Timed out waiting for response to #{@message}"
33
+ end
34
+ end
35
+
36
+ class DeadClient < Error
37
+ def initialize(message)
38
+ @message = message
39
+ end
40
+
41
+ def message
42
+ "The PhantomJS client died while processing #{@message}"
43
+ end
44
+ end
25
45
  end
26
46
  end
@@ -34,15 +34,12 @@ module Capybara::Poltergeist
34
34
 
35
35
  def set(value)
36
36
  if tag_name == 'input'
37
- type = self[:type]
38
-
39
- if type == 'radio'
37
+ case self[:type]
38
+ when 'radio'
40
39
  click
41
- elsif type == 'checkbox'
42
- if value && !checked? || !value && checked?
43
- click
44
- end
45
- elsif type == 'file'
40
+ when 'checkbox'
41
+ click if value != checked?
42
+ when 'file'
46
43
  command :select_file, value
47
44
  else
48
45
  command :set, value
@@ -11,14 +11,12 @@ module Capybara::Poltergeist
11
11
  class ServerManager
12
12
  include Singleton
13
13
 
14
- TIMEOUT = 30
15
-
16
- class TimeoutError < StandardError
17
- def initialize(message)
18
- super "Server timed out waiting for response to #{@message}"
19
- end
14
+ class << self
15
+ attr_accessor :timeout
20
16
  end
21
17
 
18
+ self.timeout = 30
19
+
22
20
  attr_reader :sockets
23
21
 
24
22
  def initialize
@@ -45,7 +43,7 @@ module Capybara::Poltergeist
45
43
  def send(port, message)
46
44
  @message = nil
47
45
 
48
- Timeout.timeout(TIMEOUT, TimeoutError.new(message)) do
46
+ Timeout.timeout(self.class.timeout) do
49
47
  # Ensure there is a socket before trying to send a message on it.
50
48
  Thread.pass until sockets[port]
51
49
 
@@ -53,10 +51,16 @@ module Capybara::Poltergeist
53
51
  thread_execute { sockets[port].send(message) }
54
52
 
55
53
  # Wait for the response message
56
- Thread.pass until @message
54
+ Thread.pass until @message || sockets[port].nil?
57
55
  end
58
56
 
59
- @message
57
+ if sockets[port]
58
+ @message
59
+ else
60
+ raise DeadClient.new(message)
61
+ end
62
+ rescue Timeout::Error
63
+ raise TimeoutError.new(message)
60
64
  end
61
65
 
62
66
  def thread_execute(&instruction)
@@ -77,13 +81,9 @@ module Capybara::Poltergeist
77
81
 
78
82
  def start_websocket_server(port)
79
83
  EventMachine.start_server('127.0.0.1', port, EventMachine::WebSocket::Connection, {}) do |socket|
80
- socket.onopen do
81
- connection_opened(port, socket)
82
- end
83
-
84
- socket.onmessage do |message|
85
- message_received(message)
86
- end
84
+ socket.onopen { connection_opened(port, socket) }
85
+ socket.onclose { connection_closed(port) }
86
+ socket.onmessage { |message| message_received(message) }
87
87
  end
88
88
  end
89
89
 
@@ -92,6 +92,10 @@ module Capybara::Poltergeist
92
92
  await_instruction
93
93
  end
94
94
 
95
+ def connection_closed(port)
96
+ sockets[port] = nil
97
+ end
98
+
95
99
  def message_received(message)
96
100
  @message = message
97
101
  await_instruction
@@ -1,5 +1,5 @@
1
1
  module Capybara
2
2
  module Poltergeist
3
- VERSION = "0.1.0"
3
+ VERSION = "0.2.0"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: poltergeist
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,22 +9,22 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2011-10-27 00:00:00.000000000Z
12
+ date: 2012-01-03 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: capybara
16
- requirement: &20250700 !ruby/object:Gem::Requirement
16
+ requirement: &17036740 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ~>
20
20
  - !ruby/object:Gem::Version
21
- version: 1.1.0
21
+ version: '1.0'
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *20250700
24
+ version_requirements: *17036740
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: em-websocket
27
- requirement: &20220020 !ruby/object:Gem::Requirement
27
+ requirement: &17036240 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ~>
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: 0.3.1
33
33
  type: :runtime
34
34
  prerelease: false
35
- version_requirements: *20220020
35
+ version_requirements: *17036240
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: json
38
- requirement: &20216800 !ruby/object:Gem::Requirement
38
+ requirement: &17024280 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ~>
@@ -43,7 +43,62 @@ dependencies:
43
43
  version: '1.6'
44
44
  type: :runtime
45
45
  prerelease: false
46
- version_requirements: *20216800
46
+ version_requirements: *17024280
47
+ - !ruby/object:Gem::Dependency
48
+ name: sfl
49
+ requirement: &17023220 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: '2.0'
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: *17023220
58
+ - !ruby/object:Gem::Dependency
59
+ name: rspec
60
+ requirement: &17022640 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ~>
64
+ - !ruby/object:Gem::Version
65
+ version: 2.7.0
66
+ type: :development
67
+ prerelease: false
68
+ version_requirements: *17022640
69
+ - !ruby/object:Gem::Dependency
70
+ name: sinatra
71
+ requirement: &17021400 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ~>
75
+ - !ruby/object:Gem::Version
76
+ version: '1.0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: *17021400
80
+ - !ruby/object:Gem::Dependency
81
+ name: rake
82
+ requirement: &17020760 !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ~>
86
+ - !ruby/object:Gem::Version
87
+ version: 0.9.2
88
+ type: :development
89
+ prerelease: false
90
+ version_requirements: *17020760
91
+ - !ruby/object:Gem::Dependency
92
+ name: image_size
93
+ requirement: &17020260 !ruby/object:Gem::Requirement
94
+ none: false
95
+ requirements:
96
+ - - ~>
97
+ - !ruby/object:Gem::Version
98
+ version: '1.0'
99
+ type: :development
100
+ prerelease: false
101
+ version_requirements: *17020260
47
102
  description: PhantomJS driver for Capybara
48
103
  email:
49
104
  - j@jonathanleighton.com
@@ -95,7 +150,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
95
150
  version: '0'
96
151
  requirements: []
97
152
  rubyforge_project:
98
- rubygems_version: 1.8.6
153
+ rubygems_version: 1.8.10
99
154
  signing_key:
100
155
  specification_version: 3
101
156
  summary: PhantomJS driver for Capybara