poltergeist 1.7.0 → 1.8.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.
@@ -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