vapir-firefox 1.7.0.rc1

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.
@@ -0,0 +1,687 @@
1
+ =begin rdoc
2
+ This is Vapir, Web Application Testing In Ruby using Firefox browser
3
+
4
+ Typical usage:
5
+ # include the controller
6
+ require "vapir-firefox"
7
+
8
+ # go to the page you want to test
9
+ ff = Vapir::Firefox.start("http://myserver/mypage")
10
+
11
+ # enter "Angrez" into an input field named "username"
12
+ ff.text_field(:name, "username").set("Angrez")
13
+
14
+ # enter "Ruby Co" into input field with id "company_ID"
15
+ ff.text_field(:id, "company_ID").set("Ruby Co")
16
+
17
+ # click on a link that has "green" somewhere in the text that is displayed
18
+ # to the user, using a regular expression
19
+ ff.link(:text, /green/)
20
+
21
+ # click button that has a caption of "Cancel"
22
+ ff.button(:value, "Cancel").click
23
+
24
+ Vapir allows your script to read and interact with HTML objects--HTML tags
25
+ and their attributes and contents. Types of objects that Vapir can identify
26
+ include:
27
+
28
+ Type Description
29
+ =========== ===============================================================
30
+ button <input> tags, with the type="button" attribute
31
+ check_box <input> tags, with the type="checkbox" attribute
32
+ div <div> tags
33
+ form
34
+ frame
35
+ hidden hidden <input> tags
36
+ image <img> tags
37
+ label
38
+ link <a> (anchor) tags
39
+ p <p> (paragraph) tags
40
+ radio radio buttons; <input> tags, with the type="radio" attribute
41
+ select_list <select> tags, known informally as drop-down boxes
42
+ span <span> tags
43
+ table <table> tags
44
+ text_field <input> tags with the type="text" attribute (a single-line
45
+ text field), the type="text_area" attribute (a multi-line
46
+ text field), and the type="password" attribute (a
47
+ single-line field in which the input is replaced with asterisks)
48
+
49
+ In general, there are several ways to identify a specific object. Vapir's
50
+ syntax is in the form (how, what), where "how" is a means of identifying
51
+ the object, and "what" is the specific string or regular expression
52
+ that Vapir will seek, as shown in the examples above. Available "how"
53
+ options depend upon the type of object, but here are a few examples:
54
+
55
+ How Description
56
+ ============ ===============================================================
57
+ :id Used to find an object that has an "id=" attribute. Since each
58
+ id should be unique, according to the XHTML specification,
59
+ this is recommended as the most reliable method to find an
60
+ object.
61
+ :name Used to find an object that has a "name=" attribute. This is
62
+ useful for older versions of HTML, but "name" is deprecated
63
+ in XHTML.
64
+ :value Used to find a text field with a given default value, or a
65
+ button with a given caption
66
+ :index Used to find the nth object of the specified type on a page.
67
+ For example, button(:index, 2) finds the second button.
68
+ Current versions of Vapir use 1-based indexing, but future
69
+ versions will use 0-based indexing.
70
+ :xpath The xpath expression for identifying the element.
71
+
72
+ Note that the XHTML specification requires that tags and their attributes be
73
+ in lower case. Vapir doesn't enforce this; Vapir will find tags and
74
+ attributes whether they're in upper, lower, or mixed case. This is either
75
+ a bug or a feature.
76
+
77
+ Vapir uses JSSh for interacting with the browser. For further information on
78
+ Firefox and DOM go to the following Web page:
79
+
80
+ http://www.xulplanet.com/references/objref/
81
+
82
+ =end
83
+
84
+ require 'vapir-common/waiter'
85
+ require 'vapir-common/browser'
86
+ require 'vapir-firefox/window'
87
+ require 'vapir-firefox/modal_dialog'
88
+ require 'vapir-firefox/jssh_socket'
89
+ require 'vapir-firefox/container'
90
+ require 'vapir-firefox/page_container'
91
+
92
+ module Vapir
93
+ include Vapir::Exception
94
+
95
+ class Firefox < Browser
96
+ include Firefox::PageContainer
97
+ include Firefox::Window
98
+ include Firefox::ModalDialogContainer
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
105
+ @@jssh_socket=JsshSocket.new
106
+ @@firewatir_jssh_objects=@@jssh_socket.object("Vapir").assign({})
107
+ @@jssh_socket
108
+ end
109
+ def self.jssh_socket(options={})
110
+ if options[:reset] || !(class_variable_defined?('@@jssh_socket') && @@jssh_socket)
111
+ initialize_jssh_socket
112
+ end
113
+ if options[:reset_if_dead]
114
+ begin
115
+ @@jssh_socket.assert_socket
116
+ rescue JsshConnectionError
117
+ initialize_jssh_socket
118
+ end
119
+ end
120
+ @@jssh_socket
121
+ end
122
+ def jssh_socket(options=nil)
123
+ options ? self.class.jssh_socket(options) : @@jssh_socket
124
+ end
125
+
126
+ # Description:
127
+ # Starts the firefox browser.
128
+ # On windows this starts the first version listed in the registry.
129
+ #
130
+ # Input:
131
+ # 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.
133
+ # This is done because if Firefox is not started and we try to connect
134
+ # to jssh on port 9997 an exception is thrown.
135
+ # :profile - The Firefox profile to use. If none is specified, Firefox will use
136
+ # 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
+ def initialize(options = {})
140
+ 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}})"
143
+ end
144
+ options=handle_options(options, {:wait_time => 20}, [:attach, :goto, :binary_path])
145
+ if options[:binary_path]
146
+ @binary_path=options[:binary_path]
147
+ end
148
+
149
+ # check for jssh not running, firefox may be open but not with -jssh
150
+ # if its not open at all, regardless of the :suppress_launch_process option start it
151
+ # error if running without jssh, we don't want to kill their current window (mac only)
152
+ begin
153
+ jssh_socket(:reset_if_dead => true).assert_socket
154
+ rescue JsshError
155
+ # here we're going to assume that since it's not connecting, we need to launch firefox.
156
+ if options[:attach]
157
+ raise Vapir::Exception::NoBrowserException, "cannot attach using #{options[:attach].inspect} - could not connect to Firefox with JSSH"
158
+ else
159
+ launch_browser
160
+ # if we just launched a the browser process, attach to the window
161
+ # that opened when we did that.
162
+ # but if options[:attach] is explicitly given as false (not nil),
163
+ # take that to mean we don't want to attach to the window launched
164
+ # when the process starts.
165
+ unless options[:attach]==false
166
+ options[:attach]=[:title, //]
167
+ end
168
+ 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
170
+ begin
171
+ jssh_socket(:reset_if_dead => true).assert_socket
172
+ true
173
+ rescue JsshUnableToStart
174
+ false
175
+ end
176
+ end
177
+ end
178
+ @browser_jssh_objects = jssh_socket.object('{}').store_rand_object_key(@@firewatir_jssh_objects) # this is an object that holds stuff for this browser
179
+
180
+ if options[:attach]
181
+ attach(*options[:attach])
182
+ else
183
+ open_window
184
+ end
185
+ set_browser_document
186
+ set_defaults
187
+ if options[:goto]
188
+ goto(options[:goto])
189
+ end
190
+ end
191
+
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
+ def mozilla_window_class_name
201
+ 'MozillaUIWindowClass'
202
+ end
203
+
204
+ def browser
205
+ self
206
+ end
207
+
208
+ def exists?
209
+ # 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)
211
+ end
212
+
213
+ # Launches firebox browser
214
+ # options as .new
215
+
216
+ def launch_browser(options = {})
217
+ if(options[:profile])
218
+ profile_opt = "-no-remote -P #{options[:profile]}"
219
+ else
220
+ profile_opt = ""
221
+ end
222
+
223
+ bin = path_to_bin()
224
+ @t = Thread.new { system("#{bin} -jssh #{profile_opt}") }
225
+ end
226
+ private :launch_browser
227
+
228
+ # Creates a new instance of Firefox. Loads the URL and return the instance.
229
+ # Input:
230
+ # url - url of the page to be loaded.
231
+ def self.start(url)
232
+ new(:goto => url)
233
+ end
234
+
235
+
236
+ # Loads the given url in the browser. Waits for the page to get loaded.
237
+ def goto(url)
238
+ assert_exists
239
+ browser_object.loadURI url
240
+ wait
241
+ end
242
+
243
+ # Loads the previous page (if there is any) in the browser. Waits for the page to get loaded.
244
+ def back
245
+ if browser_object.canGoBack
246
+ browser_object.goBack
247
+ else
248
+ raise Vapir::Exception::NavigationException, "Cannot go back!"
249
+ end
250
+ wait
251
+ end
252
+
253
+ # Loads the next page (if there is any) in the browser. Waits for the page to get loaded.
254
+ def forward
255
+ if browser_object.canGoForward
256
+ browser_object.goForward
257
+ else
258
+ raise Vapir::Exception::NavigationException, "Cannot go forward!"
259
+ end
260
+ wait
261
+ end
262
+
263
+ # Reloads the current page in the browser. Waits for the page to get loaded.
264
+ def refresh
265
+ browser_object.reload
266
+ wait
267
+ end
268
+
269
+ private
270
+ # This function creates a new socket at port 9997 and sets the default values for instance and class variables.
271
+ # Generatesi UnableToStartJSShException if cannot connect to jssh even after 3 tries.
272
+ def set_defaults(no_of_tries = 0)
273
+ @error_checkers = []
274
+ end
275
+
276
+ # Sets the document, window and browser variables to point to correct object in JSSh.
277
+ def set_browser_document
278
+ unless browser_window_object
279
+ raise "Window must be set (using open_window or attach) before the browser document can be set!"
280
+ end
281
+ @browser_object=@browser_jssh_objects[:browser]= ::Waiter.try_for(2, :exception => Vapir::Exception::NoMatchingWindowFoundException.new("The browser could not be found on the specified Firefox window!")) do
282
+ if browser_window_object.respond_to?(:getBrowser)
283
+ browser_window_object.getBrowser
284
+ end
285
+ end
286
+
287
+ # the following are not stored elsewhere; the ref will just be to attributes of the browser, so that updating the
288
+ # browser (in javascript) will cause all of these refs to reflect that as well
289
+ @document_object=browser_object.contentDocument
290
+ @content_window_object=browser_object.contentWindow
291
+ # note that browser_window_object.content is the same thing, but simpler to refer to stuff on browser_object since that is updated by the nsIWebProgressListener below
292
+ @body_object=document_object.body
293
+ @browser_jssh_objects[:requests_in_progress]=[]
294
+ @requests_in_progress=@browser_jssh_objects[:requests_in_progress].to_array
295
+ @browser_jssh_objects[:unmatched_stopped_requests_count]=0
296
+
297
+ @updated_at_epoch_ms=@browser_jssh_objects.attr(:updated_at_epoch_ms).assign_expr('new Date().getTime()')
298
+ @updated_at_offset=Time.now.to_f-jssh_socket.value_json('new Date().getTime()')/1000.0
299
+
300
+ # Add eventlistener for browser window so that we can reset the document back whenever there is redirect
301
+ # or browser loads on its own after some time. Useful when you are searching for flight results etc and
302
+ # page goes to search page after that it goes automatically to results page.
303
+ # Details : http://zenit.senecac.on.ca/wiki/index.php/Mozilla.dev.tech.xul#What_is_an_example_of_addProgressListener.3F
304
+ @browser_jssh_objects[:listener_object]={}
305
+ listener_object=@browser_jssh_objects[:listener_object]
306
+ listener_object[:QueryInterface]=jssh_socket.object(
307
+ "function(aIID)
308
+ { if(aIID.equals(Components.interfaces.nsIWebProgressListener) || aIID.equals(Components.interfaces.nsISupportsWeakReference) || aIID.equals(Components.interfaces.nsISupports))
309
+ { return this;
310
+ }
311
+ throw Components.results.NS_NOINTERFACE;
312
+ }")
313
+ listener_object[:onStateChange]= jssh_socket.object(
314
+ "function(aWebProgress, aRequest, aStateFlags, aStatus)
315
+ { var requests_in_progress=#{@requests_in_progress.ref};
316
+ if(aStateFlags & Components.interfaces.nsIWebProgressListener.STATE_STOP)
317
+ { #{@updated_at_epoch_ms.ref}=new Date().getTime();
318
+ #{browser_object.ref}=#{browser_window_object.ref}.getBrowser();
319
+ var matched=false;
320
+ for(var i=0; i<requests_in_progress.length; i+=1)
321
+ { if(requests_in_progress[i].request==aRequest)
322
+ // TODO/FIX: this doesn't seem to work reliably - possibly related to redirects?
323
+ // workaround is to just check if there are as many unmatched stop requests as requests
324
+ // in progress.
325
+ // but this ought to be fixed to correctly match STATE_STOP requests to previously-
326
+ // encountered STATE_START requests.
327
+ { requests_in_progress.splice(i, 1);
328
+ matched=true;
329
+ break;
330
+ }
331
+ }
332
+ if(!matched)
333
+ { #{@browser_jssh_objects.attr(:unmatched_stopped_requests_count).ref}++; //.push({webProgress: aWebProgress, request: aRequest, stateFlags: aStateFlags, status: aStatus});
334
+ // count any stop requests that we fail to match so that we can compare that count to the number of unmatched start requests.
335
+ }
336
+ }
337
+ if(aStateFlags & Components.interfaces.nsIWebProgressListener.STATE_START)
338
+ { requests_in_progress.push({webProgress: aWebProgress, request: aRequest, stateFlags: aStateFlags, status: aStatus});
339
+ }
340
+ // the below was kind of a hack to get rid of any requests which
341
+ // are done but were not matched to a STATE_STOP request.
342
+ // it doesn't seem to work very well, so commented.
343
+ /*
344
+ for(var i=0; i<requests_in_progress.length; ++i)
345
+ { var request_in_progress=requests_in_progress[i];
346
+ if(request_in_progress.request.loadGroup.activeCount==0)
347
+ { requests_in_progress.splice(i, 1);
348
+ --i;
349
+ }
350
+ }*/
351
+ }")
352
+ browser_object.addProgressListener(listener_object)
353
+ end
354
+
355
+ public
356
+ attr_reader :browser_window_object
357
+ attr_reader :content_window_object
358
+ attr_reader :browser_object
359
+ attr_reader :document_object
360
+ attr_reader :body_object
361
+
362
+ def updated_at
363
+ Time.at(@updated_at_epoch_ms.val/1000.0)+@updated_at_offset
364
+ end
365
+
366
+ public
367
+ # Closes the window.
368
+ def close
369
+ assert_exists
370
+ begin
371
+ browser_window_object.close
372
+ # TODO/fix timeout; this shouldn't be a hard-coded magic number.
373
+ ::Waiter.try_for(32, :exception => Exception::WindowFailedToCloseException.new("The browser window did not close")) do
374
+ !exists?
375
+ end
376
+ @@jssh_socket.assert_socket
377
+ rescue JsshConnectionError # the socket may disconnect when we close the browser, causing the JsshSocket to complain
378
+ @@jssh_socket=nil
379
+ end
380
+ @browser_window_object=@browser_object=@document_object=@content_window_object=@body_object=nil
381
+ @launched_browser_process=false #TODO/FIX: check here if we originally launched the browser process
382
+ if @launched_browser_process && @@jssh_socket
383
+ quit_browser(:force => false)
384
+ end
385
+ end
386
+
387
+ # Closes all firefox windows by quitting the browser
388
+ def close_all
389
+ quit_browser(:force => false)
390
+ end
391
+
392
+ # quits the browser.
393
+ # quit_browser(:force => true) will force the browser to quit.
394
+ def quit_browser(options={})
395
+ options=handle_options(options, :force => false)
396
+ # from https://developer.mozilla.org/en/How_to_Quit_a_XUL_Application
397
+ appStartup= jssh_socket.Components.classes['@mozilla.org/toolkit/app-startup;1'].getService(jssh_socket.Components.interfaces.nsIAppStartup)
398
+ quitSeverity = options[:force] ? jssh_socket.Components.interfaces.nsIAppStartup.eForceQuit : jssh_socket.Components.interfaces.nsIAppStartup.eAttemptQuit
399
+ begin
400
+ appStartup.quit(quitSeverity)
401
+ ::Waiter.try_for(8, :exception => Exception::WindowFailedToCloseException.new("The browser did not quit")) do
402
+ @@jssh_socket.assert_socket # this should error, going up past the waiter to the rescue block above
403
+ false
404
+ end
405
+ rescue JsshConnectionError
406
+ @@jssh_socket=nil
407
+ end
408
+ # TODO/FIX: poll to wait for the process itself to finish? the socket closes (which we wait for
409
+ # above) before the process itself has exited, so if Firefox.new is called between the socket
410
+ # closing and the process exiting, Firefox pops up with:
411
+ # Close Firefox
412
+ # A copy of Firefox is already open. Only one copy of Firefox can be open at a time.
413
+ # [OK]
414
+ # until that's implemented, just wait for an arbitrary amount of time. (ick)
415
+ sleep 2
416
+
417
+ @browser_window_object=@browser_object=@document_object=@content_window_object=@body_object=nil
418
+ nil
419
+ end
420
+
421
+ # Used for attaching pop up window to an existing Firefox window, either by url or title.
422
+ # ff.attach(:url, 'http://www.google.com')
423
+ # ff.attach(:title, 'Google')
424
+ #
425
+ # Output:
426
+ # Instance of newly attached window.
427
+ def attach(how, what)
428
+ @browser_window_object = case how
429
+ when :jssh_object
430
+ what
431
+ else
432
+ find_window(how, what)
433
+ end
434
+
435
+ unless @browser_window_object
436
+ raise Exception::NoMatchingWindowFoundException.new("Unable to locate window, using #{how} and #{what}")
437
+ end
438
+ set_browser_document
439
+ self
440
+ end
441
+
442
+ # Class method to return a browser object if a window matches for how
443
+ # and what. Window can be referenced by url or title.
444
+ # The second argument can be either a string or a regular expression.
445
+ # Vapir::Browser.attach(:url, 'http://www.google.com')
446
+ # Vapir::Browser.attach(:title, 'Google')
447
+ def self.attach how, what
448
+ new(:attach => [how, what])
449
+ end
450
+
451
+ # loads up a new window in an existing process
452
+ # Vapir::Browser.attach() with no arguments passed the attach method will create a new window
453
+ # this will only be called one time per instance we're only ever going to run in 1 window
454
+
455
+ def open_window
456
+ begin
457
+ @browser_window_name="firewatir_window_%.16x"%rand(2**64)
458
+ end while jssh_socket.value_json("$A(getWindows()).detect(function(win){return win.name==#{@browser_window_name.to_jssh}}) ? true : false")
459
+ watcher=jssh_socket.Components.classes["@mozilla.org/embedcomp/window-watcher;1"].getService(jssh_socket.Components.interfaces.nsIWindowWatcher)
460
+ # nsIWindowWatcher is used to launch new top-level windows. see https://developer.mozilla.org/en/Working_with_windows_in_chrome_code
461
+
462
+ @browser_window_object=@browser_jssh_objects[:browser_window]=watcher.openWindow(nil, 'chrome://browser/content/browser.xul', @browser_window_name, 'resizable', nil)
463
+ return @browser_window_object
464
+ end
465
+ private :open_window
466
+
467
+ def self.each
468
+ each_browser_window_object do |win|
469
+ yield self.attach(:jssh_object, win)
470
+ end
471
+ end
472
+
473
+ def self.each_browser_window_object
474
+ mediator=jssh_socket.Components.classes["@mozilla.org/appshell/window-mediator;1"].getService(jssh_socket.Components.interfaces.nsIWindowMediator)
475
+ enumerator=mediator.getEnumerator("navigator:browser")
476
+ while enumerator.hasMoreElements
477
+ win=enumerator.getNext
478
+ yield win
479
+ end
480
+ nil
481
+ end
482
+ def self.browser_window_objects
483
+ window_objects=[]
484
+ each_browser_window_object do |window_object|
485
+ window_objects << window_object
486
+ end
487
+ window_objects
488
+ end
489
+ def self.each_window_object
490
+ mediator=jssh_socket.Components.classes["@mozilla.org/appshell/window-mediator;1"].getService(jssh_socket.Components.interfaces.nsIWindowMediator)
491
+ enumerator=mediator.getEnumerator(nil)
492
+ while enumerator.hasMoreElements
493
+ win=enumerator.getNext
494
+ yield win
495
+ end
496
+ nil
497
+ end
498
+ def self.window_objects
499
+ window_objects=[]
500
+ each_window_object do |window_object|
501
+ window_objects << window_object
502
+ end
503
+ window_objects
504
+ end
505
+
506
+ # return the window jssh object for the browser window with the given title or url.
507
+ # how - :url or :title
508
+ # what - string or regexp
509
+ #
510
+ # Start searching windows in reverse order so that we attach/find the latest opened window.
511
+ def find_window(how, what)
512
+ orig_how=how
513
+ hows={ :title => proc{|content_window| content_window.title },
514
+ :URL => proc{|content_window| content_window.location.href },
515
+ }
516
+ how=hows.keys.detect{|h| h.to_s.downcase==orig_how.to_s.downcase}
517
+ raise ArgumentError, "how should be one of: #{hows.keys.inspect} (was #{orig_how.inspect})" unless how
518
+ found_win=nil
519
+ self.class.each_browser_window_object do |win|
520
+ found_win=win if Vapir::fuzzy_match(hows[how].call(win.getBrowser.contentDocument),what)
521
+ # we don't break here if found_win is set because we want the last match if there are multiple.
522
+ end
523
+ return found_win
524
+ end
525
+ private :find_window
526
+
527
+ # Returns the Status of the page currently loaded in the browser from statusbar.
528
+ #
529
+ # Output:
530
+ # Status of the page.
531
+ #
532
+ def status
533
+ #content_window_object.status
534
+ browser_window_object.XULBrowserWindow.statusText
535
+ end
536
+
537
+ # Returns the text of the page currently loaded in the browser.
538
+ def text
539
+ body_object.textContent
540
+ end
541
+
542
+ # Maximize the current browser window.
543
+ def maximize()
544
+ browser_window_object.maximize
545
+ end
546
+
547
+ # Minimize the current browser window.
548
+ def minimize()
549
+ browser_window_object.minimize
550
+ end
551
+
552
+ # Waits for the page to get loaded.
553
+ def wait(options={})
554
+ return unless exists?
555
+ unless options.is_a?(Hash)
556
+ raise ArgumentError, "given options should be a Hash, not #{options.inspect} (#{options.class})\nold conflicting arguments of no_sleep or last_url are gone"
557
+ end
558
+ options={:sleep => false, :last_url => nil, :timeout => 120}.merge(options)
559
+ started=Time.now
560
+ while browser_object.webProgress.isLoadingDocument
561
+ sleep 0.1
562
+ if Time.now - started > options[:timeout]
563
+ raise "Page Load Timeout"
564
+ end
565
+ end
566
+
567
+ # If the redirect is to a download attachment that does not reload this page, this
568
+ # method will loop forever. Therefore, we need to ensure that if this method is called
569
+ # twice with the same URL, we simply accept that we're done.
570
+ url= document_object.URL
571
+
572
+ if(url != options[:last_url])
573
+ # Check for Javascript redirect. As we are connected to Firefox via JSSh. JSSh
574
+ # doesn't detect any javascript redirects so check it here.
575
+ # If page redirects to itself that this code will enter in infinite loop.
576
+ # So we currently don't wait for such a page.
577
+ # wait variable in JSSh tells if we should wait more for the page to get loaded
578
+ # or continue. -1 means page is not redirected. Anyother positive values means wait.
579
+ metas=document_object.getElementsByTagName 'meta'
580
+ wait_time=metas.to_array.map do |meta|
581
+ return_time=true
582
+ return_time &&= meta.httpEquiv =~ /\Arefresh\z/i
583
+ return_time &&= begin
584
+ content_split=meta.content.split(';')
585
+ 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.
586
+ end
587
+ return_time ? content_split[0].to_i : nil
588
+ end.compact.max
589
+
590
+ if wait_time
591
+ if wait_time > (options[:timeout] - (Time.now - started)) # don't wait longer than what's left in the timeout would for any other timeout.
592
+ 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"
593
+ end
594
+ sleep(wait_time)
595
+ wait(:last_url => url, :timeout => options[:timeout] - (Time.now - started))
596
+ end
597
+ end
598
+ ::Waiter.try_for(options[:timeout] - (Time.now - started), :exception => "Waiting for requests in progress to complete timed out.") do
599
+ @requests_in_progress.length<=@browser_jssh_objects[:unmatched_stopped_requests_count]
600
+ end
601
+ run_error_checks
602
+ return self
603
+ end
604
+
605
+ # Add an error checker that gets called on every page load.
606
+ # * checker - a Proc object
607
+ def add_checker(checker)
608
+ @error_checkers << checker
609
+ end
610
+
611
+ # Disable an error checker
612
+ # * checker - a Proc object that is to be disabled
613
+ def disable_checker(checker)
614
+ @error_checkers.delete(checker)
615
+ end
616
+
617
+ # Run the predefined error checks. This is automatically called on every page load.
618
+ def run_error_checks
619
+ @error_checkers.each { |e| e.call(self) }
620
+ end
621
+
622
+
623
+ def startClicker(*args)
624
+ raise NotImplementedError, "startClicker is gone. Use Firefox#modal_dialog.click_button (generally preceded by a Element#click_no_wait)"
625
+ end
626
+
627
+ private
628
+
629
+ def path_to_bin
630
+ path = @binary_path || begin
631
+ case current_os
632
+ when :windows
633
+ path_from_registry
634
+ when :macosx
635
+ path_from_spotlight
636
+ when :linux
637
+ `which firefox`.strip
638
+ end
639
+ end
640
+ raise "unable to locate Firefox executable" if path.nil? || path.empty?
641
+ path
642
+ end
643
+
644
+ def current_os
645
+ @current_os ||= begin
646
+ platform= RUBY_PLATFORM =~ /java/ ? java.lang.System.getProperty("os.name") : RUBY_PLATFORM
647
+ case platform
648
+ when /mswin|windows|mingw32/i
649
+ :windows
650
+ when /darwin|mac os/i
651
+ :macosx
652
+ when /linux/i
653
+ :linux
654
+ else
655
+ raise "Unidentified platform #{platform}"
656
+ end
657
+ end
658
+ end
659
+
660
+ def path_from_registry
661
+ raise NotImplementedError, "(need to know how to access windows registry on JRuby)" if RUBY_PLATFORM =~ /java/
662
+ require 'win32/registry'
663
+ lm = ::Win32::Registry::HKEY_LOCAL_MACHINE
664
+ lm.open('SOFTWARE\Mozilla\Mozilla Firefox') do |reg|
665
+ reg1 = lm.open("SOFTWARE\\Mozilla\\Mozilla Firefox\\#{reg.keys[0]}\\Main")
666
+ if entry = reg1.find { |key, type, data| key =~ /pathtoexe/i }
667
+ return entry.last
668
+ end
669
+ end
670
+ end
671
+
672
+ def path_from_spotlight
673
+ ff = %x[mdfind 'kMDItemCFBundleIdentifier == "org.mozilla.firefox"']
674
+ ff = ff.empty? ? '/Applications/Firefox.app' : ff.split("\n").first
675
+
676
+ "#{ff}/Contents/MacOS/firefox-bin"
677
+ end
678
+
679
+ private
680
+ def base_element_class
681
+ Firefox::Element
682
+ end
683
+ def browser_class
684
+ Firefox
685
+ end
686
+ end # Firefox
687
+ end # Vapir