poltergeist 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+