vapir-common 1.7.0.rc1

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