poltergeist 0.2.0 → 0.3.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.
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