module Watir # Base class for html elements. # This is not a class that users would normally access. class Element # Wrapper include Comparable include ElementExtensions include Exception include Container # presumes @container is defined include DragAndDropHelper attr_accessor :container class << self private # @!macro attr_ole # @!method $1 # Retrieve element's $1 from the $2 OLE method. # @see http://msdn.microsoft.com/en-us/library/hh773183(v=vs.85).aspx MSDN Documentation # @return [String, Boolean, Fixnum] element's "$1" attribute value. # Return type depends of the attribute type. # @return [String] an empty String if the "$1" attribute does not exist. # @macro exists def attr_ole(method_name, ole_method_name=nil) class_eval %Q[ def #{method_name} assert_exists ole_method_name = '#{ole_method_name || method_name.to_s.gsub(/\?$/, '')}' ole_object.invoke(ole_method_name) rescue attribute_value(ole_method_name) || '' rescue '' end] end end attr_ole :id attr_ole :title attr_ole :class_name, :className attr_ole :unique_number, :uniqueNumber attr_ole :inner_html, :innerHTML attr_ole :outer_html, :outerHTML alias_method :html, :outer_html # number of spaces that separate the property from the value in the to_s method # @private TO_S_SIZE = 14 def initialize(container, specifiers) set_container container raise ArgumentError, "#{specifiers.inspect} has to be Hash" unless specifiers.is_a?(Hash) @o = specifiers[:ole_object] @specifiers = specifiers end def <=> other assert_exists other.assert_exists ole_object.sourceindex <=> other.ole_object.sourceindex end alias_method :eql?, :== # @return [WIN32OLE] OLE object of the element, allowing any methods of the DOM # that Watir doesn't support to be used. def ole_object @o end def inspect '#<%s:0x%x located=%s specifiers=%s>' % [self.class, hash*2, !!ole_object, @specifiers.inspect] end def to_s assert_exists string_creator.join("\n") end # @return [String] element's html tag name in downcase. # @macro exists def tag_name assert_exists @o.tagName.downcase end # Cast {Element} into specific subclass. # @example Convert div element to {Div} class: # browser.element(:tag_name => "div").to_subtype # => Watir::Div # @return {Element} element casted into specific sub-class of Element. # @macro exists def to_subtype assert_exists tag = tag_name.downcase if tag == "html" element(:ole_object => ole_object) elsif tag == "input" input_type = case ole_object.invoke("type") when *%w[button reset submit image] "button" when "checkbox" "checkbox" when "radio" "radio" when "file" "file_field" else "text_field" end send(input_type, :ole_object => ole_object) elsif respond_to?(tag) send(tag, :ole_object => ole_object) else self end end # Send keys to the element # @example # browser.text_field.send_keys "hello", [:control, "a"], :backspace # @param [String, Array, Symbol] keys Keys to send to the element. # @see https://github.com/jarmo/RAutomation/blob/master/lib/rautomation/adapter/win_32/window.rb RAutomation::Window#send_keys documentation. def send_keys(*keys) focus page_container.send_keys *keys end # Retrieve element's css style. # @param [String] property When property is specified then only css for that property is returned. # @return [String] css style as a one long String. # @return [String] css style for specified property if property parameter is specified. # @macro exists def style(property=nil) assert_exists css = ole_object.style.cssText if property properties = Hash[css.downcase.split(";").map { |p| p.split(":").map(&:strip) }] properties[property] else css end end # The text value of the element between html tags. # @return [String] element's text. # @return [String] empty String when element is not visible. # @macro exists def text assert_exists visible? ? ole_object.innerText.strip : "" end # Retrieve the element immediately containing self. # @return [Element] parent element of self. # @return [Element] self when parent element does not exist. # @macro exists def parent assert_exists parent_element = ole_object.parentelement return unless parent_element Element.new(self, :ole_object => parent_element).to_subtype end # Performs a left click on the element. # Will wait automatically until browser is ready after the click if page load was triggered for example. # @macro exists # @macro enabled def click click! @container.wait end # Performs a right click on the element. # Will wait automatically until browser is ready after the click if page load was triggered for example. # @macro enabled # @macro exists def right_click perform_action {fire_event("oncontextmenu"); @container.wait} end # Performs a double click on the element. # Will wait automatically until browser is ready after the click if page load was triggered for example. # @macro exists # @macro enabled def double_click perform_action {fire_event("ondblclick"); @container.wait} end # Flash the element the specified number of times for troubleshooting purposes. # @param [Fixnum] number Number times to flash the element. # @macro exists def flash(number=10) assert_exists number.times do set_highlight sleep 0.05 clear_highlight sleep 0.05 end self end # Executes a user defined "fireEvent" for element with JavaScript events. # # @example Fire a onchange event on select_list: # browser.select_list.fire_event "onchange" # # @macro exists def fire_event(event) perform_action {dispatch_event(event); @container.wait} end # Set focus on the element. # @macro exists # @macro enabled def focus assert_exists assert_enabled @container.focus ole_object.focus(0) end # @return [Boolean] true when element is in focus, false otherwise. # @macro exists # @macro enabled def focused? assert_exists assert_enabled @page_container.document.activeElement.uniqueNumber == unique_number end # @return [Boolean] true when element exists, false otherwise. def exists? begin locate rescue WIN32OLERuntimeError, UnknownObjectException @o = nil end !!@o end alias :exist? :exists? # @return [Boolean] true if the element is enabled, false otherwise. # @macro exists def enabled? assert_exists !disabled? end # @return [Boolean] true if the element is disabled, false otherwise. # @macro exists def disabled? assert_exists false end # Retrieve the status of element's visibility. # When any parent element is not also visible then the current element is determined as not visible too. # @return [Boolean] true if element is visible, false otherwise. # @macro exists def visible? # Now iterate up the DOM element tree and return false if any # parent element isn't visible assert_exists visible_child = false object = @o while object begin visibility = object.currentstyle.invoke('visibility') if visibility =~ /^visible$/i visible_child = true elsif !visible_child && visibility =~ /^hidden$/i return false end if object.currentstyle.invoke('display') =~ /^none$/i return false end rescue WIN32OLERuntimeError end object = object.parentElement end true end # Get attribute value for any attribute of the element. # @return [String] the value of the attribute. # @return [Object] nil if the attribute does not exist. # @macro exists def attribute_value(attribute_name) assert_exists ole_object.getAttribute(attribute_name) end # Make it possible to use *_no_wait commands and retrieve element html5 data-attribute # values. # # @example Use click without waiting: # browser.button.click_no_wait # # @example Retrieve html5 data attribute value: # browser.div.data_model # => value of data-model="foo" html attribute def method_missing(method_name, *args, &block) meth = method_name.to_s if meth =~ /(.*)_no_wait/ && self.respond_to?($1) perform_action do ruby_code = generate_ruby_code(self, $1, *args) system(spawned_no_wait_command(ruby_code)) end elsif meth =~ /^(aria|data)_(.+)$/ self.send(:attribute_value, meth.gsub("_", "-")) || '' else super end end # @private def locate @o = @container.locator_for(TaggedElementLocator, @specifiers, self.class).locate end # @private def __ole_inner_elements assert_exists ole_object.all end # @private def document assert_exists ole_object end # @private def assert_exists locate unless ole_object exception_class = self.is_a?(Frame) ? UnknownFrameException : UnknownObjectException raise exception_class.new(Watir::Exception.message_for_unable_to_locate(@specifiers)) end end # @private def assert_enabled raise ObjectDisabledException, "object #{@specifiers.inspect} is disabled" unless enabled? end # @private def typingspeed @container.typingspeed end # @private def type_keys @type_keys || @container.type_keys end # @private def active_object_highlight_color @container.active_object_highlight_color end # @private def click! perform_action do # Not sure why but in IE9 Document mode, passing a parameter # to click seems to work. Firing the onClick event breaks other tests # so this seems to be the safest change and also works fine in IE8 ole_object.click(0) end end # @private def dispatch_event(event) if Browser.version_parts.first.to_i >= 9 && container.page_container.document.documentMode.to_i >= 9 ole_object.dispatchEvent(create_event(event)) else ole_object.fireEvent(event) end end private def create_event(event) event =~ /on(.*)/i event = $1 if $1 event.downcase! # See http://www.howtocreate.co.uk/tutorials/javascript/domevents case event when 'abort', 'blur', 'change', 'error', 'focus', 'load', 'reset', 'resize', 'scroll', 'select', 'submit', 'unload' event_name = :initEvent event_type = 'HTMLEvents' event_args = [event, true, true] when 'select' event_name = :initUIEvent event_type = 'UIEvent' event_args = [event, true, true, @container.page_container.document.parentWindow.window,0] when 'keydown', 'keypress', 'keyup' event_name = :initKeyboardEvent event_type = 'KeyboardEvent' # 'type', bubbles, cancelable, windowObject, ctrlKey, altKey, shiftKey, metaKey, keyCode, charCode event_args = [event, true, true, @container.page_container.document.parentWindow.window, false, false, false, false, 0, 0] when 'click', 'dblclick', 'mousedown', 'mousemove', 'mouseout', 'mouseover', 'mouseup', 'contextmenu', 'drag', 'dragstart', 'dragenter', 'dragover', 'dragleave', 'dragend', 'drop', 'selectstart' event_name = :initMouseEvent event_type = 'MouseEvents' # 'type', bubbles, cancelable, windowObject, detail, screenX, screenY, clientX, clientY, ctrlKey, altKey, shiftKey, metaKey, button, relatedTarget event_args = [event, true, true, @container.page_container.document.parentWindow.window, 1, 0, 0, 0, 0, false, false, false, false, 0, @container.page_container.document] else raise UnhandledEventException, "Don't know how to trigger event '#{event}'" end event = @container.page_container.document.createEvent(event_type) event.send event_name, *event_args event end # Return an array with many of the properties, in a format to be used by the to_s method def string_creator n = [] n << "id:".ljust(TO_S_SIZE) + self.id.to_s n end # This method is responsible for setting colored highlighting on the currently active element. def set_highlight perform_highlight do @original_color = ole_object.style.backgroundColor ole_object.style.backgroundColor = @container.active_object_highlight_color end end # This method is responsible for clearing colored highlighting on the currently active element. def clear_highlight perform_highlight do ole_object.style.backgroundColor = @original_color if @original_color end end def perform_highlight yield rescue # we could be here for a number of reasons... # e.g. page may have reloaded and the reference is no longer valid end def replace_method(method) method == 'click' ? 'click!' : method end def build_method(method_name, *args) arguments = args.map do |argument| if argument.is_a?(String) argument = "'#{argument}'" else argument = argument.inspect end end "#{replace_method(method_name)}(#{arguments.join(',')})" end def generate_ruby_code(element, method_name, *args) # needs to be done like this to avoid segfault on ruby 1.9.3 tag_name = @specifiers[:tag_name].join("' << '") element = "#{self.class}.new(#{@page_container.attach_command}, :tag_name => Array.new << '#{tag_name}', :unique_number => #{unique_number})" method = build_method(method_name, *args) ruby_code = "$:.unshift(#{$LOAD_PATH.map {|p| "'#{p}'" }.join(").unshift(")});" << "require '#{File.expand_path(File.dirname(__FILE__))}/../watir-classic';#{element}.#{method};" ruby_code end def spawned_no_wait_command(command) command = "-e #{command.inspect}" unless $DEBUG "start rubyw #{command}" else puts "#no_wait command:" command = "ruby #{command}" puts command command end end def perform_action assert_exists assert_enabled set_highlight yield ensure clear_highlight end end end