Class: Vapir::Firefox

Inherits:
Browser
  • Object
show all
Extended by:
Configurable, ClearTracksMethods, FirefoxClassAndInstanceMethods
Includes:
ClearTracksMethods, FirefoxClassAndInstanceMethods, ModalDialogContainer, PageContainer, Window
Defined in:
lib/vapir-firefox/config.rb,
lib/vapir-firefox/browser.rb,
lib/vapir-firefox/version.rb,
lib/vapir-firefox/clear_tracks.rb

Defined Under Namespace

Modules: ClearTracksMethods, Container, FirefoxClassAndInstanceMethods, ModalDialogContainer, PageContainer, RadioCheckboxCommon, Window Classes: Area, Button, CheckBox, Dd, Div, Dl, Dt, Element, Em, FileField, Form, Frame, H1, H2, H3, H4, H5, H6, Hidden, Image, InputElement, Label, Li, Link, Map, ModalDialog, ModalDialogDocument, Ol, Option, P, Pre, Radio, SelectList, Span, Strong, TBody, Table, TableCell, TableRow, TextField, Ul

Constant Summary collapse

VERSION =
'1.8.0'

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from FirefoxClassAndInstanceMethods

current_os, pid, process_running?, quit_browser, wait_for_process_exit

Methods included from ClearTracksMethods

clear_all_tracks, clear_cache, clear_cookies, clear_history, sanitizer

Methods included from ModalDialogContainer

#modal_dialog, #modal_dialog!

Methods included from Window

#bring_to_front, #hwnd, #win_window

Methods included from PageContainer

#execute_script, #outer_html

Methods included from Container

#element_by_xpath, #element_object_by_xpath, #element_objects_by_xpath, #elements_by_xpath, #extra_for_contained, #visible_text_nodes

Constructor Details

#initialize(options = {}) ⇒ Firefox

Description:

Starts the firefox browser. 
On windows this starts the first version listed in the registry.

Input:

options  - Hash of any of the following options:
  :timeout  - Time to wait for Firefox to start. By default it waits for 2 seconds.
              This is done because if Firefox is not started and we try to connect
              to jssh on port 9997 an exception is thrown.
  :profile  - The Firefox profile to use. If none is specified, Firefox will use
              the last used profile.


143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
# File 'lib/vapir-firefox/browser.rb', line 143

def initialize(options = {})
  if(options.kind_of?(Integer))
    options = {:timeout => options}
    if config.warn_deprecated
      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]})"
    end
  end
  options = options_from_config(options, {:timeout => :attach_timeout, :binary_path => :firefox_binary_path, :profile => :firefox_profile, :wait => :wait}, [:attach, :goto, :wait_time])
  if options[:wait_time]
    if config.warn_deprecated
      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]})"
    end
    options[:timeout] = options[:wait_time]
  end
  if options[:binary_path]
    @binary_path=options[:binary_path]
  end
  
  # check for jssh not running, firefox may be open but not with -jssh
  # if its not open at all, regardless of the :suppress_launch_process option start it
  # error if running without jssh, we don't want to kill their current window (mac only)
  begin
    jssh_socket(:reset_if_dead => true).assert_socket
  rescue JsshError
    # here we're going to assume that since it's not connecting, we need to launch firefox. 
    if options[:attach]
      raise Vapir::Exception::NoBrowserException, "cannot attach using #{options[:attach].inspect} - could not connect to Firefox with JSSH"
    else
      launch_browser(options)
      # if we just launched a the browser process, attach to the window
      # that opened when we did that. 
      # but if options[:attach] is explicitly given as false (not nil), 
      # take that to mean we don't want to attach to the window launched 
      # when the process starts. 
      unless options[:attach]==false
        options[:attach]=[:title, //]
      end
    end
    ::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
      begin
        jssh_socket(:reset_if_dead => true).assert_socket
        true
      rescue JsshUnableToStart
        false
      end
    end
  end
  @browser_jssh_objects = jssh_socket.object('{}').store_rand_object_key(@@firewatir_jssh_objects) # this is an object that holds stuff for this browser 
  
  @pid = begin
    self.pid
  rescue NotImplementedError
    nil
  end
  
  if options[:attach]
    attach(*options[:attach])
  else
    open_window
  end
  set_browser_document
  set_defaults
  if options[:goto]
    goto(options[:goto])
  end
  wait if options[:wait]
end

Instance Attribute Details

#body_objectObject (readonly)

Returns the value of attribute body_object.



396
397
398
# File 'lib/vapir-firefox/browser.rb', line 396

def body_object
  @body_object
end

#browser_objectObject (readonly)

Returns the value of attribute browser_object.



394
395
396
# File 'lib/vapir-firefox/browser.rb', line 394

def browser_object
  @browser_object
end

#browser_window_objectObject (readonly)

Returns the value of attribute browser_window_object.



392
393
394
# File 'lib/vapir-firefox/browser.rb', line 392

def browser_window_object
  @browser_window_object
end

#content_window_objectObject (readonly)

Returns the value of attribute content_window_object.



393
394
395
# File 'lib/vapir-firefox/browser.rb', line 393

def content_window_object
  @content_window_object
end

#document_objectObject (readonly)

Returns the value of attribute document_object.



395
396
397
# File 'lib/vapir-firefox/browser.rb', line 395

def document_object
  @document_object
end

Class Method Details

.browser_window_objectsObject



636
637
638
# File 'lib/vapir-firefox/browser.rb', line 636

def browser_window_objects
  Enumerator.new(self, :each_browser_window_object)
end

.browsersObject



624
625
626
# File 'lib/vapir-firefox/browser.rb', line 624

def browsers
  Enumerator.new(self, :each_browser)
end

.each_browserObject Also known as: each



618
619
620
621
622
# File 'lib/vapir-firefox/browser.rb', line 618

def each_browser
  each_browser_window_object do |win|
    yield self.attach(:browser_window_object, win)
  end
end

.each_browser_window_objectObject



627
628
629
630
631
632
633
634
635
# File 'lib/vapir-firefox/browser.rb', line 627

def each_browser_window_object
  mediator=jssh_socket.Components.classes["@mozilla.org/appshell/window-mediator;1"].getService(jssh_socket.Components.interfaces.nsIWindowMediator)
  enumerator=mediator.getEnumerator("navigator:browser")
  while enumerator.hasMoreElements
    win=enumerator.getNext
    yield win
  end
  nil
end

.each_window_objectObject



639
640
641
642
643
644
645
646
647
# File 'lib/vapir-firefox/browser.rb', line 639

def each_window_object
  mediator=jssh_socket.Components.classes["@mozilla.org/appshell/window-mediator;1"].getService(jssh_socket.Components.interfaces.nsIWindowMediator)
  enumerator=mediator.getEnumerator(nil)
  while enumerator.hasMoreElements
    win=enumerator.getNext
    yield win
  end
  nil
end

.initialize_jssh_socketObject

initializes a JsshSocket and stores in a class variable.



101
102
103
104
105
106
# File 'lib/vapir-firefox/browser.rb', line 101

def self.initialize_jssh_socket # :nodoc:
  uninitialize_jssh_socket
  @@jssh_socket=JsshSocket.new
  @@firewatir_jssh_objects=@@jssh_socket.object("Vapir").assign({})
  @@jssh_socket
end

.jssh_socket(options = {}) ⇒ Object

returns a connected JsshSocket. pass :reset_if_dead => true if you suspect an existing socket may be dead, and you want a new one. a warning will be printed if this occurs.



109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/vapir-firefox/browser.rb', line 109

def self.jssh_socket(options={}) # :nodoc:
  if options[:reset] || !(class_variable_defined?('@@jssh_socket') && @@jssh_socket)
    initialize_jssh_socket
  end
  if options[:reset_if_dead]
    begin
      @@jssh_socket.assert_socket
    rescue JsshConnectionError
      Kernel.warn "WARNING: JsshSocket RESET: resetting jssh socket. Any active javascript references will not exist on the new socket!"
      initialize_jssh_socket
    end
  end
  @@jssh_socket
end

.uninitialize_jssh_socketObject

unsets a the current jssh socket



124
125
126
127
# File 'lib/vapir-firefox/browser.rb', line 124

def self.uninitialize_jssh_socket # :nodoc:
  @@jssh_socket=nil
  @@firewatir_jssh_objects=nil
end

.window_objectsObject



648
649
650
# File 'lib/vapir-firefox/browser.rb', line 648

def window_objects
  Enumerator.new(self, :each_window_object)
end

Instance Method Details

#add_checker(checker) ⇒ Object

Add an error checker that gets called on every page load.

  • checker - a Proc object



835
836
837
# File 'lib/vapir-firefox/browser.rb', line 835

def add_checker(checker)
  @error_checkers << checker
end

#backObject

Loads the previous page (if there is any) in the browser. Waits for the page to get loaded.



282
283
284
285
286
287
288
289
# File 'lib/vapir-firefox/browser.rb', line 282

def back
  if browser_object.canGoBack
    browser_object.goBack
  else
    raise Vapir::Exception::NavigationException, "Cannot go back!"
  end
  wait
end

#browserObject



215
216
217
# File 'lib/vapir-firefox/browser.rb', line 215

def browser
  self
end

#closeObject

Closes the browser window.

This will also quit the browser (see #quit_browser) only if this instance of Vapir::Firefox launched the browser when it was created, AND there are no other windows remaining open. On Windows, closing the last browser window quits the browser anyway; on other operating systems it does not.



408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
# File 'lib/vapir-firefox/browser.rb', line 408

def close
  assert_exists
  # we expect the browser may exit if there are no windows which aren't ourself. except on mac. 
  expect_exit = !self.class.window_objects.any?{|other_window| other_window != self.browser_window_object } && current_os != :macosx
  begin
    browser_window_object.close
    # TODO/fix timeout; this shouldn't be a hard-coded magic number. 
    ::Waiter.try_for(32, :exception => Exception::WindowFailedToCloseException.new("The browser window did not close")) do
      !exists?
    end
    if expect_exit
      ::Waiter.try_for(8, :exception => nil) do
        jssh_socket.assert_socket # this should error, going up past the waiter to the rescue block above 
        false
      end
    else
      jssh_socket.assert_socket
    end
  rescue JsshConnectionError # the socket may disconnect when we close the browser, causing the JsshSocket to complain 
    Vapir::Firefox.uninitialize_jssh_socket
    wait_for_process_exit(@pid)
  end
    
  @browser_window_object=@browser_object=@document_object=@content_window_object=@body_object=nil
  if @self_launched_browser && jssh_socket && !self.class.window_objects.any?{ true }
    quit_browser(:force => false)
  end
end

#close_allObject

Closes all firefox windows by quitting the browser



438
439
440
# File 'lib/vapir-firefox/browser.rb', line 438

def close_all
  quit_browser(:force => false)
end

#disable_checker(checker) ⇒ Object

Disable an error checker

  • checker - a Proc object that is to be disabled



841
842
843
# File 'lib/vapir-firefox/browser.rb', line 841

def disable_checker(checker)
  @error_checkers.delete(checker)
end

#exists?Boolean

Returns:

  • (Boolean)


219
220
221
222
# File 'lib/vapir-firefox/browser.rb', line 219

def exists?
  # jssh_socket may be nil if the window has closed 
  jssh_socket && browser_window_object && self.class.browser_window_objects.include?(browser_window_object)
end

#forwardObject

Loads the next page (if there is any) in the browser. Waits for the page to get loaded.



292
293
294
295
296
297
298
299
# File 'lib/vapir-firefox/browser.rb', line 292

def forward
  if browser_object.canGoForward
    browser_object.goForward
  else
    raise Vapir::Exception::NavigationException, "Cannot go forward!"
  end
  wait
end

#goto(url) ⇒ Object

Loads the given url in the browser. Waits for the page to get loaded.



240
241
242
243
244
# File 'lib/vapir-firefox/browser.rb', line 240

def goto(url)
  assert_exists
  browser_object.loadURI url
  wait
end

#jssh_socket(options = nil) ⇒ Object



128
129
130
# File 'lib/vapir-firefox/browser.rb', line 128

def jssh_socket(options=nil)
  options ? self.class.jssh_socket(options) : @@jssh_socket
end

#maximizeObject

Maximize the current browser window.



701
702
703
# File 'lib/vapir-firefox/browser.rb', line 701

def maximize()
  browser_window_object.maximize
end

#minimizeObject

Minimize the current browser window.



706
707
708
# File 'lib/vapir-firefox/browser.rb', line 706

def minimize()
  browser_window_object.minimize
end

#mozilla_window_class_nameObject



211
212
213
# File 'lib/vapir-firefox/browser.rb', line 211

def mozilla_window_class_name
  'MozillaUIWindowClass'
end

#post_to(url, post_data_hash = {}) ⇒ Object

Performs a HTTP POST action to an arbitrary URL with the given data. The data are represented to this method as a Hash, which is converted to the standard form of &-separated key=value strings POST data use.

The data hash should be keyed with strings or symbols (which are converted to strings before being sent along), and its values should all be strings.

If no post_data_hash is given, the body of the POST is empty.

Raises:

  • (ArgumentError)


254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
# File 'lib/vapir-firefox/browser.rb', line 254

def post_to(url, post_data_hash={})
  require 'cgi'
  raise ArgumentError, "post_data_hash must be a Hash!" unless post_data_hash.is_a?(Hash)
  dataString = post_data_hash.map do |(key, val)|
    unless key.is_a?(String) || key.is_a?(Symbol)
      raise ArgumentError, "post_data_hash keys must be strings or symbols; got key #{key.inspect} in hash #{post_data_hash.inspect}"
    end
    unless val.is_a?(String)
      raise ArgumentError, "post_data_hash values must all be string;s got value #{val.inspect} in hash #{post_data_hash.inspect}"
    end
    CGI.escape(key.to_s)+'='+CGI.escape(val)
  end.join("&")
  stringStream = jssh_socket.Components.classes["@mozilla.org/io/string-input-stream;1"].createInstance(jssh_socket.Components.interfaces.nsIStringInputStream)
  if stringStream.to_hash.key?('data')
    stringStream.data=dataString
  else
    stringStream.setData(dataString, dataString.unpack("U*").length)
  end
  postData = jssh_socket.Components.classes["@mozilla.org/network/mime-input-stream;1"].createInstance(jssh_socket.Components.interfaces.nsIMIMEInputStream)
  postData.addHeader("Content-Type", "application/x-www-form-urlencoded")
  postData.addContentLength = true
  postData.setData(stringStream)

  browser_object.loadURIWithFlags(url, 0, nil, nil, postData)
  wait
end

#refreshObject

Reloads the current page in the browser. Waits for the page to get loaded.



302
303
304
305
# File 'lib/vapir-firefox/browser.rb', line 302

def refresh
  browser_object.reload
  wait
end

#response_status_codeObject

the HTTP response status code for the currently loaded document



691
692
693
694
695
696
697
698
# File 'lib/vapir-firefox/browser.rb', line 691

def response_status_code
  channel = nil
  ::Waiter.try_for(8, :exception => nil) do
    channel=browser.browser_object.docShell.currentDocumentChannel
    channel.is_a?(JsshObject) && channel.instanceof(browser.jssh_socket.Components.interfaces.nsIHttpChannel) && channel.respond_to?(:responseStatus)
  end || raise(RuntimeError, "expected currentDocumentChannel to exist and be a nsIHttpChannel but it wasn't; was #{channel.is_a?(JsshObject) ? channel.toString : channel.inspect}")
  status = channel.responseStatus
end

#run_error_checksObject

Run the predefined error checks. This is automatically called on every page load.



846
847
848
# File 'lib/vapir-firefox/browser.rb', line 846

def run_error_checks
  @error_checkers.each { |e| e.call(self) }
end

#screen_capture(filename, options = {}) ⇒ Object

saves a screenshot of this browser window to the given filename.

the last argument is an optional options hash, taking options:

  • :dc => context to capture (stands for device context). default is :page. may be one of:

    • :page takes a screenshot of the full page, and none of the browser chrome. this is supported cross-platform.

    • :client takes a screenshot of the client area, which excludes the menu bar and other window trimmings. only supported on windows.

    • :window takes a screenshot of the full browser window. only supported on windows.

    • :desktop takes a screenshot of the full desktop. only supported on windows.

  • :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 default and the only supported format.



767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
# File 'lib/vapir-firefox/browser.rb', line 767

def screen_capture(filename, options = {})
  options = handle_options(options, :format => nil, :dc => :page)
  
  if options[:dc] == :page
    options[:format] ||= 'png'
    jssh_socket.call_function(:window => content_window_object, :options => options, :filename => File.expand_path(filename)) do
    %q(
      // this is adapted from Selenium's method Selenium.prototype.doCaptureEntirePageScreenshot
      var document = window.document;
      var document_element = document.documentElement;
      var width = document_element.scrollWidth;
      var height = document_element.scrollHeight;
      var styleWidth = width.toString() + 'px';
      var styleHeight = height.toString() + 'px';

      var canvas = document.createElementNS('http://www.w3.org/1999/xhtml', 'html:canvas'), grabCanvas=canvas;
      grabCanvas.style.display = 'none';
      grabCanvas.width = width;
      grabCanvas.style.width = styleWidth;
      grabCanvas.style.maxWidth = styleWidth;
      grabCanvas.height = height;
      grabCanvas.style.height = styleHeight;
      grabCanvas.style.maxHeight = styleHeight;
      
      document_element.appendChild(canvas);
      try
      {
        var context = canvas.getContext('2d');
        context.clearRect(0, 0, width, height);
        context.save();
        
        var prefs=Components.classes['@mozilla.org/preferences-service;1'].getService(Components.interfaces.nsIPrefBranch);
        var background_color = prefs.getCharPref('browser.display.background_color');
        
        context.drawWindow(window, 0, 0, width, height, background_color);
        context.restore();
        var dataUrl = canvas.toDataURL("image/" + options['format']);
        
        var nsIoService = Components.classes["@mozilla.org/network/io-service;1"].getService(Components.interfaces.nsIIOService);
        var channel = nsIoService.newChannelFromURI(nsIoService.newURI(dataUrl, null, null));
        var binaryInputStream = Components.classes["@mozilla.org/binaryinputstream;1"].createInstance(Components.interfaces.nsIBinaryInputStream);
        binaryInputStream.setInputStream(channel.open());
        var numBytes = binaryInputStream.available();
        var bytes = binaryInputStream.readBytes(numBytes);

        var nsFile = Components.classes["@mozilla.org/file/local;1"].createInstance(Components.interfaces.nsILocalFile);
        nsFile.initWithPath(filename);
        var writeFlag = 0x02; // write only
        var createFlag = 0x08; // create
        var truncateFlag = 0x20; // truncate
        var fileOutputStream = Components.classes["@mozilla.org/network/file-output-stream;1"].createInstance(Components.interfaces.nsIFileOutputStream);
        fileOutputStream.init(nsFile, writeFlag | createFlag | truncateFlag, 0664, null);
        fileOutputStream.write(bytes, numBytes);
        fileOutputStream.close();
        document_element.removeChild(canvas);
      }
      catch(e)
      { document_element.removeChild(canvas);
      }
    )
    end
  else
    screen_capture_win_window(filename, options)
  end
end

#startClicker(*args) ⇒ Object

Raises:

  • (NotImplementedError)


851
852
853
# File 'lib/vapir-firefox/browser.rb', line 851

def startClicker(*args)
  raise NotImplementedError, "startClicker is gone. Use Firefox#modal_dialog.click_button (generally preceded by a Element#click_no_wait)"
end

#statusObject

Returns the Status of the page currently loaded in the browser from statusbar.

Output:

Status of the page.


680
681
682
683
# File 'lib/vapir-firefox/browser.rb', line 680

def status
  #content_window_object.status
  browser_window_object.XULBrowserWindow.statusText
end

#textObject

Returns the text of the page currently loaded in the browser.



686
687
688
# File 'lib/vapir-firefox/browser.rb', line 686

def text
  body_object.textContent
end

#updated_atObject



398
399
400
# File 'lib/vapir-firefox/browser.rb', line 398

def updated_at
  Time.at(@updated_at_epoch_ms.val/1000.0)+@updated_at_offset
end

#wait(options = {}) ⇒ Object

Waits for the page to get loaded.



711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
# File 'lib/vapir-firefox/browser.rb', line 711

def wait(options={})
  return unless exists?
  unless options.is_a?(Hash)
    raise ArgumentError, "given options should be a Hash, not #{options.inspect} (#{options.class})\nold conflicting arguments of no_sleep or last_url are gone"
  end
  options={:sleep => false, :last_url => nil, :timeout => 120}.merge(options)
  started=Time.now
  ::Waiter.try_for(options[:timeout] - (Time.now - started), :exception => "Waiting for the document to finish loading timed out") do
    browser_object.webProgress.isLoadingDocument==false
  end

  # If the redirect is to a download attachment that does not reload this page, this
  # method will loop forever. Therefore, we need to ensure that if this method is called
  # twice with the same URL, we simply accept that we're done.
  url= document_object.URL

  if(url != options[:last_url])
    # check for meta redirects, except for redirects back to the same page (infinite
    # loop redirects). 
    metas=document_object.getElementsByTagName 'meta'
    wait_time=metas.to_array.map do |meta|
      return_time=true
      return_time &&= meta.httpEquiv =~ /\Arefresh\z/i 
      return_time &&= begin
        content_split=meta.content.split(';')
        content_split[1] && content_split[1] !~ /\A\s*url=#{Regexp.escape(url)}\s*\z/ # if there is no url, or if the url is the current url, it's just a reload, not a redirect; don't wait. 
      end
      return_time ? content_split[0].to_i : nil
    end.compact.max
    
    if wait_time
      if wait_time > (options[:timeout] - (Time.now - started)) # don't wait longer than what's left in the timeout would for any other timeout. 
        raise "waiting for a meta refresh would take #{wait_time} seconds but remaining time before timeout is #{options[:timeout] - (Time.now - started)} seconds - giving up"
      end
      sleep(wait_time)
      wait(:last_url => url, :timeout => options[:timeout] - (Time.now - started))
    end
  end
  ::Waiter.try_for(options[:timeout] - (Time.now - started), :exception => "Waiting for requests in progress to complete timed out.") do
    @requests_in_progress.length<=@browser_jssh_objects[:unmatched_stopped_requests_count]
  end
  run_error_checks
  return self
end