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.
@@ -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