poltergeist 1.7.0 → 1.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,6 +1,6 @@
1
1
  class Poltergeist
2
2
  constructor: (port, width, height) ->
3
- @browser = new Poltergeist.Browser(this, width, height)
3
+ @browser = new Poltergeist.Browser(width, height)
4
4
  @connection = new Poltergeist.Connection(this, port)
5
5
 
6
6
  # The QtWebKit bridge doesn't seem to like Function.prototype.bind
@@ -11,20 +11,21 @@ class Poltergeist
11
11
 
12
12
  runCommand: (command) ->
13
13
  @running = true
14
-
14
+ command = new Poltergeist.Cmd(this, command.id, command.name, command.args)
15
15
  try
16
- @browser.runCommand(command.name, command.args)
16
+ command.run(@browser)
17
17
  catch error
18
18
  if error instanceof Poltergeist.Error
19
- this.sendError(error)
19
+ this.sendError(command.id, error)
20
20
  else
21
- this.sendError(new Poltergeist.BrowserError(error.toString(), error.stack))
21
+ this.sendError(command.id, new Poltergeist.BrowserError(error.toString(), error.stack))
22
22
 
23
- sendResponse: (response) ->
24
- this.send(response: response)
23
+ sendResponse: (command_id, response) ->
24
+ this.send(command_id: command_id, response: response)
25
25
 
26
- sendError: (error) ->
26
+ sendError: (command_id, error) ->
27
27
  this.send(
28
+ command_id: command_id,
28
29
  error:
29
30
  name: error.name || 'Generic',
30
31
  args: error.args && error.args() || [error.toString()]
@@ -76,8 +77,9 @@ class Poltergeist.BrowserError extends Poltergeist.Error
76
77
  args: -> [@message, @stack]
77
78
 
78
79
  class Poltergeist.StatusFailError extends Poltergeist.Error
80
+ constructor: (@url) ->
79
81
  name: "Poltergeist.StatusFailError"
80
- args: -> []
82
+ args: -> [@url]
81
83
 
82
84
  class Poltergeist.NoSuchWindowError extends Poltergeist.Error
83
85
  name: "Poltergeist.NoSuchWindowError"
@@ -88,6 +90,7 @@ class Poltergeist.NoSuchWindowError extends Poltergeist.Error
88
90
  phantom.injectJs("#{phantom.libraryPath}/web_page.js")
89
91
  phantom.injectJs("#{phantom.libraryPath}/node.js")
90
92
  phantom.injectJs("#{phantom.libraryPath}/connection.js")
93
+ phantom.injectJs("#{phantom.libraryPath}/cmd.js")
91
94
  phantom.injectJs("#{phantom.libraryPath}/browser.js")
92
95
 
93
96
  system = require 'system'
@@ -3,8 +3,9 @@
3
3
  class Poltergeist.Node
4
4
  @DELEGATES = ['allText', 'visibleText', 'getAttribute', 'value', 'set', 'setAttribute', 'isObsolete',
5
5
  'removeAttribute', 'isMultiple', 'select', 'tagName', 'find', 'getAttributes',
6
- 'isVisible', 'position', 'trigger', 'parentId', 'parentIds', 'mouseEventTest',
7
- 'scrollIntoView', 'isDOMEqual', 'isDisabled', 'deleteText', 'containsSelection', 'path']
6
+ 'isVisible', 'isInViewport', 'position', 'trigger', 'parentId', 'parentIds', 'mouseEventTest',
7
+ 'scrollIntoView', 'isDOMEqual', 'isDisabled', 'deleteText', 'containsSelection',
8
+ 'path', 'getProperty']
8
9
 
9
10
  constructor: (@page, @id) ->
10
11
 
@@ -31,7 +32,8 @@ class Poltergeist.Node
31
32
  mouseEvent: (name) ->
32
33
  this.scrollIntoView()
33
34
 
34
- pos = this.mouseEventPosition()
35
+ pos = this.mouseEventPosition()
36
+
35
37
  test = this.mouseEventTest(pos.x, pos.y)
36
38
 
37
39
  if test.status == 'success'
@@ -68,3 +70,4 @@ class Poltergeist.Node
68
70
 
69
71
  isEqual: (other) ->
70
72
  @page == other.page && this.isDOMEqual(other.id)
73
+
@@ -86,7 +86,7 @@ class Poltergeist.WebPage
86
86
  else
87
87
  @lastRequestId = request.id
88
88
 
89
- if request.url == @redirectURL
89
+ if @normalizeURL(request.url) == @redirectURL
90
90
  @redirectURL = null
91
91
  @requestId = request.id
92
92
 
@@ -100,7 +100,7 @@ class Poltergeist.WebPage
100
100
 
101
101
  if @requestId == response.id
102
102
  if response.redirectURL
103
- @redirectURL = response.redirectURL
103
+ @redirectURL = @normalizeURL(response.redirectURL)
104
104
  else
105
105
  @statusCode = response.status
106
106
  @_responseHeaders = response.headers
@@ -164,10 +164,10 @@ class Poltergeist.WebPage
164
164
  title: ->
165
165
  this.native().frameTitle
166
166
 
167
- frameUrl: (frameName) ->
168
- query = (frameName) ->
169
- document.querySelector("iframe[name='#{frameName}']")?.src
170
- this.evaluate(query, frameName)
167
+ frameUrl: (frameNameOrId) ->
168
+ query = (frameNameOrId) ->
169
+ document.querySelector("iframe[name='#{frameNameOrId}'], iframe[id='#{frameNameOrId}']")?.src
170
+ this.evaluate(query, frameNameOrId)
171
171
 
172
172
  clearErrors: ->
173
173
  @errors = []
@@ -238,7 +238,16 @@ class Poltergeist.WebPage
238
238
  @frames.push(name)
239
239
  true
240
240
  else
241
- false
241
+ frame_no = this.native().evaluate(
242
+ (frame_name) ->
243
+ frames = document.querySelectorAll("iframe, frame")
244
+ (idx for f, idx in frames when f?['name'] == frame_name or f?['id'] == frame_name)[0]
245
+ , name)
246
+ if frame_no? and this.native().switchToFrame(frame_no)
247
+ @frames.push(name)
248
+ true
249
+ else
250
+ false
242
251
 
243
252
  popFrame: ->
244
253
  @frames.pop()
@@ -299,7 +308,7 @@ class Poltergeist.WebPage
299
308
  else
300
309
  # The JSON.stringify happens twice because the second time we are essentially
301
310
  # escaping the string.
302
- "(#{fn.toString()}).apply(this, JSON.parse(#{JSON.stringify(JSON.stringify(args))}))"
311
+ "(#{fn.toString()}).apply(this, PoltergeistAgent.JSON.parse(#{JSON.stringify(JSON.stringify(args))}))"
303
312
 
304
313
  # For some reason phantomjs seems to have trouble with doing 'fat arrow' binding here,
305
314
  # hence the 'that' closure.
@@ -339,3 +348,8 @@ class Poltergeist.WebPage
339
348
 
340
349
  canGoForward: ->
341
350
  this.native().canGoForward
351
+
352
+ normalizeURL: (url) ->
353
+ parser = document.createElement('a')
354
+ parser.href = url
355
+ return parser.href
@@ -0,0 +1,17 @@
1
+ require 'securerandom'
2
+
3
+ module Capybara::Poltergeist
4
+ class Command
5
+ attr_reader :id
6
+
7
+ def initialize(name, *args)
8
+ @id = SecureRandom.uuid
9
+ @name = name
10
+ @args = args
11
+ end
12
+
13
+ def message
14
+ JSON.dump({ 'id' => @id, 'name' => @name, 'args' => @args })
15
+ end
16
+ end
17
+ end
@@ -200,6 +200,18 @@ module Capybara::Poltergeist
200
200
  end
201
201
  alias_method :resize_window, :resize
202
202
 
203
+ def resize_window_to(handle, width, height)
204
+ within_window(handle) do
205
+ resize(width, height)
206
+ end
207
+ end
208
+
209
+ def window_size(handle)
210
+ within_window(handle) do
211
+ evaluate_script('[window.innerWidth, window.innerHeight]')
212
+ end
213
+ end
214
+
203
215
  def scroll_to(left, top)
204
216
  browser.scroll_to(left, top)
205
217
  end
@@ -244,7 +256,7 @@ module Capybara::Poltergeist
244
256
  if @started
245
257
  URI.parse(browser.current_url).host
246
258
  else
247
- Capybara.app_host || "127.0.0.1"
259
+ URI.parse(Capybara.app_host || '').host || "127.0.0.1"
248
260
  end
249
261
  end
250
262
 
@@ -276,7 +288,10 @@ module Capybara::Poltergeist
276
288
 
277
289
  def debug
278
290
  if @options[:inspector]
279
- inspector.open
291
+ # Fall back to default scheme
292
+ scheme = URI.parse(browser.current_url).scheme rescue nil
293
+ scheme = 'http' if scheme != 'https'
294
+ inspector.open(scheme)
280
295
  pause
281
296
  else
282
297
  raise Error, "To use the remote debugging, you have to launch the driver " \
@@ -285,8 +300,29 @@ module Capybara::Poltergeist
285
300
  end
286
301
 
287
302
  def pause
288
- STDERR.puts "Poltergeist execution paused. Press enter to continue."
289
- STDIN.gets
303
+ # STDIN is not necessarily connected to a keyboard. It might even be closed.
304
+ # So we need a method other than keypress to continue.
305
+
306
+ # In jRuby - STDIN returns immediately from select
307
+ # see https://github.com/jruby/jruby/issues/1783
308
+ read, write = IO.pipe
309
+ Thread.new { IO.copy_stream(STDIN, write); write.close }
310
+
311
+ STDERR.puts "Poltergeist execution paused. Press enter (or run 'kill -CONT #{Process.pid}') to continue."
312
+
313
+ signal = false
314
+ old_trap = trap('SIGCONT') { signal = true; STDERR.puts "\nSignal SIGCONT received" }
315
+ keyboard = IO.select([read], nil, nil, 1) until keyboard || signal # wait for data on STDIN or signal SIGCONT received
316
+
317
+ begin
318
+ input = read.read_nonblock(80) # clear out the read buffer
319
+ puts unless input && input =~ /\n\z/
320
+ rescue EOFError, IO::WaitReadable # Ignore problems reading from STDIN.
321
+ end unless signal
322
+
323
+ trap('SIGCONT', old_trap) # Restore the previuos signal handler, if there was one.
324
+
325
+ STDERR.puts 'Continuing'
290
326
  end
291
327
 
292
328
  def wait?
@@ -55,8 +55,12 @@ module Capybara
55
55
  end
56
56
 
57
57
  class StatusFailError < ClientError
58
+ def url
59
+ response['args'].first
60
+ end
61
+
58
62
  def message
59
- "Request failed to reach server, check DNS and/or server status"
63
+ "Request to '#{url}' failed to reach server, check DNS and/or server status"
60
64
  end
61
65
  end
62
66
 
@@ -18,15 +18,15 @@ module Capybara::Poltergeist
18
18
  @browser ||= self.class.detect_browser
19
19
  end
20
20
 
21
- def url
22
- "//localhost:#{port}/"
21
+ def url(scheme)
22
+ "#{scheme}://localhost:#{port}/"
23
23
  end
24
24
 
25
- def open
25
+ def open(scheme)
26
26
  if browser
27
- Process.spawn(browser, url)
27
+ Process.spawn(browser, url(scheme))
28
28
  else
29
- raise Error, "Could not find a browser executable to open #{url}. " \
29
+ raise Error, "Could not find a browser executable to open #{url(scheme)}. " \
30
30
  "You can specify one manually using e.g. `:inspector => 'chromium'` " \
31
31
  "as a configuration option for Poltergeist."
32
32
  end
@@ -50,8 +50,23 @@ module Capybara::Poltergeist
50
50
  filter_text command(:visible_text)
51
51
  end
52
52
 
53
+ def property(name)
54
+ command :property, name
55
+ end
56
+
53
57
  def [](name)
54
- command :attribute, name
58
+ # Although the attribute matters, the property is consistent. Return that in
59
+ # preference to the attribute for links and images.
60
+ if (tag_name == 'img' and name == 'src') or (tag_name == 'a' and name == 'href' )
61
+ #if attribute exists get the property
62
+ value = command(:attribute, name) && command(:property, name)
63
+ return value
64
+ end
65
+
66
+ value = property(name)
67
+ value = command(:attribute, name) if value.nil? || value.is_a?(Hash)
68
+
69
+ value
55
70
  end
56
71
 
57
72
  def attributes
@@ -29,8 +29,8 @@ module Capybara::Poltergeist
29
29
  start
30
30
  end
31
31
 
32
- def send(message)
33
- @socket.send(message) or raise DeadClient.new(message)
32
+ def send(command)
33
+ @socket.send(command.id, command.message) or raise DeadClient.new(command.message)
34
34
  end
35
35
  end
36
36
  end
@@ -1,5 +1,5 @@
1
1
  module Capybara
2
2
  module Poltergeist
3
- VERSION = "1.7.0"
3
+ VERSION = "1.8.0"
4
4
  end
5
5
  end
@@ -24,6 +24,7 @@ module Capybara::Poltergeist
24
24
  def initialize(port = nil, timeout = nil)
25
25
  @timeout = timeout
26
26
  @server = start_server(port)
27
+ @receive_mutex = Mutex.new
27
28
  end
28
29
 
29
30
  def start_server(port)
@@ -51,11 +52,14 @@ module Capybara::Poltergeist
51
52
  # and use that to initialize a Web Socket.
52
53
  def accept
53
54
  @socket = server.accept
54
- @messages = []
55
+ @messages = {}
55
56
 
56
57
  @driver = ::WebSocket::Driver.server(self)
57
58
  @driver.on(:connect) { |event| @driver.start }
58
- @driver.on(:message) { |event| @messages << event.data }
59
+ @driver.on(:message) do |event|
60
+ command_id = JSON.load(event.data)['command_id']
61
+ @messages[command_id] = event.data
62
+ end
59
63
  end
60
64
 
61
65
  def write(data)
@@ -64,25 +68,32 @@ module Capybara::Poltergeist
64
68
 
65
69
  # Block until the next message is available from the Web Socket.
66
70
  # Raises Errno::EWOULDBLOCK if timeout is reached.
67
- def receive
71
+ def receive(cmd_id)
68
72
  start = Time.now
69
73
 
70
- until @messages.any?
74
+ until @messages.has_key?(cmd_id)
71
75
  raise Errno::EWOULDBLOCK if (Time.now - start) >= timeout
72
- IO.select([socket], [], [], timeout) or raise Errno::EWOULDBLOCK
73
- data = socket.recv(RECV_SIZE)
74
- break if data.empty?
75
- driver.parse(data)
76
+ if @receive_mutex.try_lock
77
+ begin
78
+ IO.select([socket], [], [], timeout) or raise Errno::EWOULDBLOCK
79
+ data = socket.recv(RECV_SIZE)
80
+ break if data.empty?
81
+ driver.parse(data)
82
+ ensure
83
+ @receive_mutex.unlock
84
+ end
85
+ else
86
+ sleep(0.05)
87
+ end
76
88
  end
77
-
78
- @messages.shift
89
+ @messages.delete(cmd_id)
79
90
  end
80
91
 
81
92
  # Send a message and block until there is a response
82
- def send(message)
93
+ def send(cmd_id, message)
83
94
  accept unless connected?
84
95
  driver.text(message)
85
- receive
96
+ receive(cmd_id)
86
97
  rescue Errno::EWOULDBLOCK
87
98
  raise TimeoutError.new(message)
88
99
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: poltergeist
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.7.0
4
+ version: 1.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jon Leighton
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-09-29 00:00:00.000000000 Z
11
+ date: 2015-11-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: capybara
@@ -86,14 +86,14 @@ dependencies:
86
86
  requirements:
87
87
  - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: '2.12'
89
+ version: 3.3.0
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
- version: '2.12'
96
+ version: 3.3.0
97
97
  - !ruby/object:Gem::Dependency
98
98
  name: sinatra
99
99
  requirement: !ruby/object:Gem::Requirement
@@ -156,28 +156,28 @@ dependencies:
156
156
  requirements:
157
157
  - - "~>"
158
158
  - !ruby/object:Gem::Version
159
- version: 2.2.0
159
+ version: '2.2'
160
160
  type: :development
161
161
  prerelease: false
162
162
  version_requirements: !ruby/object:Gem::Requirement
163
163
  requirements:
164
164
  - - "~>"
165
165
  - !ruby/object:Gem::Version
166
- version: 2.2.0
166
+ version: '2.2'
167
167
  - !ruby/object:Gem::Dependency
168
168
  name: guard-coffeescript
169
169
  requirement: !ruby/object:Gem::Requirement
170
170
  requirements:
171
171
  - - "~>"
172
172
  - !ruby/object:Gem::Version
173
- version: 1.0.0
173
+ version: 2.0.0
174
174
  type: :development
175
175
  prerelease: false
176
176
  version_requirements: !ruby/object:Gem::Requirement
177
177
  requirements:
178
178
  - - "~>"
179
179
  - !ruby/object:Gem::Version
180
- version: 1.0.0
180
+ version: 2.0.0
181
181
  description: Poltergeist is a driver for Capybara that allows you to run your tests
182
182
  on a headless WebKit browser, provided by PhantomJS.
183
183
  email:
@@ -193,8 +193,10 @@ files:
193
193
  - lib/capybara/poltergeist/client.rb
194
194
  - lib/capybara/poltergeist/client/agent.coffee
195
195
  - lib/capybara/poltergeist/client/browser.coffee
196
+ - lib/capybara/poltergeist/client/cmd.coffee
196
197
  - lib/capybara/poltergeist/client/compiled/agent.js
197
198
  - lib/capybara/poltergeist/client/compiled/browser.js
199
+ - lib/capybara/poltergeist/client/compiled/cmd.js
198
200
  - lib/capybara/poltergeist/client/compiled/connection.js
199
201
  - lib/capybara/poltergeist/client/compiled/main.js
200
202
  - lib/capybara/poltergeist/client/compiled/node.js
@@ -203,6 +205,7 @@ files:
203
205
  - lib/capybara/poltergeist/client/main.coffee
204
206
  - lib/capybara/poltergeist/client/node.coffee
205
207
  - lib/capybara/poltergeist/client/web_page.coffee
208
+ - lib/capybara/poltergeist/command.rb
206
209
  - lib/capybara/poltergeist/cookie.rb
207
210
  - lib/capybara/poltergeist/driver.rb
208
211
  - lib/capybara/poltergeist/errors.rb