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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # Poltergeist - A PhantomJS driver for Capybara #
2
2
 
3
- Version: 0.5.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.4.1+, built against Qt 4.8, on your system.
45
+ You need PhantomJS 1.5.0. There are no other dependencies (you don't
46
+ need Qt, or Xvfb, etc.)
46
47
 
47
- ### Pre-built binaries ##
48
+ ### Mac ###
48
49
 
49
- There are [pre-built
50
- binaries](http://code.google.com/p/phantomjs/downloads/list) of
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
- Note that if you have a 'dynamic' package, it's important to maintain
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
- ln -s /path/to/phantomjs/bin/phantomjs /usr/local/bin/phantomjs
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
- ### Compiling PhantomJS ###
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
- If you're having trouble with a pre-built binary package, you can
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
- [Download the tarball](http://code.google.com/p/phantomjs/downloads/detail?name=phantomjs-1.4.1-source.tar.gz&can=2&q=)
70
- and run either `deploy/build-linux.sh --qt-4.8` or `cd deploy; ./build-mac.sh`.
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
- ## Running on a CI ##
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
- Currently PhantomJS is not yet 'truly headless' (but that's planned for the future),
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
- ### On any generic server ###
78
+ Supported: MRI 1.8.7, MRI 1.9.2, MRI 1.9.3, JRuby 1.8, JRuby 1.9.
84
79
 
85
- Install PhantomJS and invoke your tests with `xvfb-run`, (e.g. `xvfb-run
86
- rake`).
80
+ Not supported:
87
81
 
88
- ### Using [Travis CI](http://travis-ci.org/) ###
82
+ * Rubinius (due to some unknown socket related issues)
83
+ * Windows
89
84
 
90
- Travis CI has PhantomJS installed already! So all you need to do is add
91
- the following to your `.travis.yml`:
85
+ Contributions are welcome in order to move 'unsupported'
86
+ items into the 'supported' list.
92
87
 
93
- ``` yaml
94
- before_script:
95
- - "export DISPLAY=:99.0"
96
- - "sh -e /etc/init.d/xvfb start"
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]
@@ -1,7 +1,7 @@
1
1
  module Capybara::Poltergeist
2
2
  class Client
3
3
  PHANTOMJS_SCRIPT = File.expand_path('../client/compiled/main.js', __FILE__)
4
- PHANTOMJS_VERSION = '1.4.1'
4
+ PHANTOMJS_VERSION = '1.5.0'
5
5
  PHANTOMJS_NAME = 'phantomjs'
6
6
 
7
7
  def self.start(*args)
@@ -8,10 +8,7 @@ class PoltergeistAgent
8
8
  this.pushWindow(window)
9
9
 
10
10
  externalCall: (name, arguments) ->
11
- try
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
- node: (page_id, id) ->
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
- throw new Poltergeist.ObsoleteNode
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.sendResponse this.node(page_id, id).find(selector)
69
+ this.nodeCall(page_id, id, 'find', selector, this.sendResponse)
56
70
 
57
71
  text: (page_id, id) ->
58
- this.sendResponse this.node(page_id, id).text()
72
+ this.nodeCall(page_id, id, 'text', this.sendResponse)
59
73
 
60
74
  attribute: (page_id, id, name) ->
61
- this.sendResponse this.node(page_id, id).getAttribute(name)
75
+ this.nodeCall(page_id, id, 'getAttribute', name, this.sendResponse)
62
76
 
63
77
  value: (page_id, id) ->
64
- this.sendResponse this.node(page_id, id).value()
78
+ this.nodeCall(page_id, id, 'value', this.sendResponse)
65
79
 
66
80
  set: (page_id, id, value) ->
67
- this.node(page_id, id).set(value)
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
- element = this.node(page_id, id)
79
-
80
- multiple = element.isMultiple()
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
- element.removeAttribute('multiple') if multiple
83
- element.setAttribute('_poltergeist_selected', '')
97
+ @page.uploadFile('[_poltergeist_selected]', value)
84
98
 
85
- @page.uploadFile('[_poltergeist_selected]', value)
99
+ node.removeAttribute('_poltergeist_selected')
100
+ node.setAttribute('multiple', 'multiple') if multiple
86
101
 
87
- element.removeAttribute('_poltergeist_selected')
88
- element.setAttribute('multiple', 'multiple') if multiple
89
-
90
- this.sendResponse(true)
102
+ this.sendResponse(true)
103
+ )
91
104
 
92
105
  select: (page_id, id, value) ->
93
- this.sendResponse this.node(page_id, id).select(value)
106
+ this.nodeCall(page_id, id, 'select', value, this.sendResponse)
94
107
 
95
108
  tag_name: (page_id, id) ->
96
- this.sendResponse this.node(page_id, id).tagName()
109
+ this.nodeCall(page_id, id, 'tagName', this.sendResponse)
97
110
 
98
111
  visible: (page_id, id) ->
99
- this.sendResponse this.node(page_id, id).isVisible()
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
- # If the click event triggers onLoadStarted, we will transition to the 'loading'
118
- # state and wait for onLoadFinished before sending a response.
119
- @state = 'clicked'
120
-
121
- this.node(page_id, id).click()
122
-
123
- # Use a timeout in order to let the stack clear, so that the @page.onLoadStarted
124
- # callback can (possibly) fire, before we decide whether to send a response.
125
- setTimeout(
126
- =>
127
- if @state == 'clicked'
128
- @state = 'default'
129
- this.sendResponse(true)
130
- ,
131
- 10
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.node(page_id, id).dragTo(@page.get(other_id))
136
- this.sendResponse(true)
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.node(page_id, id).trigger(event)
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
- try {
12
- return {
13
- value: this[name].apply(this, arguments)
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.node = function(page_id, id) {
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
- throw new Poltergeist.ObsoleteNode;
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.sendResponse(this.node(page_id, id).find(selector));
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.sendResponse(this.node(page_id, id).text());
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.sendResponse(this.node(page_id, id).getAttribute(name));
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.sendResponse(this.node(page_id, id).value());
89
+ return this.nodeCall(page_id, id, 'value', this.sendResponse);
76
90
  };
77
91
  Browser.prototype.set = function(page_id, id, value) {
78
- this.node(page_id, id).set(value);
79
- return this.sendResponse(true);
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
- var element, multiple;
83
- element = this.node(page_id, id);
84
- multiple = element.isMultiple();
85
- if (multiple) {
86
- element.removeAttribute('multiple');
87
- }
88
- element.setAttribute('_poltergeist_selected', '');
89
- this.page.uploadFile('[_poltergeist_selected]', value);
90
- element.removeAttribute('_poltergeist_selected');
91
- if (multiple) {
92
- element.setAttribute('multiple', 'multiple');
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.sendResponse(this.node(page_id, id).select(value));
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.sendResponse(this.node(page_id, id).tagName());
114
+ return this.nodeCall(page_id, id, 'tagName', this.sendResponse);
101
115
  };
102
116
  Browser.prototype.visible = function(page_id, id) {
103
- return this.sendResponse(this.node(page_id, id).isVisible());
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.state = 'clicked';
122
- this.node(page_id, id).click();
123
- return setTimeout(__bind(function() {
124
- if (this.state === 'clicked') {
125
- this.state = 'default';
126
- return this.sendResponse(true);
127
- }
128
- }, this), 10);
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.node(page_id, id).dragTo(this.page.get(other_id));
132
- return this.sendResponse(true);
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.node(page_id, id).trigger(event);
136
- return this.sendResponse(event);
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
- try {
9
- return this.browser[command.name].apply(this.browser, command.args);
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.connection.send({
27
+ return this.send({
16
28
  response: response
17
29
  });
18
30
  };
19
31
  Poltergeist.prototype.sendError = function(error) {
20
- return this.connection.send({
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
- function ObsoleteNode() {}
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
- phantom.injectJs('web_page.js');
60
- phantom.injectJs('node.js');
61
- phantom.injectJs('connection.js');
62
- phantom.injectJs('browser.js');
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
- throw new Poltergeist.ClickFailed(test.selector, pos);
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('agent.js');
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 (line === 0 && file === "undefined") {
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
- if (result.error) {
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
- try
8
- @browser[command.name].apply(@browser, command.args)
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
- @connection.send(response: response)
17
+ this.send(response: response)
14
18
 
15
19
  sendError: (error) ->
16
- @connection.send(
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.ObsoleteNode
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
- phantom.injectJs('web_page.js')
42
- phantom.injectJs('node.js')
43
- phantom.injectJs('connection.js')
44
- phantom.injectJs('browser.js')
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
- throw new Poltergeist.ClickFailed(test.selector, pos)
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('agent.js')
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
- if line == 0 && file == "undefined"
51
- # file:line will always be "undefined:0" in current release of
52
- # PhantomJS ;(
53
- @_errors.push(message)
54
- else
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
- if result.error
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 text
20
- response['args'].first
43
+ def javascript_error
44
+ JSErrorItem.new(*response['args'])
21
45
  end
22
46
 
23
47
  def message
24
- "Received error from PhantomJS client: #{text}"
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 javascript_messages
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: #{javascript_messages.inspect} " \
35
- "Unfortunately, it is not currently possible to provide a stack trace, or even the line/file where " \
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
 
@@ -1,5 +1,5 @@
1
1
  module Capybara
2
2
  module Poltergeist
3
- VERSION = "0.5.0"
3
+ VERSION = "0.6.0"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: poltergeist
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.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-03-18 00:00:00.000000000 Z
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: &20962640 !ruby/object:Gem::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: *20962640
24
+ version_requirements: *13258700
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: multi_json
27
- requirement: &20960260 !ruby/object:Gem::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: *20960260
35
+ version_requirements: *13258160
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: childprocess
38
- requirement: &20958720 !ruby/object:Gem::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: *20958720
46
+ version_requirements: *13257680
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: http_parser.rb
49
- requirement: &20957520 !ruby/object:Gem::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: *20957520
57
+ version_requirements: *13257220
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: faye-websocket
60
- requirement: &21890060 !ruby/object:Gem::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: *21890060
71
+ version_requirements: *13256720
72
72
  - !ruby/object:Gem::Dependency
73
73
  name: rspec
74
- requirement: &21888340 !ruby/object:Gem::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.0
79
+ version: '2.8'
80
80
  type: :development
81
81
  prerelease: false
82
- version_requirements: *21888340
82
+ version_requirements: *13255860
83
83
  - !ruby/object:Gem::Dependency
84
84
  name: sinatra
85
- requirement: &21886720 !ruby/object:Gem::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: *21886720
93
+ version_requirements: *13255300
94
94
  - !ruby/object:Gem::Dependency
95
95
  name: rake
96
- requirement: &21886100 !ruby/object:Gem::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: *21886100
104
+ version_requirements: *13254740
105
105
  - !ruby/object:Gem::Dependency
106
106
  name: image_size
107
- requirement: &21212360 !ruby/object:Gem::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: *21212360
115
+ version_requirements: *13253860
116
116
  description: PhantomJS driver for Capybara
117
117
  email:
118
118
  - j@jonathanleighton.com