vapir-firefox 1.7.2 → 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.
@@ -97,16 +97,16 @@ module Vapir
97
97
  include Firefox::Window
98
98
  include Firefox::ModalDialogContainer
99
99
 
100
- def self.initialize_jssh_socket
101
- # if it already exists and is not nil, then we are clobbering an existing one, presumably dead. but a new socket will not have any objects of the old one, so warn
102
- if class_variable_defined?('@@jssh_socket') && @@jssh_socket
103
- Kernel.warn "WARNING: JSSH_SOCKET RESET: resetting jssh socket. Any active javascript references will not exist on the new socket!"
104
- end
100
+ # initializes a JsshSocket and stores in a class variable.
101
+ def self.initialize_jssh_socket # :nodoc:
102
+ uninitialize_jssh_socket
105
103
  @@jssh_socket=JsshSocket.new
106
104
  @@firewatir_jssh_objects=@@jssh_socket.object("Vapir").assign({})
107
105
  @@jssh_socket
108
106
  end
109
- def self.jssh_socket(options={})
107
+ # returns a connected JsshSocket. pass :reset_if_dead => true if you suspect an existing
108
+ # socket may be dead, and you want a new one. a warning will be printed if this occurs.
109
+ def self.jssh_socket(options={}) # :nodoc:
110
110
  if options[:reset] || !(class_variable_defined?('@@jssh_socket') && @@jssh_socket)
111
111
  initialize_jssh_socket
112
112
  end
@@ -114,11 +114,17 @@ module Vapir
114
114
  begin
115
115
  @@jssh_socket.assert_socket
116
116
  rescue JsshConnectionError
117
+ Kernel.warn "WARNING: JsshSocket RESET: resetting jssh socket. Any active javascript references will not exist on the new socket!"
117
118
  initialize_jssh_socket
118
119
  end
119
120
  end
120
121
  @@jssh_socket
121
122
  end
123
+ # unsets a the current jssh socket
124
+ def self.uninitialize_jssh_socket # :nodoc:
125
+ @@jssh_socket=nil
126
+ @@firewatir_jssh_objects=nil
127
+ end
122
128
  def jssh_socket(options=nil)
123
129
  options ? self.class.jssh_socket(options) : @@jssh_socket
124
130
  end
@@ -129,19 +135,25 @@ module Vapir
129
135
  #
130
136
  # Input:
131
137
  # options - Hash of any of the following options:
132
- # :wait_time - Time to wait for Firefox to start. By default it waits for 2 seconds.
138
+ # :timeout - Time to wait for Firefox to start. By default it waits for 2 seconds.
133
139
  # This is done because if Firefox is not started and we try to connect
134
140
  # to jssh on port 9997 an exception is thrown.
135
141
  # :profile - The Firefox profile to use. If none is specified, Firefox will use
136
142
  # the last used profile.
137
- # :suppress_launch_process - do not create a new firefox process. Connect to an existing one.
138
- # TODO: Start the firefox version given by user.
139
143
  def initialize(options = {})
140
144
  if(options.kind_of?(Integer))
141
- options = {:wait_time => options}
142
- Kernel.warn "DEPRECATION WARNING: #{self.class.name}.new takes an options hash - passing a number is deprecated. Please use #{self.class.name}.new(:wait_time => #{options[:wait_time]})\n(called from #{caller.map{|c|"\n"+c}})"
145
+ options = {:timeout => options}
146
+ if config.warn_deprecated
147
+ Kernel.warn_with_caller "DEPRECATION WARNING: #{self.class.name}.new takes an options hash - passing a number is deprecated. Please use #{self.class.name}.new(:timeout => #{options[:timeout]})"
148
+ end
149
+ end
150
+ options = options_from_config(options, {:timeout => :attach_timeout, :binary_path => :firefox_binary_path, :profile => :firefox_profile, :wait => :wait}, [:attach, :goto, :wait_time])
151
+ if options[:wait_time]
152
+ if config.warn_deprecated
153
+ Kernel.warn_with_caller "DEPRECATION WARNING: the :wait_time option for #{self.class.name}.new has been renamed to :timeout for consistency. Please use #{self.class.name}.new(:timeout => #{options[:wait_time]})"
154
+ end
155
+ options[:timeout] = options[:wait_time]
143
156
  end
144
- options=handle_options(options, {:wait_time => 20}, [:attach, :goto, :binary_path])
145
157
  if options[:binary_path]
146
158
  @binary_path=options[:binary_path]
147
159
  end
@@ -156,7 +168,7 @@ module Vapir
156
168
  if options[:attach]
157
169
  raise Vapir::Exception::NoBrowserException, "cannot attach using #{options[:attach].inspect} - could not connect to Firefox with JSSH"
158
170
  else
159
- launch_browser
171
+ launch_browser(options)
160
172
  # if we just launched a the browser process, attach to the window
161
173
  # that opened when we did that.
162
174
  # but if options[:attach] is explicitly given as false (not nil),
@@ -166,7 +178,7 @@ module Vapir
166
178
  options[:attach]=[:title, //]
167
179
  end
168
180
  end
169
- ::Waiter.try_for(options[:wait_time], :exception => Vapir::Exception::NoBrowserException.new("Could not connect to the JSSH socket on the browser after #{options[:wait_time]} seconds. Either Firefox did not start or JSSH is not installed and listening.")) do
181
+ ::Waiter.try_for(options[:timeout], :exception => Vapir::Exception::NoBrowserException.new("Could not connect to the JSSH socket on the browser after #{options[:timeout]} seconds. Either Firefox did not start or JSSH is not installed and listening.")) do
170
182
  begin
171
183
  jssh_socket(:reset_if_dead => true).assert_socket
172
184
  true
@@ -177,6 +189,12 @@ module Vapir
177
189
  end
178
190
  @browser_jssh_objects = jssh_socket.object('{}').store_rand_object_key(@@firewatir_jssh_objects) # this is an object that holds stuff for this browser
179
191
 
192
+ @pid = begin
193
+ self.pid
194
+ rescue NotImplementedError
195
+ nil
196
+ end
197
+
180
198
  if options[:attach]
181
199
  attach(*options[:attach])
182
200
  else
@@ -187,16 +205,9 @@ module Vapir
187
205
  if options[:goto]
188
206
  goto(options[:goto])
189
207
  end
208
+ wait if options[:wait]
190
209
  end
191
210
 
192
- # def self.firefox_is_running?
193
- # TODO/FIX: implement!
194
- # true
195
- # end
196
- # def firefox_is_running?
197
- # self.class.firefox_is_running?
198
- # end
199
-
200
211
  def mozilla_window_class_name
201
212
  'MozillaUIWindowClass'
202
213
  end
@@ -207,7 +218,7 @@ module Vapir
207
218
 
208
219
  def exists?
209
220
  # jssh_socket may be nil if the window has closed
210
- jssh_socket && browser_window_object && jssh_socket.object('getWindows()').to_js_array.include(browser_window_object)
221
+ jssh_socket && browser_window_object && self.class.browser_window_objects.include?(browser_window_object)
211
222
  end
212
223
 
213
224
  # Launches firebox browser
@@ -216,22 +227,15 @@ module Vapir
216
227
  def launch_browser(options = {})
217
228
  ff_options = []
218
229
  if(options[:profile])
219
- ff_options << ['-no-remote', '-P', options[:profile]]
230
+ ff_options += ['-no-remote', '-P', options[:profile]]
220
231
  end
221
232
 
222
233
  bin = path_to_bin()
223
- @t = Thread.new { system(bin, '-jssh', *ff_options) }
234
+ @self_launched_browser = true
235
+ @t = Thread.new { system(bin, '-jssh', *ff_options) } # TODO: launch process in such a way that @pid can be noted
224
236
  end
225
237
  private :launch_browser
226
238
 
227
- # Creates a new instance of Firefox. Loads the URL and return the instance.
228
- # Input:
229
- # url - url of the page to be loaded.
230
- def self.start(url)
231
- new(:goto => url)
232
- end
233
-
234
-
235
239
  # Loads the given url in the browser. Waits for the page to get loaded.
236
240
  def goto(url)
237
241
  assert_exists
@@ -239,6 +243,41 @@ module Vapir
239
243
  wait
240
244
  end
241
245
 
246
+ # Performs a HTTP POST action to an arbitrary URL with the given data. The data are represented
247
+ # to this method as a Hash, which is converted to the standard form of &-separated key=value
248
+ # strings POST data use.
249
+ #
250
+ # The data hash should be keyed with strings or symbols (which are converted to strings before
251
+ # being sent along), and its values should all be strings.
252
+ #
253
+ # If no post_data_hash is given, the body of the POST is empty.
254
+ def post_to(url, post_data_hash={})
255
+ require 'cgi'
256
+ raise ArgumentError, "post_data_hash must be a Hash!" unless post_data_hash.is_a?(Hash)
257
+ dataString = post_data_hash.map do |(key, val)|
258
+ unless key.is_a?(String) || key.is_a?(Symbol)
259
+ raise ArgumentError, "post_data_hash keys must be strings or symbols; got key #{key.inspect} in hash #{post_data_hash.inspect}"
260
+ end
261
+ unless val.is_a?(String)
262
+ raise ArgumentError, "post_data_hash values must all be string;s got value #{val.inspect} in hash #{post_data_hash.inspect}"
263
+ end
264
+ CGI.escape(key.to_s)+'='+CGI.escape(val)
265
+ end.join("&")
266
+ stringStream = jssh_socket.Components.classes["@mozilla.org/io/string-input-stream;1"].createInstance(jssh_socket.Components.interfaces.nsIStringInputStream)
267
+ if stringStream.to_hash.key?('data')
268
+ stringStream.data=dataString
269
+ else
270
+ stringStream.setData(dataString, dataString.unpack("U*").length)
271
+ end
272
+ postData = jssh_socket.Components.classes["@mozilla.org/network/mime-input-stream;1"].createInstance(jssh_socket.Components.interfaces.nsIMIMEInputStream)
273
+ postData.addHeader("Content-Type", "application/x-www-form-urlencoded")
274
+ postData.addContentLength = true
275
+ postData.setData(stringStream)
276
+
277
+ browser_object.loadURIWithFlags(url, 0, nil, nil, postData)
278
+ wait
279
+ end
280
+
242
281
  # Loads the previous page (if there is any) in the browser. Waits for the page to get loaded.
243
282
  def back
244
283
  if browser_object.canGoBack
@@ -266,8 +305,6 @@ module Vapir
266
305
  end
267
306
 
268
307
  private
269
- # This function creates a new socket at port 9997 and sets the default values for instance and class variables.
270
- # Generatesi UnableToStartJSShException if cannot connect to jssh even after 3 tries.
271
308
  def set_defaults(no_of_tries = 0)
272
309
  @error_checkers = []
273
310
  end
@@ -302,16 +339,15 @@ module Vapir
302
339
  # Details : http://zenit.senecac.on.ca/wiki/index.php/Mozilla.dev.tech.xul#What_is_an_example_of_addProgressListener.3F
303
340
  @browser_jssh_objects[:listener_object]={}
304
341
  listener_object=@browser_jssh_objects[:listener_object]
305
- listener_object[:QueryInterface]=jssh_socket.object(
306
- "function(aIID)
307
- { if(aIID.equals(Components.interfaces.nsIWebProgressListener) || aIID.equals(Components.interfaces.nsISupportsWeakReference) || aIID.equals(Components.interfaces.nsISupports))
342
+ listener_object[:QueryInterface]=jssh_socket.function(:aIID) do %Q(
343
+ if(aIID.equals(Components.interfaces.nsIWebProgressListener) || aIID.equals(Components.interfaces.nsISupportsWeakReference) || aIID.equals(Components.interfaces.nsISupports))
308
344
  { return this;
309
345
  }
310
346
  throw Components.results.NS_NOINTERFACE;
311
- }")
312
- listener_object[:onStateChange]= jssh_socket.object(
313
- "function(aWebProgress, aRequest, aStateFlags, aStatus)
314
- { var requests_in_progress=#{@requests_in_progress.ref};
347
+ )
348
+ end
349
+ listener_object[:onStateChange]= jssh_socket.function(:aWebProgress, :aRequest, :aStateFlag, :aStatus) do %Q(
350
+ var requests_in_progress=#{@requests_in_progress.ref};
315
351
  if(aStateFlags & Components.interfaces.nsIWebProgressListener.STATE_STOP)
316
352
  { #{@updated_at_epoch_ms.ref}=new Date().getTime();
317
353
  #{browser_object.ref}=#{browser_window_object.ref}.getBrowser();
@@ -347,7 +383,8 @@ module Vapir
347
383
  --i;
348
384
  }
349
385
  }*/
350
- }")
386
+ )
387
+ end
351
388
  browser_object.addProgressListener(listener_object)
352
389
  end
353
390
 
@@ -363,22 +400,36 @@ module Vapir
363
400
  end
364
401
 
365
402
  public
366
- # Closes the window.
403
+ # Closes the browser window.
404
+ #
405
+ # This will also quit the browser (see #quit_browser) only if this instance of Vapir::Firefox launched the browser when
406
+ # it was created, AND there are no other windows remaining open. On Windows, closing the last browser window quits
407
+ # the browser anyway; on other operating systems it does not.
367
408
  def close
368
409
  assert_exists
410
+ # we expect the browser may exit if there are no windows which aren't ourself. except on mac.
411
+ expect_exit = !self.class.window_objects.any?{|other_window| other_window != self.browser_window_object } && current_os != :macosx
369
412
  begin
370
413
  browser_window_object.close
371
414
  # TODO/fix timeout; this shouldn't be a hard-coded magic number.
372
415
  ::Waiter.try_for(32, :exception => Exception::WindowFailedToCloseException.new("The browser window did not close")) do
373
416
  !exists?
374
417
  end
375
- @@jssh_socket.assert_socket
418
+ if expect_exit
419
+ ::Waiter.try_for(8, :exception => nil) do
420
+ jssh_socket.assert_socket # this should error, going up past the waiter to the rescue block above
421
+ false
422
+ end
423
+ else
424
+ jssh_socket.assert_socket
425
+ end
376
426
  rescue JsshConnectionError # the socket may disconnect when we close the browser, causing the JsshSocket to complain
377
- @@jssh_socket=nil
427
+ Vapir::Firefox.uninitialize_jssh_socket
428
+ wait_for_process_exit(@pid)
378
429
  end
430
+
379
431
  @browser_window_object=@browser_object=@document_object=@content_window_object=@body_object=nil
380
- @launched_browser_process=false #TODO/FIX: check here if we originally launched the browser process
381
- if @launched_browser_process && @@jssh_socket
432
+ if @self_launched_browser && jssh_socket && !self.class.window_objects.any?{ true }
382
433
  quit_browser(:force => false)
383
434
  end
384
435
  end
@@ -388,34 +439,143 @@ module Vapir
388
439
  quit_browser(:force => false)
389
440
  end
390
441
 
391
- # quits the browser.
392
- # quit_browser(:force => true) will force the browser to quit.
393
- def quit_browser(options={})
394
- options=handle_options(options, :force => false)
395
- # from https://developer.mozilla.org/en/How_to_Quit_a_XUL_Application
396
- appStartup= jssh_socket.Components.classes['@mozilla.org/toolkit/app-startup;1'].getService(jssh_socket.Components.interfaces.nsIAppStartup)
397
- quitSeverity = options[:force] ? jssh_socket.Components.interfaces.nsIAppStartup.eForceQuit : jssh_socket.Components.interfaces.nsIAppStartup.eAttemptQuit
398
- begin
399
- appStartup.quit(quitSeverity)
400
- ::Waiter.try_for(8, :exception => Exception::WindowFailedToCloseException.new("The browser did not quit")) do
401
- @@jssh_socket.assert_socket # this should error, going up past the waiter to the rescue block above
402
- false
442
+ module FirefoxClassAndInstanceMethods
443
+ # quits the browser.
444
+ #
445
+ # quit_browser(:force => true) will force the browser to quit.
446
+ #
447
+ # if there is no existing connection to JSSH, this will attempt to create one. If that fails, JsshUnableToStart will be raised.
448
+ def quit_browser(options={})
449
+ jssh_socket(:reset_if_dead => true).assert_socket
450
+ options=handle_options(options, :force => false)
451
+
452
+ pid = @pid || begin
453
+ self.pid
454
+ rescue NotImplementedError
455
+ nil
456
+ end
457
+
458
+ # from https://developer.mozilla.org/en/How_to_Quit_a_XUL_Application
459
+ appStartup= jssh_socket.Components.classes['@mozilla.org/toolkit/app-startup;1'].getService(jssh_socket.Components.interfaces.nsIAppStartup)
460
+ quitSeverity = options[:force] ? jssh_socket.Components.interfaces.nsIAppStartup.eForceQuit : jssh_socket.Components.interfaces.nsIAppStartup.eAttemptQuit
461
+ begin
462
+ appStartup.quit(quitSeverity)
463
+ ::Waiter.try_for(8, :exception => Exception::WindowFailedToCloseException.new("The browser did not quit")) do
464
+ jssh_socket.assert_socket # this should error, going up past the waiter to the rescue block above
465
+ false
466
+ end
467
+ rescue JsshConnectionError
468
+ Vapir::Firefox.uninitialize_jssh_socket
469
+ end
470
+
471
+ wait_for_process_exit(pid)
472
+
473
+ @browser_window_object=@browser_object=@document_object=@content_window_object=@body_object=nil
474
+ nil
475
+ end
476
+
477
+ # returns the pid of the currently-attached Firefox process.
478
+ #
479
+ # This only works on Firefox >= 3.6, on platforms supported (see #current_os), and raises
480
+ # NotImplementedError if it can't get the pid.
481
+ def pid
482
+ begin
483
+ begin
484
+ ctypes = jssh_socket.Components.utils.import("resource://gre/modules/ctypes.jsm").ctypes
485
+ rescue JsshJavascriptError
486
+ raise NotImplementedError, "Firefox 3.6 or greater is required for this method.\n\nOriginal error from firefox: #{$!.class}: #{$!.message}", $!.backtrace
487
+ end
488
+ lib, pidfunction, abi = *case current_os
489
+ when :macosx
490
+ ["libc.dylib", 'getpid', ctypes.default_abi]
491
+ when :linux
492
+ ["libc.so.6", 'getpid', ctypes.default_abi]
493
+ when :windows
494
+ ['kernel32', 'GetCurrentProcessId', ctypes.stdcall_abi]
495
+ else
496
+ raise NotImplementedError, "don't know how to get pid for #{current_os}"
497
+ end
498
+ getpid = ctypes.open(lib).declare(pidfunction, abi, ctypes.int32_t)
499
+ getpid.call()
403
500
  end
404
- rescue JsshConnectionError
405
- @@jssh_socket=nil
406
- end
407
- # TODO/FIX: poll to wait for the process itself to finish? the socket closes (which we wait for
408
- # above) before the process itself has exited, so if Firefox.new is called between the socket
409
- # closing and the process exiting, Firefox pops up with:
410
- # Close Firefox
411
- # A copy of Firefox is already open. Only one copy of Firefox can be open at a time.
412
- # [OK]
413
- # until that's implemented, just wait for an arbitrary amount of time. (ick)
414
- sleep 2
501
+ end
502
+
503
+ # attempts to determine whether the given process is still running. will raise
504
+ # NotImplementedError if it can't determine this.
505
+ def process_running?(pid)
506
+ case current_os
507
+ when :windows
508
+ kernel32 = Vapir::Firefox.instance_eval do # define this on the class for reuse
509
+ @kernel32 ||= begin
510
+ require 'ffi'
511
+ kernel32 = Module.new
512
+ kernel32.send :extend, FFI::Library
513
+ kernel32.ffi_lib 'kernel32'
514
+ kernel32.ffi_convention :stdcall
515
+ kernel32.attach_function :OpenProcess, [:ulong, :char, :ulong], :pointer
516
+ kernel32.attach_function :GetExitCodeProcess, [:pointer, :pointer], :char
517
+ kernel32.const_set('PROCESS_QUERY_INFORMATION', 0x0400)
518
+ kernel32.const_set('STILL_ACTIVE', 259)
519
+ kernel32
520
+ end
521
+ end
522
+ process_handle = kernel32.OpenProcess(kernel32::PROCESS_QUERY_INFORMATION, 0, pid)
523
+ exit_code=FFI::MemoryPointer.new(:ulong)
524
+ result = kernel32.GetExitCodeProcess(process_handle, exit_code)
525
+ if result == 0
526
+ raise "GetExitCodeProcess failed"
527
+ end
528
+ return exit_code.get_ulong(0)==kernel32::STILL_ACTIVE
529
+ when :linux, :macosx
530
+ `ps -p #{pid}`
531
+ $? == 0
532
+ else
533
+ raise NotImplementedError
534
+ end
535
+ end
415
536
 
416
- @browser_window_object=@browser_object=@document_object=@content_window_object=@body_object=nil
417
- nil
537
+ # waits until the Firefox process with the given pid has exited.
538
+ #
539
+ # if no pid is given, waits the configured amount of time until it is safe to assume
540
+ # the pfirefox process has exited.
541
+ def wait_for_process_exit(pid)
542
+ if pid
543
+ ::Waiter.try_for(16, :exception => Exception::WindowFailedToCloseException.new("The browser did not quit")) do
544
+ !process_running?(pid)
545
+ end
546
+ else
547
+ sleep config.firefox_quit_sleep_time
548
+ end
549
+ end
550
+
551
+ # returns a symbol representing the platform we're currently running on - currently
552
+ # implemented platforms are :windows, :macosx, and :linux. raises NotImplementedError if the
553
+ # current platform isn't one of those.
554
+ def current_os
555
+ @current_os ||= begin
556
+ platform= if RUBY_PLATFORM =~ /java/
557
+ require 'java'
558
+ java.lang.System.getProperty("os.name")
559
+ else
560
+ RUBY_PLATFORM
561
+ end
562
+ case platform
563
+ when /mswin|windows|mingw32/i
564
+ :windows
565
+ when /darwin|mac os/i
566
+ :macosx
567
+ when /linux/i
568
+ :linux
569
+ else
570
+ raise NotImplementedError, "Unidentified platform #{platform}"
571
+ end
572
+ end
573
+ end
418
574
  end
575
+ include FirefoxClassAndInstanceMethods
576
+ extend FirefoxClassAndInstanceMethods
577
+
578
+
419
579
 
420
580
  # Used for attaching pop up window to an existing Firefox window, either by url or title.
421
581
  # ff.attach(:url, 'http://www.google.com')
@@ -425,7 +585,7 @@ module Vapir
425
585
  # Instance of newly attached window.
426
586
  def attach(how, what)
427
587
  @browser_window_object = case how
428
- when :jssh_object
588
+ when :browser_window_object
429
589
  what
430
590
  else
431
591
  find_window(how, what)
@@ -437,24 +597,15 @@ module Vapir
437
597
  set_browser_document
438
598
  self
439
599
  end
440
-
441
- # Class method to return a browser object if a window matches for how
442
- # and what. Window can be referenced by url or title.
443
- # The second argument can be either a string or a regular expression.
444
- # Vapir::Browser.attach(:url, 'http://www.google.com')
445
- # Vapir::Browser.attach(:title, 'Google')
446
- def self.attach how, what
447
- new(:attach => [how, what])
448
- end
600
+ private :attach
449
601
 
450
602
  # loads up a new window in an existing process
451
603
  # Vapir::Browser.attach() with no arguments passed the attach method will create a new window
452
604
  # this will only be called one time per instance we're only ever going to run in 1 window
453
-
454
605
  def open_window
455
606
  begin
456
607
  @browser_window_name="firewatir_window_%.16x"%rand(2**64)
457
- end while jssh_socket.value_json("$A(getWindows()).detect(function(win){return win.name==#{@browser_window_name.to_jssh}}) ? true : false")
608
+ end while self.class.browser_window_objects.any?{|browser_window_object| browser_window_object.name == @browser_window_name }
458
609
  watcher=jssh_socket.Components.classes["@mozilla.org/embedcomp/window-watcher;1"].getService(jssh_socket.Components.interfaces.nsIWindowWatcher)
459
610
  # nsIWindowWatcher is used to launch new top-level windows. see https://developer.mozilla.org/en/Working_with_windows_in_chrome_code
460
611
 
@@ -463,47 +614,44 @@ module Vapir
463
614
  end
464
615
  private :open_window
465
616
 
466
- def self.each
467
- each_browser_window_object do |win|
468
- yield self.attach(:jssh_object, win)
617
+ class << self
618
+ def each_browser
619
+ each_browser_window_object do |win|
620
+ yield self.attach(:browser_window_object, win)
621
+ end
469
622
  end
470
- end
471
-
472
- def self.each_browser_window_object
473
- mediator=jssh_socket.Components.classes["@mozilla.org/appshell/window-mediator;1"].getService(jssh_socket.Components.interfaces.nsIWindowMediator)
474
- enumerator=mediator.getEnumerator("navigator:browser")
475
- while enumerator.hasMoreElements
476
- win=enumerator.getNext
477
- yield win
623
+ alias each each_browser
624
+ def browsers
625
+ Enumerator.new(self, :each_browser)
478
626
  end
479
- nil
480
- end
481
- def self.browser_window_objects
482
- window_objects=[]
483
- each_browser_window_object do |window_object|
484
- window_objects << window_object
627
+ def each_browser_window_object
628
+ mediator=jssh_socket.Components.classes["@mozilla.org/appshell/window-mediator;1"].getService(jssh_socket.Components.interfaces.nsIWindowMediator)
629
+ enumerator=mediator.getEnumerator("navigator:browser")
630
+ while enumerator.hasMoreElements
631
+ win=enumerator.getNext
632
+ yield win
633
+ end
634
+ nil
485
635
  end
486
- window_objects
487
- end
488
- def self.each_window_object
489
- mediator=jssh_socket.Components.classes["@mozilla.org/appshell/window-mediator;1"].getService(jssh_socket.Components.interfaces.nsIWindowMediator)
490
- enumerator=mediator.getEnumerator(nil)
491
- while enumerator.hasMoreElements
492
- win=enumerator.getNext
493
- yield win
636
+ def browser_window_objects
637
+ Enumerator.new(self, :each_browser_window_object)
494
638
  end
495
- nil
496
- end
497
- def self.window_objects
498
- window_objects=[]
499
- each_window_object do |window_object|
500
- window_objects << window_object
639
+ def each_window_object
640
+ mediator=jssh_socket.Components.classes["@mozilla.org/appshell/window-mediator;1"].getService(jssh_socket.Components.interfaces.nsIWindowMediator)
641
+ enumerator=mediator.getEnumerator(nil)
642
+ while enumerator.hasMoreElements
643
+ win=enumerator.getNext
644
+ yield win
645
+ end
646
+ nil
647
+ end
648
+ def window_objects
649
+ Enumerator.new(self, :each_window_object)
501
650
  end
502
- window_objects
503
651
  end
504
652
 
505
653
  # return the window jssh object for the browser window with the given title or url.
506
- # how - :url or :title
654
+ # how - :url, :title, or :name
507
655
  # what - string or regexp
508
656
  #
509
657
  # Start searching windows in reverse order so that we attach/find the latest opened window.
@@ -511,6 +659,7 @@ module Vapir
511
659
  orig_how=how
512
660
  hows={ :title => proc{|content_window| content_window.title },
513
661
  :URL => proc{|content_window| content_window.location.href },
662
+ :name => proc{|content_window| content_window.name },
514
663
  }
515
664
  how=hows.keys.detect{|h| h.to_s.downcase==orig_how.to_s.downcase}
516
665
  raise ArgumentError, "how should be one of: #{hows.keys.inspect} (was #{orig_how.inspect})" unless how
@@ -538,6 +687,16 @@ module Vapir
538
687
  body_object.textContent
539
688
  end
540
689
 
690
+ # the HTTP response status code for the currently loaded document
691
+ def response_status_code
692
+ channel = nil
693
+ ::Waiter.try_for(8, :exception => nil) do
694
+ channel=browser.browser_object.docShell.currentDocumentChannel
695
+ channel.is_a?(JsshObject) && channel.instanceof(browser.jssh_socket.Components.interfaces.nsIHttpChannel) && channel.respond_to?(:responseStatus)
696
+ end || raise(RuntimeError, "expected currentDocumentChannel to exist and be a nsIHttpChannel but it wasn't; was #{channel.is_a?(JsshObject) ? channel.toString : channel.inspect}")
697
+ status = channel.responseStatus
698
+ end
699
+
541
700
  # Maximize the current browser window.
542
701
  def maximize()
543
702
  browser_window_object.maximize
@@ -556,11 +715,8 @@ module Vapir
556
715
  end
557
716
  options={:sleep => false, :last_url => nil, :timeout => 120}.merge(options)
558
717
  started=Time.now
559
- while browser_object.webProgress.isLoadingDocument
560
- sleep 0.1
561
- if Time.now - started > options[:timeout]
562
- raise "Page Load Timeout"
563
- end
718
+ ::Waiter.try_for(options[:timeout] - (Time.now - started), :exception => "Waiting for the document to finish loading timed out") do
719
+ browser_object.webProgress.isLoadingDocument==false
564
720
  end
565
721
 
566
722
  # If the redirect is to a download attachment that does not reload this page, this
@@ -569,12 +725,8 @@ module Vapir
569
725
  url= document_object.URL
570
726
 
571
727
  if(url != options[:last_url])
572
- # Check for Javascript redirect. As we are connected to Firefox via JSSh. JSSh
573
- # doesn't detect any javascript redirects so check it here.
574
- # If page redirects to itself that this code will enter in infinite loop.
575
- # So we currently don't wait for such a page.
576
- # wait variable in JSSh tells if we should wait more for the page to get loaded
577
- # or continue. -1 means page is not redirected. Anyother positive values means wait.
728
+ # check for meta redirects, except for redirects back to the same page (infinite
729
+ # loop redirects).
578
730
  metas=document_object.getElementsByTagName 'meta'
579
731
  wait_time=metas.to_array.map do |meta|
580
732
  return_time=true
@@ -601,6 +753,83 @@ module Vapir
601
753
  return self
602
754
  end
603
755
 
756
+ # saves a screenshot of this browser window to the given filename.
757
+ #
758
+ # the last argument is an optional options hash, taking options:
759
+ # - :dc => context to capture (stands for device context). default is :page. may be one of:
760
+ # - :page takes a screenshot of the full page, and none of the browser chrome. this is supported cross-platform.
761
+ # - :client takes a screenshot of the client area, which excludes the menu bar and other window trimmings.
762
+ # only supported on windows.
763
+ # - :window takes a screenshot of the full browser window. only supported on windows.
764
+ # - :desktop takes a screenshot of the full desktop. only supported on windows.
765
+ # - :format => a valid format. if :dc is :window, the default is 'png' ('jpeg' is also supported); if :dc is anything else, 'bmp' is both the
766
+ # default and the only supported format.
767
+ def screen_capture(filename, options = {})
768
+ options = handle_options(options, :format => nil, :dc => :page)
769
+
770
+ if options[:dc] == :page
771
+ options[:format] ||= 'png'
772
+ jssh_socket.call_function(:window => content_window_object, :options => options, :filename => File.expand_path(filename)) do
773
+ %q(
774
+ // this is adapted from Selenium's method Selenium.prototype.doCaptureEntirePageScreenshot
775
+ var document = window.document;
776
+ var document_element = document.documentElement;
777
+ var width = document_element.scrollWidth;
778
+ var height = document_element.scrollHeight;
779
+ var styleWidth = width.toString() + 'px';
780
+ var styleHeight = height.toString() + 'px';
781
+
782
+ var canvas = document.createElementNS('http://www.w3.org/1999/xhtml', 'html:canvas'), grabCanvas=canvas;
783
+ grabCanvas.style.display = 'none';
784
+ grabCanvas.width = width;
785
+ grabCanvas.style.width = styleWidth;
786
+ grabCanvas.style.maxWidth = styleWidth;
787
+ grabCanvas.height = height;
788
+ grabCanvas.style.height = styleHeight;
789
+ grabCanvas.style.maxHeight = styleHeight;
790
+
791
+ document_element.appendChild(canvas);
792
+ try
793
+ {
794
+ var context = canvas.getContext('2d');
795
+ context.clearRect(0, 0, width, height);
796
+ context.save();
797
+
798
+ var prefs=Components.classes['@mozilla.org/preferences-service;1'].getService(Components.interfaces.nsIPrefBranch);
799
+ var background_color = prefs.getCharPref('browser.display.background_color');
800
+
801
+ context.drawWindow(window, 0, 0, width, height, background_color);
802
+ context.restore();
803
+ var dataUrl = canvas.toDataURL("image/" + options['format']);
804
+
805
+ var nsIoService = Components.classes["@mozilla.org/network/io-service;1"].getService(Components.interfaces.nsIIOService);
806
+ var channel = nsIoService.newChannelFromURI(nsIoService.newURI(dataUrl, null, null));
807
+ var binaryInputStream = Components.classes["@mozilla.org/binaryinputstream;1"].createInstance(Components.interfaces.nsIBinaryInputStream);
808
+ binaryInputStream.setInputStream(channel.open());
809
+ var numBytes = binaryInputStream.available();
810
+ var bytes = binaryInputStream.readBytes(numBytes);
811
+
812
+ var nsFile = Components.classes["@mozilla.org/file/local;1"].createInstance(Components.interfaces.nsILocalFile);
813
+ nsFile.initWithPath(filename);
814
+ var writeFlag = 0x02; // write only
815
+ var createFlag = 0x08; // create
816
+ var truncateFlag = 0x20; // truncate
817
+ var fileOutputStream = Components.classes["@mozilla.org/network/file-output-stream;1"].createInstance(Components.interfaces.nsIFileOutputStream);
818
+ fileOutputStream.init(nsFile, writeFlag | createFlag | truncateFlag, 0664, null);
819
+ fileOutputStream.write(bytes, numBytes);
820
+ fileOutputStream.close();
821
+ document_element.removeChild(canvas);
822
+ }
823
+ catch(e)
824
+ { document_element.removeChild(canvas);
825
+ }
826
+ )
827
+ end
828
+ else
829
+ screen_capture_win_window(filename, options)
830
+ end
831
+ end
832
+
604
833
  # Add an error checker that gets called on every page load.
605
834
  # * checker - a Proc object
606
835
  def add_checker(checker)
@@ -640,22 +869,6 @@ module Vapir
640
869
  path
641
870
  end
642
871
 
643
- def current_os
644
- @current_os ||= begin
645
- platform= RUBY_PLATFORM =~ /java/ ? java.lang.System.getProperty("os.name") : RUBY_PLATFORM
646
- case platform
647
- when /mswin|windows|mingw32/i
648
- :windows
649
- when /darwin|mac os/i
650
- :macosx
651
- when /linux/i
652
- :linux
653
- else
654
- raise "Unidentified platform #{platform}"
655
- end
656
- end
657
- end
658
-
659
872
  def path_from_registry
660
873
  raise NotImplementedError, "(need to know how to access windows registry on JRuby)" if RUBY_PLATFORM =~ /java/
661
874
  require 'win32/registry'