vapir-firefox 1.7.2 → 1.8.0

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