module Celerity
#
# Superclass for all HTML elements.
#
class Element
include Exception
include Container
attr_reader :container, :object
# number of spaces that separate the property from the value in the create_string method
TO_S_SIZE = 14
# HTML 4.01 Transitional DTD
HTML_401_TRANSITIONAL = {
:core => [:class, :id, :style, :title],
:cell_halign => [:align, :char, :charoff],
:cell_valign => [:valign],
:i18n => [:dir, :lang],
:event => [:onclick, :ondblclick, :onmousedown, :onmouseup, :onmouseover,
:onmousemove, :onmouseout, :onkeypress, :onkeydown, :onkeyup],
:sloppy => [:name, :value]
}
CELLHALIGN_ATTRIBUTES = HTML_401_TRANSITIONAL[:cell_halign]
CELLVALIGN_ATTRIBUTES = HTML_401_TRANSITIONAL[:cell_valign]
BASE_ATTRIBUTES = HTML_401_TRANSITIONAL.values_at(:core, :i18n, :event, :sloppy).flatten
ATTRIBUTES = BASE_ATTRIBUTES
TAGS = []
DEFAULT_HOW = nil
# @api private
def initialize(container, *args)
self.container = container
case args.size
when 2
@conditions = { args[0] => args[1] }
when 1
if args.first.is_a? Hash
@conditions = args.first
elsif (how = self.class::DEFAULT_HOW)
@conditions = { how => args.first }
else
raise ArgumentError, "wrong number of arguments (1 for 2)"
end
else
raise ArgumentError, "wrong number of arguments (#{args.size} for 2)"
end
@conditions.freeze
end
#
# Get the parent element
# @return [Celerity::Element, nil] subclass of Celerity::Element, or nil if no parent was found
#
def parent
assert_exists
obj = @object.parentNode
until element_class = Celerity::Util.htmlunit2celerity(obj.class)
return nil if obj.nil?
obj = obj.parentNode
end
element_class.new(@container, :object, obj)
end
#
# Sets the focus to this element.
#
def focus
assert_exists
@object.focus
end
#
# Used internally. Find the element on the page.
# @api private
#
def locate
@object = ElementLocator.new(@container, self.class).find_by_conditions(@conditions)
end
#
# @return [String] A string representation of the element.
#
def to_s
assert_exists
create_string(@object)
end
#
# @param [String, #to_s] The attribute.
# @return [String] The value of the given attribute.
#
def attribute_value(attribute)
assert_exists
@object.getAttribute(attribute.to_s)
end
#
# Check if the element is visible to the user or not.
# Note that this only takes the _style attribute_ of the element (and
# its parents) into account - styles from applied CSS is not considered.
#
# @return [boolean]
#
def visible?
obj = self
while obj
return false if obj.respond_to?(:type) && obj.type == 'hidden'
return false if obj.style =~ /display\s*:\s*none|visibility\s*:\s*hidden/
obj = obj.parent
end
return true
end
#
# Used internally to ensure the element actually exists.
#
# @raise [Celerity::Exception::UnknownObjectException] if the element can't be found.
# @api private
#
def assert_exists
locate
unless @object
raise UnknownObjectException, "Unable to locate #{self.class.name[/::(.*)$/, 1]}, using #{identifier_string}"
end
end
#
# Checks if the element exists.
# @return [true, false]
#
def exists?
assert_exists
true
rescue UnknownObjectException, UnknownFrameException
false
end
alias_method :exist?, :exists?
#
# Return a text representation of the element as it would appear in a browser.
#
# @see inner_text
# @return [String]
#
def text
assert_exists
@object.asText.strip # this must behave like ElementLocator
end
#
# Return the text content of this DOM node, disregarding its visibility.
#
# (Celerity-specific?)
#
# @see text
# @return [String]
#
def inner_text
assert_exists
Celerity::Util.normalize_text @object.getTextContent
end
#
# @return [String] The normative XML representation of the element (including children).
#
def to_xml
assert_exists
@object.asXml
end
alias_method :asXml, :to_xml
alias_method :as_xml, :to_xml
alias_method :html, :to_xml
#
# @return [String] A string representation of the element's attributes.
#
def attribute_string
assert_exists
result = ''
@object.getAttributes.each do |attribute|
result << %Q{#{attribute.getName}="#{attribute.getHtmlValue}"}
end
result
end
#
# return the canonical xpath for this element (Celerity-specific)
#
def xpath
assert_exists
@object.getCanonicalXPath
end
#
# Dynamically get element attributes.
#
# @see ATTRIBUTES constant for a list of valid methods for a given element.
#
# @return [String] The resulting attribute.
# @raise [NoMethodError] if the element doesn't support this attribute.
#
def method_missing(meth, *args, &blk)
assert_exists
meth = selector_to_attribute(meth)
if self.class::ATTRIBUTES.include?(meth)
return @object.getAttributeValue(meth.to_s)
end
Log.warn "Element\#method_missing calling super with #{meth.inspect}"
super
end
def methods(*args)
ms = super
ms += self.class::ATTRIBUTES.map { |e| e.to_s }
ms.sort
end
def respond_to?(meth, include_private = false)
meth = selector_to_attribute(meth)
return true if self.class::ATTRIBUTES.include?(meth)
super
end
private
def create_string(element)
ret = []
unless (tag = element.getTagName).empty?
ret << "tag:".ljust(TO_S_SIZE) + tag
end
element.getAttributes.each do |attribute|
ret << " #{attribute.getName}:".ljust(TO_S_SIZE+2) + attribute.getHtmlValue.to_s
end
unless (text = element.asText).empty?
ret << " text:".ljust(TO_S_SIZE+2) + element.asText
end
ret.join("\n")
end
def identifier_string
if @conditions.size == 1
how, what = @conditions.to_a.first
"#{how.inspect} and #{what.inspect}"
else
@conditions.inspect
end
end
def selector_to_attribute(meth)
case meth
when :class_name then :class
when :caption then :value
when :url then :href
else meth
end
end
end # Element
end # Celerity