vapir-firefox 1.7.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|