poltergeist 0.5.0 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +63 -54
- data/lib/capybara/poltergeist/client.rb +1 -1
- data/lib/capybara/poltergeist/client/agent.coffee +1 -4
- data/lib/capybara/poltergeist/client/browser.coffee +67 -41
- data/lib/capybara/poltergeist/client/compiled/agent.js +3 -9
- data/lib/capybara/poltergeist/client/compiled/browser.js +62 -38
- data/lib/capybara/poltergeist/client/compiled/main.js +61 -12
- data/lib/capybara/poltergeist/client/compiled/node.js +2 -2
- data/lib/capybara/poltergeist/client/compiled/web_page.js +10 -16
- data/lib/capybara/poltergeist/client/main.coffee +54 -13
- data/lib/capybara/poltergeist/client/node.coffee +2 -2
- data/lib/capybara/poltergeist/client/web_page.coffee +15 -16
- data/lib/capybara/poltergeist/errors.rb +31 -9
- data/lib/capybara/poltergeist/version.rb +1 -1
- metadata +21 -21
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Poltergeist - A PhantomJS driver for Capybara #
|
2
2
|
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.6.0
|
4
4
|
|
5
5
|
[![Build Status](https://secure.travis-ci.org/jonleighton/poltergeist.png)](http://travis-ci.org/jonleighton/poltergeist)
|
6
6
|
[![Dependency Status](https://gemnasium.com/jonleighton/poltergeist.png)](https://gemnasium.com/jonleighton/poltergeist)
|
@@ -42,59 +42,59 @@ this doesn't affect you.
|
|
42
42
|
|
43
43
|
## Installing PhantomJS ##
|
44
44
|
|
45
|
-
You need PhantomJS 1.
|
45
|
+
You need PhantomJS 1.5.0. There are no other dependencies (you don't
|
46
|
+
need Qt, or Xvfb, etc.)
|
46
47
|
|
47
|
-
###
|
48
|
+
### Mac ###
|
48
49
|
|
49
|
-
|
50
|
-
|
51
|
-
PhantomJS for Linux, Mac and Windows. This is the easiest and best way
|
52
|
-
to install it. The binaries including a patched version of Qt 4.8 so you
|
53
|
-
don't need to install that separately.
|
50
|
+
* *With homebrew*: `brew install phantomjs`
|
51
|
+
* *Without homebrew*: [Download this](http://code.google.com/p/phantomjs/downloads/detail?name=phantomjs-1.5.0-macosx-static.zip&can=2&q=)
|
54
52
|
|
55
|
-
|
56
|
-
the relationship between `bin/phantomjs` and `lib/`. This is because the
|
57
|
-
`bin/phantomjs` binary looks in `../lib/` for its library files. So the
|
58
|
-
best thing to do is to link (rather than copy) it into your `PATH`:
|
53
|
+
### Linux ###
|
59
54
|
|
60
|
-
|
55
|
+
* Download the [32
|
56
|
+
bit](http://code.google.com/p/phantomjs/downloads/detail?name=phantomjs-1.5.0-linux-x86-dynamic.tar.gz&can=2&q=)
|
57
|
+
or [64
|
58
|
+
bit](http://code.google.com/p/phantomjs/downloads/detail?name=phantomjs-1.5.0-linux-x86_64-dynamic.tar.gz&can=2&q=)
|
59
|
+
binary.
|
60
|
+
* Extract it: `sudo tar xvzf phantomjs-1.5.0-linux-*-dynamic.tar.gz -C /usr/local`
|
61
|
+
* Link it: `sudo ln -s /usr/local/phantomjs/bin/phantomjs /usr/local/bin/phantomjs`
|
61
62
|
|
62
|
-
|
63
|
+
(Note that you cannot copy the `/usr/local/phantomjs/bin/phantomjs`
|
64
|
+
binary elsewhere on its own as it dynamically links with other files in
|
65
|
+
`/usr/local/phantomjs/lib`.)
|
63
66
|
|
64
|
-
|
65
|
-
compile PhantomJS yourself. PhantomJS must be built against Qt 4.8, and
|
66
|
-
some patches must be applied, so note that you cannot build it against
|
67
|
-
your system install of Qt.
|
67
|
+
### Manual compilation ###
|
68
68
|
|
69
|
-
|
70
|
-
|
71
|
-
The script will
|
72
|
-
download Qt, apply some patches, build it, and then build PhantomJS
|
73
|
-
against the patched build of Qt. It takes quite a while, around 30
|
74
|
-
minutes on a modern computer with two hyperthreaded cores. Afterwards,
|
75
|
-
you should copy (or link) the `bin/phantomjs` binary into your `PATH`.
|
69
|
+
Do this as a last resort if the binaries don't work for you. It will
|
70
|
+
take quite a long time as it has to build WebKit.
|
76
71
|
|
77
|
-
|
72
|
+
* Download [the source tarball](http://code.google.com/p/phantomjs/downloads/detail?name=phantomjs-1.5.0-source.tar.gz&can=2&q=)
|
73
|
+
* Extract and cd in
|
74
|
+
* `./build.sh`
|
78
75
|
|
79
|
-
|
80
|
-
so to run it on a continuous integration
|
81
|
-
server you will need to install [Xvfb](http://en.wikipedia.org/wiki/Xvfb).
|
76
|
+
## Compatibility ##
|
82
77
|
|
83
|
-
|
78
|
+
Supported: MRI 1.8.7, MRI 1.9.2, MRI 1.9.3, JRuby 1.8, JRuby 1.9.
|
84
79
|
|
85
|
-
|
86
|
-
rake`).
|
80
|
+
Not supported:
|
87
81
|
|
88
|
-
|
82
|
+
* Rubinius (due to some unknown socket related issues)
|
83
|
+
* Windows
|
89
84
|
|
90
|
-
|
91
|
-
|
85
|
+
Contributions are welcome in order to move 'unsupported'
|
86
|
+
items into the 'supported' list.
|
92
87
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
88
|
+
## Running on a CI ##
|
89
|
+
|
90
|
+
There are no special steps to take. You don't need Xvfb or any running X
|
91
|
+
server at all.
|
92
|
+
|
93
|
+
[Travis CI](http://travis-ci.org/) has PhantomJS 1.5.0 installed.
|
94
|
+
|
95
|
+
You may like to use their [chef
|
96
|
+
cookbook](https://github.com/travis-ci/travis-cookbooks/tree/master/ci_environment/phantomjs)
|
97
|
+
on your own servers.
|
98
98
|
|
99
99
|
## What's supported? ##
|
100
100
|
|
@@ -117,6 +117,19 @@ the entire page, use `page.driver.render('/path/to/file.png', :full => true)`.
|
|
117
117
|
Sometimes the window size is important to how things are rendered. Poltergeist sets the window
|
118
118
|
size to 1024x768 by default, but you can set this yourself with `page.driver.resize(width, height)`.
|
119
119
|
|
120
|
+
### Remote debugging (experimental) ###
|
121
|
+
|
122
|
+
If you use the `:inspector => true` option (see below), remote debugging
|
123
|
+
will be enabled.
|
124
|
+
|
125
|
+
When this option is enabled, you can insert `page.driver.debug` into
|
126
|
+
your tests to pause the test and launch a browser which gives you the
|
127
|
+
WebKit inspector to view your test run with.
|
128
|
+
|
129
|
+
(This feature is considered experimental - it needs more polish
|
130
|
+
and [apparently will only work on
|
131
|
+
Linux](http://code.google.com/p/phantomjs/issues/detail?id=430).)
|
132
|
+
|
120
133
|
## Customization ##
|
121
134
|
|
122
135
|
You can customize the way that Capybara sets up Poltegeist via the following code in your
|
@@ -136,6 +149,7 @@ end
|
|
136
149
|
* `:timeout` (Numeric) - The number of seconds we'll wait for a response
|
137
150
|
when communicating with PhantomJS. `nil` means wait forever. Default
|
138
151
|
is 30.
|
152
|
+
* `:inspector` (Boolean, String) - See 'Remote Debugging', above.
|
139
153
|
|
140
154
|
## Bugs ##
|
141
155
|
|
@@ -143,20 +157,6 @@ Please file bug reports on Github and include example code to reproduce the prob
|
|
143
157
|
possible. (Tests are even better.) Please also provide the output with
|
144
158
|
`:debug` turned on, and screenshots if you think it's relevant.
|
145
159
|
|
146
|
-
## Differences from [capybara-webkit](https://github.com/thoughtbot/capybara-webkit) ##
|
147
|
-
|
148
|
-
Poltergeist is similar to capybara-webkit, but here are the key
|
149
|
-
differences:
|
150
|
-
|
151
|
-
* It's more hackable. Poltergeist is written in Ruby + CoffeeScript.
|
152
|
-
We only have to worry about C++ when dealing with issues in
|
153
|
-
PhantomJS itself. In contrast, the majority of capybara-webkit is
|
154
|
-
written in C++.
|
155
|
-
|
156
|
-
* We're able to tap into the PhantomJS community. When PhantomJS
|
157
|
-
improves, Poltergeist improves. User's don't have to install Qt
|
158
|
-
because self-contained PhantomJS binary packages are available.
|
159
|
-
|
160
160
|
## Hacking ##
|
161
161
|
|
162
162
|
Contributions are very welcome and I will happily give commit access to
|
@@ -173,6 +173,13 @@ makes debugging easier). Running `rake autocompile` will watch the
|
|
173
173
|
|
174
174
|
## Changes ##
|
175
175
|
|
176
|
+
### 0.6.0 ###
|
177
|
+
|
178
|
+
#### Features ####
|
179
|
+
|
180
|
+
* Updated to PhantomJS 1.5.0, giving us proper support for reporting
|
181
|
+
Javascript exception backtraces.
|
182
|
+
|
176
183
|
### 0.5.0 ###
|
177
184
|
|
178
185
|
#### Features ####
|
@@ -199,6 +206,8 @@ makes debugging easier). Running `rake autocompile` will watch the
|
|
199
206
|
* Errors produced by Javascript on the page will now generate an
|
200
207
|
exception within Ruby. [Issue #27]
|
201
208
|
|
209
|
+
* JRuby support. [Issue #20]
|
210
|
+
|
202
211
|
#### Bug fixes ####
|
203
212
|
|
204
213
|
* Fix bug where we could end up interacting with an obsolete element. [Issue #30]
|
@@ -8,10 +8,7 @@ class PoltergeistAgent
|
|
8
8
|
this.pushWindow(window)
|
9
9
|
|
10
10
|
externalCall: (name, arguments) ->
|
11
|
-
|
12
|
-
{ value: this[name].apply(this, arguments) }
|
13
|
-
catch error
|
14
|
-
{ error: error.toString() }
|
11
|
+
{ value: this[name].apply(this, arguments) }
|
15
12
|
|
16
13
|
pushWindow: (new_window) ->
|
17
14
|
@windows.push(new_window)
|
@@ -29,11 +29,25 @@ class Poltergeist.Browser
|
|
29
29
|
else
|
30
30
|
@owner.sendResponse(response)
|
31
31
|
|
32
|
-
|
32
|
+
getNode: (page_id, id, callback) ->
|
33
33
|
if page_id == @page_id
|
34
|
-
@page.get(id)
|
34
|
+
callback.call this, @page.get(id)
|
35
35
|
else
|
36
|
-
|
36
|
+
@owner.sendError(new Poltergeist.ObsoleteNode)
|
37
|
+
|
38
|
+
nodeCall: (page_id, id, fn, args...) ->
|
39
|
+
callback = args.pop()
|
40
|
+
|
41
|
+
this.getNode(
|
42
|
+
page_id, id,
|
43
|
+
(node) ->
|
44
|
+
result = node[fn](args...)
|
45
|
+
|
46
|
+
if result instanceof Poltergeist.ObsoleteNode
|
47
|
+
@owner.sendError(result)
|
48
|
+
else
|
49
|
+
callback.call(this, result, node)
|
50
|
+
)
|
37
51
|
|
38
52
|
visit: (url) ->
|
39
53
|
@state = 'loading'
|
@@ -52,20 +66,19 @@ class Poltergeist.Browser
|
|
52
66
|
this.sendResponse(page_id: @page_id, ids: @page.find(selector))
|
53
67
|
|
54
68
|
find_within: (page_id, id, selector) ->
|
55
|
-
this.
|
69
|
+
this.nodeCall(page_id, id, 'find', selector, this.sendResponse)
|
56
70
|
|
57
71
|
text: (page_id, id) ->
|
58
|
-
this.
|
72
|
+
this.nodeCall(page_id, id, 'text', this.sendResponse)
|
59
73
|
|
60
74
|
attribute: (page_id, id, name) ->
|
61
|
-
this.
|
75
|
+
this.nodeCall(page_id, id, 'getAttribute', name, this.sendResponse)
|
62
76
|
|
63
77
|
value: (page_id, id) ->
|
64
|
-
this.
|
78
|
+
this.nodeCall(page_id, id, 'value', this.sendResponse)
|
65
79
|
|
66
80
|
set: (page_id, id, value) ->
|
67
|
-
this.
|
68
|
-
this.sendResponse(true)
|
81
|
+
this.nodeCall(page_id, id, 'set', value, -> this.sendResponse(true))
|
69
82
|
|
70
83
|
# PhantomJS only allows us to reference the element by CSS selector, not XPath,
|
71
84
|
# so we have to add an attribute to the element to identify it, then remove it
|
@@ -75,28 +88,28 @@ class Poltergeist.Browser
|
|
75
88
|
# by temporarily changing it to a single-file input. This obviously could break
|
76
89
|
# things in various ways, which is not ideal, but it works in the simplest case.
|
77
90
|
select_file: (page_id, id, value) ->
|
78
|
-
|
79
|
-
|
80
|
-
|
91
|
+
this.nodeCall(
|
92
|
+
page_id, id, 'isMultiple',
|
93
|
+
(multiple, node) ->
|
94
|
+
node.removeAttribute('multiple') if multiple
|
95
|
+
node.setAttribute('_poltergeist_selected', '')
|
81
96
|
|
82
|
-
|
83
|
-
element.setAttribute('_poltergeist_selected', '')
|
97
|
+
@page.uploadFile('[_poltergeist_selected]', value)
|
84
98
|
|
85
|
-
|
99
|
+
node.removeAttribute('_poltergeist_selected')
|
100
|
+
node.setAttribute('multiple', 'multiple') if multiple
|
86
101
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
this.sendResponse(true)
|
102
|
+
this.sendResponse(true)
|
103
|
+
)
|
91
104
|
|
92
105
|
select: (page_id, id, value) ->
|
93
|
-
this.
|
106
|
+
this.nodeCall(page_id, id, 'select', value, this.sendResponse)
|
94
107
|
|
95
108
|
tag_name: (page_id, id) ->
|
96
|
-
this.
|
109
|
+
this.nodeCall(page_id, id, 'tagName', this.sendResponse)
|
97
110
|
|
98
111
|
visible: (page_id, id) ->
|
99
|
-
this.
|
112
|
+
this.nodeCall(page_id, id, 'isVisible', this.sendResponse)
|
100
113
|
|
101
114
|
evaluate: (script) ->
|
102
115
|
this.sendResponse JSON.parse(@page.evaluate("function() { return JSON.stringify(#{script}) }"))
|
@@ -114,30 +127,43 @@ class Poltergeist.Browser
|
|
114
127
|
this.sendResponse(true)
|
115
128
|
|
116
129
|
click: (page_id, id) ->
|
117
|
-
#
|
118
|
-
#
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
130
|
+
# We just check the node is not obsolete before proceeding. If it is,
|
131
|
+
# the callback will not fire.
|
132
|
+
this.nodeCall(
|
133
|
+
page_id, id, 'isObsolete',
|
134
|
+
(obsolete, node) ->
|
135
|
+
# If the click event triggers onLoadStarted, we will transition to the 'loading'
|
136
|
+
# state and wait for onLoadFinished before sending a response.
|
137
|
+
@state = 'clicked'
|
138
|
+
|
139
|
+
click = node.click()
|
140
|
+
|
141
|
+
# Use a timeout in order to let the stack clear, so that the @page.onLoadStarted
|
142
|
+
# callback can (possibly) fire, before we decide whether to send a response.
|
143
|
+
setTimeout(
|
144
|
+
=>
|
145
|
+
if @state == 'clicked'
|
146
|
+
@state = 'default'
|
147
|
+
|
148
|
+
if click instanceof Poltergeist.ClickFailed
|
149
|
+
@owner.sendError(click)
|
150
|
+
else
|
151
|
+
this.sendResponse(true)
|
152
|
+
,
|
153
|
+
10
|
154
|
+
)
|
132
155
|
)
|
133
156
|
|
134
157
|
drag: (page_id, id, other_id) ->
|
135
|
-
this.
|
136
|
-
|
158
|
+
this.nodeCall(
|
159
|
+
page_id, id, 'isObsolete'
|
160
|
+
(obsolete, node) ->
|
161
|
+
node.dragTo(@page.get(other_id))
|
162
|
+
this.sendResponse(true)
|
163
|
+
)
|
137
164
|
|
138
165
|
trigger: (page_id, id, event) ->
|
139
|
-
this.
|
140
|
-
this.sendResponse(event)
|
166
|
+
this.nodeCall(page_id, id, 'trigger', event, -> this.sendResponse(event))
|
141
167
|
|
142
168
|
reset: ->
|
143
169
|
this.resetPage()
|
@@ -8,15 +8,9 @@ PoltergeistAgent = (function() {
|
|
8
8
|
this.pushWindow(window);
|
9
9
|
}
|
10
10
|
PoltergeistAgent.prototype.externalCall = function(name, arguments) {
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
};
|
15
|
-
} catch (error) {
|
16
|
-
return {
|
17
|
-
error: error.toString()
|
18
|
-
};
|
19
|
-
}
|
11
|
+
return {
|
12
|
+
value: this[name].apply(this, arguments)
|
13
|
+
};
|
20
14
|
};
|
21
15
|
PoltergeistAgent.prototype.pushWindow = function(new_window) {
|
22
16
|
this.windows.push(new_window);
|
@@ -1,4 +1,4 @@
|
|
1
|
-
var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
|
1
|
+
var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, __slice = Array.prototype.slice;
|
2
2
|
Poltergeist.Browser = (function() {
|
3
3
|
function Browser(owner) {
|
4
4
|
this.owner = owner;
|
@@ -36,13 +36,27 @@ Poltergeist.Browser = (function() {
|
|
36
36
|
return this.owner.sendResponse(response);
|
37
37
|
}
|
38
38
|
};
|
39
|
-
Browser.prototype.
|
39
|
+
Browser.prototype.getNode = function(page_id, id, callback) {
|
40
40
|
if (page_id === this.page_id) {
|
41
|
-
return this.page.get(id);
|
41
|
+
return callback.call(this, this.page.get(id));
|
42
42
|
} else {
|
43
|
-
|
43
|
+
return this.owner.sendError(new Poltergeist.ObsoleteNode);
|
44
44
|
}
|
45
45
|
};
|
46
|
+
Browser.prototype.nodeCall = function() {
|
47
|
+
var args, callback, fn, id, page_id;
|
48
|
+
page_id = arguments[0], id = arguments[1], fn = arguments[2], args = 4 <= arguments.length ? __slice.call(arguments, 3) : [];
|
49
|
+
callback = args.pop();
|
50
|
+
return this.getNode(page_id, id, function(node) {
|
51
|
+
var result;
|
52
|
+
result = node[fn].apply(node, args);
|
53
|
+
if (result instanceof Poltergeist.ObsoleteNode) {
|
54
|
+
return this.owner.sendError(result);
|
55
|
+
} else {
|
56
|
+
return callback.call(this, result, node);
|
57
|
+
}
|
58
|
+
});
|
59
|
+
};
|
46
60
|
Browser.prototype.visit = function(url) {
|
47
61
|
this.state = 'loading';
|
48
62
|
return this.page.open(url);
|
@@ -63,44 +77,44 @@ Poltergeist.Browser = (function() {
|
|
63
77
|
});
|
64
78
|
};
|
65
79
|
Browser.prototype.find_within = function(page_id, id, selector) {
|
66
|
-
return this.
|
80
|
+
return this.nodeCall(page_id, id, 'find', selector, this.sendResponse);
|
67
81
|
};
|
68
82
|
Browser.prototype.text = function(page_id, id) {
|
69
|
-
return this.
|
83
|
+
return this.nodeCall(page_id, id, 'text', this.sendResponse);
|
70
84
|
};
|
71
85
|
Browser.prototype.attribute = function(page_id, id, name) {
|
72
|
-
return this.
|
86
|
+
return this.nodeCall(page_id, id, 'getAttribute', name, this.sendResponse);
|
73
87
|
};
|
74
88
|
Browser.prototype.value = function(page_id, id) {
|
75
|
-
return this.
|
89
|
+
return this.nodeCall(page_id, id, 'value', this.sendResponse);
|
76
90
|
};
|
77
91
|
Browser.prototype.set = function(page_id, id, value) {
|
78
|
-
this.
|
79
|
-
|
92
|
+
return this.nodeCall(page_id, id, 'set', value, function() {
|
93
|
+
return this.sendResponse(true);
|
94
|
+
});
|
80
95
|
};
|
81
96
|
Browser.prototype.select_file = function(page_id, id, value) {
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
}
|
94
|
-
return this.sendResponse(true);
|
97
|
+
return this.nodeCall(page_id, id, 'isMultiple', function(multiple, node) {
|
98
|
+
if (multiple) {
|
99
|
+
node.removeAttribute('multiple');
|
100
|
+
}
|
101
|
+
node.setAttribute('_poltergeist_selected', '');
|
102
|
+
this.page.uploadFile('[_poltergeist_selected]', value);
|
103
|
+
node.removeAttribute('_poltergeist_selected');
|
104
|
+
if (multiple) {
|
105
|
+
node.setAttribute('multiple', 'multiple');
|
106
|
+
}
|
107
|
+
return this.sendResponse(true);
|
108
|
+
});
|
95
109
|
};
|
96
110
|
Browser.prototype.select = function(page_id, id, value) {
|
97
|
-
return this.
|
111
|
+
return this.nodeCall(page_id, id, 'select', value, this.sendResponse);
|
98
112
|
};
|
99
113
|
Browser.prototype.tag_name = function(page_id, id) {
|
100
|
-
return this.
|
114
|
+
return this.nodeCall(page_id, id, 'tagName', this.sendResponse);
|
101
115
|
};
|
102
116
|
Browser.prototype.visible = function(page_id, id) {
|
103
|
-
return this.
|
117
|
+
return this.nodeCall(page_id, id, 'isVisible', this.sendResponse);
|
104
118
|
};
|
105
119
|
Browser.prototype.evaluate = function(script) {
|
106
120
|
return this.sendResponse(JSON.parse(this.page.evaluate("function() { return JSON.stringify(" + script + ") }")));
|
@@ -118,22 +132,32 @@ Poltergeist.Browser = (function() {
|
|
118
132
|
return this.sendResponse(true);
|
119
133
|
};
|
120
134
|
Browser.prototype.click = function(page_id, id) {
|
121
|
-
this.
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
135
|
+
return this.nodeCall(page_id, id, 'isObsolete', function(obsolete, node) {
|
136
|
+
var click;
|
137
|
+
this.state = 'clicked';
|
138
|
+
click = node.click();
|
139
|
+
return setTimeout(__bind(function() {
|
140
|
+
if (this.state === 'clicked') {
|
141
|
+
this.state = 'default';
|
142
|
+
if (click instanceof Poltergeist.ClickFailed) {
|
143
|
+
return this.owner.sendError(click);
|
144
|
+
} else {
|
145
|
+
return this.sendResponse(true);
|
146
|
+
}
|
147
|
+
}
|
148
|
+
}, this), 10);
|
149
|
+
});
|
129
150
|
};
|
130
151
|
Browser.prototype.drag = function(page_id, id, other_id) {
|
131
|
-
this.
|
132
|
-
|
152
|
+
return this.nodeCall(page_id, id, 'isObsolete', function(obsolete, node) {
|
153
|
+
node.dragTo(this.page.get(other_id));
|
154
|
+
return this.sendResponse(true);
|
155
|
+
});
|
133
156
|
};
|
134
157
|
Browser.prototype.trigger = function(page_id, id, event) {
|
135
|
-
this.
|
136
|
-
|
158
|
+
return this.nodeCall(page_id, id, 'trigger', event, function() {
|
159
|
+
return this.sendResponse(event);
|
160
|
+
});
|
137
161
|
};
|
138
162
|
Browser.prototype.reset = function() {
|
139
163
|
this.resetPage();
|
@@ -1,41 +1,77 @@
|
|
1
1
|
var Poltergeist;
|
2
|
+
var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, __hasProp = Object.prototype.hasOwnProperty, __extends = function(child, parent) {
|
3
|
+
for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; }
|
4
|
+
function ctor() { this.constructor = child; }
|
5
|
+
ctor.prototype = parent.prototype;
|
6
|
+
child.prototype = new ctor;
|
7
|
+
child.__super__ = parent.prototype;
|
8
|
+
return child;
|
9
|
+
};
|
2
10
|
Poltergeist = (function() {
|
3
11
|
function Poltergeist(port) {
|
12
|
+
this.onError = __bind(this.onError, this);
|
13
|
+
var that;
|
4
14
|
this.browser = new Poltergeist.Browser(this);
|
5
15
|
this.connection = new Poltergeist.Connection(this, port);
|
16
|
+
that = this;
|
17
|
+
phantom.onError = function(message, stack) {
|
18
|
+
return that.onError(message, stack);
|
19
|
+
};
|
20
|
+
this.running = false;
|
6
21
|
}
|
7
22
|
Poltergeist.prototype.runCommand = function(command) {
|
8
|
-
|
9
|
-
|
10
|
-
} catch (error) {
|
11
|
-
return this.sendError(error);
|
12
|
-
}
|
23
|
+
this.running = true;
|
24
|
+
return this.browser[command.name].apply(this.browser, command.args);
|
13
25
|
};
|
14
26
|
Poltergeist.prototype.sendResponse = function(response) {
|
15
|
-
return this.
|
27
|
+
return this.send({
|
16
28
|
response: response
|
17
29
|
});
|
18
30
|
};
|
19
31
|
Poltergeist.prototype.sendError = function(error) {
|
20
|
-
return this.
|
32
|
+
return this.send({
|
21
33
|
error: {
|
22
34
|
name: error.name || 'Generic',
|
23
35
|
args: error.args && error.args() || [error.toString()]
|
24
36
|
}
|
25
37
|
});
|
26
38
|
};
|
39
|
+
Poltergeist.prototype.onError = function(message, stack) {
|
40
|
+
if (message === 'PoltergeistAgent.ObsoleteNode') {
|
41
|
+
return this.sendError(new Poltergeist.ObsoleteNode);
|
42
|
+
} else {
|
43
|
+
return this.sendError(new Poltergeist.BrowserError(message, stack));
|
44
|
+
}
|
45
|
+
};
|
46
|
+
Poltergeist.prototype.send = function(data) {
|
47
|
+
if (this.running) {
|
48
|
+
this.connection.send(data);
|
49
|
+
return this.running = false;
|
50
|
+
}
|
51
|
+
};
|
27
52
|
return Poltergeist;
|
28
53
|
})();
|
29
54
|
window.Poltergeist = Poltergeist;
|
55
|
+
Poltergeist.Error = (function() {
|
56
|
+
function Error() {}
|
57
|
+
return Error;
|
58
|
+
})();
|
30
59
|
Poltergeist.ObsoleteNode = (function() {
|
31
|
-
|
60
|
+
__extends(ObsoleteNode, Poltergeist.Error);
|
61
|
+
function ObsoleteNode() {
|
62
|
+
ObsoleteNode.__super__.constructor.apply(this, arguments);
|
63
|
+
}
|
32
64
|
ObsoleteNode.prototype.name = "Poltergeist.ObsoleteNode";
|
33
65
|
ObsoleteNode.prototype.args = function() {
|
34
66
|
return [];
|
35
67
|
};
|
68
|
+
ObsoleteNode.prototype.toString = function() {
|
69
|
+
return this.name;
|
70
|
+
};
|
36
71
|
return ObsoleteNode;
|
37
72
|
})();
|
38
73
|
Poltergeist.ClickFailed = (function() {
|
74
|
+
__extends(ClickFailed, Poltergeist.Error);
|
39
75
|
function ClickFailed(selector, position) {
|
40
76
|
this.selector = selector;
|
41
77
|
this.position = position;
|
@@ -47,6 +83,7 @@ Poltergeist.ClickFailed = (function() {
|
|
47
83
|
return ClickFailed;
|
48
84
|
})();
|
49
85
|
Poltergeist.JavascriptError = (function() {
|
86
|
+
__extends(JavascriptError, Poltergeist.Error);
|
50
87
|
function JavascriptError(errors) {
|
51
88
|
this.errors = errors;
|
52
89
|
}
|
@@ -56,8 +93,20 @@ Poltergeist.JavascriptError = (function() {
|
|
56
93
|
};
|
57
94
|
return JavascriptError;
|
58
95
|
})();
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
96
|
+
Poltergeist.BrowserError = (function() {
|
97
|
+
__extends(BrowserError, Poltergeist.Error);
|
98
|
+
function BrowserError(message, stack) {
|
99
|
+
this.message = message;
|
100
|
+
this.stack = stack;
|
101
|
+
}
|
102
|
+
BrowserError.prototype.name = "Poltergeist.BrowserError";
|
103
|
+
BrowserError.prototype.args = function() {
|
104
|
+
return [this.message, this.stack];
|
105
|
+
};
|
106
|
+
return BrowserError;
|
107
|
+
})();
|
108
|
+
phantom.injectJs("" + phantom.libraryPath + "/web_page.js");
|
109
|
+
phantom.injectJs("" + phantom.libraryPath + "/node.js");
|
110
|
+
phantom.injectJs("" + phantom.libraryPath + "/connection.js");
|
111
|
+
phantom.injectJs("" + phantom.libraryPath + "/browser.js");
|
63
112
|
new Poltergeist(phantom.args[0]);
|
@@ -1,7 +1,7 @@
|
|
1
1
|
var __slice = Array.prototype.slice, __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
|
2
2
|
Poltergeist.Node = (function() {
|
3
3
|
var name, _fn, _i, _len, _ref;
|
4
|
-
Node.DELEGATES = ['text', 'getAttribute', 'value', 'set', 'setAttribute', 'removeAttribute', 'isMultiple', 'select', 'tagName', 'find', 'isVisible', 'position', 'trigger', 'parentId', 'clickTest'];
|
4
|
+
Node.DELEGATES = ['text', 'getAttribute', 'value', 'set', 'setAttribute', 'isObsolete', 'removeAttribute', 'isMultiple', 'select', 'tagName', 'find', 'isVisible', 'position', 'trigger', 'parentId', 'clickTest'];
|
5
5
|
function Node(page, id) {
|
6
6
|
this.page = page;
|
7
7
|
this.id = id;
|
@@ -64,7 +64,7 @@ Poltergeist.Node = (function() {
|
|
64
64
|
if (test.status === 'success') {
|
65
65
|
return this.page.sendEvent('click', pos.x, pos.y);
|
66
66
|
} else {
|
67
|
-
|
67
|
+
return new Poltergeist.ClickFailed(test.selector, pos);
|
68
68
|
}
|
69
69
|
};
|
70
70
|
Node.prototype.dragTo = function(other) {
|
@@ -1,7 +1,7 @@
|
|
1
1
|
var __slice = Array.prototype.slice, __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
|
2
2
|
Poltergeist.WebPage = (function() {
|
3
3
|
var command, delegate, _fn, _fn2, _i, _j, _len, _len2, _ref, _ref2;
|
4
|
-
WebPage.CALLBACKS = ['onAlert', 'onConsoleMessage', 'onLoadFinished', 'onInitialized', 'onLoadStarted', 'onResourceRequested', 'onResourceReceived'];
|
4
|
+
WebPage.CALLBACKS = ['onAlert', 'onConsoleMessage', 'onLoadFinished', 'onInitialized', 'onLoadStarted', 'onResourceRequested', 'onResourceReceived', 'onError'];
|
5
5
|
WebPage.DELEGATES = ['open', 'sendEvent', 'uploadFile', 'release', 'render'];
|
6
6
|
WebPage.COMMANDS = ['currentUrl', 'find', 'nodeCall', 'pushFrame', 'popFrame', 'documentSize'];
|
7
7
|
function WebPage() {
|
@@ -54,7 +54,7 @@ Poltergeist.WebPage = (function() {
|
|
54
54
|
if (this.evaluate(function() {
|
55
55
|
return typeof __poltergeist;
|
56
56
|
}) === "undefined") {
|
57
|
-
this["native"].injectJs(
|
57
|
+
this["native"].injectJs("" + phantom.libraryPath + "/agent.js");
|
58
58
|
return this.nodes = {};
|
59
59
|
}
|
60
60
|
};
|
@@ -68,12 +68,16 @@ Poltergeist.WebPage = (function() {
|
|
68
68
|
return this._source || (this._source = this["native"].content);
|
69
69
|
};
|
70
70
|
WebPage.prototype.onConsoleMessage = function(message, line, file) {
|
71
|
-
if (
|
72
|
-
return this._errors.push(message);
|
73
|
-
} else {
|
71
|
+
if (!(this._errors.length && this._errors[this._errors.length - 1].message === message)) {
|
74
72
|
return console.log(message);
|
75
73
|
}
|
76
74
|
};
|
75
|
+
WebPage.prototype.onErrorNative = function(message, stack) {
|
76
|
+
return this._errors.push({
|
77
|
+
message: message,
|
78
|
+
stack: stack
|
79
|
+
});
|
80
|
+
};
|
77
81
|
WebPage.prototype.content = function() {
|
78
82
|
return this["native"].content;
|
79
83
|
};
|
@@ -178,17 +182,7 @@ Poltergeist.WebPage = (function() {
|
|
178
182
|
result = this.evaluate(function(name, arguments) {
|
179
183
|
return __poltergeist.externalCall(name, arguments);
|
180
184
|
}, name, arguments);
|
181
|
-
|
182
|
-
switch (result.error) {
|
183
|
-
case "PoltergeistAgent.ObsoleteNode":
|
184
|
-
throw new Poltergeist.ObsoleteNode;
|
185
|
-
break;
|
186
|
-
default:
|
187
|
-
throw result.error;
|
188
|
-
}
|
189
|
-
} else {
|
190
|
-
return result.value;
|
191
|
-
}
|
185
|
+
return result && result.value;
|
192
186
|
};
|
193
187
|
return WebPage;
|
194
188
|
}).call(this);
|
@@ -3,44 +3,85 @@ class Poltergeist
|
|
3
3
|
@browser = new Poltergeist.Browser(this)
|
4
4
|
@connection = new Poltergeist.Connection(this, port)
|
5
5
|
|
6
|
+
# The QtWebKit bridge doesn't seem to like Function.prototype.bind
|
7
|
+
that = this
|
8
|
+
phantom.onError = (message, stack) -> that.onError(message, stack)
|
9
|
+
|
10
|
+
@running = false
|
11
|
+
|
6
12
|
runCommand: (command) ->
|
7
|
-
|
8
|
-
|
9
|
-
catch error
|
10
|
-
this.sendError(error)
|
13
|
+
@running = true
|
14
|
+
@browser[command.name].apply(@browser, command.args)
|
11
15
|
|
12
16
|
sendResponse: (response) ->
|
13
|
-
|
17
|
+
this.send(response: response)
|
14
18
|
|
15
19
|
sendError: (error) ->
|
16
|
-
|
20
|
+
this.send(
|
17
21
|
error:
|
18
22
|
name: error.name || 'Generic',
|
19
23
|
args: error.args && error.args() || [error.toString()]
|
20
24
|
)
|
21
25
|
|
26
|
+
# This is a bit of a mess. We can't wrap the runCommand code in
|
27
|
+
# a try ... catch, because that will prevent stack traces being
|
28
|
+
# reported, and there is no error.stack property.
|
29
|
+
#
|
30
|
+
# And thrown errors get toString() called, which becomes the
|
31
|
+
# value of the message variable here. So we can basically only
|
32
|
+
# use strings as exceptions at the moment, which means we throw
|
33
|
+
# exceptions with extra data and then retrieve it here.
|
34
|
+
#
|
35
|
+
# The solution will be for PhantomJS to support an e.stack
|
36
|
+
# property on exceptions.
|
37
|
+
#
|
38
|
+
# See http://code.google.com/p/phantomjs/issues/detail?id=166.
|
39
|
+
onError: (message, stack) =>
|
40
|
+
if message == 'PoltergeistAgent.ObsoleteNode'
|
41
|
+
this.sendError(new Poltergeist.ObsoleteNode)
|
42
|
+
else
|
43
|
+
this.sendError(new Poltergeist.BrowserError(message, stack))
|
44
|
+
|
45
|
+
send: (data) ->
|
46
|
+
# Prevents more than one response being sent for a single
|
47
|
+
# command. This can happen in some scenarios where an error
|
48
|
+
# is raised but the script can still continue.
|
49
|
+
if @running
|
50
|
+
@connection.send(data)
|
51
|
+
@running = false
|
52
|
+
|
22
53
|
# This is necessary because the remote debugger will wrap the
|
23
54
|
# script in a function, causing the Poltergeist variable to
|
24
55
|
# become local.
|
25
56
|
window.Poltergeist = Poltergeist
|
26
57
|
|
27
|
-
class Poltergeist.
|
58
|
+
class Poltergeist.Error
|
59
|
+
|
60
|
+
class Poltergeist.ObsoleteNode extends Poltergeist.Error
|
28
61
|
name: "Poltergeist.ObsoleteNode"
|
29
62
|
args: -> []
|
63
|
+
toString: -> this.name
|
30
64
|
|
31
|
-
class Poltergeist.ClickFailed
|
65
|
+
class Poltergeist.ClickFailed extends Poltergeist.Error
|
32
66
|
constructor: (@selector, @position) ->
|
33
67
|
name: "Poltergeist.ClickFailed"
|
34
68
|
args: -> [@selector, @position]
|
35
69
|
|
36
|
-
class Poltergeist.JavascriptError
|
70
|
+
class Poltergeist.JavascriptError extends Poltergeist.Error
|
37
71
|
constructor: (@errors) ->
|
38
72
|
name: "Poltergeist.JavascriptError"
|
39
73
|
args: -> [@errors]
|
40
74
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
75
|
+
class Poltergeist.BrowserError extends Poltergeist.Error
|
76
|
+
constructor: (@message, @stack) ->
|
77
|
+
name: "Poltergeist.BrowserError"
|
78
|
+
args: -> [@message, @stack]
|
79
|
+
|
80
|
+
# We're using phantom.libraryPath so that any stack traces
|
81
|
+
# report the full path.
|
82
|
+
phantom.injectJs("#{phantom.libraryPath}/web_page.js")
|
83
|
+
phantom.injectJs("#{phantom.libraryPath}/node.js")
|
84
|
+
phantom.injectJs("#{phantom.libraryPath}/connection.js")
|
85
|
+
phantom.injectJs("#{phantom.libraryPath}/browser.js")
|
45
86
|
|
46
87
|
new Poltergeist(phantom.args[0])
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# Proxy object for forwarding method calls to the node object inside the page.
|
2
2
|
|
3
3
|
class Poltergeist.Node
|
4
|
-
@DELEGATES = ['text', 'getAttribute', 'value', 'set', 'setAttribute',
|
4
|
+
@DELEGATES = ['text', 'getAttribute', 'value', 'set', 'setAttribute', 'isObsolete',
|
5
5
|
'removeAttribute', 'isMultiple', 'select', 'tagName', 'find',
|
6
6
|
'isVisible', 'position', 'trigger', 'parentId', 'clickTest']
|
7
7
|
|
@@ -59,7 +59,7 @@ class Poltergeist.Node
|
|
59
59
|
if test.status == 'success'
|
60
60
|
@page.sendEvent('click', pos.x, pos.y)
|
61
61
|
else
|
62
|
-
|
62
|
+
new Poltergeist.ClickFailed(test.selector, pos)
|
63
63
|
|
64
64
|
dragTo: (other) ->
|
65
65
|
position = this.clickPosition()
|
@@ -1,6 +1,7 @@
|
|
1
1
|
class Poltergeist.WebPage
|
2
2
|
@CALLBACKS = ['onAlert', 'onConsoleMessage', 'onLoadFinished', 'onInitialized',
|
3
|
-
'onLoadStarted', 'onResourceRequested', 'onResourceReceived'
|
3
|
+
'onLoadStarted', 'onResourceRequested', 'onResourceReceived',
|
4
|
+
'onError']
|
4
5
|
|
5
6
|
@DELEGATES = ['open', 'sendEvent', 'uploadFile', 'release', 'render']
|
6
7
|
|
@@ -35,7 +36,7 @@ class Poltergeist.WebPage
|
|
35
36
|
|
36
37
|
injectAgent: ->
|
37
38
|
if this.evaluate(-> typeof __poltergeist) == "undefined"
|
38
|
-
@native.injectJs(
|
39
|
+
@native.injectJs("#{phantom.libraryPath}/agent.js")
|
39
40
|
@nodes = {}
|
40
41
|
|
41
42
|
onConsoleMessageNative: (message) ->
|
@@ -47,14 +48,16 @@ class Poltergeist.WebPage
|
|
47
48
|
@_source or= @native.content
|
48
49
|
|
49
50
|
onConsoleMessage: (message, line, file) ->
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
# here line == 1 && file == "". don't ask me why!
|
51
|
+
# The conditional works around a PhantomJS bug where an error can
|
52
|
+
# get wrongly reported to be onError and onConsoleMessage:
|
53
|
+
#
|
54
|
+
# http://code.google.com/p/phantomjs/issues/detail?id=166#c18
|
55
|
+
unless @_errors.length && @_errors[@_errors.length - 1].message == message
|
56
56
|
console.log(message)
|
57
57
|
|
58
|
+
onErrorNative: (message, stack) ->
|
59
|
+
@_errors.push(message: message, stack: stack)
|
60
|
+
|
58
61
|
content: ->
|
59
62
|
@native.content
|
60
63
|
|
@@ -143,17 +146,13 @@ class Poltergeist.WebPage
|
|
143
146
|
if result != false && that[name]? # For externally set callbacks
|
144
147
|
that[name].apply(that, arguments)
|
145
148
|
|
149
|
+
# Any error raised here or inside the evaluate will get reported to
|
150
|
+
# phantom.onError. If result is null, that means there was an error
|
151
|
+
# inside the agent.
|
146
152
|
runCommand: (name, arguments) ->
|
147
153
|
result = this.evaluate(
|
148
154
|
(name, arguments) -> __poltergeist.externalCall(name, arguments),
|
149
155
|
name, arguments
|
150
156
|
)
|
151
157
|
|
152
|
-
|
153
|
-
switch result.error
|
154
|
-
when "PoltergeistAgent.ObsoleteNode"
|
155
|
-
throw new Poltergeist.ObsoleteNode
|
156
|
-
else
|
157
|
-
throw result.error
|
158
|
-
else
|
159
|
-
result.value
|
158
|
+
result && result.value
|
@@ -11,30 +11,52 @@ module Capybara
|
|
11
11
|
end
|
12
12
|
end
|
13
13
|
|
14
|
+
class JSErrorItem
|
15
|
+
attr_reader :message, :stack
|
16
|
+
|
17
|
+
def initialize(message, stack)
|
18
|
+
@message = message
|
19
|
+
@stack = stack
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_s
|
23
|
+
message + "\n\n" + formatted_stack
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def formatted_stack
|
29
|
+
stack = self.stack.map do |item|
|
30
|
+
s = " #{item['file']}:#{item['line']}"
|
31
|
+
s << " in #{item['function']}" if item['function'] && !item['function'].empty?
|
32
|
+
s
|
33
|
+
end
|
34
|
+
stack.join("\n")
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
14
38
|
class BrowserError < ClientError
|
15
39
|
def name
|
16
40
|
response['name']
|
17
41
|
end
|
18
42
|
|
19
|
-
def
|
20
|
-
response['args']
|
43
|
+
def javascript_error
|
44
|
+
JSErrorItem.new(*response['args'])
|
21
45
|
end
|
22
46
|
|
23
47
|
def message
|
24
|
-
"
|
48
|
+
"There was an error inside the PhantomJS portion of Poltergeist:\n\n#{javascript_error}"
|
25
49
|
end
|
26
50
|
end
|
27
51
|
|
28
52
|
class JavascriptError < ClientError
|
29
|
-
def
|
30
|
-
response['args'].first
|
53
|
+
def javascript_errors
|
54
|
+
response['args'].first.map { |data| JSErrorItem.new(data['message'], data['stack']) }
|
31
55
|
end
|
32
56
|
|
33
57
|
def message
|
34
|
-
"One or more errors were raised in the Javascript code on the page
|
35
|
-
"
|
36
|
-
"the error occurred. (This is due to lack of support within QtWebKit.) Fixing this is a high " \
|
37
|
-
"priority, but we're not there yet."
|
58
|
+
"One or more errors were raised in the Javascript code on the page:\n\n" +
|
59
|
+
javascript_errors.map(&:to_s).join("\n")
|
38
60
|
end
|
39
61
|
end
|
40
62
|
|
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.6.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-
|
12
|
+
date: 2012-04-09 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: capybara
|
16
|
-
requirement: &
|
16
|
+
requirement: &13258700 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ~>
|
@@ -21,10 +21,10 @@ dependencies:
|
|
21
21
|
version: '1.0'
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *13258700
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: multi_json
|
27
|
-
requirement: &
|
27
|
+
requirement: &13258160 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ~>
|
@@ -32,10 +32,10 @@ dependencies:
|
|
32
32
|
version: '1.0'
|
33
33
|
type: :runtime
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *13258160
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: childprocess
|
38
|
-
requirement: &
|
38
|
+
requirement: &13257680 !ruby/object:Gem::Requirement
|
39
39
|
none: false
|
40
40
|
requirements:
|
41
41
|
- - ~>
|
@@ -43,10 +43,10 @@ dependencies:
|
|
43
43
|
version: '0.3'
|
44
44
|
type: :runtime
|
45
45
|
prerelease: false
|
46
|
-
version_requirements: *
|
46
|
+
version_requirements: *13257680
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: http_parser.rb
|
49
|
-
requirement: &
|
49
|
+
requirement: &13257220 !ruby/object:Gem::Requirement
|
50
50
|
none: false
|
51
51
|
requirements:
|
52
52
|
- - ~>
|
@@ -54,10 +54,10 @@ dependencies:
|
|
54
54
|
version: 0.5.3
|
55
55
|
type: :runtime
|
56
56
|
prerelease: false
|
57
|
-
version_requirements: *
|
57
|
+
version_requirements: *13257220
|
58
58
|
- !ruby/object:Gem::Dependency
|
59
59
|
name: faye-websocket
|
60
|
-
requirement: &
|
60
|
+
requirement: &13256720 !ruby/object:Gem::Requirement
|
61
61
|
none: false
|
62
62
|
requirements:
|
63
63
|
- - ~>
|
@@ -68,21 +68,21 @@ dependencies:
|
|
68
68
|
version: 0.4.4
|
69
69
|
type: :runtime
|
70
70
|
prerelease: false
|
71
|
-
version_requirements: *
|
71
|
+
version_requirements: *13256720
|
72
72
|
- !ruby/object:Gem::Dependency
|
73
73
|
name: rspec
|
74
|
-
requirement: &
|
74
|
+
requirement: &13255860 !ruby/object:Gem::Requirement
|
75
75
|
none: false
|
76
76
|
requirements:
|
77
77
|
- - ~>
|
78
78
|
- !ruby/object:Gem::Version
|
79
|
-
version: 2.8
|
79
|
+
version: '2.8'
|
80
80
|
type: :development
|
81
81
|
prerelease: false
|
82
|
-
version_requirements: *
|
82
|
+
version_requirements: *13255860
|
83
83
|
- !ruby/object:Gem::Dependency
|
84
84
|
name: sinatra
|
85
|
-
requirement: &
|
85
|
+
requirement: &13255300 !ruby/object:Gem::Requirement
|
86
86
|
none: false
|
87
87
|
requirements:
|
88
88
|
- - ~>
|
@@ -90,10 +90,10 @@ dependencies:
|
|
90
90
|
version: '1.0'
|
91
91
|
type: :development
|
92
92
|
prerelease: false
|
93
|
-
version_requirements: *
|
93
|
+
version_requirements: *13255300
|
94
94
|
- !ruby/object:Gem::Dependency
|
95
95
|
name: rake
|
96
|
-
requirement: &
|
96
|
+
requirement: &13254740 !ruby/object:Gem::Requirement
|
97
97
|
none: false
|
98
98
|
requirements:
|
99
99
|
- - ~>
|
@@ -101,10 +101,10 @@ dependencies:
|
|
101
101
|
version: 0.9.2
|
102
102
|
type: :development
|
103
103
|
prerelease: false
|
104
|
-
version_requirements: *
|
104
|
+
version_requirements: *13254740
|
105
105
|
- !ruby/object:Gem::Dependency
|
106
106
|
name: image_size
|
107
|
-
requirement: &
|
107
|
+
requirement: &13253860 !ruby/object:Gem::Requirement
|
108
108
|
none: false
|
109
109
|
requirements:
|
110
110
|
- - ~>
|
@@ -112,7 +112,7 @@ dependencies:
|
|
112
112
|
version: '1.0'
|
113
113
|
type: :development
|
114
114
|
prerelease: false
|
115
|
-
version_requirements: *
|
115
|
+
version_requirements: *13253860
|
116
116
|
description: PhantomJS driver for Capybara
|
117
117
|
email:
|
118
118
|
- j@jonathanleighton.com
|