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.
- data/History.txt +74 -0
- data/LICENSE.txt +42 -0
- data/README.txt +0 -0
- data/lib/vapir/ff.rb +1 -0
- data/lib/vapir/firefox.rb +1 -0
- data/lib/vapir-firefox/container.rb +462 -0
- data/lib/vapir-firefox/element.rb +277 -0
- data/lib/vapir-firefox/elements/button.rb +21 -0
- data/lib/vapir-firefox/elements/file_field.rb +27 -0
- data/lib/vapir-firefox/elements/form.rb +8 -0
- data/lib/vapir-firefox/elements/frame.rb +23 -0
- data/lib/vapir-firefox/elements/hidden.rb +12 -0
- data/lib/vapir-firefox/elements/image.rb +38 -0
- data/lib/vapir-firefox/elements/input_element.rb +12 -0
- data/lib/vapir-firefox/elements/link.rb +30 -0
- data/lib/vapir-firefox/elements/non_control_elements.rb +89 -0
- data/lib/vapir-firefox/elements/option.rb +12 -0
- data/lib/vapir-firefox/elements/radio_check_common.rb +36 -0
- data/lib/vapir-firefox/elements/select_list.rb +12 -0
- data/lib/vapir-firefox/elements/table.rb +16 -0
- data/lib/vapir-firefox/elements/table_cell.rb +12 -0
- data/lib/vapir-firefox/elements/table_row.rb +12 -0
- data/lib/vapir-firefox/elements/text_field.rb +64 -0
- data/lib/vapir-firefox/elements.rb +17 -0
- data/lib/vapir-firefox/firefox.rb +687 -0
- data/lib/vapir-firefox/jssh_socket.rb +1066 -0
- data/lib/vapir-firefox/modal_dialog.rb +144 -0
- data/lib/vapir-firefox/page_container.rb +91 -0
- data/lib/vapir-firefox/prototype.functional.js +1219 -0
- data/lib/vapir-firefox/version.rb +5 -0
- data/lib/vapir-firefox/window.rb +38 -0
- data/lib/vapir-firefox.rb +15 -0
- metadata +120 -0
@@ -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
|