poltergeist 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # Poltergeist - A PhantomJS driver for Capybara #
2
2
 
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
+
5
+ [![Build Status](https://secure.travis-ci.org/jonleighton/poltergeist.png)](http://travis-ci.org/jonleighton/poltergeist)
4
6
 
5
7
  Poltergeist is a driver for [Capybara](https://github.com/jnicklas/capybara). It allows you to
6
8
  run your Capybara tests on a headless [WebKit](http://webkit.org) browser,
@@ -13,54 +15,62 @@ Add `poltergeist` to your Gemfile, and add in your test setup add:
13
15
  require 'capybara/poltergeist'
14
16
  Capybara.javascript_driver = :poltergeist
15
17
 
16
- Currently PhantomJS is not 'truly headless', so to run it on a continuous integration
17
- server you will need to use [Xvfb](http://en.wikipedia.org/wiki/Xvfb). You can either use the
18
- [headless gem](https://github.com/leonid-shevtsov/headless) for this,
19
- or make sure that Xvfb is running and the `DISPLAY` environment variable is set.
20
-
21
18
  ## Installing PhantomJS ##
22
19
 
23
20
  You need PhantomJS 1.4.1+, built against Qt 4.8, on your system.
24
21
 
25
- ### Mac users ##
22
+ ### Pre-built binaries ##
23
+
24
+ There are [pre-built
25
+ binaries](http://code.google.com/p/phantomjs/downloads/list) of
26
+ PhantomJS for Linux, Mac and Windows. This is the easiest and best way
27
+ to install it. The binaries including a patched version of Qt 4.8 so you
28
+ don't need to install that separately.
29
+
30
+ Note that if you have a 'dynamic' package, it's important to maintain
31
+ the relationship between `bin/phantomjs` and `lib/`. This is because the
32
+ `bin/phantomjs` binary looks in `../lib/` for its library files. So the
33
+ best thing to do is to link (rather than copy) it into your `PATH`:
26
34
 
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.
35
+ ```
36
+ ln -s /path/to/phantomjs/bin/phantomjs /usr/local/bin/phantomjs
37
+ ```
30
38
 
31
- ### Linux users, or if the pre-built Mac binary doesn't work ###
39
+ ### Compiling PhantomJS ###
32
40
 
33
- You need to build PhantomJS manually. Unfortunately, this not
34
- currently straightforward, for two reasons:
41
+ If you're having trouble with a pre-built binary package, you can
42
+ compile PhantomJS yourself. PhantomJS must be built against Qt 4.8, and
43
+ some patches must be applied, so note that you cannot build it against
44
+ your system install of Qt.
35
45
 
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.
46
+ [Download the tarball](http://code.google.com/p/phantomjs/downloads/detail?name=phantomjs-1.4.1-source.tar.gz&can=2&q=)
47
+ and run either `deploy/build-linux.sh --qt-4.8` or `cd deploy; ./build-mac.sh`.
48
+ The script will
49
+ download Qt, apply some patches, build it, and then build PhantomJS
50
+ against the patched build of Qt. It takes quite a while, around 30
51
+ minutes on a modern computer with two hyperthreaded cores. Afterwards,
52
+ you should copy (or link) the `bin/phantomjs` binary into your `PATH`.
41
53
 
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.
54
+ ## Running on a CI ##
45
55
 
46
- So, you basically have two options:
56
+ Currently PhantomJS is not 'truly headless', so to run it on a continuous integration
57
+ server you will need to install [Xvfb](http://en.wikipedia.org/wiki/Xvfb).
58
+
59
+ ### On any generic server ###
60
+
61
+ Install PhantomJS and invoke your tests with `xvfb-run`, (e.g. `xvfb-run
62
+ rake`).
47
63
 
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).
64
+ ### Using [Travis CI](http://travis-ci.org/) ###
50
65
 
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`.
66
+ Travis CI has PhantomJS installed already! So all you need to do is add
67
+ the following to your `.travis.yml`:
58
68
 
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.
69
+ ``` yaml
70
+ before_script:
71
+ - "export DISPLAY=:99.0"
72
+ - "sh -e /etc/init.d/xvfb start"
73
+ ```
64
74
 
65
75
  ## What's supported? ##
66
76
 
@@ -97,6 +107,9 @@ test setup:
97
107
  * `:phantomjs` (String) - A custom path to the phantomjs executable
98
108
  * `:debug` (Boolean) - When true, debug output is logged to `STDERR`
99
109
  * `:logger` (Object responding to `puts`) - When present, debug output is written to this object
110
+ * `:timeout` (Numeric) - The number of seconds we'll wait for a response
111
+ when communicating with PhantomJS. `nil` means wait forever. Default
112
+ is 30.
100
113
 
101
114
  ## Bugs ##
102
115
 
@@ -140,6 +153,36 @@ makes debugging easier). Running `rake autocompile` will watch the
140
153
  `.coffee` files for changes, and compile them into
141
154
  `lib/capybara/client/compiled`.
142
155
 
156
+ ## Changes ##
157
+
158
+ ### 0.3 ###
159
+
160
+ * There was a bad bug to do with clicking elements in a page where the
161
+ page is smaller than the window. The incorrect position would be
162
+ calculated, and so the click would happen in the wrong place. This is
163
+ fixed. [Issue #8]
164
+
165
+ * Poltergeist didn't work in conjunction with the Thin web server,
166
+ because that server uses Event Machine, and Poltergeist was assuming
167
+ that it was the only thing in the process using EventMachine.
168
+
169
+ To solve this, EventMachine usage has been completely removed, which
170
+ has the welcome side-effect of being more efficient because we
171
+ no longer have the overhead of running a mostly-idle event loop.
172
+
173
+ [Issue #6]
174
+
175
+ * Added the `:timeout` option to configure the timeout when talking to
176
+ PhantomJS.
177
+
178
+ ### 0.2 ###
179
+
180
+ * First version considered 'ready', hopefully fewer problems.
181
+
182
+ ### 0.1 ###
183
+
184
+ * First version, various problems.
185
+
143
186
  ## License ##
144
187
 
145
188
  Copyright (c) 2011 Jonathan Leighton
@@ -2,12 +2,13 @@ require 'capybara'
2
2
 
3
3
  module Capybara
4
4
  module Poltergeist
5
- autoload :Driver, 'capybara/poltergeist/driver'
6
- autoload :Browser, 'capybara/poltergeist/browser'
7
- autoload :Node, 'capybara/poltergeist/node'
8
- autoload :ServerManager, 'capybara/poltergeist/server_manager'
9
- autoload :Server, 'capybara/poltergeist/server'
10
- autoload :Client, 'capybara/poltergeist/client'
5
+ autoload :Driver, 'capybara/poltergeist/driver'
6
+ autoload :Browser, 'capybara/poltergeist/browser'
7
+ autoload :Node, 'capybara/poltergeist/node'
8
+ autoload :ServerManager, 'capybara/poltergeist/server_manager'
9
+ autoload :Server, 'capybara/poltergeist/server'
10
+ autoload :WebSocketServer, 'capybara/poltergeist/web_socket_server'
11
+ autoload :Client, 'capybara/poltergeist/client'
11
12
 
12
13
  require 'capybara/poltergeist/errors'
13
14
  end
@@ -4,12 +4,22 @@ module Capybara::Poltergeist
4
4
  class Browser
5
5
  attr_reader :options, :server, :client
6
6
 
7
+ DEFAULT_TIMEOUT = 30
8
+
7
9
  def initialize(options = {})
8
10
  @options = options
9
- @server = Server.new
11
+ @server = Server.new(options.fetch(:timeout, DEFAULT_TIMEOUT))
10
12
  @client = Client.new(server.port, options[:phantomjs])
11
13
  end
12
14
 
15
+ def timeout
16
+ server.timeout
17
+ end
18
+
19
+ def timeout=(sec)
20
+ server.timeout = sec
21
+ end
22
+
13
23
  def restart
14
24
  server.restart
15
25
  client.restart
@@ -81,8 +81,25 @@ class PoltergeistAgent.Node
81
81
  event.initEvent("change", true, false)
82
82
  @element.dispatchEvent(event)
83
83
 
84
+ insideBody: ->
85
+ @element == @agent.document.body ||
86
+ @agent.document.evaluate('ancestor::body', @element, null, XPathResult.BOOLEAN_TYPE, null).booleanValue
87
+
84
88
  text: ->
85
- @element.textContent
89
+ return '' unless this.isVisible()
90
+
91
+ if this.insideBody()
92
+ el = @element
93
+ else
94
+ el = @agent.document.body
95
+
96
+ results = @agent.document.evaluate('.//text()[not(ancestor::script)]', el, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null)
97
+ text = ''
98
+
99
+ for i in [0...results.snapshotLength]
100
+ node = results.snapshotItem(i)
101
+ text += node.textContent if this.isVisible(node.parentNode)
102
+ text
86
103
 
87
104
  getAttribute: (name) ->
88
105
  if name == 'checked' || name == 'selected'
@@ -123,19 +140,17 @@ class PoltergeistAgent.Node
123
140
  tagName: ->
124
141
  @element.tagName
125
142
 
126
- elementVisible: (element) ->
143
+ isVisible: (element) ->
144
+ element = @element unless element
127
145
 
128
- isVisible: (id) ->
129
- visible = (element) ->
130
- if @window.getComputedStyle(element).display == 'none'
131
- false
132
- else if element.parentElement
133
- visible element.parentElement
134
- else
135
- true
136
- visible @element
146
+ if @agent.window.getComputedStyle(element).display == 'none'
147
+ false
148
+ else if element.parentElement
149
+ this.isVisible element.parentElement
150
+ else
151
+ true
137
152
 
138
- position: (id) ->
153
+ position: ->
139
154
  pos = (element) ->
140
155
  x = element.offsetLeft
141
156
  y = element.offsetTop
@@ -170,6 +185,3 @@ document.addEventListener(
170
185
  'DOMContentLoaded',
171
186
  -> console.log('__DOMContentLoaded')
172
187
  )
173
-
174
- # Important to return true here - Phantom seems to choke otherwise
175
- true
@@ -142,3 +142,6 @@ class Poltergeist.Browser
142
142
 
143
143
  exit: ->
144
144
  phantom.exit()
145
+
146
+ noop: ->
147
+ # NOOOOOOP!
@@ -90,8 +90,28 @@ PoltergeistAgent.Node = (function() {
90
90
  event.initEvent("change", true, false);
91
91
  return this.element.dispatchEvent(event);
92
92
  };
93
+ Node.prototype.insideBody = function() {
94
+ return this.element === this.agent.document.body || this.agent.document.evaluate('ancestor::body', this.element, null, XPathResult.BOOLEAN_TYPE, null).booleanValue;
95
+ };
93
96
  Node.prototype.text = function() {
94
- return this.element.textContent;
97
+ var el, i, node, results, text, _ref;
98
+ if (!this.isVisible()) {
99
+ return '';
100
+ }
101
+ if (this.insideBody()) {
102
+ el = this.element;
103
+ } else {
104
+ el = this.agent.document.body;
105
+ }
106
+ results = this.agent.document.evaluate('.//text()[not(ancestor::script)]', el, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
107
+ text = '';
108
+ for (i = 0, _ref = results.snapshotLength; 0 <= _ref ? i < _ref : i > _ref; 0 <= _ref ? i++ : i--) {
109
+ node = results.snapshotItem(i);
110
+ if (this.isVisible(node.parentNode)) {
111
+ text += node.textContent;
112
+ }
113
+ }
114
+ return text;
95
115
  };
96
116
  Node.prototype.getAttribute = function(name) {
97
117
  if (name === 'checked' || name === 'selected') {
@@ -144,21 +164,19 @@ PoltergeistAgent.Node = (function() {
144
164
  Node.prototype.tagName = function() {
145
165
  return this.element.tagName;
146
166
  };
147
- Node.prototype.elementVisible = function(element) {};
148
- Node.prototype.isVisible = function(id) {
149
- var visible;
150
- visible = function(element) {
151
- if (this.window.getComputedStyle(element).display === 'none') {
152
- return false;
153
- } else if (element.parentElement) {
154
- return visible(element.parentElement);
155
- } else {
156
- return true;
157
- }
158
- };
159
- return visible(this.element);
167
+ Node.prototype.isVisible = function(element) {
168
+ if (!element) {
169
+ element = this.element;
170
+ }
171
+ if (this.agent.window.getComputedStyle(element).display === 'none') {
172
+ return false;
173
+ } else if (element.parentElement) {
174
+ return this.isVisible(element.parentElement);
175
+ } else {
176
+ return true;
177
+ }
160
178
  };
161
- Node.prototype.position = function(id) {
179
+ Node.prototype.position = function() {
162
180
  var pos;
163
181
  pos = function(element) {
164
182
  var parentPos, x, y;
@@ -194,5 +212,4 @@ PoltergeistAgent.Node = (function() {
194
212
  window.__poltergeist = new PoltergeistAgent;
195
213
  document.addEventListener('DOMContentLoaded', function() {
196
214
  return console.log('__DOMContentLoaded');
197
- });
198
- true;
215
+ });
@@ -154,5 +154,6 @@ Poltergeist.Browser = (function() {
154
154
  Browser.prototype.exit = function() {
155
155
  return phantom.exit();
156
156
  };
157
+ Browser.prototype.noop = function() {};
157
158
  return Browser;
158
159
  })();
@@ -1,5 +1,5 @@
1
1
  var Poltergeist;
2
- if (phantom.version.major < 1 || phantom.version.minor < 4 || phantom.version.patch < 1) {
2
+ if (("" + phantom.version.major + "." + phantom.version.minor + "." + phantom.version.patch) < "1.4.1") {
3
3
  console.log("Poltergeist requires a PhantomJS version of at least 1.4.1");
4
4
  phantom.exit(1);
5
5
  }
@@ -107,21 +107,21 @@ Poltergeist.WebPage = (function() {
107
107
  };
108
108
  };
109
109
  WebPage.prototype.validatedDimensions = function() {
110
- var changed, dimensions, document;
110
+ var changed, dimensions, document, orig_left, orig_top;
111
111
  dimensions = this.dimensions();
112
112
  document = dimensions.document;
113
113
  changed = false;
114
+ orig_left = dimensions.left;
115
+ orig_top = dimensions.top;
114
116
  if (dimensions.right > document.width) {
115
- dimensions.left -= dimensions.right - document.width;
117
+ dimensions.left = Math.max(0, dimensions.left - (dimensions.right - document.width));
116
118
  dimensions.right = document.width;
117
- changed = true;
118
119
  }
119
120
  if (dimensions.bottom > document.height) {
120
- dimensions.top -= dimensions.bottom - document.height;
121
+ dimensions.top = Math.max(0, dimensions.top - (dimensions.bottom - document.height));
121
122
  dimensions.bottom = document.height;
122
- changed = true;
123
123
  }
124
- if (changed) {
124
+ if (dimensions.left !== orig_left || dimensions.top !== orig_top) {
125
125
  this.setScrollPosition({
126
126
  left: dimensions.left,
127
127
  top: dimensions.top
@@ -1,6 +1,4 @@
1
- if phantom.version.major < 1 ||
2
- phantom.version.minor < 4 ||
3
- phantom.version.patch < 1
1
+ if "#{phantom.version.major}.#{phantom.version.minor}.#{phantom.version.patch}" < "1.4.1"
4
2
  console.log "Poltergeist requires a PhantomJS version of at least 1.4.1"
5
3
  phantom.exit(1)
6
4
 
@@ -85,17 +85,18 @@ class Poltergeist.WebPage
85
85
  document = dimensions.document
86
86
  changed = false
87
87
 
88
+ orig_left = dimensions.left
89
+ orig_top = dimensions.top
90
+
88
91
  if dimensions.right > document.width
89
- dimensions.left -= dimensions.right - document.width
92
+ dimensions.left = Math.max(0, dimensions.left - (dimensions.right - document.width))
90
93
  dimensions.right = document.width
91
- changed = true
92
94
 
93
95
  if dimensions.bottom > document.height
94
- dimensions.top -= dimensions.bottom - document.height
96
+ dimensions.top = Math.max(0, dimensions.top - (dimensions.bottom - document.height))
95
97
  dimensions.bottom = document.height
96
- changed = true
97
98
 
98
- if changed
99
+ if dimensions.left != orig_left || dimensions.top != orig_top
99
100
  this.setScrollPosition(left: dimensions.left, top: dimensions.top)
100
101
 
101
102
  dimensions
@@ -40,7 +40,7 @@ module Capybara::Poltergeist
40
40
  end
41
41
 
42
42
  def source
43
- browser.source
43
+ browser.source.to_s
44
44
  end
45
45
 
46
46
  def find(selector)
@@ -21,7 +21,7 @@ module Capybara::Poltergeist
21
21
  end
22
22
 
23
23
  def text
24
- command :text
24
+ command(:text).strip.gsub(/\s+/, ' ')
25
25
  end
26
26
 
27
27
  def [](name)
@@ -1,30 +1,32 @@
1
1
  module Capybara::Poltergeist
2
2
  class Server
3
- attr_reader :port
3
+ attr_reader :port, :socket, :timeout
4
4
 
5
- def initialize
6
- @port = find_available_port
5
+ def initialize(timeout = nil)
6
+ @port = find_available_port
7
+ @timeout = timeout
7
8
  start
8
9
  end
9
10
 
11
+ def timeout=(sec)
12
+ @timeout = @socket.timeout = sec
13
+ end
14
+
10
15
  def start
11
- server_manager.start(port)
16
+ @socket = WebSocketServer.new(port, timeout)
12
17
  end
13
18
 
14
19
  def restart
15
- server_manager.restart(port)
20
+ @socket.close
21
+ @socket = WebSocketServer.new(port, timeout)
16
22
  end
17
23
 
18
24
  def send(message)
19
- server_manager.send(port, message)
25
+ @socket.send(message) or raise DeadClient.new(message)
20
26
  end
21
27
 
22
28
  private
23
29
 
24
- def server_manager
25
- ServerManager.instance
26
- end
27
-
28
30
  def find_available_port
29
31
  server = TCPServer.new('127.0.0.1', 0)
30
32
  server.addr[1]
@@ -1,5 +1,5 @@
1
1
  module Capybara
2
2
  module Poltergeist
3
- VERSION = "0.2.0"
3
+ VERSION = "0.3.0"
4
4
  end
5
5
  end
@@ -0,0 +1,135 @@
1
+ require 'socket'
2
+ require 'stringio'
3
+ require 'http/parser'
4
+ require 'faye/websocket'
5
+
6
+ module Capybara::Poltergeist
7
+ # This is a 'custom' Web Socket server that is designed to be synchronous. What
8
+ # this means is that it sends a message, and then waits for a response. It does
9
+ # not expect to receive a message at any other time than right after it has sent
10
+ # a message. So it is basically operating a request/response cycle (which is not
11
+ # how Web Sockets are usually used, but it's what we want here, as we want to
12
+ # send a message to PhantomJS and then wait for it to respond).
13
+ class WebSocketServer
14
+ class FayeHandler
15
+ attr_reader :owner, :env, :parser
16
+
17
+ def initialize(owner, env)
18
+ @owner = owner
19
+ @env = env
20
+ @parser = Faye::WebSocket.parser(env).new(self)
21
+ @messages = []
22
+ end
23
+
24
+ def url
25
+ "ws://#{env['SERVER_NAME']}:#{env['SERVER_PORT']}/"
26
+ end
27
+
28
+ def handshake_response
29
+ parser.handshake_response
30
+ end
31
+
32
+ def parse(data)
33
+ parser.parse(data)
34
+ end
35
+
36
+ def encode(message)
37
+ parser.frame(Faye::WebSocket.encode(message))
38
+ end
39
+
40
+ def receive(message)
41
+ @messages << message
42
+ end
43
+
44
+ def message?
45
+ @messages.any?
46
+ end
47
+
48
+ def next_message
49
+ @messages.shift
50
+ end
51
+ end
52
+
53
+ # How much to try to read from the socket at once (it's kinda arbitrary because we
54
+ # just keep reading until we've received a full frame)
55
+ RECV_SIZE = 1024
56
+
57
+ attr_reader :port, :parser, :socket, :handler, :server
58
+ attr_accessor :timeout
59
+
60
+ def initialize(port, timeout = nil)
61
+ @port = port
62
+ @parser = Http::Parser.new
63
+ @server = TCPServer.open(port)
64
+ @timeout = timeout
65
+ end
66
+
67
+ def connected?
68
+ !socket.nil?
69
+ end
70
+
71
+ # Accept a client on the TCP server socket, then receive its initial HTTP request
72
+ # and use that to initialize a Web Socket.
73
+ def accept
74
+ @socket = server.accept
75
+
76
+ while msg = socket.gets
77
+ parser << msg
78
+ break if msg == "\r\n"
79
+ end
80
+
81
+ @handler = FayeHandler.new(self, env)
82
+ socket.write handler.handshake_response
83
+ end
84
+
85
+ # Note that the socket.read(8) assumes we're using the hixie-76 parser. This is
86
+ # fine for now as it corresponds to the version of Web Sockets that the version of
87
+ # WebKit in PhantomJS uses, but it might need to change in the future.
88
+ def env
89
+ @env ||= begin
90
+ env = {
91
+ 'REQUEST_METHOD' => parser.http_method,
92
+ 'SCRIPT_NAME' => '',
93
+ 'PATH_INFO' => '',
94
+ 'QUERY_STRING' => '',
95
+ 'SERVER_NAME' => '127.0.0.1',
96
+ 'SERVER_PORT' => port.to_s,
97
+ 'HTTP_ORIGIN' => 'http://127.0.0.1:2000/',
98
+ 'rack.input' => StringIO.new(socket.read(8))
99
+ }
100
+ parser.headers.each do |header, value|
101
+ env['HTTP_' + header.upcase.gsub('-', '_')] = value
102
+ end
103
+ env
104
+ end
105
+ end
106
+
107
+ # Block until the next message is available from the Web Socket
108
+ def receive
109
+ until handler.message?
110
+ IO.select([socket], [], [], timeout)
111
+ data = socket.recv_nonblock(RECV_SIZE)
112
+ break if data.empty?
113
+ handler.parse(data)
114
+ end
115
+
116
+ handler.next_message
117
+ end
118
+
119
+ # Send a message and block until there is a response
120
+ def send(message)
121
+ accept unless connected?
122
+ socket.write handler.encode(message)
123
+ receive
124
+ rescue Errno::EAGAIN, Errno::EWOULDBLOCK
125
+ raise TimeoutError.new(message)
126
+ end
127
+
128
+ def close
129
+ [server, socket].each do |s|
130
+ s.close_read
131
+ s.close_write
132
+ end
133
+ end
134
+ end
135
+ 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.2.0
4
+ version: 0.3.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-01-03 00:00:00.000000000 Z
12
+ date: 2012-01-13 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: capybara
16
- requirement: &17036740 !ruby/object:Gem::Requirement
16
+ requirement: &17990880 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ~>
@@ -21,43 +21,54 @@ dependencies:
21
21
  version: '1.0'
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *17036740
24
+ version_requirements: *17990880
25
25
  - !ruby/object:Gem::Dependency
26
- name: em-websocket
27
- requirement: &17036240 !ruby/object:Gem::Requirement
26
+ name: json
27
+ requirement: &17990000 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ~>
31
31
  - !ruby/object:Gem::Version
32
- version: 0.3.1
32
+ version: '1.6'
33
33
  type: :runtime
34
34
  prerelease: false
35
- version_requirements: *17036240
35
+ version_requirements: *17990000
36
36
  - !ruby/object:Gem::Dependency
37
- name: json
38
- requirement: &17024280 !ruby/object:Gem::Requirement
37
+ name: sfl
38
+ requirement: &17988720 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ~>
42
42
  - !ruby/object:Gem::Version
43
- version: '1.6'
43
+ version: '2.0'
44
44
  type: :runtime
45
45
  prerelease: false
46
- version_requirements: *17024280
46
+ version_requirements: *17988720
47
47
  - !ruby/object:Gem::Dependency
48
- name: sfl
49
- requirement: &17023220 !ruby/object:Gem::Requirement
48
+ name: http_parser.rb
49
+ requirement: &17987320 !ruby/object:Gem::Requirement
50
50
  none: false
51
51
  requirements:
52
52
  - - ~>
53
53
  - !ruby/object:Gem::Version
54
- version: '2.0'
54
+ version: 0.5.3
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: *17987320
58
+ - !ruby/object:Gem::Dependency
59
+ name: faye-websocket
60
+ requirement: &17986640 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ~>
64
+ - !ruby/object:Gem::Version
65
+ version: 0.2.0
55
66
  type: :runtime
56
67
  prerelease: false
57
- version_requirements: *17023220
68
+ version_requirements: *17986640
58
69
  - !ruby/object:Gem::Dependency
59
70
  name: rspec
60
- requirement: &17022640 !ruby/object:Gem::Requirement
71
+ requirement: &17985780 !ruby/object:Gem::Requirement
61
72
  none: false
62
73
  requirements:
63
74
  - - ~>
@@ -65,10 +76,10 @@ dependencies:
65
76
  version: 2.7.0
66
77
  type: :development
67
78
  prerelease: false
68
- version_requirements: *17022640
79
+ version_requirements: *17985780
69
80
  - !ruby/object:Gem::Dependency
70
81
  name: sinatra
71
- requirement: &17021400 !ruby/object:Gem::Requirement
82
+ requirement: &17984960 !ruby/object:Gem::Requirement
72
83
  none: false
73
84
  requirements:
74
85
  - - ~>
@@ -76,10 +87,10 @@ dependencies:
76
87
  version: '1.0'
77
88
  type: :development
78
89
  prerelease: false
79
- version_requirements: *17021400
90
+ version_requirements: *17984960
80
91
  - !ruby/object:Gem::Dependency
81
92
  name: rake
82
- requirement: &17020760 !ruby/object:Gem::Requirement
93
+ requirement: &17967380 !ruby/object:Gem::Requirement
83
94
  none: false
84
95
  requirements:
85
96
  - - ~>
@@ -87,10 +98,10 @@ dependencies:
87
98
  version: 0.9.2
88
99
  type: :development
89
100
  prerelease: false
90
- version_requirements: *17020760
101
+ version_requirements: *17967380
91
102
  - !ruby/object:Gem::Dependency
92
103
  name: image_size
93
- requirement: &17020260 !ruby/object:Gem::Requirement
104
+ requirement: &17966680 !ruby/object:Gem::Requirement
94
105
  none: false
95
106
  requirements:
96
107
  - - ~>
@@ -98,7 +109,7 @@ dependencies:
98
109
  version: '1.0'
99
110
  type: :development
100
111
  prerelease: false
101
- version_requirements: *17020260
112
+ version_requirements: *17966680
102
113
  description: PhantomJS driver for Capybara
103
114
  email:
104
115
  - j@jonathanleighton.com
@@ -111,6 +122,7 @@ files:
111
122
  - lib/capybara/poltergeist/server.rb
112
123
  - lib/capybara/poltergeist/browser.rb
113
124
  - lib/capybara/poltergeist/errors.rb
125
+ - lib/capybara/poltergeist/web_socket_server.rb
114
126
  - lib/capybara/poltergeist/version.rb
115
127
  - lib/capybara/poltergeist/driver.rb
116
128
  - lib/capybara/poltergeist/client/connection.coffee
@@ -125,11 +137,9 @@ files:
125
137
  - lib/capybara/poltergeist/client/web_page.coffee
126
138
  - lib/capybara/poltergeist/client/browser.coffee
127
139
  - lib/capybara/poltergeist/client/agent.coffee
128
- - lib/capybara/poltergeist/server_manager.rb
129
140
  - lib/capybara/poltergeist/node.rb
130
141
  - LICENSE
131
142
  - README.md
132
- - CHANGELOG.md
133
143
  homepage: http://github.com/jonleighton/poltergeist
134
144
  licenses: []
135
145
  post_install_message:
@@ -155,3 +165,4 @@ signing_key:
155
165
  specification_version: 3
156
166
  summary: PhantomJS driver for Capybara
157
167
  test_files: []
168
+ has_rdoc:
data/CHANGELOG.md DELETED
@@ -1,7 +0,0 @@
1
- ## 0.2 ##
2
-
3
- * First version considered 'ready', hopefully fewer problems.
4
-
5
- ## 0.1 ##
6
-
7
- * First version, various problems.
@@ -1,122 +0,0 @@
1
- require 'em-websocket'
2
- require 'timeout'
3
- require 'singleton'
4
-
5
- module Capybara::Poltergeist
6
- # The reason for the lolzy thread code is because the EM reactor blocks the thread, so
7
- # we have to put it in its own thread.
8
- #
9
- # The reason we are using EM, is because it has a WebSocket library. If there's a decent
10
- # WebSocket library that doesn't require an event loop, we can use that.
11
- class ServerManager
12
- include Singleton
13
-
14
- class << self
15
- attr_accessor :timeout
16
- end
17
-
18
- self.timeout = 30
19
-
20
- attr_reader :sockets
21
-
22
- def initialize
23
- @instruction = nil
24
- @response = nil
25
- @sockets = {}
26
- @waiting = false
27
-
28
- @main = Thread.current
29
- @thread = Thread.new { start_event_loop }
30
- @thread.abort_on_exception = true
31
- end
32
-
33
- def start(port)
34
- thread_execute { start_websocket_server(port) }
35
- end
36
-
37
- # This isn't a 'proper' restart. It's more like 'wait for the client to connect again'.
38
- def restart(port)
39
- sockets[port] = nil
40
- @thread.run
41
- end
42
-
43
- def send(port, message)
44
- @message = nil
45
-
46
- Timeout.timeout(self.class.timeout) do
47
- # Ensure there is a socket before trying to send a message on it.
48
- Thread.pass until sockets[port]
49
-
50
- # Send the message
51
- thread_execute { sockets[port].send(message) }
52
-
53
- # Wait for the response message
54
- Thread.pass until @message || sockets[port].nil?
55
- end
56
-
57
- if sockets[port]
58
- @message
59
- else
60
- raise DeadClient.new(message)
61
- end
62
- rescue Timeout::Error
63
- raise TimeoutError.new(message)
64
- end
65
-
66
- def thread_execute(&instruction)
67
- # Ensure that the thread is waiting for an instruction before we wake it up
68
- # to receive the instruction
69
- Thread.pass until @waiting
70
-
71
- @instruction = instruction
72
- @waiting = false
73
-
74
- # Bring the EM thread out of its sleep so that it can execute the instruction.
75
- @thread.run
76
- end
77
-
78
- def start_event_loop
79
- EM.run { await_instruction }
80
- end
81
-
82
- def start_websocket_server(port)
83
- EventMachine.start_server('127.0.0.1', port, EventMachine::WebSocket::Connection, {}) do |socket|
84
- socket.onopen { connection_opened(port, socket) }
85
- socket.onclose { connection_closed(port) }
86
- socket.onmessage { |message| message_received(message) }
87
- end
88
- end
89
-
90
- def connection_opened(port, socket)
91
- sockets[port] = socket
92
- await_instruction
93
- end
94
-
95
- def connection_closed(port)
96
- sockets[port] = nil
97
- end
98
-
99
- def message_received(message)
100
- @message = message
101
- await_instruction
102
- end
103
-
104
- # Stop the thread so that it can be manually scheduled by the parent once there is
105
- # something to do
106
- def await_instruction
107
- # Sleep this thread. The main thread will wake us up when there is an instruction
108
- # to perform.
109
- @waiting = true
110
- Thread.stop
111
-
112
- # Main thread has woken us up, so execute the current instruction.
113
- if @instruction
114
- @instruction.call
115
- @instruction = nil
116
- end
117
-
118
- # Continue execution of the thread until a socket callback fires, which will
119
- # trigger then method again and send us back to sleep.
120
- end
121
- end
122
- end