poltergeist 0.1.0 → 0.2.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/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
|