vapir-common 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 +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
|