vapir-common 1.7.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +5 -0
- data/README.txt +11 -0
- data/lib/vapir/common.rb +1 -0
- data/lib/vapir-common/browser.rb +184 -0
- data/lib/vapir-common/browsers.rb +9 -0
- data/lib/vapir-common/container.rb +163 -0
- data/lib/vapir-common/element.rb +1078 -0
- data/lib/vapir-common/element_collection.rb +83 -0
- data/lib/vapir-common/elements/elements.rb +858 -0
- data/lib/vapir-common/elements.rb +2 -0
- data/lib/vapir-common/exceptions.rb +56 -0
- data/lib/vapir-common/handle_options.rb +15 -0
- data/lib/vapir-common/modal_dialog.rb +27 -0
- data/lib/vapir-common/options.rb +49 -0
- data/lib/vapir-common/specifier.rb +322 -0
- data/lib/vapir-common/testcase.rb +89 -0
- data/lib/vapir-common/waiter.rb +141 -0
- data/lib/vapir-common/win_window.rb +1227 -0
- data/lib/vapir-common.rb +7 -0
- data/lib/vapir.rb +1 -0
- data/lib/watir-vapir.rb +15 -0
- metadata +89 -0
@@ -0,0 +1,1078 @@
|
|
1
|
+
require 'vapir-common/element_collection'
|
2
|
+
require 'set'
|
3
|
+
require 'matrix'
|
4
|
+
|
5
|
+
class Module
|
6
|
+
def alias_deprecated(to, from)
|
7
|
+
define_method to do |*args|
|
8
|
+
Kernel.warn "DEPRECATION WARNING: #{self.class.name}\##{to} is deprecated. Please use #{self.class.name}\##{from}\n(called from #{caller.map{|c|"\n"+c}})"
|
9
|
+
send(from, *args)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
module Vapir
|
14
|
+
# this module is for methods that should go on both common element modules (ie, TextField) as well
|
15
|
+
# as browser-specific element classes (ie, Firefox::TextField).
|
16
|
+
module ElementClassAndModuleMethods
|
17
|
+
# takes an element_object (JsshObject or WIN32OLE), and finds the most specific class
|
18
|
+
# that is < self whose specifiers match it. Returns an instance of that class using the given
|
19
|
+
# element_object.
|
20
|
+
#
|
21
|
+
# second argument, extra, is passed as the 'extra' argument to the Element constructor (see its documentation).
|
22
|
+
#
|
23
|
+
# if you give a different how/what (third and fourth arguments, optional), then those are passed
|
24
|
+
# to the Element constructor.
|
25
|
+
def factory(element_object, extra={}, how=nil, what=nil)
|
26
|
+
curr_klass=self
|
27
|
+
# since this gets included in the Element modules, too, check where we are
|
28
|
+
unless self.is_a?(Class) && self < Vapir::Element
|
29
|
+
raise TypeError, "factory was called on #{self} (#{self.class}), which is not a Class that is < Element"
|
30
|
+
end
|
31
|
+
if how
|
32
|
+
# use how and what as given
|
33
|
+
elsif what
|
34
|
+
raise ArgumentError, "'what' was given as #{what.inspect} (#{what.class}) but how was not given"
|
35
|
+
else
|
36
|
+
how=:element_object
|
37
|
+
what=element_object
|
38
|
+
end
|
39
|
+
ObjectSpace.each_object(Class) do |klass|
|
40
|
+
if klass < curr_klass
|
41
|
+
Vapir::ElementObjectCandidates.match_candidates([element_object], klass.specifiers, klass.all_dom_attr_aliases) do |match|
|
42
|
+
curr_klass=klass
|
43
|
+
break
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
curr_klass.new(how, what, extra)
|
48
|
+
end
|
49
|
+
|
50
|
+
# takes any number of arguments, where each argument is either:
|
51
|
+
# - a symbol or strings representing a method that is the same in ruby and on the dom
|
52
|
+
# - or a hash of key/value pairs where each key is a dom attribute, and each value
|
53
|
+
# is a is a corresponding ruby method name or list of ruby method names.
|
54
|
+
def dom_attr(*dom_attrs)
|
55
|
+
dom_attrs.each do |arg|
|
56
|
+
hash=arg.is_a?(Hash) ? arg : arg.is_a?(Symbol) || arg.is_a?(String) ? {arg => arg} : raise(ArgumentError, "don't know what to do with arg #{arg.inspect} (#{arg.class})")
|
57
|
+
hash.each_pair do |dom_attr, ruby_method_names|
|
58
|
+
ruby_method_names= ruby_method_names.is_a?(Array) ? ruby_method_names : [ruby_method_names]
|
59
|
+
class_array_append 'dom_attrs', dom_attr
|
60
|
+
ruby_method_names.each do |ruby_method_name|
|
61
|
+
dom_attr_locate_alias(dom_attr, ruby_method_name)
|
62
|
+
define_method ruby_method_name do
|
63
|
+
method_from_element_object(dom_attr)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# creates aliases for locating by
|
71
|
+
def dom_attr_locate_alias(dom_attr, alias_name)
|
72
|
+
dom_attr_aliases=class_hash_get('dom_attr_aliases')
|
73
|
+
dom_attr_aliases[dom_attr] ||= Set.new
|
74
|
+
dom_attr_aliases[dom_attr] << alias_name
|
75
|
+
end
|
76
|
+
|
77
|
+
# dom_function is about the same as dom_attr, but dom_attr doesn't take arguments.
|
78
|
+
# also, dom_function methods call #wait; dom_attr ones don't.
|
79
|
+
def dom_function(*dom_functions)
|
80
|
+
dom_functions.each do |arg|
|
81
|
+
hash=arg.is_a?(Hash) ? arg : arg.is_a?(Symbol) || arg.is_a?(String) ? {arg => arg} : raise(ArgumentError, "don't know what to do with arg #{arg.inspect} (#{arg.class})")
|
82
|
+
hash.each_pair do |dom_function, ruby_method_names|
|
83
|
+
ruby_method_names= ruby_method_names.is_a?(Array) ? ruby_method_names : [ruby_method_names]
|
84
|
+
class_array_append 'dom_functions', dom_function
|
85
|
+
ruby_method_names.each do |ruby_method_name|
|
86
|
+
define_method ruby_method_name do |*args|
|
87
|
+
result=method_from_element_object(dom_function, *args)
|
88
|
+
wait
|
89
|
+
result
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# dom_setter takes arguments in the same format as dom_attr, but sends the given setter method (plus = sign)
|
97
|
+
# to the element object. eg,
|
98
|
+
# module TextField
|
99
|
+
# dom_setter :value
|
100
|
+
# dom_setter :maxLength => :maxlength
|
101
|
+
# end
|
102
|
+
# the #value= method in ruby will send to #value= on the element object
|
103
|
+
# the #maxlength= method in ruby will send to #maxLength= on the element object (note case difference).
|
104
|
+
def dom_setter(*dom_setters)
|
105
|
+
dom_setters.each do |arg|
|
106
|
+
hash=arg.is_a?(Hash) ? arg : arg.is_a?(Symbol) || arg.is_a?(String) ? {arg => arg} : raise(ArgumentError, "don't know what to do with arg #{arg.inspect} (#{arg.class})")
|
107
|
+
hash.each_pair do |dom_setter, ruby_method_names|
|
108
|
+
ruby_method_names= ruby_method_names.is_a?(Array) ? ruby_method_names : [ruby_method_names]
|
109
|
+
class_array_append 'dom_setters', dom_setter
|
110
|
+
ruby_method_names.each do |ruby_method_name|
|
111
|
+
define_method(ruby_method_name.to_s+'=') do |value|
|
112
|
+
element_object.send(dom_setter.to_s+'=', value)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# defines an element collection method on the given element - such as SelectList#options
|
120
|
+
# or Table#rows. takes the name of the dom method that returns a collection
|
121
|
+
# of element objects, a ruby method name, and an element class - actually this is
|
122
|
+
# generally an Element module; this method goes ahead and finds the browser-specific
|
123
|
+
# class that will actually be instantiated. the defined method returns an
|
124
|
+
# ElementCollection.
|
125
|
+
def element_collection(dom_attr, ruby_method_name, element_class)
|
126
|
+
define_method ruby_method_name do
|
127
|
+
assert_exists do
|
128
|
+
ElementCollection.new(self, element_class_for(element_class), extra_for_contained.merge(:candidates => dom_attr))
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# notes the given arguments to be inspected by #inspect and #to_s on each inheriting element.
|
134
|
+
# each argument may be a symbol, in which case the corresponding method is called on the element, or
|
135
|
+
# a hash, with the following keys:
|
136
|
+
# - :label - how the the attribute is labeled in the string returned by #inspect or #to_s.
|
137
|
+
# should be a string or symbol (but anything works; #to_s is called on the label).
|
138
|
+
# - :value - can be one of:
|
139
|
+
# - String starting with '@' - assumes this is an instance variable; gets the value of that instance variable
|
140
|
+
# - Symbol - assumes it is a method name, gives this to #send on the element. this is most commonly-used.
|
141
|
+
# - Proc - calls the proc, giving this element as an argument. should return a string. #to_s is called on its return value.
|
142
|
+
# - anything else - just assumes that that is the value that is wanted in the string.
|
143
|
+
# (see Element#attributes_for_stringifying)
|
144
|
+
# - :if - if defined, should be a proc that returns false/nil if this should not be included in the
|
145
|
+
# string, or anything else (that is, any value considered 'true') if it should. this element is passed
|
146
|
+
# as an argument to the proc.
|
147
|
+
def inspect_these(*inspect_these)
|
148
|
+
inspect_these.each do |inspect_this|
|
149
|
+
attribute_to_inspect=case inspect_this
|
150
|
+
when Hash
|
151
|
+
inspect_this
|
152
|
+
when Symbol
|
153
|
+
{:label => inspect_this, :value => inspect_this}
|
154
|
+
else
|
155
|
+
raise ArgumentError, "unrecognized thing to inspect: #{inspect_this} (#{inspect_this.class})"
|
156
|
+
end
|
157
|
+
class_array_append 'attributes_to_inspect', attribute_to_inspect
|
158
|
+
end
|
159
|
+
end
|
160
|
+
alias inspect_this inspect_these
|
161
|
+
# inspect_this_if(inspect_this, &block) is shorthand for
|
162
|
+
# inspect_this({:label => inspect_this, :value => inspect_this, :if => block)
|
163
|
+
# if a block isn't given, the :if proc is the result of sending the inspect_this symbol to the element.
|
164
|
+
# if inspect_this isn't a symbol, and no block is given, raises ArgumentError.
|
165
|
+
def inspect_this_if inspect_this, &block
|
166
|
+
unless inspect_this.is_a?(Symbol) || block
|
167
|
+
raise ArgumentError, "Either give a block, or specify a symbol as the first argument, instead of #{inspect_this.inspect} (#{inspect_this.class})"
|
168
|
+
end
|
169
|
+
to_inspect={:label => inspect_this, :value => inspect_this}
|
170
|
+
to_inspect[:if]= block || proc {|element| element.send(inspect_this) }
|
171
|
+
class_array_append 'attributes_to_inspect', to_inspect
|
172
|
+
end
|
173
|
+
|
174
|
+
def class_array_append(name, *elements)
|
175
|
+
=begin
|
176
|
+
name='@@'+name.to_s
|
177
|
+
unless self.class_variable_defined?(name)
|
178
|
+
class_variable_set(name, [])
|
179
|
+
end
|
180
|
+
class_variable_get(name).push(*elements)
|
181
|
+
=end
|
182
|
+
name=name.to_s.capitalize
|
183
|
+
unless self.const_defined?(name)
|
184
|
+
self.const_set(name, [])
|
185
|
+
end
|
186
|
+
self.const_get(name).push(*elements)
|
187
|
+
end
|
188
|
+
|
189
|
+
def class_array_get(name)
|
190
|
+
# just return the value of appending nothing
|
191
|
+
class_array_append(name)
|
192
|
+
end
|
193
|
+
def class_hash_merge(name, hash)
|
194
|
+
name=name.to_s.capitalize
|
195
|
+
unless self.const_defined?(name)
|
196
|
+
self.const_set(name, {})
|
197
|
+
end
|
198
|
+
self.const_get(name).merge!(hash)
|
199
|
+
end
|
200
|
+
def class_hash_get(name)
|
201
|
+
class_hash_merge(name, {})
|
202
|
+
end
|
203
|
+
def set_or_get_class_var(class_var, *arg)
|
204
|
+
if arg.length==0
|
205
|
+
class_variable_defined?(class_var) ? class_variable_get(class_var) : nil
|
206
|
+
elsif arg.length==1
|
207
|
+
class_variable_set(class_var, arg.first)
|
208
|
+
else
|
209
|
+
raise ArgumentError, "#{arg.length} arguments given; expected one or two. arguments were #{arg.inspect}"
|
210
|
+
end
|
211
|
+
end
|
212
|
+
def default_how(*arg)
|
213
|
+
set_or_get_class_var('@@default_how', *arg)
|
214
|
+
end
|
215
|
+
def add_container_method_extra_args(*args)
|
216
|
+
class_array_append('container_method_extra_args', *args)
|
217
|
+
end
|
218
|
+
def container_method_extra_args
|
219
|
+
class_array_get('container_method_extra_args')
|
220
|
+
end
|
221
|
+
def specifiers
|
222
|
+
class_array_get 'specifiers'
|
223
|
+
end
|
224
|
+
def container_single_methods
|
225
|
+
class_array_get 'container_single_methods'
|
226
|
+
end
|
227
|
+
def container_collection_methods
|
228
|
+
class_array_get 'container_collection_methods'
|
229
|
+
end
|
230
|
+
|
231
|
+
def parent_element_module(*arg)
|
232
|
+
defined_parent=set_or_get_class_var('@@parent_element_module', *arg)
|
233
|
+
defined_parent || (self==Watir::Element ? nil : Watir::Element)
|
234
|
+
end
|
235
|
+
def all_dom_attrs
|
236
|
+
super_attrs= parent_element_module ? parent_element_module.all_dom_attrs : []
|
237
|
+
super_attrs + class_array_get('dom_attrs')
|
238
|
+
end
|
239
|
+
def all_dom_attr_aliases
|
240
|
+
aliases=class_hash_get('dom_attr_aliases').dup
|
241
|
+
super_aliases= parent_element_module ? parent_element_module.all_dom_attr_aliases : {}
|
242
|
+
super_aliases.each_pair do |attr, alias_list|
|
243
|
+
aliases[attr] = (aliases[attr] || Set.new) + alias_list
|
244
|
+
end
|
245
|
+
aliases
|
246
|
+
end
|
247
|
+
end
|
248
|
+
module ElementHelper
|
249
|
+
def add_specifier(specifier)
|
250
|
+
class_array_append 'specifiers', specifier
|
251
|
+
end
|
252
|
+
|
253
|
+
def container_single_method(*method_names)
|
254
|
+
class_array_append 'container_single_methods', *method_names
|
255
|
+
element_module=self
|
256
|
+
method_names.each do |method_name|
|
257
|
+
Vapir::Element.module_eval do
|
258
|
+
# these methods (Element#parent_table, Element#parent_div, etc)
|
259
|
+
# iterate through parent nodes looking for a parent of the specified
|
260
|
+
# type. if no element of that type is found which is a parent of
|
261
|
+
# self, returns nil.
|
262
|
+
define_method("parent_#{method_name}") do
|
263
|
+
element_class=element_class_for(element_module)
|
264
|
+
parentNode=element_object
|
265
|
+
while true
|
266
|
+
parentNode=parentNode.parentNode
|
267
|
+
unless parentNode && parentNode != document_object # don't ascend up to the document. #TODO/Fix - for IE, comparing WIN32OLEs doesn't really work, this comparison is pointless.
|
268
|
+
return nil
|
269
|
+
end
|
270
|
+
matched=Vapir::ElementObjectCandidates.match_candidates([parentNode], element_class.specifiers, element_class.all_dom_attr_aliases)
|
271
|
+
if matched.size > 0
|
272
|
+
return element_class.new(:element_object, parentNode, extra_for_contained) # this is a little weird, passing extra_for_contained so that this is the container of its parent.
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
279
|
+
def container_collection_method(*method_names)
|
280
|
+
class_array_append 'container_collection_methods', *method_names
|
281
|
+
end
|
282
|
+
|
283
|
+
include ElementClassAndModuleMethods
|
284
|
+
|
285
|
+
def included(including_class)
|
286
|
+
including_class.send :extend, ElementClassAndModuleMethods
|
287
|
+
|
288
|
+
# get Container modules that the including_class includes (ie, Vapir::Firefox::TextField includes the Vapir::Firefox::Container Container module)
|
289
|
+
container_modules=including_class.included_modules.select do |mod|
|
290
|
+
mod.included_modules.include?(Vapir::Container)
|
291
|
+
end
|
292
|
+
|
293
|
+
container_modules.each do |container_module|
|
294
|
+
class_array_get('container_single_methods').each do |container_single_method|
|
295
|
+
# define both bang-methods (like #text_field!) and not (#text_field) with corresponding :locate option for element_by_howwhat
|
296
|
+
[ {:method_name => container_single_method, :locate => true},
|
297
|
+
{:method_name => container_single_method.to_s+'!', :locate => :assert},
|
298
|
+
{:method_name => container_single_method.to_s+'?', :locate => :nil_unless_exists},
|
299
|
+
].each do |method_hash|
|
300
|
+
unless container_module.method_defined?(method_hash[:method_name])
|
301
|
+
container_module.module_eval do
|
302
|
+
define_method(method_hash[:method_name]) do |how, *what_args| # can't take how, what as args because blocks don't do default values so it will want 2 args
|
303
|
+
#locate! # make sure self is located before trying contained stuff
|
304
|
+
what=what_args.shift # what is the first what_arg
|
305
|
+
other_attribute_keys=including_class.container_method_extra_args
|
306
|
+
if what_args.size>other_attribute_keys.length
|
307
|
+
raise ArgumentError, "\##{method_hash[:method_name]} takes 1 to #{2+other_attribute_keys.length} arguments! Got #{([how, what]+what_args).map{|a|a.inspect}.join(', ')}}"
|
308
|
+
end
|
309
|
+
if what_args.size == 0
|
310
|
+
other_attributes= nil
|
311
|
+
else
|
312
|
+
other_attributes={}
|
313
|
+
what_args.each_with_index do |arg, i|
|
314
|
+
other_attributes[other_attribute_keys[i]]=arg
|
315
|
+
end
|
316
|
+
end
|
317
|
+
element_by_howwhat(including_class, how, what, :locate => method_hash[:locate], :other_attributes => other_attributes)
|
318
|
+
end
|
319
|
+
end
|
320
|
+
end
|
321
|
+
end
|
322
|
+
end
|
323
|
+
class_array_get('container_collection_methods').each do |container_multiple_method|
|
324
|
+
container_module.module_eval do
|
325
|
+
# returns an ElementCollection of Elements that are instances of the including class
|
326
|
+
define_method(container_multiple_method) do
|
327
|
+
ElementCollection.new(self, including_class, extra_for_contained)
|
328
|
+
end
|
329
|
+
end
|
330
|
+
container_module.module_eval do
|
331
|
+
define_method('child_'+container_multiple_method.to_s) do
|
332
|
+
ElementCollection.new(self, including_class, extra_for_contained.merge(:candidates => :childNodes))
|
333
|
+
end
|
334
|
+
define_method('show_'+container_multiple_method.to_s) do |*io|
|
335
|
+
io=io.first||$stdout # io is a *array so that you don't have to give an arg (since procs don't do default args)
|
336
|
+
element_collection=ElementCollection.new(self, including_class, extra_for_contained)
|
337
|
+
io.write("There are #{element_collection.length} #{container_multiple_method}\n")
|
338
|
+
element_collection.each do |element|
|
339
|
+
io.write(element.to_s)
|
340
|
+
end
|
341
|
+
end
|
342
|
+
alias_deprecated "show#{container_multiple_method.to_s.capitalize}", "show_"+container_multiple_method.to_s
|
343
|
+
end
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
# copy constants (like Specifiers) onto classes when inherited
|
348
|
+
# this is here to set the constants of the Element modules below onto the actual classes that instantiate
|
349
|
+
# per-browser (Vapir::IE::TextField, Vapir::Firefox::TextField, etc) so that calling #const_defined? on those
|
350
|
+
# returns true, and so that the constants defined here clobber any inherited stuff from superclasses
|
351
|
+
# which is unwanted.
|
352
|
+
self.constants.each do |const| # copy all of its constants onto wherever it was included
|
353
|
+
to_copy=self.const_get(const)
|
354
|
+
to_copy=to_copy.dup if [Hash, Array, Set].any?{|klass| to_copy.is_a?(klass) }
|
355
|
+
including_class.const_set(const, to_copy)
|
356
|
+
end
|
357
|
+
|
358
|
+
# now the constants (above) have switched away from constants to class variables, pretty much, so copy those.
|
359
|
+
self.class_variables.each do |class_var|
|
360
|
+
to_copy=class_variable_get(class_var)
|
361
|
+
to_copy=to_copy.dup if [Hash, Array, Set].any?{|klass| to_copy.is_a?(klass) }
|
362
|
+
including_class.send(:class_variable_set, class_var, to_copy)
|
363
|
+
end
|
364
|
+
|
365
|
+
class << including_class
|
366
|
+
def attributes_to_inspect
|
367
|
+
super_attrs=superclass.respond_to?(:attributes_to_inspect) ? superclass.attributes_to_inspect : []
|
368
|
+
super_attrs + class_array_get('attributes_to_inspect')
|
369
|
+
end
|
370
|
+
def all_dom_attrs
|
371
|
+
super_attrs=superclass.respond_to?(:all_dom_attrs) ? superclass.all_dom_attrs : []
|
372
|
+
super_attrs + class_array_get('dom_attrs')
|
373
|
+
end
|
374
|
+
def all_dom_attr_aliases
|
375
|
+
aliases=class_hash_get('dom_attr_aliases').dup
|
376
|
+
super_aliases=superclass.respond_to?(:all_dom_attr_aliases) ? superclass.all_dom_attr_aliases : {}
|
377
|
+
super_aliases.each_pair do |attr, alias_list|
|
378
|
+
aliases[attr] = (aliases[attr] || Set.new) + alias_list
|
379
|
+
end
|
380
|
+
aliases
|
381
|
+
end
|
382
|
+
end
|
383
|
+
end
|
384
|
+
|
385
|
+
end
|
386
|
+
|
387
|
+
# this is included by every Element. it relies on the including class implementing a
|
388
|
+
# #element_object method
|
389
|
+
# some stuff assumes the element has a defined @container.
|
390
|
+
module Element
|
391
|
+
extend ElementHelper
|
392
|
+
add_specifier({}) # one specifier with no criteria - note that having no specifiers
|
393
|
+
# would match no elements; having a specifier with no criteria matches any
|
394
|
+
# element.
|
395
|
+
container_single_method :element
|
396
|
+
container_collection_method :elements
|
397
|
+
|
398
|
+
private
|
399
|
+
# invokes the given method on the element_object, passing it the given args.
|
400
|
+
# if the element_object doesn't respond to the method name:
|
401
|
+
# - if you don't give it any arguments, returns element_object.getAttributeNode(dom_method_name).value
|
402
|
+
# - if you give it any arguments, raises ArgumentError, as you can't pass more arguments to getAttributeNode.
|
403
|
+
#
|
404
|
+
# it may support setter methods (that is, method_from_element_object('value=', 'foo')), but this has
|
405
|
+
# caused issues in the past - WIN32OLE complaining about doing stuff with a terminated object, and then
|
406
|
+
# when garbage collection gets called, ruby terminating abnormally when garbage-collecting an
|
407
|
+
# unrecognized type. so, not so much recommended.
|
408
|
+
def method_from_element_object(dom_method_name, *args)
|
409
|
+
assert_exists do
|
410
|
+
if Object.const_defined?('WIN32OLE') && element_object.is_a?(WIN32OLE)
|
411
|
+
# avoid respond_to? on WIN32OLE because it's slow. just call the method and rescue if it fails.
|
412
|
+
# the else block works fine for win32ole, but it's slower, so optimizing for IE here.
|
413
|
+
# todo: move this into the ie flavor, doesn't need to be in common
|
414
|
+
got_attribute=false
|
415
|
+
attribute=nil
|
416
|
+
begin
|
417
|
+
attribute=element_object.method_missing(dom_method_name)
|
418
|
+
got_attribute=true
|
419
|
+
rescue WIN32OLERuntimeError
|
420
|
+
end
|
421
|
+
if !got_attribute
|
422
|
+
if args.length==0
|
423
|
+
begin
|
424
|
+
if (node=element_object.getAttributeNode(dom_method_name.to_s))
|
425
|
+
attribute=node.value
|
426
|
+
got_attribute=true
|
427
|
+
end
|
428
|
+
rescue WIN32OLERuntimeError
|
429
|
+
end
|
430
|
+
else
|
431
|
+
raise ArgumentError, "Arguments were given to #{ruby_method_name} but there is no function #{dom_method_name} to pass them to!"
|
432
|
+
end
|
433
|
+
end
|
434
|
+
attribute
|
435
|
+
else
|
436
|
+
if element_object.object_respond_to?(dom_method_name)
|
437
|
+
element_object.method_missing(dom_method_name, *args)
|
438
|
+
# note: using method_missing (not invoke) so that attribute= methods can be used.
|
439
|
+
# but that is problematic. see documentation above.
|
440
|
+
elsif args.length==0
|
441
|
+
if element_object.object_respond_to?(:getAttributeNode)
|
442
|
+
if (node=element_object.getAttributeNode(dom_method_name.to_s))
|
443
|
+
node.value
|
444
|
+
else
|
445
|
+
nil
|
446
|
+
end
|
447
|
+
else
|
448
|
+
nil
|
449
|
+
end
|
450
|
+
else
|
451
|
+
raise ArgumentError, "Arguments were given to #{ruby_method_name} but there is no function #{dom_method_name} to pass them to!"
|
452
|
+
end
|
453
|
+
end
|
454
|
+
end
|
455
|
+
end
|
456
|
+
public
|
457
|
+
|
458
|
+
dom_attr :id
|
459
|
+
inspect_this :how
|
460
|
+
inspect_this_if(:what) do |element|
|
461
|
+
![:element_object, :index].include?(element.how) # if how==:element_object, don't show the element object in inspect. if how==:index, what is nil.
|
462
|
+
end
|
463
|
+
inspect_this_if(:index) # uses the default 'if'; shows index if it's not nil
|
464
|
+
inspect_these :tag_name, :id
|
465
|
+
|
466
|
+
dom_attr :name # this isn't really valid on elements but is used so much that we define it here. (it may be repeated on elements where it is actually is valid)
|
467
|
+
|
468
|
+
dom_attr :title, :tagName => [:tagName, :tag_name], :innerHTML => [:innerHTML, :inner_html], :className => [:className, :class_name]
|
469
|
+
dom_attr_locate_alias :className, :class # this isn't defined as a dom_attr because we don't want to clobber ruby's #class method
|
470
|
+
dom_attr :style
|
471
|
+
dom_function :scrollIntoView => [:scrollIntoView, :scroll_into_view]
|
472
|
+
|
473
|
+
# Get attribute value for any attribute of the element.
|
474
|
+
# Returns null if attribute doesn't exist.
|
475
|
+
dom_function :getAttribute => [:get_attribute_value, :attribute_value]
|
476
|
+
|
477
|
+
# #text is defined on browser-specific Element classes
|
478
|
+
alias_deprecated :innerText, :text
|
479
|
+
alias_deprecated :textContent, :text
|
480
|
+
alias_deprecated :fireEvent, :fire_event
|
481
|
+
|
482
|
+
attr_reader :how
|
483
|
+
attr_reader :what
|
484
|
+
attr_reader :index
|
485
|
+
|
486
|
+
def html
|
487
|
+
Kernel.warn "#html is deprecated, please use #outer_html or #inner_html. #html currently returns #outer_html (note that it previously returned inner_html on firefox)\n(called from #{caller.map{|c|"\n"+c}})"
|
488
|
+
outer_html
|
489
|
+
end
|
490
|
+
|
491
|
+
include ElementObjectCandidates
|
492
|
+
|
493
|
+
public
|
494
|
+
|
495
|
+
# the class-specific Elements may implement their own #initialize, but should call to this
|
496
|
+
# after they've done their stuff
|
497
|
+
def default_initialize(how, what, extra={})
|
498
|
+
@how, @what=how, what
|
499
|
+
raise ArgumentError, "how (first argument) should be a Symbol, not: #{how.inspect}" unless how.is_a?(Symbol)
|
500
|
+
@extra=extra
|
501
|
+
@index=begin
|
502
|
+
valid_symbols=[:first, :last]
|
503
|
+
if valid_symbols.include?(@extra[:index]) || @extra[:index].nil? || (@extra[:index].is_a?(Integer) && @extra[:index] > 0)
|
504
|
+
@extra[:index]
|
505
|
+
elsif valid_symbols.map{|sym| sym.to_s}.include?(@extra[:index])
|
506
|
+
@extra[:index].to_sym
|
507
|
+
elsif @extra[:index] =~ /\A\d+\z/
|
508
|
+
Integer(@extra[:index])
|
509
|
+
else
|
510
|
+
raise ArgumentError, "expected extra[:index] to be a positive integer, a string that looks like a positive integer, :first, or :last. received #{@extra[:index]} (#{@extra[:index].class})"
|
511
|
+
end
|
512
|
+
end
|
513
|
+
@container=extra[:container]
|
514
|
+
@browser=extra[:browser]
|
515
|
+
@page_container=extra[:page_container]
|
516
|
+
@element_object=extra[:element_object] # this will in most cases not be set, but may be set in some cases from ElementCollection enumeration
|
517
|
+
extra[:locate]=true unless @extra.key?(:locate) # set default
|
518
|
+
case extra[:locate]
|
519
|
+
when :assert
|
520
|
+
locate!
|
521
|
+
when true
|
522
|
+
locate
|
523
|
+
when false
|
524
|
+
else
|
525
|
+
raise ArgumentError, "Unrecognized value given for extra[:locate]: #{extra[:locate].inspect} (#{extra[:locate].class})"
|
526
|
+
end
|
527
|
+
end
|
528
|
+
|
529
|
+
# alias it in case class-specific ones don't need to override
|
530
|
+
alias initialize default_initialize
|
531
|
+
|
532
|
+
private
|
533
|
+
# returns whether the specified index for this element is equivalent to finding the first element
|
534
|
+
def index_is_first
|
535
|
+
[nil, :first, 1].include?(index)
|
536
|
+
end
|
537
|
+
def assert_no_index
|
538
|
+
unless index_is_first
|
539
|
+
raise NotImplementedError, "Specifying an index is not supported for locating by #{@how}"
|
540
|
+
end
|
541
|
+
end
|
542
|
+
# iterates over the element object candidates yielded by the given method.
|
543
|
+
# returns the match at the given index. if a block is given, the block should
|
544
|
+
# return true when the yielded element object is a match and false when it is not.
|
545
|
+
# if no block is given, then it is assumed that every element object candidate
|
546
|
+
# returned by the candidates_method is a match. candidates_method_args are
|
547
|
+
# passed to the candidates method untouched.
|
548
|
+
def candidate_match_at_index(index, candidates_method, *candidates_method_args)
|
549
|
+
matched_candidate=nil
|
550
|
+
matched_count=0
|
551
|
+
candidates_method.call(*candidates_method_args) do |candidate|
|
552
|
+
candidate_matches=block_given? ? yield(candidate) : true
|
553
|
+
if candidate_matches
|
554
|
+
matched_count+=1
|
555
|
+
if index==matched_count || index_is_first || index==:last
|
556
|
+
matched_candidate=candidate
|
557
|
+
break unless index==:last
|
558
|
+
end
|
559
|
+
end
|
560
|
+
end
|
561
|
+
matched_candidate
|
562
|
+
end
|
563
|
+
public
|
564
|
+
# locates the element object for this element
|
565
|
+
#
|
566
|
+
# takes options hash. currently the only option is
|
567
|
+
# - :relocate => nil, :recursive, true, false
|
568
|
+
# - nil or not set (default): this Element is only relocated if the browser is updated (in firefox) or the WIN32OLE stops existing (IE).
|
569
|
+
# - :recursive: this element and its containers are relocated, recursively up to the containing browser.
|
570
|
+
# - false: no relocating is done even if the browser is updated or the element_object stops existing.
|
571
|
+
# - true: this Element is relocated. the container is relocated only if the browser is updated or the element_object stops existing.
|
572
|
+
def locate(options={})
|
573
|
+
if options[:relocate]==nil && @element_object # don't override if it is set to false; only if it's nil, and don't set :relocate there's no @element_object (that's an initial locate, not a relocate)
|
574
|
+
if @browser && @updated_at && @browser.respond_to?(:updated_at) && @browser.updated_at > @updated_at # TODO: implement this for IE; only exists for Firefox now.
|
575
|
+
options[:relocate]=:recursive
|
576
|
+
elsif !element_object_exists?
|
577
|
+
options[:relocate]=true
|
578
|
+
end
|
579
|
+
end
|
580
|
+
container_locate_options={}
|
581
|
+
if options[:relocate]==:recursive
|
582
|
+
container_locate_options[:relocate]= options[:relocate]
|
583
|
+
end
|
584
|
+
if options[:relocate]
|
585
|
+
@element_object=nil
|
586
|
+
end
|
587
|
+
element_object_existed=!!@element_object
|
588
|
+
@element_object||= begin
|
589
|
+
case @how
|
590
|
+
when :element_object
|
591
|
+
assert_no_index
|
592
|
+
@element_object=@what # this is needed for checking its existence
|
593
|
+
if options[:relocate] && !element_object_exists?
|
594
|
+
raise Vapir::Exception::UnableToRelocateException, "This #{self.class.name} was specified using #{how.inspect} and cannot be relocated."
|
595
|
+
end
|
596
|
+
@what
|
597
|
+
when :xpath
|
598
|
+
assert_container
|
599
|
+
@container.locate!(container_locate_options)
|
600
|
+
unless @container.respond_to?(:element_object_by_xpath)
|
601
|
+
raise Vapir::Exception::MissingWayOfFindingObjectException, "Locating by xpath is not supported on the container #{@container.inspect}"
|
602
|
+
end
|
603
|
+
# todo/fix: implement index for this, using element_objects_by_xpath ?
|
604
|
+
assert_no_index
|
605
|
+
by_xpath=@container.element_object_by_xpath(@what)
|
606
|
+
match_candidates(by_xpath ? [by_xpath] : [], self.class.specifiers, self.class.all_dom_attr_aliases).first
|
607
|
+
when :label
|
608
|
+
assert_no_index
|
609
|
+
unless document_object
|
610
|
+
raise "No document object found for this #{self.inspect} - needed to search by id for label from #{@container.inspect}"
|
611
|
+
end
|
612
|
+
unless what.is_a?(Label)
|
613
|
+
raise "how=:label specified on this #{self.class}, but 'what' is not a Label! what=#{what.inspect} (#{what.class})"
|
614
|
+
end
|
615
|
+
what.locate!(container_locate_options) # 'what' is not the container; our container is the label's container, but the options for locating should be the same.
|
616
|
+
by_label=document_object.getElementById(what.for)
|
617
|
+
match_candidates(by_label ? [by_label] : [], self.class.specifiers, self.class.all_dom_attr_aliases).first
|
618
|
+
when :attributes
|
619
|
+
assert_container
|
620
|
+
@container.locate!(container_locate_options)
|
621
|
+
specified_attributes=@what
|
622
|
+
specifiers=self.class.specifiers.map{|spec| spec.merge(specified_attributes)}
|
623
|
+
|
624
|
+
candidate_match_at_index(@index, method(:matched_candidates), specifiers, self.class.all_dom_attr_aliases)
|
625
|
+
when :index
|
626
|
+
unless @what.nil?
|
627
|
+
raise ArgumentError, "'what' was specified, but when 'how'=:index, no 'what' is used (just extra[:index])"
|
628
|
+
end
|
629
|
+
unless @index
|
630
|
+
raise ArgumentError, "'how' was given as :index but no index was given"
|
631
|
+
end
|
632
|
+
candidate_match_at_index(@index, method(:matched_candidates), self.class.specifiers, self.class.all_dom_attr_aliases)
|
633
|
+
when :custom
|
634
|
+
# this allows a proc to be given as 'what', which is called yielding candidates, each being
|
635
|
+
# an instanted Element of this class. this might seem a bit odd - instantiating a bunch
|
636
|
+
# of elements in order to figure out which element_object to use in locating this one.
|
637
|
+
# the purpose is so that this Element can be relocated if we lose the element_object.
|
638
|
+
# the Elements that are yielded are instantiated by :element object which cannot be
|
639
|
+
# relocated.
|
640
|
+
#
|
641
|
+
# the alternative to this would be for the calling code to loop over the element collection
|
642
|
+
# for this class on the container - that is, instead of:
|
643
|
+
# found_div=frame.divs.detect{|div| weird_criteria_for(div) }
|
644
|
+
# which can't be relocated - since element collections use :element object - you'd do
|
645
|
+
# found_div=frame.div(:custom, proc{|div| weird_criteria_for(div) })
|
646
|
+
# this way, found_div can be relocated. yay!
|
647
|
+
#
|
648
|
+
# the proc should return true (that is, not false or nil) when it likes the given Element -
|
649
|
+
# when it matches what it expects of this Element.
|
650
|
+
candidate_match_at_index(@index, method(:matched_candidates), self.class.specifiers, self.class.all_dom_attr_aliases) do |candidate|
|
651
|
+
what.call(self.class.new(:element_object, candidate, @extra))
|
652
|
+
end
|
653
|
+
else
|
654
|
+
raise Vapir::Exception::MissingWayOfFindingObjectException, "Unknown 'how' given: #{@how.inspect} (#{@how.class}). 'what' was #{@what.inspect} (#{@what.class})"
|
655
|
+
end
|
656
|
+
end
|
657
|
+
if !element_object_existed && @element_object
|
658
|
+
@updated_at=Time.now
|
659
|
+
end
|
660
|
+
@element_object
|
661
|
+
end
|
662
|
+
def locate!(options={})
|
663
|
+
locate(options) || begin
|
664
|
+
klass=self.is_a?(Frame) ? Vapir::Exception::UnknownFrameException : Vapir::Exception::UnknownObjectException
|
665
|
+
message="Unable to locate #{self.class}, using #{@how}"+(@what ? ": "+@what.inspect : '')+(@index ? ", index #{@index}" : "")
|
666
|
+
message+="\non container: #{@container.inspect}" if @container
|
667
|
+
raise(klass, message)
|
668
|
+
end
|
669
|
+
end
|
670
|
+
|
671
|
+
public
|
672
|
+
# Returns whether this element actually exists.
|
673
|
+
def exists?
|
674
|
+
begin
|
675
|
+
!!locate
|
676
|
+
rescue Vapir::Exception::UnknownObjectException, Exception::NoMatchingWindowFoundException # if the window itself is gone, certainly we don't exist.
|
677
|
+
false
|
678
|
+
end
|
679
|
+
end
|
680
|
+
alias :exist? :exists?
|
681
|
+
|
682
|
+
# method to access dom attributes by defined aliases.
|
683
|
+
# unlike get_attribute, this only looks at the specific dom attributes that Watir knows about, and
|
684
|
+
# the aliases for those that Watir defines.
|
685
|
+
def attr(attribute)
|
686
|
+
unless attribute.is_a?(String) || attribute.is_a?(Symbol)
|
687
|
+
raise TypeError, "attribute should be string or symbol; got #{attribute.inspect}"
|
688
|
+
end
|
689
|
+
attribute=attribute.to_sym
|
690
|
+
all_aliases=self.class.all_dom_attr_aliases
|
691
|
+
dom_attrs=all_aliases.reject{|dom_attr, attr_aliases| !attr_aliases.include?(attribute) }.keys
|
692
|
+
case dom_attrs.size
|
693
|
+
when 0
|
694
|
+
raise ArgumentError, "Not a recognized attribute: #{attribute}"
|
695
|
+
when 1
|
696
|
+
method_from_element_object(dom_attrs.first)
|
697
|
+
else
|
698
|
+
raise ArgumentError, "Ambiguously aliased attribute #{attribute} may refer to any of: #{dom_attrs.join(', ')}"
|
699
|
+
end
|
700
|
+
end
|
701
|
+
|
702
|
+
# returns an Element that represents the same object as self, but is an instance of the
|
703
|
+
# most-specific class < self.class that can represent that object.
|
704
|
+
#
|
705
|
+
# For example, if we have a table, get its first element, and call #to_factory on it:
|
706
|
+
#
|
707
|
+
# a_table=browser.tables.first
|
708
|
+
# => #<Vapir::IE::Table:0x071bc70c how=:index index=:first tagName="TABLE">
|
709
|
+
# a_element=a_table.elements.first
|
710
|
+
# => #<Vapir::IE::Element:0x071b856c how=:index index=:first tagName="TBODY" id="">
|
711
|
+
# a_element.to_factory
|
712
|
+
# => #<Vapir::IE::TableBody:0x071af78c how=:index index=:first tagName="TBODY" id="">
|
713
|
+
#
|
714
|
+
# we get back a Vapir::TableBody.
|
715
|
+
def to_factory
|
716
|
+
self.class.factory(element_object, @extra, @how, @what)
|
717
|
+
end
|
718
|
+
|
719
|
+
# takes a block. sets highlight on this element; calls the block; clears the highlight.
|
720
|
+
# the clear is in an ensure block so that you can call return from the given block.
|
721
|
+
# doesn't actually perform the highlighting if argument do_highlight is false.
|
722
|
+
#
|
723
|
+
# also, you can nest these safely; it checks if you're already highlighting before trying
|
724
|
+
# to set and subsequently clear the highlight.
|
725
|
+
#
|
726
|
+
# the block is called within an assert_exists block, so for methods that highlight, the
|
727
|
+
# assert_exists can generally be omitted from there.
|
728
|
+
def with_highlight(options={})
|
729
|
+
highlight_option_keys=[:color]
|
730
|
+
#options=handle_options(options, {:highlight => true}, highlight_option_keys)
|
731
|
+
options={:highlight => true}.merge(options)
|
732
|
+
highlight_options=options.reject{|(k,v)| !highlight_option_keys.include?(k) }
|
733
|
+
assert_exists do
|
734
|
+
was_highlighting=@highlighting
|
735
|
+
if (!@highlighting && options[:highlight])
|
736
|
+
set_highlight(highlight_options)
|
737
|
+
end
|
738
|
+
@highlighting=true
|
739
|
+
begin
|
740
|
+
result=yield
|
741
|
+
ensure
|
742
|
+
@highlighting=was_highlighting
|
743
|
+
if !@highlighting && options[:highlight] && exists? # if we stopped existing during the highlight, don't try to clear.
|
744
|
+
if Object.const_defined?('WIN32OLE') # if WIN32OLE exists, calling clear_highlight may raise WIN32OLERuntimeError, even though we just checked existence.
|
745
|
+
exception_to_rescue=WIN32OLERuntimeError
|
746
|
+
else # otherwise, make a dummy class, inheriting from Exception that won't ever be instantiated to be rescued.
|
747
|
+
exception_to_rescue=(@@dummy_exception ||= Class.new(::Exception))
|
748
|
+
end
|
749
|
+
begin
|
750
|
+
clear_highlight(highlight_options)
|
751
|
+
rescue exception_to_rescue
|
752
|
+
# apparently despite checking existence above, sometimes the element object actually disappears between checking its existence
|
753
|
+
# and clear_highlight using it, raising WIN32OLERuntimeError.
|
754
|
+
# don't actually do anything in the rescue block here.
|
755
|
+
end
|
756
|
+
end
|
757
|
+
end
|
758
|
+
result
|
759
|
+
end
|
760
|
+
end
|
761
|
+
|
762
|
+
private
|
763
|
+
# The default color for highlighting objects as they are accessed.
|
764
|
+
DEFAULT_HIGHLIGHT_COLOR = "yellow"
|
765
|
+
|
766
|
+
# Sets or clears the colored highlighting on the currently active element.
|
767
|
+
# set_or_clear - should be
|
768
|
+
# :set - To set highlight
|
769
|
+
# :clear - To restore the element to its original color
|
770
|
+
#
|
771
|
+
# todo: is this used anymore? I think it's all with_highlight.
|
772
|
+
def highlight(set_or_clear)
|
773
|
+
if set_or_clear == :set
|
774
|
+
set_highlight
|
775
|
+
elsif set_or_clear==:clear
|
776
|
+
clear_highlight
|
777
|
+
else
|
778
|
+
raise ArgumentError, "argument must be :set or :clear; got #{set_or_clear.inspect}"
|
779
|
+
end
|
780
|
+
end
|
781
|
+
|
782
|
+
def set_highlight_color(options={})
|
783
|
+
#options=handle_options(options, :color => DEFAULT_HIGHLIGHT_COLOR)
|
784
|
+
options={:color => DEFAULT_HIGHLIGHT_COLOR}.merge(options)
|
785
|
+
assert_exists do
|
786
|
+
@original_color=element_object.style.backgroundColor
|
787
|
+
element_object.style.backgroundColor=options[:color]
|
788
|
+
end
|
789
|
+
end
|
790
|
+
def clear_highlight_color(options={})
|
791
|
+
#options=handle_options(options, {}) # no options yet
|
792
|
+
begin
|
793
|
+
element_object.style.backgroundColor=@original_color
|
794
|
+
ensure
|
795
|
+
@original_color=nil
|
796
|
+
end
|
797
|
+
end
|
798
|
+
# Highlights the image by adding a border
|
799
|
+
def set_highlight_border(options={})
|
800
|
+
#options=handle_options(options, {}) # no options yet
|
801
|
+
assert_exists do
|
802
|
+
@original_border= element_object.border.to_i
|
803
|
+
element_object.border= @original_border+1
|
804
|
+
end
|
805
|
+
end
|
806
|
+
# restores the image to its original border
|
807
|
+
# TODO: and border color
|
808
|
+
def clear_highlight_border(options={})
|
809
|
+
#options=handle_options(options, {}) # no options yet
|
810
|
+
assert_exists do
|
811
|
+
begin
|
812
|
+
element_object.border = @original_border
|
813
|
+
ensure
|
814
|
+
@original_border = nil
|
815
|
+
end
|
816
|
+
end
|
817
|
+
end
|
818
|
+
alias set_highlight set_highlight_color
|
819
|
+
alias clear_highlight clear_highlight_color
|
820
|
+
|
821
|
+
public
|
822
|
+
# Flash the element the specified number of times.
|
823
|
+
# Defaults to 10 flashes.
|
824
|
+
def flash(options={})
|
825
|
+
if options.is_a?(Fixnum)
|
826
|
+
options={:count => options}
|
827
|
+
Kernel.warn "DEPRECATION WARNING: #{self.class.name}\#flash takes an options hash - passing a number is deprecated. Please use #{self.class.name}\#flash(:count => #{options[:count]})\n(called from #{caller.map{|c|"\n"+c}})"
|
828
|
+
end
|
829
|
+
options={:count => 10, :sleep => 0.05}.merge(options)
|
830
|
+
#options=handle_options(options, {:count => 10, :sleep => 0.05}, [:color])
|
831
|
+
assert_exists do
|
832
|
+
options[:count].times do
|
833
|
+
with_highlight(options) do
|
834
|
+
sleep options[:sleep]
|
835
|
+
end
|
836
|
+
sleep options[:sleep]
|
837
|
+
end
|
838
|
+
end
|
839
|
+
nil
|
840
|
+
end
|
841
|
+
|
842
|
+
# Return the element immediately containing this element.
|
843
|
+
# returns nil if there is no parent, or if the parent is the document.
|
844
|
+
#
|
845
|
+
# this is cached; call parent(:reload => true) if you wish to uncache it.
|
846
|
+
def parent(options={})
|
847
|
+
@parent=nil if options[:reload]
|
848
|
+
@parent||=begin
|
849
|
+
parentNode=element_object.parentNode
|
850
|
+
if parentNode && parentNode != document_object # don't ascend up to the document. #TODO/Fix - for IE, comparing WIN32OLEs doesn't really work, this comparison is pointless.
|
851
|
+
base_element_class.factory(parentNode, extra_for_contained) # this is a little weird, passing extra_for_contained so that this is the container of its parent.
|
852
|
+
else
|
853
|
+
nil
|
854
|
+
end
|
855
|
+
end
|
856
|
+
end
|
857
|
+
|
858
|
+
# Checks this element and its parents for display: none or visibility: hidden, these are
|
859
|
+
# the most common methods to hide an html element. Returns false if this seems to be hidden
|
860
|
+
# or a parent is hidden.
|
861
|
+
def visible?
|
862
|
+
assert_exists do
|
863
|
+
element_to_check=element_object
|
864
|
+
#nsIDOMDocument=jssh_socket.Components.interfaces.nsIDOMDocument
|
865
|
+
really_visible=nil
|
866
|
+
while element_to_check #&& !element_to_check.instanceof(nsIDOMDocument)
|
867
|
+
if (style=element_object_style(element_to_check, document_object))
|
868
|
+
# only pay attention to the innermost definition that really defines visibility - one of 'hidden', 'collapse' (only for table elements),
|
869
|
+
# or 'visible'. ignore 'inherit'; keep looking upward.
|
870
|
+
# this makes it so that if we encounter an explicit 'visible', we don't pay attention to any 'hidden' further up.
|
871
|
+
# this style is inherited - may be pointless for firefox, but IE uses the 'inherited' value. not sure if/when ff does.
|
872
|
+
if really_visible==nil && (visibility=style.invoke('visibility'))
|
873
|
+
visibility=visibility.strip.downcase
|
874
|
+
if visibility=='hidden' || visibility=='collapse'
|
875
|
+
really_visible=false
|
876
|
+
return false # don't need to continue knowing it's not visible.
|
877
|
+
elsif visibility=='visible'
|
878
|
+
really_visible=true # we don't return true yet because a parent with display of 'none' can override
|
879
|
+
end
|
880
|
+
end
|
881
|
+
# check for display property. this is not inherited, and a parent with display of 'none' overrides an immediate visibility='visible'
|
882
|
+
display=style.invoke('display')
|
883
|
+
if display && display.strip.downcase=='none'
|
884
|
+
return false
|
885
|
+
end
|
886
|
+
end
|
887
|
+
element_to_check=element_to_check.parentNode
|
888
|
+
end
|
889
|
+
end
|
890
|
+
return true
|
891
|
+
end
|
892
|
+
private
|
893
|
+
# this is defined on each class to reflect the browser's particular implementation.
|
894
|
+
def element_object_style(element_object, document_object)
|
895
|
+
self.class.element_object_style(element_object, document_object)
|
896
|
+
end
|
897
|
+
|
898
|
+
public
|
899
|
+
# returns a Vector with two elements, the x,y
|
900
|
+
# coordinates of this element (its top left point)
|
901
|
+
# from the top left edge of the window
|
902
|
+
def document_offset
|
903
|
+
xy=Vector[0,0]
|
904
|
+
el=element_object
|
905
|
+
begin
|
906
|
+
xy+=Vector[el.offsetLeft, el.offsetTop]
|
907
|
+
el=el.offsetParent
|
908
|
+
end while el
|
909
|
+
xy
|
910
|
+
end
|
911
|
+
|
912
|
+
# returns a two-element Vector containing the offset of this element on the client area.
|
913
|
+
# see also #client_center
|
914
|
+
def client_offset
|
915
|
+
document_offset-scroll_offset
|
916
|
+
end
|
917
|
+
|
918
|
+
# returns a two-element Vector with the position of the center of this element
|
919
|
+
# on the client area.
|
920
|
+
# intended to be used with mouse events' clientX and clientY.
|
921
|
+
# https://developer.mozilla.org/en/DOM/event.clientX
|
922
|
+
# https://developer.mozilla.org/en/DOM/event.clientY
|
923
|
+
def client_center
|
924
|
+
client_offset+dimensions.map{|dim| dim/2}
|
925
|
+
end
|
926
|
+
|
927
|
+
# returns a two-element Vector containing the current scroll offset of this element relative
|
928
|
+
# to any scrolling parents.
|
929
|
+
# this is basically stolen from prototype - see http://www.prototypejs.org/api/element/cumulativescrolloffset
|
930
|
+
def scroll_offset
|
931
|
+
xy=Vector[0,0]
|
932
|
+
el=element_object
|
933
|
+
begin
|
934
|
+
if el.respond_to?(:scrollLeft) && el.respond_to?(:scrollTop) && (scroll_left=el.scrollLeft).is_a?(Numeric) && (scroll_top=el.scrollTop).is_a?(Numeric)
|
935
|
+
xy+=Vector[scroll_left, scroll_top]
|
936
|
+
end
|
937
|
+
el=el.parentNode
|
938
|
+
end while el
|
939
|
+
xy
|
940
|
+
end
|
941
|
+
|
942
|
+
# returns a two-element Vector containing the position of this element on the screen.
|
943
|
+
# see also #screen_center
|
944
|
+
# not yet implemented.
|
945
|
+
def screen_offset
|
946
|
+
raise NotImplementedError
|
947
|
+
end
|
948
|
+
|
949
|
+
# returns a two-element Vector containing the current position of the center of
|
950
|
+
# this element on the screen.
|
951
|
+
# intended to be used with mouse events' screenX and screenY.
|
952
|
+
# https://developer.mozilla.org/en/DOM/event.screenX
|
953
|
+
# https://developer.mozilla.org/en/DOM/event.screenY
|
954
|
+
#
|
955
|
+
# not yet implemented.
|
956
|
+
def screen_center
|
957
|
+
screen_offset+dimensions.map{|dim| dim/2}
|
958
|
+
end
|
959
|
+
|
960
|
+
# returns a two-element Vector with the width and height of this element.
|
961
|
+
def dimensions
|
962
|
+
Vector[element_object.offsetWidth, element_object.offsetHeight]
|
963
|
+
end
|
964
|
+
# returns a two-element Vector with the position of the center of this element
|
965
|
+
# on the document.
|
966
|
+
def document_center
|
967
|
+
document_offset+dimensions.map{|dim| dim/2}
|
968
|
+
end
|
969
|
+
|
970
|
+
# accesses the object representing this Element in the DOM.
|
971
|
+
def element_object
|
972
|
+
assert_exists
|
973
|
+
@element_object
|
974
|
+
end
|
975
|
+
def container
|
976
|
+
assert_container
|
977
|
+
@container
|
978
|
+
end
|
979
|
+
|
980
|
+
attr_reader :browser
|
981
|
+
attr_reader :page_container
|
982
|
+
|
983
|
+
def document_object
|
984
|
+
assert_container
|
985
|
+
@container.document_object
|
986
|
+
end
|
987
|
+
def content_window_object
|
988
|
+
assert_container
|
989
|
+
@container.content_window_object
|
990
|
+
end
|
991
|
+
def browser_window_object
|
992
|
+
assert_container
|
993
|
+
@container.browser_window_object
|
994
|
+
end
|
995
|
+
|
996
|
+
def attributes_for_stringifying
|
997
|
+
attributes_to_inspect=self.class.attributes_to_inspect
|
998
|
+
unless exists?
|
999
|
+
attributes_to_inspect=[{:value => :exists?, :label => :exists?}]+attributes_to_inspect.select{|inspect_hash| [:how, :what, :index].include?(inspect_hash[:label]) }
|
1000
|
+
end
|
1001
|
+
attributes_to_inspect.map do |inspect_hash|
|
1002
|
+
if !inspect_hash[:if] || inspect_hash[:if].call(self)
|
1003
|
+
value=case inspect_hash[:value]
|
1004
|
+
when /\A@/ # starts with @, look for instance variable
|
1005
|
+
instance_variable_get(inspect_hash[:value]).inspect
|
1006
|
+
when Symbol
|
1007
|
+
send(inspect_hash[:value])
|
1008
|
+
when Proc
|
1009
|
+
inspect_hash[:value].call(self)
|
1010
|
+
else
|
1011
|
+
inspect_hash[:value]
|
1012
|
+
end
|
1013
|
+
[inspect_hash[:label].to_s, value]
|
1014
|
+
end
|
1015
|
+
end.compact
|
1016
|
+
end
|
1017
|
+
def inspect
|
1018
|
+
"\#<#{self.class.name}:0x#{"%.8x"%(self.hash*2)}"+attributes_for_stringifying.map do |attr|
|
1019
|
+
" "+attr.first+'='+attr.last.inspect
|
1020
|
+
end.join('') + ">"
|
1021
|
+
end
|
1022
|
+
def to_s
|
1023
|
+
attrs=attributes_for_stringifying
|
1024
|
+
longest_label=attrs.inject(0) {|max, attr| [max, attr.first.size].max }
|
1025
|
+
"#{self.class.name}:0x#{"%.8x"%(self.hash*2)}\n"+attrs.map do |attr|
|
1026
|
+
(attr.first+": ").ljust(longest_label+2)+attr.last.inspect+"\n"
|
1027
|
+
end.join('')
|
1028
|
+
end
|
1029
|
+
|
1030
|
+
def pretty_print(pp)
|
1031
|
+
pp.object_address_group(self) do
|
1032
|
+
pp.seplist(attributes_for_stringifying, lambda { pp.text ',' }) do |attr|
|
1033
|
+
pp.breakable ' '
|
1034
|
+
pp.group(0) do
|
1035
|
+
pp.text attr.first
|
1036
|
+
pp.text ':'
|
1037
|
+
pp.breakable
|
1038
|
+
pp.pp attr.last
|
1039
|
+
end
|
1040
|
+
end
|
1041
|
+
end
|
1042
|
+
end
|
1043
|
+
|
1044
|
+
# for a common module, such as a TextField, returns an elements-specific class (such as
|
1045
|
+
# Firefox::TextField) that inherits from the base_element_class of self. That is, this returns
|
1046
|
+
# a sibling class, as it were, of whatever class inheriting from Element is instantiated.
|
1047
|
+
def element_class_for(common_module)
|
1048
|
+
element_class=nil
|
1049
|
+
ObjectSpace.each_object(Class) do |klass|
|
1050
|
+
if klass < common_module && klass < base_element_class
|
1051
|
+
element_class= klass
|
1052
|
+
end
|
1053
|
+
end
|
1054
|
+
unless element_class
|
1055
|
+
raise RuntimeError, "No class found that inherits from both #{common_module} and #{base_element_class}"
|
1056
|
+
end
|
1057
|
+
element_class
|
1058
|
+
end
|
1059
|
+
|
1060
|
+
module_function
|
1061
|
+
def object_collection_to_enumerable(object)
|
1062
|
+
if object.is_a?(Enumerable)
|
1063
|
+
object
|
1064
|
+
elsif Object.const_defined?('JsshObject') && object.is_a?(JsshObject)
|
1065
|
+
object.to_array
|
1066
|
+
elsif Object.const_defined?('WIN32OLE') && object.is_a?(WIN32OLE)
|
1067
|
+
array=[]
|
1068
|
+
(0...object.length).each do |i|
|
1069
|
+
array << object.item(i)
|
1070
|
+
end
|
1071
|
+
array
|
1072
|
+
else
|
1073
|
+
raise TypeError, "Don't know how to make enumerable from given object #{object.inspect} (#{object.class})"
|
1074
|
+
end
|
1075
|
+
end
|
1076
|
+
|
1077
|
+
end
|
1078
|
+
end
|