class Insite::Component

Attributes

component_elements[R]
args[R]
browser[R]
non_relative[R]
query_scope[R]
selector[R]
site[R]
target[R]
type[R]

Public Class Methods

collection?() click to toggle source
# File lib/insite/component/component.rb, line 39
def self.collection?
  false
end
inherited(subclass) click to toggle source
# File lib/insite/component/component.rb, line 43
def self.inherited(subclass)
  name_string     = subclass.name.demodulize.underscore
  collection_name = name_string + '_collection'

  if name_string == name_string.pluralize
    collection_method_name = name_string + 'es'
  else
    collection_method_name = name_string.pluralize
  end

  # Create a collection class for a component when a component is defined.
  collection_class = Class.new(Insite::ComponentCollection) do
    attr_reader :collection_member_type
    @collection_member_type = subclass
  end
  Insite.const_set(collection_name.camelize, collection_class)

  # Defines class-level methods for defining component accessor methods.
  # Does this for both the individual instance of the component AND the
  # collection. When these methods are call within page objects, they define
  # accessor methods for components and component collections when an
  # INSTANCE of the page object is being used.
  #
  # If a block is provided when using a method to provide access to a
  # component a MODIFIED version of the component class is created within
  # the page object where the method is invoked.
  #
  # If no block is provided, the base component class is used without
  # modifications.
  {
    name_string => subclass,
    collection_method_name => collection_class
  }.each do |nstring, klass|
    ComponentMethods.send(:define_method, nstring) do |mname, *a, &block|
      unless nstring == 'Component'
        @component_elements ||= []
        unless @component_elements.include?(mname.to_sym)
          @component_elements << mname.to_sym
        end

        # One way or another there must be some arguments to identify the
        # component.
        if klass.selector.present?
          hsh = parse_args(a.to_a).merge(klass.selector)
        elsif a.present?
          hsh = parse_args(a)
        else
          raise(
            Insite::Errors::ComponentReferenceError,
            "Unable to initialize the #{nstring} component. No selector " \
            "options were defined in the component's class definition and no " \
            "selector options were defined in the class or class instance " \
            "method call."
          )
        end

        # Accessor instance method gets defined here.
        define_method(mname) do
          # If a block is provided then we need to create a modified version
          # of the component or component collection to contain the added
          # functionality. This new class gets created within the page object
          # class and its name is different from the base class.
          if block
            # Create the modified class UNLESS it's already there.
            new_class_name = "#{c}For#{name.to_s.camelcase}"
            unless self.class.const_defined? new_class_name
              target_class = Class.new(klass) do
                class_eval(&block) if block
              end
              const_set(new_class_name, new_klass)
            end
          else
            target_class = klass
          end

          target_class.new(self, hsh)
        end
      end
    end

    ComponentInstanceMethods.send(:define_method, nstring) do |*a|
      hsh = parse_args(a).merge(subclass.selector)
      klass.new(self, hsh)
    end
  end
end
new(query_scope, *args) click to toggle source

This method gets used 2 different ways. Most of the time, dom_type and args will be a symbol and a set of hash arguments that will be used to select an element.

In some cases, dom_type can also be a Watir DOM object, and in this case, the args are ignored and the component is initialized using the DOM object.

# File lib/insite/component/component.rb, line 196
def initialize(query_scope, *args)
  @site     = query_scope.class.ancestors.include?(Insite) ? query_scope : query_scope.site
  @browser  = @site.browser
  @component_elements = self.class.component_elements

  if args[0].is_a?(Insite::Element) || args[0].is_a?(Watir::Element)
    @args     = nil
    @target   = args[0].target
  elsif args[0].is_a?(Insite::ElementCollection) || args[0].is_a?(Watir::ElementCollection)
    @args     = nil
    @target   = args[0]
  else
    unless self.class.selector.present? || parse_args(args).present?
      raise(
        Insite::Errors::ComponentSelectorError,
        "Unable to initialize a #{self.class} Component for #{query_scope.class}. " \
        "A Component selector wasn't defined in this Component's class " \
        "definition and the method call did not include selector arguments.",
        caller
      )
    end

    @selector     = self.class.selector.merge(parse_args(args))
    @args         = @selector

    # Figure out the correct query scope.
    @non_relative = @args.delete(:non_relative) || false
    if @non_relative
      @query_scope = query_scope.site
    else
      query_scope.respond_to?(:target) ? obj = query_scope : obj = query_scope.site
      @query_scope = obj
    end

    # See if there's a Watir DOM method for the class. If not, then
    # initialize using the default HTML element.
    watir_class = Insite::CLASS_MAP.key(self.class)
    if watir_class && watir_class != Watir::HTMLElement
      @target = watir_class.new(@query_scope.target, @args)
    else
      @target = Watir::HTMLElement.new(@query_scope.target, @args)
    end

    # New webdriver approach.
    # begin
    #   @target.scroll.to
    #   sleep 0.1
    # rescue => e
    #   t = ::Time.now + 2
    #   while ::Time.now <= t do
    #     break if @target.present?
    #     sleep 0.1
    #   end
    # end
  end
end
select_by(hsh = {}) click to toggle source
# File lib/insite/component/component.rb, line 130
def self.select_by(hsh = {})
  tmp = selector.clone
  hsh.each do |k, v|
    if %i(css, xpath).include?(k) && tmp.keys.any? { |key| key != k }
      raise ArgumentError, "The :#{k} selector argument cannot be used in conjunction with other " \
      "selector arguments. Current selector arguments: :#{tmp.keys.join(", :")}."
      # "Current selector arguments: #{tmp.map { |k, v| "#{k}:"} }is not currently allowed for component definitions."
    elsif k == :tag_name && tmp[k] && v && tmp[k] != v
      raise(
        ArgumentError,
        "\n\nInvalid use of the :tag_name selector in the #{self} component class. This component inherits " \
        "from the #{superclass} component, which already defines #{superclass.selector[:tag_name]} as " \
        "the tag name. If you are intentionally trying to overwrite the tag name in the inherited class, " \
        "use #{self}.select_by! in the page definition in place of #{self}.select_by. Warning: The " \
        "select_by! method arguments overwrite the selector that were inherited from #{superclass}. " \
        "So if you DO use it you'll need to specify ALL of the selector needed to properly identify the " \
        "#{self} component.\n\n",
        caller
      )
    elsif tmp[k].is_a?(Array)
        tmp[k] = ([tmp[k]].flatten + [v].flatten).uniq
    else
      tmp[k] = v
    end
  end
  self.selector = tmp
end
select_by!(hsh = {}) click to toggle source
# File lib/insite/component/component.rb, line 172
def self.select_by!(hsh = {})
  self.selector = hsh
end

Public Instance Methods

attributes() click to toggle source
# File lib/insite/component/component.rb, line 176
def attributes
  nokogiri.xpath("//#{selector[:tag_name]}")[0].attributes.values.map do |x|
    [x.name, x.value]
  end.to_h
end
classes() click to toggle source
# File lib/insite/component/component.rb, line 182
def classes
  attribute('class').split
end
collection?() click to toggle source
# File lib/insite/component/component.rb, line 186
def collection?
  false
end
inspect() click to toggle source
# File lib/insite/component/component.rb, line 253
def inspect
  if @target.selector.present?
    s = @selector.to_s
  else
    s = '{element: (selenium element)}'
  end
  "#<#{self.class}: located: #{!!@target.element}; @selector=#{s}>"
end
method_missing(mth, *args, &block) click to toggle source

Delegates method calls down to the component's wrapped element if the element supports the method being called.

Supports dynamic link methods. Examples:

s.accounts_page account

# Nav to linked page only.
s.account_actions.edit_account_info

# Update linked page after nav:
s.account_actions.edit_account_info username: 'foo'

# Link with modal (if the modal requires args they should be passed as hash keys):
# s.hosted_pages.refresh_urls
# File lib/insite/component/component.rb, line 281
def method_missing(mth, *args, &block)
  if @target.respond_to? mth
    out = @target.send(mth, *args, &block)

    if out == @target
      self
    elsif out.is_a?(Watir::Element) || out.is_a?(Watir::ElementCollection)
      Insite::CLASS_MAP[out.class].new(@query_scope, out)
    else
      out
    end
  elsif @target.respond_to?(:to_subtype) &&
        @target.class.descendants.any? do |klass|
          klass.instance_methods.include?(mth)
        end
    out = @target.to_subtype.send(mth, *args, &block)
    if klass = Insite::CLASS_MAP[out.class]
      klass.new(@site, out)
    else
      out
    end
  else
    if args[0].is_a? Hash
      page_arguments = args[0]
    elsif args.empty?
      raise NoMethodError, "undefined method `#{mth}' for #{self}: #{self.class}."
    elsif args[0].nil?
      raise ArgumentError, "Optional argument for :#{mth} must be a hash. Got NilClass."
    else
      raise ArgumentError, "Optional argument must be a hash (got #{args[0].class}.)"
    end

    if present?
      # # TODO: Lame and overly specific.
      # # If it's a component we want to hover over it to ensure links are visible
      # # before trying to find them.
      # if self.is_a?(Component)
      #   t = ::Time.now
      #   puts t
      #   loop do
      #     begin
      #       scroll.to
      #       hover
      #       sleep 0.2
      #       break
      #     rescue => e
      #       break if ::Time.now > t + 10
      #       sleep 0.2
      #     end
      #
      #     break if present?
      #     break if ::Time.now > t + 10
      #   end
      # end

      # Dynamic helper method, returns DOM object for link (no validation).
      if mth.to_s =~ /_link$/
        return a(text: /^#{mth.to_s.sub(/_link$/, '').gsub('_', '.*')}/i)
      # Dynamic helper method, returns DOM object for button (no validation).
      elsif mth.to_s =~ /_button$/
        return button(value: /^#{mth.to_s.sub(/_button$/, '').gsub('_', '.*')}/i)
      # Dynamic helper method for links. If a match is found, clicks on the
      # link and performs follow up actions. Start by seeing if there's a
      # matching button and treat it as a method call if so.
    elsif !collection? && elem = as.to_a.find { |x| x.text =~ /^#{mth.to_s.gsub('_', '.*')}/i }
        elem.click
        sleep 1

        current_page = @site.page

        if page_arguments.present?

          if current_page.respond_to?(:submit)
            current_page.submit page_arguments
          elsif @browser.input(xpath: "//div[starts-with(@class,'Row') and last()]//input[@type='submit' and last()]").present?
            current_page.update_page page_arguments
            @browser.input(xpath: "//div[starts-with(@class,'Row') and last()]//input[@type='submit' and last()]").click
          end
          current_page = @site.page
        end
      # Dynamic helper method for buttons. If a match is found, clicks on the link and performs follow up actions.
    elsif !collection? && elem = buttons.to_a.find { |x| x.text =~ /^#{mth.to_s.gsub('_', '.*')}/i } # See if there's a matching button and treat it as a method call if so.
        elem.click
        sleep 1

        # TODO: Legacy support. Revisit.
        if @site.respond_to?(:modal) && @site.modal.present?
          @site.modal.continue(page_arguments)
        else
          current_page = @site.page

          if page_arguments.present?
            if current_page.respond_to?(:submit)
              current_page.submit page_arguments
            elsif @browser.input(xpath: "//div[starts-with(@class,'Row') and last()]//input[@type='submit' and last()]").present?
              current_page.update_page page_arguments
              @browser.input(xpath: "//div[starts-with(@class,'Row') and last()]//input[@type='submit' and last()]").click
            end
            current_page = @site.page
          end
        end
      else
        raise NoMethodError, "undefined method `#{mth}' for #{self.class}.", caller
      end
    else
      raise NoMethodError, "Unhandled method call `#{mth}' for #{self.class} (The component was not present in the DOM at the point that the method was called.)", caller
    end

    page_arguments.present? ? page_arguments : current_page
  end
end
present?() click to toggle source
# File lib/insite/component/component.rb, line 393
def present?
  sleep 0.1
  begin
    if @query_scope
      if @query_scope.present? && @target.present?
        true
      else
        false
      end
    else
      if @target.present?
        true
      else
        false
      end
    end
  rescue => e
    false
  end
end

Private Instance Methods

merge_selector_args(other = {}) click to toggle source
# File lib/insite/component/component.rb, line 415
def merge_selector_args(other = {})
  tmp = self.class.selector.clone

  if tmp.empty? && other.empty?
    raise ArgumentError, "No selector values have been specified for the " \
    "#{self.class} component. And no selector arguments were specified " \
    "when calling the instance component's accessor method. "
  end

  other.each do |k, v|
    if k == :tag_name && tmp[k] != v
      raise(
        ArgumentError,
        "\n\nInvalid use of the :tag_name selector in the #{self} component class. This component inherits " \
        "from the #{superclass} component, which already defines #{superclass.selector[:tag_name]} as " \
        "the tag name. If you are intentionally trying to overwrite the tag name in the inherited class, " \
        "use #{self}.select_by! in the page definition in place of #{self}.select_by. Warning: The " \
        "select_by! method arguments overwrite the selector arguments that were inherited from #{superclass}. " \
        "So if you DO use it you'll need to specify ALL of the selector needed to properly identify the " \
        "#{self} component (because nothing will be inherited.)\n\n",
        caller
      )
    elsif tmp[k].is_a?(Array) && v.is_a?(Array) # TODO: class check here?
      tmp[k] = (tmp[k].flatten + [v].flatten).uniq
    else
      tmp[k] = v
    end
  end

  tmp
end
process_value(value) click to toggle source
# File lib/insite/component/component.rb, line 447
def process_value(value)
  value.is_a?(Regexp) ? value : /^#{value}/i
end