module FireWatir include Watir::Exception class Firefox include FireWatir::Container # XPath Result type. Return only first node that matches the xpath expression. # More details: "http://developer.mozilla.org/en/docs/DOM:document.evaluate" FIRST_ORDERED_NODE_TYPE = 9 # 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: # :waitTime - 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. # :suppress_launch_process - do not create a new firefox process. Connect to an existing one. # TODO: Start the firefox version given by user. def initialize(options = {}) if(options.kind_of?(Integer)) options = {:waitTime => options} 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) jssh_down = false begin set_defaults() rescue Watir::Exception::UnableToStartJSShException jssh_down = true end if current_os == :macosx && !%x{ps x | grep firefox-bin | grep -v grep}.empty? raise "Firefox is running without -jssh" if jssh_down open_window unless options[:suppress_launch_process] elsif not options[:suppress_launch_process] launch_browser(options) end set_defaults() get_window_number() set_browser_document() end def inspect '#<%s:0x%x url=%s title=%s>' % [self.class, hash*2, url.inspect, title.inspect] end # Launches firebox browser # options as .new def launch_browser(options = {}) if(options[:profile]) profile_opt = "-no-remote -P #{options[:profile]}" else profile_opt = "" end bin = path_to_bin() @t = Thread.new { system("#{bin} -jssh #{profile_opt}") } sleep options[:waitTime] || 2 end private :launch_browser # Creates a new instance of Firefox. Loads the URL and return the instance. # Input: # url - url of the page to be loaded. def self.start(url) ff = Firefox.new ff.goto(url) return ff end # Gets the window number opened. # Currently, this returns the most recently opened window, which may or may # not be the current window. def get_window_number() # If at any time a non-browser window like the "Downloads" window # pops up, it will become the topmost window, so make sure we # ignore it. window_count = js_eval("getWindows().length").to_i - 1 while js_eval("getWindows()[#{window_count}].getBrowser") == '' window_count -= 1; end # now correctly handles instances where only browserless windows are open # opens one we can use if count is 0 if window_count < 0 open_window window_count = 1 end @window_index = window_count end private :get_window_number # Loads the given url in the browser. Waits for the page to get loaded. def goto(url) get_window_number() set_browser_document() js_eval "#{browser_var}.loadURI(\"#{url}\")" wait() end # Loads the previous page (if there is any) in the browser. Waits for the page to get loaded. def back() js_eval "if(#{browser_var}.canGoBack) #{browser_var}.goBack()" wait() end # Loads the next page (if there is any) in the browser. Waits for the page to get loaded. def forward() js_eval "if(#{browser_var}.canGoForward) #{browser_var}.goForward()" wait() end # Reloads the current page in the browser. Waits for the page to get loaded. def refresh() js_eval "#{browser_var}.reload()" wait() end # Executes the given JavaScript string def execute_script(source) result = js_eval source.to_s wait() result end private # This function creates a new socket at port 9997 and sets the default values for instance and class variables. # Generatesi UnableToStartJSShException if cannot connect to jssh even after 3 tries. def set_defaults(no_of_tries = 0) # JSSH listens on port 9997. Create a new socket to connect to port 9997. begin $jssh_socket = TCPSocket::new(MACHINE_IP, "9997") $jssh_socket.sync = true read_socket() rescue no_of_tries += 1 retry if no_of_tries < 3 raise UnableToStartJSShException, "Unable to connect to machine : #{MACHINE_IP} on port 9997. Make sure that JSSh is properly installed and Firefox is running with '-jssh' option" end @error_checkers = [] end # Sets the document, window and browser variables to point to correct object in JSSh. def set_browser_document # Add eventlistener for browser window so that we can reset the document back whenever there is redirect # or browser loads on its own after some time. Useful when you are searching for flight results etc and # page goes to search page after that it goes automatically to results page. # Details : http://zenit.senecac.on.ca/wiki/index.php/Mozilla.dev.tech.xul#What_is_an_example_of_addProgressListener.3F jssh_command = "var listObj = new Object();"; # create new object jssh_command << "listObj.wpl = Components.interfaces.nsIWebProgressListener;"; # set the web progress listener. jssh_command << "listObj.QueryInterface = function(aIID) { if (aIID.equals(listObj.wpl) || aIID.equals(Components.interfaces.nsISupportsWeakReference) || aIID.equals(Components.interfaces.nsISupports)) return this; throw Components.results.NS_NOINTERFACE; };" # set function to locate the object via QueryInterface jssh_command << "listObj.onStateChange = function(aProgress, aRequest, aFlag, aStatus) { if (aFlag & listObj.wpl.STATE_STOP) { if ( aFlag & listObj.wpl.STATE_IS_NETWORK ) { #{document_var} = #{browser_var}.contentDocument; #{body_var} = #{document_var}.body; } } };" # add function to be called when window state is change. When state is STATE_STOP & # STATE_IS_NETWORK then only everything is loaded. Now we can reset our variables. jssh_command.gsub!(/\n/, "") js_eval jssh_command jssh_command = "var #{window_var} = getWindows()[#{@window_index}];" jssh_command << "var #{browser_var} = #{window_var}.getBrowser();" # Add listener create above to browser object jssh_command << "#{browser_var}.addProgressListener( listObj,Components.interfaces.nsIWebProgress.NOTIFY_STATE_WINDOW );" jssh_command << "var #{document_var} = #{browser_var}.contentDocument;" jssh_command << "var #{body_var} = #{document_var}.body;" js_eval jssh_command @window_title = js_eval "#{document_var}.title" @window_url = js_eval "#{document_var}.URL" end public def window_var "window" end #private def browser_var "browser" end def document_var # unfinished "document" end def body_var # unfinished "body" end public # Closes the window. def close if js_eval("getWindows().length").to_i == 1 js_eval("getWindows()[0].close()") if current_os == :macosx %x{ osascript -e 'tell application "Firefox" to quit' } end # wait for the app to close properly @t.join if @t else # Check if window exists, because there may be the case that it has been closed by click event on some element. # For e.g: Close Button, Close this Window link etc. window_number = find_window(:url, @window_url) # If matching window found. Close the window. if window_number > 0 js_eval "getWindows()[#{window_number}].close()" end end end # Closes all firefox windows def close_all total_windows = js_eval("getWindows().length").to_i # start from last window while(total_windows > 0) do js_eval "getWindows()[#{total_windows - 1}].close()" total_windows = total_windows - 1 end if current_os == :macosx %x{ osascript -e 'tell application "Firefox" to quit' } end if current_os == :windows system("taskkill /im firefox.exe /f /t >nul 2>&1") end end # Used for attaching pop up window to an existing Firefox window, either by url or title. # ff.attach(:url, 'http://www.google.com') # ff.attach(:title, 'Google') # # Output: # Instance of newly attached window. def attach(how, what) $stderr.puts("warning: #{self.class}.attach is experimental") if $VERBOSE window_number = find_window(how, what) if(window_number.nil?) raise NoMatchingWindowFoundException.new("Unable to locate window, using #{how} and #{what}") elsif(window_number >= 0) @window_index = window_number set_browser_document() end self end # Class method to return a browser object if a window matches for how # and what. Window can be referenced by url or title. # The second argument can be either a string or a regular expression. # Watir::Browser.attach(:url, 'http://www.google.com') # Watir::Browser.attach(:title, 'Google') def self.attach how, what br = new :suppress_launch_process => true # don't create window br.attach(how, what) br end # loads up a new window in an existing process # Watir::Browser.attach() with no arguments passed the attach method will create a new window # this will only be called one time per instance we're only ever going to run in 1 window def open_window if @opened_new_window return @opened_new_window end jssh_command = "var windows = getWindows(); var window = windows[0]; window.open(); var windows = getWindows(); var window_number = windows.length - 1; window_number;" window_number = js_eval(jssh_command).to_i @opened_new_window = window_number return window_number if window_number >= 0 end private :open_window # return the window index for the browser window with the given title or url. # how - :url or :title # what - string or regexp # Start searching windows in reverse order so that we attach/find the latest opened window. def find_window(how, what) jssh_command = "var windows = getWindows(); var window_number = false; var found = false; for(var i = windows.length - 1; i >= 0; i--) { var attribute = ''; if(typeof(windows[i].getBrowser) != 'function') { continue; } var browser = windows[i].getBrowser(); if(!browser) { continue; } if(\"#{how}\" == \"url\") { attribute = browser.contentDocument.URL; } if(\"#{how}\" == \"title\") { attribute = browser.contentDocument.title; }" if(what.class == Regexp) jssh_command << "var regExp = new RegExp(#{what.inspect}); found = regExp.test(attribute);" else jssh_command << "found = (attribute == \"#{what}\");" end jssh_command << "if(found) { window_number = i; break; } } window_number;" window_number = js_eval(jssh_command).to_s return window_number == 'false' ? nil : window_number.to_i end private :find_window # # Description: # Matches the given text with the current text shown in the browser. # # Input: # target - Text to match. Can be a string or regex # # Output: # Returns the index if the specified text was found. # Returns matchdata object if the specified regexp was found. # def contains_text(target) #puts "Text to match is : #{match_text}" #puts "Html is : #{self.text}" case target when Regexp self.text.match(target) when String self.text.index(target) else raise TypeError, "Argument #{target} should be a string or regexp." end end # Returns the url of the page currently loaded in the browser. def url @window_url = js_eval "#{document_var}.URL" end # Returns the title of the page currently loaded in the browser. def title @window_title = js_eval "#{document_var}.title" end # Returns the Status of the page currently loaded in the browser from statusbar. # # Output: # Status of the page. # def status js_status = js_eval("#{window_var}.status") js_status.empty? ? js_eval("#{window_var}.XULBrowserWindow.statusText;") : js_status end # Returns the html of the page currently loaded in the browser. def html result = js_eval("var htmlelem = #{document_var}.getElementsByTagName('html')[0]; htmlelem.innerHTML") return "" + result + "" end # Returns the text of the page currently loaded in the browser. def text js_eval("#{body_var}.textContent").strip end # Maximize the current browser window. def maximize() js_eval "#{window_var}.maximize()" end # Minimize the current browser window. def minimize() js_eval "#{window_var}.minimize()" end # Waits for the page to get loaded. def wait(last_url = nil) #puts "In wait function " isLoadingDocument = "" start = Time.now while isLoadingDocument != "false" isLoadingDocument = js_eval("#{browser_var}=#{window_var}.getBrowser(); #{browser_var}.webProgress.isLoadingDocument;") #puts "Is browser still loading page: #{isLoadingDocument}" # Raise an exception if the page fails to load if (Time.now - start) > 300 raise "Page Load Timeout" end 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 = js_eval("#{browser_var}.contentDocument.URL") if(url != last_url) # Check for Javascript redirect. As we are connected to Firefox via JSSh. JSSh # doesn't detect any javascript redirects so check it here. # If page redirects to itself that this code will enter in infinite loop. # So we currently don't wait for such a page. # wait variable in JSSh tells if we should wait more for the page to get loaded # or continue. -1 means page is not redirected. Anyother positive values means wait. jssh_command = "var wait = -1; var meta = null; meta = #{browser_var}.contentDocument.getElementsByTagName('meta'); if(meta != null) { var doc_url = #{browser_var}.contentDocument.URL; for(var i=0; i< meta.length;++i) { var content = meta[i].content; var regex = new RegExp(\"^refresh$\", \"i\"); if(regex.test(meta[i].httpEquiv)) { var arrContent = content.split(';'); var redirect_url = null; if(arrContent.length > 0) { if(arrContent.length > 1) redirect_url = arrContent[1]; if(redirect_url != null) { regex = new RegExp(\"^.*\" + redirect_url + \"$\"); if(!regex.test(doc_url)) { wait = arrContent[0]; } } break; } } } } wait;" wait_time = js_eval(jssh_command).to_i begin if(wait_time != -1) sleep(wait_time) # Call wait again. In case there are multiple redirects. js_eval "#{browser_var} = #{window_var}.getBrowser()" wait(url) end rescue end end set_browser_document() run_error_checks() return self end # Add an error checker that gets called on every page load. # * checker - a Proc object def add_checker(checker) @error_checkers << checker end # Disable an error checker # * checker - a Proc object that is to be disabled def disable_checker(checker) @error_checkers.delete(checker) end # Run the predefined error checks. This is automatically called on every page load. def run_error_checks @error_checkers.each { |e| e.call(self) } end #def jspopup_appeared(popupText = "", wait = 2) # winHelper = WindowHelper.new() # return winHelper.hasPopupAppeared(popupText, wait) #end # # Description: # Redefines the alert and confirm methods on the basis of button to be clicked. # This is done so that JSSh doesn't get blocked. You should use click_no_wait method before calling this function. # # Typical Usage: # ff.button(:id, "button").click_no_wait # ff.click_jspopup_button("OK") # # Input: # button - JavaScript button to be clicked. Values can be OK or Cancel # #def click_jspopup_button(button) # button = button.downcase # element = Element.new(nil) # element.click_js_popup(button) #end # # Description: # Tells FireWatir to click javascript button in case one comes after performing some action on an element. Matches # text of pop up with one if supplied as parameter. If text matches clicks the button else stop script execution until # pop up is dismissed by manual intervention. # # Input: # button - JavaScript button to be clicked. Values can be OK or Cancel # waitTime - Time to wait for pop up to come. Not used just for compatibility with Watir. # userInput - Not used just for compatibility with Watir # text - Text that should appear on pop up. # def startClicker(button, waitTime = 1, userInput = nil, text = nil) jssh_command = "var win = #{browser_var}.contentWindow;" if(button =~ /ok/i) jssh_command << "var popuptext = ''; var old_alert = win.alert; var old_confirm = win.confirm; win.alert = function(param) {" if(text != nil) jssh_command << "if(param == \"#{text}\") { popuptext = param; return true; } else { popuptext = param; win.alert = old_alert; win.alert(param); }" else jssh_command << "popuptext = param; return true;" end jssh_command << "}; win.confirm = function(param) {" if(text != nil) jssh_command << "if(param == \"#{text}\") { popuptext = param; return true; } else { win.confirm = old_confirm; win.confirm(param); }" else jssh_command << "popuptext = param; return true;" end jssh_command << "};" elsif(button =~ /cancel/i) jssh_command = "var old_confirm = win.confirm; win.confirm = function(param) {" if(text != nil) jssh_command << "if(param == \"#{text}\") { popuptext = param; return false; } else { win.confirm = old_confirm; win.confirm(param); }" else jssh_command << "popuptext = param; return false;" end jssh_command << "};" end js_eval jssh_command end # # Description: # Returns text of javascript pop up in case it comes. # # Output: # Text shown in javascript pop up. # def get_popup_text() return_value = js_eval "popuptext" # reset the variable js_eval "popuptext = ''" return return_value end # Returns the document element of the page currently loaded in the browser. def document Document.new(self) end # Returns the first element that matches the given xpath expression or query. def element_by_xpath(xpath) temp = Element.new(nil, self) element_name = temp.element_by_xpath(self, xpath) return element_factory(element_name) end # Return object of correct Element class while using XPath to get the element. def element_factory(element_name) jssh_type = Element.new(element_name,self).element_type #puts "jssh type is : #{jssh_type}" # DEBUG candidate_class = jssh_type =~ /HTML(.*)Element/ ? $1 : '' #puts candidate_class # DEBUG if candidate_class == 'Input' input_type = js_eval("#{element_name}.type").downcase.strip firewatir_class = input_class(input_type) else firewatir_class = jssh2firewatir(candidate_class) end #puts firewatir_class # DEBUG klass = FireWatir.const_get(firewatir_class) if klass == Element klass.new(element_name,self) elsif klass == CheckBox klass.new(self,:jssh_name,element_name,["checkbox"]) elsif klass == Radio klass.new(self,:jssh_name,element_name,["radio"]) else klass.new(self,:jssh_name,element_name) end end private :element_factory # Return the class name for element of input type depending upon its type like checkbox, radio etc. def input_class(input_type) hash = { 'select-one' => 'SelectList', 'select-multiple' => 'SelectList', 'text' => 'TextField', 'password' => 'TextField', 'textarea' => 'TextField', # TODO when there's no type, it's a TextField 'file' => 'FileField', 'checkbox' => 'CheckBox', 'radio' => 'Radio', 'reset' => 'Button', 'button' => 'Button', 'submit' => 'Button', 'image' => 'Button' } hash.default = 'Element' hash[input_type] end private :input_class # For a provided element type returned by JSSh like HTMLDivElement, # returns its corresponding class in Firewatir. def jssh2firewatir(candidate_class) hash = { 'Div' => 'Div', 'Button' => 'Button', 'Frame' => 'Frame', 'Span' => 'Span', 'Paragraph' => 'P', 'Label' => 'Label', 'Form' => 'Form', 'Image' => 'Image', 'Table' => 'Table', 'TableCell' => 'TableCell', 'TableRow' => 'TableRow', 'Select' => 'SelectList', 'Link' => 'Link', 'Anchor' => 'Link' # FIXME is this right? #'Option' => 'Option' #Option uses a different constructor } hash.default = 'Element' hash[candidate_class] end private :jssh2firewatir # # Description: # Returns the array of elements that matches the xpath query. # # Input: # Xpath expression or query. # # Output: # Array of elements matching xpath query. # def elements_by_xpath(xpath) element = Element.new(nil, self) elem_names = element.elements_by_xpath(self, xpath) elem_names.inject([]) {|elements,name| elements << element_factory(name)} end # # Description: # Show all the forms available on the page. # # Output: # Name, id, method and action of all the forms available on the page. # def show_forms forms = Document.new(self).get_forms() count = forms.length puts "There are #{count} forms" for i in 0..count - 1 do puts "Form name: " + forms[i].name puts " id: " + forms[i].id puts " method: " + forms[i].attribute_value("method") puts " action: " + forms[i].action end end alias showForms show_forms # # Description: # Show all the images available on the page. # # Output: # Name, id, src and index of all the images available on the page. # def show_images images = Document.new(self).get_images puts "There are #{images.length} images" index = 1 images.each do |l| puts "image: name: #{l.name}" puts " id: #{l.id}" puts " src: #{l.src}" puts " index: #{index}" index += 1 end end alias showImages show_images # # Description: # Show all the links available on the page. # # Output: # Name, id, href and index of all the links available on the page. # def show_links links = Document.new(self).get_links puts "There are #{links.length} links" index = 1 links.each do |l| puts "link: name: #{l.name}" puts " id: #{l.id}" puts " href: #{l.href}" puts " index: #{index}" index += 1 end end alias showLinks show_links # # Description: # Show all the divs available on the page. # # Output: # Name, id, class and index of all the divs available on the page. # def show_divs divs = Document.new(self).get_divs puts "There are #{divs.length} divs" index = 1 divs.each do |l| puts "div: name: #{l.name}" puts " id: #{l.id}" puts " class: #{l.className}" puts " index: #{index}" index += 1 end end alias showDivs show_divs # # Description: # Show all the tables available on the page. # # Output: # Id, row count, column count (only first row) and index of all the tables available on the page. # def show_tables tables = Document.new(self).get_tables puts "There are #{tables.length} tables" index = 1 tables.each do |l| puts "table: id: #{l.id}" puts " rows: #{l.row_count}" puts " columns: #{l.column_count}" puts " index: #{index}" index += 1 end end alias showTables show_tables # # Description: # Show all the pre elements available on the page. # # Output: # Id, name and index of all the pre elements available on the page. # def show_pres pres = Document.new(self).get_pres puts "There are #{pres.length} pres" index = 1 pres.each do |l| puts "pre: id: #{l.id}" puts " name: #{l.name}" puts " index: #{index}" index += 1 end end alias showPres show_pres # # Description: # Show all the spans available on the page. # # Output: # Name, id, class and index of all the spans available on the page. # def show_spans spans = Document.new(self).get_spans puts "There are #{spans.length} spans" index = 1 spans.each do |l| puts "span: name: #{l.name}" puts " id: #{l.id}" puts " class: #{l.className}" puts " index: #{index}" index += 1 end end alias showSpans show_spans # # Description: # Show all the labels available on the page. # # Output: # Name, id, for and index of all the labels available on the page. # def show_labels labels = Document.new(self).get_labels puts "There are #{labels.length} labels" index = 1 labels.each do |l| puts "label: name: #{l.name}" puts " id: #{l.id}" puts " for: #{l.for}" puts " index: #{index}" index += 1 end end alias showLabels show_labels # # Description: # Show all the frames available on the page. Doesn't show nested frames. # # Output: # Name, and index of all the frames available on the page. # def show_frames jssh_command = "var frameset = #{window_var}.frames; var elements_frames = new Array(); for(var i = 0; i < frameset.length; i++) { var frames = frameset[i].frames; for(var j = 0; j < frames.length; j++) { elements_frames.push(frames[j].frameElement); } } elements_frames.length;" length = js_eval(jssh_command).to_i puts "There are #{length} frames" frames = Array.new(length) for i in 0..length - 1 do frames[i] = Frame.new(self, :jssh_name, "elements_frames[#{i}]") end for i in 0..length - 1 do puts "frame: name: #{frames[i].name}" puts " index: #{i+1}" end end alias showFrames show_frames private def path_to_bin path = case current_os() when :windows path_from_registry when :macosx path_from_spotlight when :linux `which firefox`.strip end raise "unable to locate Firefox executable" if path.nil? || path.empty? path end def current_os return @current_os if defined?(@current_os) platform = RUBY_PLATFORM =~ /java/ ? Java::java.lang.System.getProperty("os.name") : RUBY_PLATFORM @current_os = case platform when /mingw32|mswin|windows/i :windows when /darwin|mac os/i :macosx when /linux/i :linux end end def path_from_registry require 'win32/registry.rb' lm = Win32::Registry::HKEY_LOCAL_MACHINE lm.open('SOFTWARE\Mozilla\Mozilla Firefox') do |reg| reg1 = lm.open("SOFTWARE\\Mozilla\\Mozilla Firefox\\#{reg.keys[0]}\\Main") if entry = reg1.find { |key, type, data| key =~ /pathtoexe/i } return entry.last end end rescue LoadError if RUBY_PLATFORM =~ /java/ return(ENV['FIREFOX_HOME'] or raise( NotImplementedError, 'No Registry support in this JRuby; upgrade or set FIREFOX_HOME')) else raise end end def path_from_spotlight ff = %x[mdfind 'kMDItemCFBundleIdentifier == "org.mozilla.firefox"'] ff = ff.empty? ? '/Applications/Firefox.app' : ff.split("\n").first "#{ff}/Contents/MacOS/firefox-bin" end end # Firefox end # FireWatir