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 +7 -0
- data/README.md +89 -10
- data/lib/capybara/poltergeist.rb +1 -3
- data/lib/capybara/poltergeist/browser.rb +10 -2
- data/lib/capybara/poltergeist/client.rb +3 -30
- data/lib/capybara/poltergeist/client/browser.coffee +29 -6
- data/lib/capybara/poltergeist/client/compiled/browser.js +47 -6
- data/lib/capybara/poltergeist/client/compiled/main.js +3 -3
- data/lib/capybara/poltergeist/client/compiled/node.js +11 -10
- data/lib/capybara/poltergeist/client/compiled/web_page.js +43 -7
- data/lib/capybara/poltergeist/client/main.coffee +4 -2
- data/lib/capybara/poltergeist/client/node.coffee +10 -9
- data/lib/capybara/poltergeist/client/web_page.coffee +40 -7
- data/lib/capybara/poltergeist/driver.rb +6 -2
- data/lib/capybara/poltergeist/errors.rb +20 -0
- data/lib/capybara/poltergeist/node.rb +5 -8
- data/lib/capybara/poltergeist/server_manager.rb +20 -16
- data/lib/capybara/poltergeist/version.rb +1 -1
- metadata +65 -10
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Poltergeist - A PhantomJS driver for Capybara #
|
2
2
|
|
3
|
-
Version: 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
|
12
|
-
in your test setup.
|
11
|
+
Add `poltergeist` to your Gemfile, and add in your test setup add:
|
13
12
|
|
14
|
-
|
15
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
data/lib/capybara/poltergeist.rb
CHANGED
@@ -9,9 +9,7 @@ module Capybara
|
|
9
9
|
autoload :Server, 'capybara/poltergeist/server'
|
10
10
|
autoload :Client, 'capybara/poltergeist/client'
|
11
11
|
|
12
|
-
|
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 '
|
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 =
|
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() {
|
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 = =>
|
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
|
106
|
+
@owner.sendResponse(true) unless load_detected
|
103
107
|
,
|
104
|
-
|
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.
|
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() {
|
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
|
-
|
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 (!
|
98
|
+
if (!load_detected) {
|
96
99
|
return this.owner.sendResponse(true);
|
97
100
|
}
|
98
|
-
}, this),
|
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
|
-
|
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
|
-
console.log("Poltergeist requires a PhantomJS version of at least 1.
|
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,
|
33
|
-
|
34
|
-
|
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:
|
38
|
-
top:
|
38
|
+
left: dimensions.left,
|
39
|
+
top: dimensions.top
|
39
40
|
};
|
40
|
-
if (!((
|
41
|
-
scroll.left = Math.min(pos.x,
|
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 (!((
|
44
|
-
scroll.top = Math.min(pos.y,
|
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 !==
|
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.
|
84
|
-
|
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
|
-
|
99
|
+
viewport = this.viewportSize();
|
87
100
|
return {
|
88
101
|
top: scroll.top,
|
89
|
-
bottom: scroll.top +
|
102
|
+
bottom: scroll.top + viewport.height,
|
90
103
|
left: scroll.left,
|
91
|
-
right: scroll.left +
|
92
|
-
|
93
|
-
|
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 ||
|
2
|
-
|
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
|
-
|
25
|
-
|
26
|
-
|
24
|
+
dimensions = @page.validatedDimensions()
|
25
|
+
document = dimensions.document
|
26
|
+
viewport = dimensions.viewport
|
27
|
+
pos = this.position()
|
27
28
|
|
28
|
-
scroll = { left:
|
29
|
+
scroll = { left: dimensions.left, top: dimensions.top }
|
29
30
|
|
30
|
-
unless
|
31
|
-
scroll.left = Math.min(pos.x,
|
31
|
+
unless dimensions.left <= pos.x < dimensions.right
|
32
|
+
scroll.left = Math.min(pos.x, document.width - viewport.width)
|
32
33
|
|
33
|
-
unless
|
34
|
-
scroll.top = Math.min(pos.y,
|
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 !=
|
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
|
-
|
63
|
-
|
64
|
-
|
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
|
-
|
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
|
-
|
38
|
-
|
39
|
-
if type == 'radio'
|
37
|
+
case self[:type]
|
38
|
+
when 'radio'
|
40
39
|
click
|
41
|
-
|
42
|
-
if value
|
43
|
-
|
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
|
-
|
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(
|
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
|
-
|
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
|
81
|
-
|
82
|
-
|
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
|
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.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:
|
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: &
|
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.
|
21
|
+
version: '1.0'
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *17036740
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: em-websocket
|
27
|
-
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: *
|
35
|
+
version_requirements: *17036240
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: json
|
38
|
-
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: *
|
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.
|
153
|
+
rubygems_version: 1.8.10
|
99
154
|
signing_key:
|
100
155
|
specification_version: 3
|
101
156
|
summary: PhantomJS driver for Capybara
|