poltergeist 0.4.0 → 0.5.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.
@@ -7,8 +7,8 @@ Poltergeist.WebPage = (function() {
7
7
  function WebPage() {
8
8
  var callback, _i, _len, _ref;
9
9
  this["native"] = require('webpage').create();
10
- this.nodes = {};
11
10
  this._source = "";
11
+ this._errors = [];
12
12
  this.setViewportSize({
13
13
  width: 1024,
14
14
  height: 768
@@ -54,7 +54,8 @@ Poltergeist.WebPage = (function() {
54
54
  if (this.evaluate(function() {
55
55
  return typeof __poltergeist;
56
56
  }) === "undefined") {
57
- return this["native"].injectJs('agent.js');
57
+ this["native"].injectJs('agent.js');
58
+ return this.nodes = {};
58
59
  }
59
60
  };
60
61
  WebPage.prototype.onConsoleMessageNative = function(message) {
@@ -66,8 +67,12 @@ Poltergeist.WebPage = (function() {
66
67
  WebPage.prototype.onLoadFinishedNative = function() {
67
68
  return this._source || (this._source = this["native"].content);
68
69
  };
69
- WebPage.prototype.onConsoleMessage = function(message) {
70
- return console.log(message);
70
+ WebPage.prototype.onConsoleMessage = function(message, line, file) {
71
+ if (line === 0 && file === "undefined") {
72
+ return this._errors.push(message);
73
+ } else {
74
+ return console.log(message);
75
+ }
71
76
  };
72
77
  WebPage.prototype.content = function() {
73
78
  return this["native"].content;
@@ -75,6 +80,12 @@ Poltergeist.WebPage = (function() {
75
80
  WebPage.prototype.source = function() {
76
81
  return this._source;
77
82
  };
83
+ WebPage.prototype.errors = function() {
84
+ return this._errors;
85
+ };
86
+ WebPage.prototype.clearErrors = function() {
87
+ return this._errors = [];
88
+ };
78
89
  WebPage.prototype.viewportSize = function() {
79
90
  return this["native"].viewportSize;
80
91
  };
@@ -135,7 +146,7 @@ Poltergeist.WebPage = (function() {
135
146
  WebPage.prototype.evaluate = function() {
136
147
  var args, fn;
137
148
  fn = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
138
- return this["native"].evaluate("function() { return " + (this.stringifyCall(fn, args)) + " }");
149
+ return JSON.parse(this["native"].evaluate("function() { return JSON.stringify(" + (this.stringifyCall(fn, args)) + ") }"));
139
150
  };
140
151
  WebPage.prototype.execute = function() {
141
152
  var args, fn;
@@ -163,9 +174,21 @@ Poltergeist.WebPage = (function() {
163
174
  };
164
175
  };
165
176
  WebPage.prototype.runCommand = function(name, arguments) {
166
- return this.evaluate(function(name, arguments) {
167
- return __poltergeist[name].apply(__poltergeist, arguments);
177
+ var result;
178
+ result = this.evaluate(function(name, arguments) {
179
+ return __poltergeist.externalCall(name, arguments);
168
180
  }, 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
+ }
169
192
  };
170
193
  return WebPage;
171
194
  }).call(this);
@@ -7,13 +7,36 @@ class Poltergeist
7
7
  try
8
8
  @browser[command.name].apply(@browser, command.args)
9
9
  catch error
10
- @connection.send({ error: error.toString() })
10
+ this.sendError(error)
11
11
 
12
12
  sendResponse: (response) ->
13
- @connection.send({ response: response })
13
+ @connection.send(response: response)
14
+
15
+ sendError: (error) ->
16
+ @connection.send(
17
+ error:
18
+ name: error.name || 'Generic',
19
+ args: error.args && error.args() || [error.toString()]
20
+ )
21
+
22
+ # This is necessary because the remote debugger will wrap the
23
+ # script in a function, causing the Poltergeist variable to
24
+ # become local.
25
+ window.Poltergeist = Poltergeist
14
26
 
15
27
  class Poltergeist.ObsoleteNode
16
- toString: -> "Poltergeist.ObsoleteNode"
28
+ name: "Poltergeist.ObsoleteNode"
29
+ args: -> []
30
+
31
+ class Poltergeist.ClickFailed
32
+ constructor: (@selector, @position) ->
33
+ name: "Poltergeist.ClickFailed"
34
+ args: -> [@selector, @position]
35
+
36
+ class Poltergeist.JavascriptError
37
+ constructor: (@errors) ->
38
+ name: "Poltergeist.JavascriptError"
39
+ args: -> [@errors]
17
40
 
18
41
  phantom.injectJs('web_page.js')
19
42
  phantom.injectJs('node.js')
@@ -1,26 +1,21 @@
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', 'removeAttribute',
5
- 'isMultiple', 'select', 'tagName', 'isVisible', 'position', 'trigger', 'parentId']
4
+ @DELEGATES = ['text', 'getAttribute', 'value', 'set', 'setAttribute',
5
+ 'removeAttribute', 'isMultiple', 'select', 'tagName', 'find',
6
+ 'isVisible', 'position', 'trigger', 'parentId', 'clickTest']
6
7
 
7
8
  constructor: (@page, @id) ->
8
9
 
9
10
  parent: ->
10
11
  new Poltergeist.Node(@page, this.parentId())
11
12
 
12
- isObsolete: ->
13
- @page.nodeCall(@id, 'isObsolete')
14
-
15
13
  for name in @DELEGATES
16
14
  do (name) =>
17
15
  this.prototype[name] = (arguments...) ->
18
- if this.isObsolete()
19
- throw new Poltergeist.ObsoleteNode
20
- else
21
- @page.nodeCall(@id, name, arguments)
16
+ @page.nodeCall(@id, name, arguments)
22
17
 
23
- scrollIntoView: ->
18
+ clickPosition: (scrollIntoView = true) ->
24
19
  dimensions = @page.validatedDimensions()
25
20
  document = dimensions.document
26
21
  viewport = dimensions.viewport
@@ -41,23 +36,35 @@ class Poltergeist.Node
41
36
  scroll[coord] + pos[coord] - viewport[measurement] + (viewport[measurement] / 2)
42
37
  )
43
38
 
44
- adjust('left', 'width')
45
- adjust('top', 'height')
39
+ if scrollIntoView
40
+ adjust('left', 'width')
41
+ adjust('top', 'height')
42
+
43
+ if scroll.left != dimensions.left || scroll.top != dimensions.top
44
+ @page.setScrollPosition(scroll)
45
+ pos = this.position()
46
46
 
47
- if scroll.left != dimensions.left || scroll.top != dimensions.top
48
- @page.setScrollPosition(scroll)
49
- pos = this.position()
47
+ middle = (start, end, size) ->
48
+ start + ((Math.min(end, size) - start) / 2)
50
49
 
51
- pos
50
+ {
51
+ x: middle(pos.left, pos.right, viewport.width),
52
+ y: middle(pos.top, pos.bottom, viewport.height)
53
+ }
52
54
 
53
55
  click: ->
54
- position = this.scrollIntoView()
55
- @page.sendEvent('click', position.left, position.top)
56
+ pos = this.clickPosition()
57
+ test = this.clickTest(pos.x, pos.y)
58
+
59
+ if test.status == 'success'
60
+ @page.sendEvent('click', pos.x, pos.y)
61
+ else
62
+ throw new Poltergeist.ClickFailed(test.selector, pos)
56
63
 
57
64
  dragTo: (other) ->
58
- position = this.scrollIntoView()
59
- otherPosition = other.position()
65
+ position = this.clickPosition()
66
+ otherPosition = other.clickPosition(false)
60
67
 
61
- @page.sendEvent('mousedown', position.left, position.top)
62
- @page.sendEvent('mousemove', otherPosition.left, otherPosition.top)
63
- @page.sendEvent('mouseup', otherPosition.left, otherPosition.top)
68
+ @page.sendEvent('mousedown', position.x, position.y)
69
+ @page.sendEvent('mousemove', otherPosition.x, otherPosition.y)
70
+ @page.sendEvent('mouseup', otherPosition.x, otherPosition.y)
@@ -1,13 +1,15 @@
1
1
  class Poltergeist.WebPage
2
2
  @CALLBACKS = ['onAlert', 'onConsoleMessage', 'onLoadFinished', 'onInitialized',
3
3
  'onLoadStarted', 'onResourceRequested', 'onResourceReceived']
4
+
4
5
  @DELEGATES = ['open', 'sendEvent', 'uploadFile', 'release', 'render']
6
+
5
7
  @COMMANDS = ['currentUrl', 'find', 'nodeCall', 'pushFrame', 'popFrame', 'documentSize']
6
8
 
7
9
  constructor: ->
8
10
  @native = require('webpage').create()
9
- @nodes = {}
10
11
  @_source = ""
12
+ @_errors = []
11
13
 
12
14
  this.setViewportSize(width: 1024, height: 768)
13
15
 
@@ -29,11 +31,12 @@ class Poltergeist.WebPage
29
31
  onInitializedNative: ->
30
32
  @_source = null
31
33
  this.injectAgent()
32
- this.setScrollPosition({ left: 0, top: 0 })
34
+ this.setScrollPosition(left: 0, top: 0)
33
35
 
34
36
  injectAgent: ->
35
37
  if this.evaluate(-> typeof __poltergeist) == "undefined"
36
38
  @native.injectJs('agent.js')
39
+ @nodes = {}
37
40
 
38
41
  onConsoleMessageNative: (message) ->
39
42
  if message == '__DOMContentLoaded'
@@ -43,8 +46,14 @@ class Poltergeist.WebPage
43
46
  onLoadFinishedNative: ->
44
47
  @_source or= @native.content
45
48
 
46
- onConsoleMessage: (message) ->
47
- console.log(message)
49
+ 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!
56
+ console.log(message)
48
57
 
49
58
  content: ->
50
59
  @native.content
@@ -52,6 +61,12 @@ class Poltergeist.WebPage
52
61
  source: ->
53
62
  @_source
54
63
 
64
+ errors: ->
65
+ @_errors
66
+
67
+ clearErrors: ->
68
+ @_errors = []
69
+
55
70
  viewportSize: ->
56
71
  @native.viewportSize
57
72
 
@@ -104,7 +119,7 @@ class Poltergeist.WebPage
104
119
  @nodes[id] or= new Poltergeist.Node(this, id)
105
120
 
106
121
  evaluate: (fn, args...) ->
107
- @native.evaluate("function() { return #{this.stringifyCall(fn, args)} }")
122
+ JSON.parse @native.evaluate("function() { return JSON.stringify(#{this.stringifyCall(fn, args)}) }")
108
123
 
109
124
  execute: (fn, args...) ->
110
125
  @native.evaluate("function() { #{this.stringifyCall(fn, args)} }")
@@ -129,7 +144,16 @@ class Poltergeist.WebPage
129
144
  that[name].apply(that, arguments)
130
145
 
131
146
  runCommand: (name, arguments) ->
132
- this.evaluate(
133
- (name, arguments) -> __poltergeist[name].apply(__poltergeist, arguments),
147
+ result = this.evaluate(
148
+ (name, arguments) -> __poltergeist.externalCall(name, arguments),
134
149
  name, arguments
135
150
  )
151
+
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
@@ -1,34 +1,65 @@
1
1
  module Capybara::Poltergeist
2
2
  class Driver < Capybara::Driver::Base
3
- attr_reader :app, :server, :browser, :options
3
+ DEFAULT_TIMEOUT = 30
4
+
5
+ attr_reader :app, :app_server, :server, :client, :browser, :options
4
6
 
5
7
  def initialize(app, options = {})
6
- @app = app
7
- @options = options
8
- @server = Capybara::Server.new(app)
9
- @browser = nil
8
+ @app = app
9
+ @options = options
10
+ @browser = nil
11
+ @inspector = nil
12
+ @server = nil
13
+ @client = nil
10
14
 
11
- @server.boot if Capybara.run_server
15
+ @app_server = Capybara::Server.new(app)
16
+ @app_server.boot if Capybara.run_server
12
17
  end
13
18
 
14
19
  def browser
15
- @browser ||= Browser.new(
16
- :logger => logger,
17
- :phantomjs => options[:phantomjs]
18
- )
20
+ @browser ||= Browser.new(server, client, logger)
21
+ end
22
+
23
+ def inspector
24
+ @inspector ||= options[:inspector] && Inspector.new(options[:inspector])
25
+ end
26
+
27
+ def server
28
+ @server ||= Server.new(options.fetch(:timeout, DEFAULT_TIMEOUT))
29
+ end
30
+
31
+ def client
32
+ @client ||= Client.start(server.port, inspector, options[:phantomjs])
33
+ end
34
+
35
+ def client_pid
36
+ client.pid
37
+ end
38
+
39
+ def timeout
40
+ server.timeout
41
+ end
42
+
43
+ def timeout=(sec)
44
+ server.timeout = sec
19
45
  end
20
46
 
21
47
  def restart
22
48
  browser.restart
23
49
  end
24
50
 
51
+ def quit
52
+ server.stop
53
+ client.stop
54
+ end
55
+
25
56
  # logger should be an object that responds to puts, or nil
26
57
  def logger
27
58
  options[:logger] || (options[:debug] && STDERR)
28
59
  end
29
60
 
30
- def visit(path, attributes = {})
31
- browser.visit(url(path), attributes)
61
+ def visit(path)
62
+ browser.visit app_server.url(path)
32
63
  end
33
64
 
34
65
  def current_url
@@ -44,7 +75,7 @@ module Capybara::Poltergeist
44
75
  end
45
76
 
46
77
  def find(selector)
47
- browser.find(selector).map { |node| Capybara::Poltergeist::Node.new(self, node) }
78
+ browser.find(selector).map { |page_id, id| Capybara::Poltergeist::Node.new(self, page_id, id) }
48
79
  end
49
80
 
50
81
  def evaluate_script(script)
@@ -72,6 +103,16 @@ module Capybara::Poltergeist
72
103
  browser.resize(width, height)
73
104
  end
74
105
 
106
+ def debug
107
+ inspector.open
108
+ pause
109
+ end
110
+
111
+ def pause
112
+ STDERR.puts "Poltergeist execution paused. Press enter to continue."
113
+ STDIN.gets
114
+ end
115
+
75
116
  def wait?
76
117
  true
77
118
  end
@@ -79,11 +120,5 @@ module Capybara::Poltergeist
79
120
  def invalid_element_errors
80
121
  [Capybara::Poltergeist::ObsoleteNode]
81
122
  end
82
-
83
- private
84
-
85
- def url(path)
86
- server.url(path)
87
- end
88
123
  end
89
124
  end
@@ -3,11 +3,21 @@ module Capybara
3
3
  class Error < StandardError
4
4
  end
5
5
 
6
- class BrowserError < Error
7
- attr_reader :text
6
+ class ClientError < Error
7
+ attr_reader :response
8
8
 
9
- def initialize(text)
10
- @text = text
9
+ def initialize(response)
10
+ @response = response
11
+ end
12
+ end
13
+
14
+ class BrowserError < ClientError
15
+ def name
16
+ response['name']
17
+ end
18
+
19
+ def text
20
+ response['args'].first
11
21
  end
12
22
 
13
23
  def message
@@ -15,11 +25,44 @@ module Capybara
15
25
  end
16
26
  end
17
27
 
18
- class ObsoleteNode < Error
28
+ class JavascriptError < ClientError
29
+ def javascript_messages
30
+ response['args'].first
31
+ end
32
+
33
+ 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."
38
+ end
39
+ end
40
+
41
+ class NodeError < ClientError
19
42
  attr_reader :node
20
43
 
21
- def initialize(node)
44
+ def initialize(node, response)
22
45
  @node = node
46
+ super(response)
47
+ end
48
+ end
49
+
50
+ class ObsoleteNode < NodeError
51
+ end
52
+
53
+ class ClickFailed < NodeError
54
+ def selector
55
+ response['args'][0]
56
+ end
57
+
58
+ def position
59
+ [response['args'][1]['x'], response['args'][1]['y']]
60
+ end
61
+
62
+ def message
63
+ "Click at co-ordinates [#{position.join(', ')}] failed. Poltergeist detected " \
64
+ "another element with CSS selector '#{selector}' at this position. " \
65
+ "It may be overlapping the element you are trying to click."
23
66
  end
24
67
  end
25
68
 
@@ -54,5 +97,19 @@ module Capybara
54
97
  "PhantomJS version #{version} is too old. You must use at least version #{Client::PHANTOMJS_VERSION}"
55
98
  end
56
99
  end
100
+
101
+ class PhantomJSFailed < Error
102
+ attr_reader :status
103
+
104
+ def initialize(status)
105
+ @status = status
106
+ end
107
+
108
+ def message
109
+ "PhantomJS returned non-zero exit status #{status.exitstatus}. Ensure there is an X display available and " \
110
+ "that DISPLAY is set. (See the Poltergeist README for details.) Make sure 'phantomjs --version' " \
111
+ "runs successfully on your system."
112
+ end
113
+ end
57
114
  end
58
115
  end