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 +79 -36
- data/lib/capybara/poltergeist.rb +7 -6
- data/lib/capybara/poltergeist/browser.rb +11 -1
- data/lib/capybara/poltergeist/client/agent.coffee +27 -15
- data/lib/capybara/poltergeist/client/browser.coffee +3 -0
- data/lib/capybara/poltergeist/client/compiled/agent.js +34 -17
- data/lib/capybara/poltergeist/client/compiled/browser.js +1 -0
- data/lib/capybara/poltergeist/client/compiled/main.js +1 -1
- data/lib/capybara/poltergeist/client/compiled/web_page.js +6 -6
- data/lib/capybara/poltergeist/client/main.coffee +1 -3
- data/lib/capybara/poltergeist/client/web_page.coffee +6 -5
- data/lib/capybara/poltergeist/driver.rb +1 -1
- data/lib/capybara/poltergeist/node.rb +1 -1
- data/lib/capybara/poltergeist/server.rb +12 -10
- data/lib/capybara/poltergeist/version.rb +1 -1
- data/lib/capybara/poltergeist/web_socket_server.rb +135 -0
- metadata +37 -26
- data/CHANGELOG.md +0 -7
- data/lib/capybara/poltergeist/server_manager.rb +0 -122
data/README.md
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
# Poltergeist - A PhantomJS driver for Capybara #
|
2
2
|
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.3.0
|
4
|
+
|
5
|
+
[](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
|
-
###
|
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
|
-
|
28
|
-
|
29
|
-
|
35
|
+
```
|
36
|
+
ln -s /path/to/phantomjs/bin/phantomjs /usr/local/bin/phantomjs
|
37
|
+
```
|
30
38
|
|
31
|
-
###
|
39
|
+
### Compiling PhantomJS ###
|
32
40
|
|
33
|
-
|
34
|
-
|
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.
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
52
|
-
|
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
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
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
|
data/lib/capybara/poltergeist.rb
CHANGED
@@ -2,12 +2,13 @@ require 'capybara'
|
|
2
2
|
|
3
3
|
module Capybara
|
4
4
|
module Poltergeist
|
5
|
-
autoload :Driver,
|
6
|
-
autoload :Browser,
|
7
|
-
autoload :Node,
|
8
|
-
autoload :ServerManager,
|
9
|
-
autoload :Server,
|
10
|
-
autoload :
|
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
|
-
|
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
|
-
|
143
|
+
isVisible: (element) ->
|
144
|
+
element = @element unless element
|
127
145
|
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
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:
|
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
|
@@ -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
|
-
|
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.
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
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(
|
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
|
+
});
|
@@ -1,5 +1,5 @@
|
|
1
1
|
var Poltergeist;
|
2
|
-
if (phantom.version.major
|
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
|
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
|
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 (
|
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
|
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
|
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
|
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
|
@@ -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
|
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
|
-
|
16
|
+
@socket = WebSocketServer.new(port, timeout)
|
12
17
|
end
|
13
18
|
|
14
19
|
def restart
|
15
|
-
|
20
|
+
@socket.close
|
21
|
+
@socket = WebSocketServer.new(port, timeout)
|
16
22
|
end
|
17
23
|
|
18
24
|
def send(message)
|
19
|
-
|
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]
|
@@ -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.
|
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-
|
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: &
|
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: *
|
24
|
+
version_requirements: *17990880
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
|
-
name:
|
27
|
-
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:
|
32
|
+
version: '1.6'
|
33
33
|
type: :runtime
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *17990000
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
|
-
name:
|
38
|
-
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: '
|
43
|
+
version: '2.0'
|
44
44
|
type: :runtime
|
45
45
|
prerelease: false
|
46
|
-
version_requirements: *
|
46
|
+
version_requirements: *17988720
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
|
-
name:
|
49
|
-
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:
|
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: *
|
68
|
+
version_requirements: *17986640
|
58
69
|
- !ruby/object:Gem::Dependency
|
59
70
|
name: rspec
|
60
|
-
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: *
|
79
|
+
version_requirements: *17985780
|
69
80
|
- !ruby/object:Gem::Dependency
|
70
81
|
name: sinatra
|
71
|
-
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: *
|
90
|
+
version_requirements: *17984960
|
80
91
|
- !ruby/object:Gem::Dependency
|
81
92
|
name: rake
|
82
|
-
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: *
|
101
|
+
version_requirements: *17967380
|
91
102
|
- !ruby/object:Gem::Dependency
|
92
103
|
name: image_size
|
93
|
-
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: *
|
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,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
|