Module: Vapir::Element

Extended by:
ElementHelper
Includes:
Configurable, ElementObjectCandidates
Defined in:
lib/vapir-common/element.rb

Overview

this is included by every Element. it relies on the including class implementing a #element_object method some stuff assumes the element has a defined @container.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from ElementHelper

add_specifier, container_collection_method, container_single_method, included

Methods included from ElementClassAndModuleMethods

#add_container_method_extra_args, #all_dom_attr_aliases, #all_dom_attrs, #class_array_append, #class_array_get, #class_hash_get, #class_hash_merge, #container_collection_methods, #container_method_extra_args, #container_single_methods, #default_how, #dom_attr, #dom_attr_locate_alias, #dom_function, #dom_setter, #element_collection, #factory, #inspect_these, #inspect_this_if, #parent_element_module, #set_or_get_class_var, #specifiers

Methods included from Configurable

#config, #with_config, #without_waiting

Instance Attribute Details

#browserObject (readonly)

the Vapir::Browser this element is on



586
587
588
# File 'lib/vapir-common/element.rb', line 586

def browser
  @browser
end

#howObject (readonly)

Returns the value of attribute how.



107
108
109
# File 'lib/vapir-common/element.rb', line 107

def how
  @how
end

#indexObject (readonly)

Returns the value of attribute index.



109
110
111
# File 'lib/vapir-common/element.rb', line 109

def index
  @index
end

#page_containerObject (readonly)

the Vapir::PageContainer containing this element (a Browser, Frame, or ModalDialogDocument)



588
589
590
# File 'lib/vapir-common/element.rb', line 588

def page_container
  @page_container
end

#whatObject (readonly)

Returns the value of attribute what.



108
109
110
# File 'lib/vapir-common/element.rb', line 108

def what
  @what
end

Class Method Details

.object_collection_to_enumerable(object) ⇒ Object



659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
# File 'lib/vapir-common/element.rb', line 659

def object_collection_to_enumerable(object)
  if object.is_a?(Enumerable)
    object
  elsif Object.const_defined?('JavascriptObject') && object.is_a?(JavascriptObject)
    object.to_array
  elsif Object.const_defined?('WIN32OLE') && object.is_a?(WIN32OLE)
    array=[]
    length = object.length
    (0...length).each do |i|
      begin
        array << object.item(i)
      rescue WIN32OLERuntimeError
        # not rescuing, just adding information
        raise $!.class, "accessing item #{i} of #{length}, encountered:\n"+$!.message, $!.backtrace
      end
    end
    array
  else
    raise TypeError, "Don't know how to make enumerable from given object #{object.inspect} (#{object.class})"
  end
end

Instance Method Details

#attr(attribute) ⇒ Object

method to access dom attributes by defined aliases. unlike get_attribute, this only looks at the specific dom attributes that Watir knows about, and the aliases for those that Watir defines.



291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
# File 'lib/vapir-common/element.rb', line 291

def attr(attribute)
  unless attribute.is_a?(String) || attribute.is_a?(Symbol)
    raise TypeError, "attribute should be string or symbol; got #{attribute.inspect}"
  end
  attribute=attribute.to_sym
  all_aliases=self.class.all_dom_attr_aliases
  dom_attrs=all_aliases.reject{|dom_attr, attr_aliases| !attr_aliases.include?(attribute) }.keys
  case dom_attrs.size
  when 0
    raise ArgumentError, "Not a recognized attribute: #{attribute}"
  when 1
    method_from_element_object(dom_attrs.first)
  else
    raise ArgumentError, "Ambiguously aliased attribute #{attribute} may refer to any of: #{dom_attrs.join(', ')}"
  end
end

#attributes_for_stringifyingObject

used by inspect, to_s, and pretty_print to determine what to show



607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
# File 'lib/vapir-common/element.rb', line 607

def attributes_for_stringifying
  attributes_to_inspect=self.class.attributes_to_inspect
  unless exists?
    attributes_to_inspect=[{:value => :exists?, :label => :exists?}]+attributes_to_inspect.select{|inspect_hash| [:how, :what, :index].include?(inspect_hash[:label]) }
  end
  attributes_to_inspect.map do |inspect_hash|
    if !inspect_hash[:if] || inspect_hash[:if].call(self)
      value=case inspect_hash[:value]
      when /\A@/ # starts with @, look for instance variable
        instance_variable_get(inspect_hash[:value]).inspect
      when Symbol
        send(inspect_hash[:value])
      when Proc
        inspect_hash[:value].call(self)
      else
        inspect_hash[:value]
      end
      [inspect_hash[:label].to_s, value]
    end
  end.compact
end

#browser_window_objectObject

returns the underlying object representing the browser.



601
602
603
604
# File 'lib/vapir-common/element.rb', line 601

def browser_window_object
  assert_container
  @container.browser_window_object
end

#client_centerObject

returns a two-element Vector with the position of the center of this element on the client area. intended to be used with mouse events’ clientX and clientY. developer.mozilla.org/en/DOM/event.clientX developer.mozilla.org/en/DOM/event.clientY



528
529
530
# File 'lib/vapir-common/element.rb', line 528

def client_center
  client_offset+dimensions.map{|dim| dim/2}
end

#client_offsetObject

returns a two-element Vector containing the offset of this element on the client area. see also #client_center



519
520
521
# File 'lib/vapir-common/element.rb', line 519

def client_offset
  document_offset-scroll_offset
end

#configuration_parentObject



13
14
15
# File 'lib/vapir-common/element.rb', line 13

def configuration_parent
  @container ? @container.config : @browser ? @browser.config : browser_class.config
end

#containerObject



580
581
582
583
# File 'lib/vapir-common/element.rb', line 580

def container
  assert_container
  @container
end

#content_window_objectObject

returns the content window object of the current page on the browser (this is the ‘window’ object in javascript).



596
597
598
599
# File 'lib/vapir-common/element.rb', line 596

def content_window_object
  assert_container
  @container.content_window_object
end

#default_initialize(how, what, extra = {}) ⇒ Object Also known as: initialize

the class-specific Elements may implement their own #initialize, but should call to this after they’ve done their stuff

Raises:

  • (ArgumentError)


121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/vapir-common/element.rb', line 121

def default_initialize(how, what, extra={})
  @how, @what=how, what
  raise ArgumentError, "how (first argument) should be a Symbol, not: #{how.inspect}" unless how.is_a?(Symbol) || how==nil
  @extra=extra
  @index=begin
    valid_symbols=[:first, :last]
    if valid_symbols.include?(@extra[:index]) || @extra[:index].nil? || (@extra[:index].is_a?(Integer) && @extra[:index] > 0)
      @extra[:index]
    elsif valid_symbols.map{|sym| sym.to_s}.include?(@extra[:index])
      @extra[:index].to_sym
    elsif @extra[:index] =~ /\A\d+\z/
      Integer(@extra[:index])
    else
      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})"
    end
  end
  @container=extra[:container]
  @browser=extra[:browser]
  @page_container=extra[:page_container]
  @element_object=extra[:element_object] # this will in most cases not be set, but may be set in some cases from ElementCollection enumeration 
  extra[:locate]=true unless @extra.key?(:locate) # set default 
  case extra[:locate]
  when :assert
    locate!
  when true
    locate
  when false
  else
    raise ArgumentError, "Unrecognized value given for extra[:locate]: #{extra[:locate].inspect} (#{extra[:locate].class})"
  end
end

#dimensionsObject

returns a two-element Vector with the width and height of this element.



566
567
568
# File 'lib/vapir-common/element.rb', line 566

def dimensions
  Vector[element_object.offsetWidth, element_object.offsetHeight]
end

#document_centerObject

returns a two-element Vector with the position of the center of this element on the document.



571
572
573
# File 'lib/vapir-common/element.rb', line 571

def document_center
  document_offset+dimensions.map{|dim| dim/2}
end

#document_objectObject



590
591
592
593
# File 'lib/vapir-common/element.rb', line 590

def document_object
  assert_container
  @container.document_object
end

#document_offsetObject

returns a Vector with two elements, the x,y coordinates of this element (its top left point) from the top left edge of the window



507
508
509
510
511
512
513
514
515
# File 'lib/vapir-common/element.rb', line 507

def document_offset
  xy=Vector[0,0]
  el=element_object
  begin
    xy+=Vector[el.offsetLeft, el.offsetTop]
    el=el.offsetParent
  end while el
  xy
end

#element_objectObject

accesses the object representing this Element in the DOM.



576
577
578
579
# File 'lib/vapir-common/element.rb', line 576

def element_object
  assert_exists
  @element_object
end

#exists?Boolean Also known as: exist?

Returns whether this element actually exists.

Returns:

  • (Boolean)


281
282
283
284
285
# File 'lib/vapir-common/element.rb', line 281

def exists?
  handling_existence_failure(:handle => proc { return false }, :assert_exists => false) do
    return !!locate
  end
end

#flash(options = {}) ⇒ Object

Flash the element the specified number of times. Defaults to 10 flashes.



413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
# File 'lib/vapir-common/element.rb', line 413

def flash(options={})
  if options.is_a?(Fixnum)
    options={:count => options}
    if config.warn_deprecated
      Kernel.warn_with_caller "DEPRECATION WARNING: #{self.class.name}\#flash takes an options hash - passing a number is deprecated. Please use #{self.class.name}\#flash(:count => #{options[:count]})"
    end
  end
  options={:count => 10, :sleep => 0.05}.merge(options)
  #options=handle_options(options, {:count => 10, :sleep => 0.05}, [:color])
  assert_exists do
    options[:count].times do
      with_highlight(options) do
        sleep options[:sleep]
      end
      sleep options[:sleep]
    end
  end
  nil
end

#htmlObject



111
112
113
# File 'lib/vapir-common/element.rb', line 111

def html
  outer_html
end

#inspectObject



628
629
630
631
632
# File 'lib/vapir-common/element.rb', line 628

def inspect
  "\#<#{self.class.name}:0x#{"%.8x"%(self.hash*2)}"+attributes_for_stringifying.map do |attr|
    " "+attr.first+'='+attr.last.inspect
  end.join('') + ">"
end

#locateObject

locates the element object for this element



189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
# File 'lib/vapir-common/element.rb', line 189

def locate
  if element_object_exists?
    return @element_object
  end
  new_element_object= begin
    case @how
    when :element_object
      assert_no_index
      if @element_object # if @element_object is already set, it must not exist, since we check #element_object_exists? above. 
        raise Vapir::Exception::UnableToRelocateException, "This #{self.class.name} has stopped existing. Tried to relocate it, but it was specified using #{how.inspect} and cannot be relocated."
      else
        @what
      end
    when :xpath
      assert_container_exists
      unless @container.respond_to?(:element_object_by_xpath)
        raise Vapir::Exception::MissingWayOfFindingObjectException, "Locating by xpath is not supported on the container #{@container.inspect}"
      end
      # todo/fix: implement index for this, using element_objects_by_xpath ? 
      assert_no_index
      by_xpath=@container.element_object_by_xpath(@what)
      match_candidates(by_xpath ? [by_xpath] : [], self.class.specifiers, self.class.all_dom_attr_aliases).first
    when :css
      assert_container_exists
      candidate_match_at_index(@index, method(:match_candidates), @container.containing_object.querySelectorAll(@what), self.class.specifiers, self.class.all_dom_attr_aliases)
    when :label
      assert_no_index
      unless document_object
        raise "No document object found for this #{self.inspect} - needed to search by id for label from #{@container.inspect}"
      end
      label_element = case @what
      when Label
        @what.locate!
        @what
      when String, Regexp
        page_container.label(:text, @what)
      else
        raise Vapir::Exception::MissingWayOfFindingObjectException, "This #{self.class} was specified as 'how'=:label; 'what' was expected to be a Label element or a String or Regexp to match label text. Given 'what'=#{@what.inspect} (#{@what.class})"
      end
      if label_element.exists?
        by_label=document_object.getElementById(label_element.for)
      end
      match_candidates(by_label ? [by_label] : [], self.class.specifiers, self.class.all_dom_attr_aliases).first
    when :attributes
      assert_container_exists
      specified_attributes=@what
      specifiers=self.class.specifiers.map{|spec| spec.merge(specified_attributes)}
      
      candidate_match_at_index(@index, method(:matched_candidates), specifiers, self.class.all_dom_attr_aliases)
    when nil
      assert_container_exists
      unless @what.nil?
        raise ArgumentError, "'what' was specified, but 'how' was not given (is nil)"
      end
      candidate_match_at_index(@index, method(:matched_candidates), self.class.specifiers, self.class.all_dom_attr_aliases)
    when :custom
      assert_container_exists
      # this allows a proc to be given as 'what', which is called yielding candidates, each being 
      # an instanted Element of this class. this might seem a bit odd - instantiating a bunch 
      # of elements in order to figure out which element_object to use in locating this one. 
      # the purpose is so that this Element can be relocated if we lose the element_object. 
      # the Elements that are yielded are instantiated by :element object which cannot be 
      # relocated. 
      #
      # this integrates with ElementCollection, where Enumerable methods #detect,
      # #select, and #reject are overridden to use it. 
      # 
      # the proc should return true (that is, not false or nil) when it likes the given Element - 
      # when it matches what it expects of this Element. 
      candidate_match_at_index(@index, method(:matched_candidates), self.class.specifiers, self.class.all_dom_attr_aliases) do |candidate|
        what.call(self.class.new(:element_object, candidate, @container.extra_for_contained))
      end
    else
      raise Vapir::Exception::MissingWayOfFindingObjectException, "Unknown 'how' given: #{@how.inspect} (#{@how.class}). 'what' was #{@what.inspect} (#{@what.class})"
    end
  end
  @element_object=new_element_object
end

#locate!Object



267
268
269
270
271
272
273
274
275
276
277
# File 'lib/vapir-common/element.rb', line 267

def locate!
  locate || begin
    klass=self.is_a?(Frame) ? Vapir::Exception::UnknownFrameException : Vapir::Exception::UnknownObjectException
    using = []
    using << "#{@how}: #{@what.inspect}" if @how
    using << "index: #{@index}" if @index
    message="Unable to locate #{self.class}" + (using.any? ? ", using #{using.join(", ")}" : "")
    message+="\non container: #{@container.inspect}" if @container
    raise(klass, message)
  end
end

#parent(options = {}) ⇒ Object

Return the element immediately containing this element. returns nil if there is no parent, or if the parent is the document.

this is cached; call parent(:reload => true) if you wish to uncache it.



437
438
439
440
441
442
443
444
445
446
447
# File 'lib/vapir-common/element.rb', line 437

def parent(options={})
  @parent=nil if options[:reload]
  @parent||=begin
    parentNode=element_object.parentNode
    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. 
      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. 
    else
      nil
    end
  end
end

#pretty_print(pp) ⇒ Object



644
645
646
647
648
649
650
651
652
653
654
655
656
# File 'lib/vapir-common/element.rb', line 644

def pretty_print(pp)
  pp.object_address_group(self) do
    pp.seplist(attributes_for_stringifying, lambda { pp.text ',' }) do |attr|
      pp.breakable ' '
      pp.group(0) do
        pp.text attr.first
        pp.text ':'
        pp.breakable
        pp.pp attr.last
      end
    end
  end
end

#screen_centerObject

returns a two-element Vector containing the current position of the center of this element on the screen. intended to be used with mouse events’ screenX and screenY. developer.mozilla.org/en/DOM/event.screenX developer.mozilla.org/en/DOM/event.screenY

not yet implemented.



561
562
563
# File 'lib/vapir-common/element.rb', line 561

def screen_center
  screen_offset+dimensions.map{|dim| dim/2}
end

#screen_offsetObject

returns a two-element Vector containing the position of this element on the screen. see also #screen_center not yet implemented.

Raises:

  • (NotImplementedError)


550
551
552
# File 'lib/vapir-common/element.rb', line 550

def screen_offset
  raise NotImplementedError
end

#scroll_offsetObject

returns a two-element Vector containing the current scroll offset of this element relative to any scrolling parents. this is basically stolen from prototype - see www.prototypejs.org/api/element/cumulativescrolloffset



535
536
537
538
539
540
541
542
543
544
545
# File 'lib/vapir-common/element.rb', line 535

def scroll_offset
  xy=Vector[0,0]
  el=element_object
  begin
    if el.respond_to?(:scrollLeft) && el.respond_to?(:scrollTop) && (scroll_left=el.scrollLeft).is_a?(Numeric) && (scroll_top=el.scrollTop).is_a?(Numeric)
      xy+=Vector[scroll_left, scroll_top]
    end
    el=el.parentNode
  end while el
  xy
end

#text_nodesObject

returns an array of all text nodes below this element in the DOM heirarchy



485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
# File 'lib/vapir-common/element.rb', line 485

def text_nodes
  # TODO: needs tests 
  assert_exists do
    recurse_text_nodes=proc do |rproc, e_obj|
      case e_obj.nodeType
      when 1 # TODO: name a constant ELEMENT_NODE, rather than magic number 
        object_collection_to_enumerable(e_obj.childNodes).inject([]) do |result, c_obj|
          result + rproc.call(rproc, c_obj)
        end
      when 3 # TODO: name a constant TEXT_NODE, rather than magic number 
        [e_obj.data]
      else
        #Kernel.warn("ignoring node of type #{e_obj.nodeType}")
        []
      end
    end
    recurse_text_nodes.call(recurse_text_nodes, element_object)
  end
end

#to_sObject

returns a string representation of this element with each attribute on its own line. this returns the same information as #inspect, but formatted somewhat more readably. you might also be interested in pretty-printing the element; see the pp library.



636
637
638
639
640
641
642
# File 'lib/vapir-common/element.rb', line 636

def to_s
  attrs=attributes_for_stringifying
  longest_label=attrs.inject(0) {|max, attr| [max, attr.first.size].max }
  "#{self.class.name}:0x#{"%.8x"%(self.hash*2)}\n"+attrs.map do |attr|
    (attr.first+": ").ljust(longest_label+2)+attr.last.inspect+"\n"
  end.join('')
end

#to_subtypeObject Also known as: to_factory

returns an Element that represents the same object as self, but is an instance of the most-specific class < self.class that can represent that object.

For example, if we have a table, get its first element, and call #to_factory on it:

a_table=browser.tables.first
=> #<Vapir::IE::Table:0x071bc70c index=:first tagName="TABLE">
a_element=a_table.elements.first
=> #<Vapir::IE::Element:0x071b856c index=:first tagName="TBODY" id="">
a_element.to_factory
=> #<Vapir::IE::TableBody:0x071af78c index=:first tagName="TBODY" id="">

we get back a Vapir::TableBody.



321
322
323
# File 'lib/vapir-common/element.rb', line 321

def to_subtype
  self.class.factory(element_object, @extra, @how, @what)
end

#visible?Boolean

Checks this element and its parents for display: none or visibility: hidden, these are the most common methods to hide an html element. Returns false if this seems to be hidden or a parent is hidden.

Returns:

  • (Boolean)


452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
# File 'lib/vapir-common/element.rb', line 452

def visible? 
  assert_exists do
    element_to_check=element_object
    #nsIDOMDocument=firefox_socket.Components.interfaces.nsIDOMDocument
    really_visible=nil
    while element_to_check #&& !element_to_check.instanceof(nsIDOMDocument)
      if (style=element_object_style(element_to_check, document_object))
        # only pay attention to the innermost definition that really defines visibility - one of 'hidden', 'collapse' (only for table elements), 
        # or 'visible'. ignore 'inherit'; keep looking upward. 
        # this makes it so that if we encounter an explicit 'visible', we don't pay attention to any 'hidden' further up. 
        # this style is inherited - may be pointless for firefox, but IE uses the 'inherited' value. not sure if/when ff does.
        if really_visible==nil && (visibility=style.invoke('visibility'))
          visibility=visibility.strip.downcase
          if visibility=='hidden' || visibility=='collapse'
            really_visible=false
            return false # don't need to continue knowing it's not visible. 
          elsif visibility=='visible'
            really_visible=true # we don't return true yet because a parent with display of 'none' can override 
          end
        end
        # check for display property. this is not inherited, and a parent with display of 'none' overrides an immediate visibility='visible' 
        display=style.invoke('display')
        if display && display.strip.downcase=='none'
          return false
        end
      end
      element_to_check=element_to_check.parentNode
    end
  end
  return true
end

#with_highlight(options = {}) ⇒ Object

takes a block. sets highlight on this element; calls the block; clears the highlight. the clear is in an ensure block so that you can call return from the given block.

takes an options hash; every argument is ignored except :highlight, which defaults to true; if set to false then the highlighting won’t actually happen, the block will just be called and its value returned.

also, you can nest these safely; it checks if you’re already highlighting before trying to set and subsequently clear the highlight.

the block is called within an #assert_exists block, so methods that highlight don’t need to also check existence as that’d be redundant.



338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
# File 'lib/vapir-common/element.rb', line 338

def with_highlight(options={})
  assert_exists do
    # yeah, this line is an unreadable mess, but I have to skip over it so many times debugging that it's worth just sticking it on one line 
    (options={:highlight => true}.merge(options)); (was_highlighting=@highlighting); (set_highlight(options) if !@highlighting && options[:highlight]); (@highlighting=true)
    begin; result=yield
    ensure
      @highlighting=was_highlighting
      if !@highlighting && options[:highlight]
        handling_existence_failure do
          clear_highlight(options)
        end
      end
    end
    result
  end
end