poltergeist 0.6.0 → 0.7.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.
@@ -3,7 +3,8 @@
3
3
  class Poltergeist.Node
4
4
  @DELEGATES = ['text', 'getAttribute', 'value', 'set', 'setAttribute', 'isObsolete',
5
5
  'removeAttribute', 'isMultiple', 'select', 'tagName', 'find',
6
- 'isVisible', 'position', 'trigger', 'parentId', 'clickTest']
6
+ 'isVisible', 'position', 'trigger', 'parentId', 'clickTest',
7
+ 'scrollIntoView', 'isDOMEqual']
7
8
 
8
9
  constructor: (@page, @id) ->
9
10
 
@@ -12,37 +13,12 @@ class Poltergeist.Node
12
13
 
13
14
  for name in @DELEGATES
14
15
  do (name) =>
15
- this.prototype[name] = (arguments...) ->
16
- @page.nodeCall(@id, name, arguments)
16
+ this.prototype[name] = (args...) ->
17
+ @page.nodeCall(@id, name, args)
17
18
 
18
- clickPosition: (scrollIntoView = true) ->
19
- dimensions = @page.validatedDimensions()
20
- document = dimensions.document
21
- viewport = dimensions.viewport
22
- pos = this.position()
23
-
24
- scroll = { left: dimensions.left, top: dimensions.top }
25
-
26
- adjust = (coord, measurement) ->
27
- if pos[coord] < 0
28
- scroll[coord] = Math.max(
29
- 0,
30
- scroll[coord] + pos[coord] - (viewport[measurement] / 2)
31
- )
32
-
33
- else if pos[coord] >= viewport[measurement]
34
- scroll[coord] = Math.min(
35
- document[measurement] - viewport[measurement],
36
- scroll[coord] + pos[coord] - viewport[measurement] + (viewport[measurement] / 2)
37
- )
38
-
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()
19
+ clickPosition: ->
20
+ viewport = @page.viewportSize()
21
+ pos = this.position()
46
22
 
47
23
  middle = (start, end, size) ->
48
24
  start + ((Math.min(end, size) - start) / 2)
@@ -53,18 +29,25 @@ class Poltergeist.Node
53
29
  }
54
30
 
55
31
  click: ->
32
+ this.scrollIntoView()
33
+
56
34
  pos = this.clickPosition()
57
35
  test = this.clickTest(pos.x, pos.y)
58
36
 
59
37
  if test.status == 'success'
60
38
  @page.sendEvent('click', pos.x, pos.y)
61
39
  else
62
- new Poltergeist.ClickFailed(test.selector, pos)
40
+ throw new Poltergeist.ClickFailed(test.selector, pos)
63
41
 
64
42
  dragTo: (other) ->
43
+ this.scrollIntoView()
44
+
65
45
  position = this.clickPosition()
66
- otherPosition = other.clickPosition(false)
46
+ otherPosition = other.clickPosition()
67
47
 
68
48
  @page.sendEvent('mousedown', position.x, position.y)
69
49
  @page.sendEvent('mousemove', otherPosition.x, otherPosition.y)
70
50
  @page.sendEvent('mouseup', otherPosition.x, otherPosition.y)
51
+
52
+ isEqual: (other) ->
53
+ @page == other.page && this.isDOMEqual(other.id)
@@ -1,18 +1,19 @@
1
1
  class Poltergeist.WebPage
2
2
  @CALLBACKS = ['onAlert', 'onConsoleMessage', 'onLoadFinished', 'onInitialized',
3
3
  'onLoadStarted', 'onResourceRequested', 'onResourceReceived',
4
- 'onError']
4
+ 'onError', 'onNavigationRequested', 'onUrlChanged']
5
5
 
6
6
  @DELEGATES = ['open', 'sendEvent', 'uploadFile', 'release', 'render']
7
7
 
8
8
  @COMMANDS = ['currentUrl', 'find', 'nodeCall', 'pushFrame', 'popFrame', 'documentSize']
9
9
 
10
- constructor: ->
11
- @native = require('webpage').create()
12
- @_source = ""
13
- @_errors = []
10
+ constructor: (width, height) ->
11
+ @native = require('webpage').create()
12
+ @_source = ""
13
+ @_errors = []
14
+ @_networkTraffic = {}
14
15
 
15
- this.setViewportSize(width: 1024, height: 768)
16
+ this.setViewportSize(width: width, height: height)
16
17
 
17
18
  for callback in WebPage.CALLBACKS
18
19
  this.bindCallback(callback)
@@ -22,7 +23,7 @@ class Poltergeist.WebPage
22
23
  for command in @COMMANDS
23
24
  do (command) =>
24
25
  this.prototype[command] =
25
- (arguments...) -> this.runCommand(command, arguments)
26
+ (args...) -> this.runCommand(command, args)
26
27
 
27
28
  for delegate in @DELEGATES
28
29
  do (delegate) =>
@@ -35,7 +36,7 @@ class Poltergeist.WebPage
35
36
  this.setScrollPosition(left: 0, top: 0)
36
37
 
37
38
  injectAgent: ->
38
- if this.evaluate(-> typeof __poltergeist) == "undefined"
39
+ if @native.evaluate(-> typeof __poltergeist) == "undefined"
39
40
  @native.injectJs("#{phantom.libraryPath}/agent.js")
40
41
  @nodes = {}
41
42
 
@@ -44,19 +45,44 @@ class Poltergeist.WebPage
44
45
  @_source = @native.content
45
46
  false
46
47
 
48
+ onLoadStartedNative: ->
49
+ @requestId = @lastRequestId
50
+
47
51
  onLoadFinishedNative: ->
48
52
  @_source or= @native.content
49
53
 
50
- onConsoleMessage: (message, line, file) ->
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
- console.log(message)
54
+ onConsoleMessage: (message) ->
55
+ console.log(message)
57
56
 
58
57
  onErrorNative: (message, stack) ->
59
- @_errors.push(message: message, stack: stack)
58
+ stackString = message
59
+
60
+ stack.forEach (frame) ->
61
+ stackString += "\n"
62
+ stackString += " at #{frame.file}:#{frame.line}"
63
+ stackString += " in #{frame.function}" if frame.function && frame.function != ''
64
+
65
+ @_errors.push(message: message, stack: stackString)
66
+
67
+ onResourceRequestedNative: (request) ->
68
+ @lastRequestId = request.id
69
+
70
+ @_networkTraffic[request.id] = {
71
+ request: request,
72
+ responseParts: []
73
+ }
74
+
75
+ onResourceReceivedNative: (response) ->
76
+ @_networkTraffic[response.id].responseParts.push(response)
77
+
78
+ if @requestId == response.id
79
+ if response.redirectURL
80
+ @requestId = response.id
81
+ else
82
+ @_statusCode = response.status
83
+
84
+ networkTraffic: ->
85
+ @_networkTraffic
60
86
 
61
87
  content: ->
62
88
  @native.content
@@ -70,6 +96,9 @@ class Poltergeist.WebPage
70
96
  clearErrors: ->
71
97
  @_errors = []
72
98
 
99
+ statusCode: ->
100
+ @_statusCode
101
+
73
102
  viewportSize: ->
74
103
  @native.viewportSize
75
104
 
@@ -102,9 +131,6 @@ class Poltergeist.WebPage
102
131
  dimensions = this.dimensions()
103
132
  document = dimensions.document
104
133
 
105
- orig_left = dimensions.left
106
- orig_top = dimensions.top
107
-
108
134
  if dimensions.right > document.width
109
135
  dimensions.left = Math.max(0, dimensions.left - (dimensions.right - document.width))
110
136
  dimensions.right = document.width
@@ -113,8 +139,7 @@ class Poltergeist.WebPage
113
139
  dimensions.top = Math.max(0, dimensions.top - (dimensions.bottom - document.height))
114
140
  dimensions.bottom = document.height
115
141
 
116
- if dimensions.left != orig_left || dimensions.top != orig_top
117
- this.setScrollPosition(left: dimensions.left, top: dimensions.top)
142
+ this.setScrollPosition(left: dimensions.left, top: dimensions.top)
118
143
 
119
144
  dimensions
120
145
 
@@ -122,7 +147,7 @@ class Poltergeist.WebPage
122
147
  @nodes[id] or= new Poltergeist.Node(this, id)
123
148
 
124
149
  evaluate: (fn, args...) ->
125
- JSON.parse @native.evaluate("function() { return JSON.stringify(#{this.stringifyCall(fn, args)}) }")
150
+ JSON.parse @native.evaluate("function() { return PoltergeistAgent.stringify(#{this.stringifyCall(fn, args)}) }")
126
151
 
127
152
  execute: (fn, args...) ->
128
153
  @native.evaluate("function() { #{this.stringifyCall(fn, args)} }")
@@ -149,10 +174,16 @@ class Poltergeist.WebPage
149
174
  # Any error raised here or inside the evaluate will get reported to
150
175
  # phantom.onError. If result is null, that means there was an error
151
176
  # inside the agent.
152
- runCommand: (name, arguments) ->
177
+ runCommand: (name, args) ->
153
178
  result = this.evaluate(
154
- (name, arguments) -> __poltergeist.externalCall(name, arguments),
155
- name, arguments
179
+ (name, args) -> __poltergeist.externalCall(name, args),
180
+ name, args
156
181
  )
157
182
 
158
- result && result.value
183
+ if result.error?
184
+ if result.error.message == 'PoltergeistAgent.ObsoleteNode'
185
+ throw new Poltergeist.ObsoleteNode
186
+ else
187
+ throw new Poltergeist.JavascriptError([result.error])
188
+ else
189
+ result.value
@@ -3,6 +3,7 @@ module Capybara::Poltergeist
3
3
  DEFAULT_TIMEOUT = 30
4
4
 
5
5
  attr_reader :app, :app_server, :server, :client, :browser, :options
6
+ attr_accessor :headers
6
7
 
7
8
  def initialize(app, options = {})
8
9
  @app = app
@@ -11,13 +12,14 @@ module Capybara::Poltergeist
11
12
  @inspector = nil
12
13
  @server = nil
13
14
  @client = nil
15
+ @headers = {}
14
16
 
15
17
  @app_server = Capybara::Server.new(app)
16
18
  @app_server.boot if Capybara.run_server
17
19
  end
18
20
 
19
21
  def browser
20
- @browser ||= Browser.new(server, client, logger)
22
+ @browser ||= Browser.new(server, client, logger, js_errors)
21
23
  end
22
24
 
23
25
  def inspector
@@ -29,7 +31,17 @@ module Capybara::Poltergeist
29
31
  end
30
32
 
31
33
  def client
32
- @client ||= Client.start(server.port, inspector, options[:phantomjs])
34
+ @client ||= Client.start(server.port,
35
+ :path => options[:phantomjs],
36
+ :window_size => options[:window_size],
37
+ :phantomjs_options => phantomjs_options
38
+ )
39
+ end
40
+
41
+ def phantomjs_options
42
+ list = options[:phantomjs_options] || []
43
+ list += ["--remote-debugger-port=#{inspector.port}", "--remote-debugger-autorun=yes"] if inspector
44
+ list
33
45
  end
34
46
 
35
47
  def client_pid
@@ -58,14 +70,22 @@ module Capybara::Poltergeist
58
70
  options[:logger] || (options[:debug] && STDERR)
59
71
  end
60
72
 
73
+ def js_errors
74
+ options.fetch(:js_errors, true)
75
+ end
76
+
61
77
  def visit(path)
62
- browser.visit app_server.url(path)
78
+ browser.visit app_server.url(path), @headers
63
79
  end
64
80
 
65
81
  def current_url
66
82
  browser.current_url
67
83
  end
68
84
 
85
+ def status_code
86
+ browser.status_code
87
+ end
88
+
69
89
  def body
70
90
  browser.body
71
91
  end
@@ -93,6 +113,7 @@ module Capybara::Poltergeist
93
113
 
94
114
  def reset!
95
115
  browser.reset
116
+ @headers = {}
96
117
  end
97
118
 
98
119
  def render(path, options = {})
@@ -103,9 +124,18 @@ module Capybara::Poltergeist
103
124
  browser.resize(width, height)
104
125
  end
105
126
 
127
+ def network_traffic
128
+ browser.network_traffic
129
+ end
130
+
106
131
  def debug
107
- inspector.open
108
- pause
132
+ if @options[:inspector]
133
+ inspector.open
134
+ pause
135
+ else
136
+ raise Error, "To use the remote debugging, you have to launch the driver " \
137
+ "with `:inspector => true` configuration option"
138
+ end
109
139
  end
110
140
 
111
141
  def pause
@@ -118,7 +148,7 @@ module Capybara::Poltergeist
118
148
  end
119
149
 
120
150
  def invalid_element_errors
121
- [Capybara::Poltergeist::ObsoleteNode]
151
+ [Capybara::Poltergeist::ObsoleteNode, Capybara::Poltergeist::ClickFailed]
122
152
  end
123
153
  end
124
154
  end
@@ -20,18 +20,7 @@ module Capybara
20
20
  end
21
21
 
22
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")
23
+ stack
35
24
  end
36
25
  end
37
26
 
@@ -128,9 +117,9 @@ module Capybara
128
117
  end
129
118
 
130
119
  def message
131
- "PhantomJS returned non-zero exit status #{status.exitstatus}. Ensure there is an X display available and " \
132
- "that DISPLAY is set. (See the Poltergeist README for details.) Make sure 'phantomjs --version' " \
133
- "runs successfully on your system."
120
+ "PhantomJS returned non-zero exit status #{status.exitstatus}. Make sure phantomjs runs successfully " \
121
+ "on your system. You can test this by just running the `phantomjs` command which should give you " \
122
+ "a Javascript REPL."
134
123
  end
135
124
  end
136
125
  end
@@ -0,0 +1,25 @@
1
+ module Capybara::Poltergeist
2
+ module JSON
3
+ def self.load(message)
4
+ if dumpy_multi_json?
5
+ MultiJson.load(message)
6
+ else
7
+ MultiJson.decode(message)
8
+ end
9
+ end
10
+
11
+ def self.dump(message)
12
+ if dumpy_multi_json?
13
+ MultiJson.dump(message)
14
+ else
15
+ MultiJson.encode(message)
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def self.dumpy_multi_json?
22
+ MultiJson.respond_to?(:dump) && MultiJson.respond_to?(:load)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,6 @@
1
+ module Capybara::Poltergeist
2
+ module NetworkTraffic
3
+ require 'capybara/poltergeist/network_traffic/request'
4
+ require 'capybara/poltergeist/network_traffic/response'
5
+ end
6
+ end
@@ -0,0 +1,26 @@
1
+ module Capybara::Poltergeist::NetworkTraffic
2
+ class Request
3
+ attr_reader :response_parts
4
+
5
+ def initialize(data, response_parts = [])
6
+ @data = data
7
+ @response_parts = response_parts
8
+ end
9
+
10
+ def url
11
+ @data['url']
12
+ end
13
+
14
+ def method
15
+ @data['method']
16
+ end
17
+
18
+ def headers
19
+ @data['headers']
20
+ end
21
+
22
+ def time
23
+ @data['time'] && Time.parse(@data['time'])
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,40 @@
1
+ module Capybara::Poltergeist::NetworkTraffic
2
+ class Response
3
+ def initialize(data)
4
+ @data = data
5
+ end
6
+
7
+ def url
8
+ @data['url']
9
+ end
10
+
11
+ def status
12
+ @data['status']
13
+ end
14
+
15
+ def status_text
16
+ @data['statusText']
17
+ end
18
+
19
+ def headers
20
+ @data['headers']
21
+ end
22
+
23
+ def redirect_url
24
+ @data['redirectUrl']
25
+ end
26
+
27
+ def body_size
28
+ @data['bodySize']
29
+ end
30
+
31
+ def content_type
32
+ @data['contentType']
33
+ end
34
+
35
+ def time
36
+ @data['time'] && Time.parse(@data['time'])
37
+ end
38
+ end
39
+ end
40
+