Class: Insite::Component

Inherits:
Object
  • Object
show all
Extended by:
Forwardable, ComponentMethods, DOMMethods
Includes:
CommonMethods, ComponentInstanceMethods, ElementInstanceMethods
Defined in:
lib/insite/component/component.rb

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from ElementInstanceMethods

#a, #abbr, #abbrs, #address, #addresses, #area, #areas, #article, #articles, #as, #aside, #asides, #audio, #audios, #b, #base, #bases, #bdi, #bdis, #bdo, #bdos, #blockquote, #blockquotes, #body, #bodys, #br, #brs, #bs, #button, #buttons, #canvas, #canvases, #caption, #captions, #cell, #cells, #checkbox, #checkboxes, #circle, #circles, #cite, #cites, #code, #codes, #col, #colgroup, #colgroups, #cols, #data, #datalist, #datalists, #datas, #date_field, #date_fields, #date_time_field, #date_time_fields, #dd, #dds, #defs, #defss, #del, #dels, #desc, #descs, #details, #detailses, #dfn, #dfns, #div, #divs, #dl, #dls, #dt, #dts, #element, #elements, #ellipse, #ellipses, #em, #embed, #embeds, #ems, #field_set, #field_sets, #fieldset, #fieldsets, #figcaption, #figcaptions, #figure, #figures, #file_field, #file_fields, #font, #fonts, #footer, #footers, #foreign_object, #foreign_objects, #form, #forms, #frame, #frames, #frameset, #framesets, #g, #gs, #h1, #h1s, #h2, #h2s, #h3, #h3s, #h4, #h4s, #h5, #h5s, #h6, #h6s, #head, #header, #headers, #heads, #hidden, #hiddens, #hr, #hrs, #htmls, #i, #iframe, #iframes, #image, #images, #img, #imgs, #input, #inputs, #ins, #inses, #is, #kbd, #kbds, #label, #labels, #legend, #legends, #li, #line, #linear_gradient, #linear_gradients, #lines, #link, #links, #lis, #main, #mains, #map, #maps, #mark, #marker, #markers, #marks, #meta, #metadata, #metadatas, #metas, #meter, #meters, #nav, #navs, #noscript, #noscripts, #object, #objects, #ol, #ols, #optgroup, #optgroups, #option, #options, #output, #outputs, #p, #param, #params, #path, #paths, #pattern, #patterns, #picture, #pictures, #polygon, #polygons, #polyline, #polylines, #pre, #pres, #progress, #progresses, #ps, #q, #qs, #radial_gradient, #radial_gradients, #radio, #radios, #rb, #rbs, #rect, #rects, #row, #rows, #rp, #rps, #rt, #rtc, #rtcs, #rts, #rubies, #ruby, #s, #samp, #samps, #script, #scripts, #section, #sections, #select, #select_list, #select_lists, #selects, #small, #smalls, #source, #sources, #span, #spans, #ss, #stop, #stops, #strong, #strongs, #style, #styles, #sub, #subs, #summaries, #summary, #sup, #sups, #svg, #svgs, #switch, #switches, #symbol, #symbols, #table, #tables, #tbody, #tbodys, #td, #tds, #template, #templates, #text_field, #text_fields, #text_path, #text_paths, #textarea, #textareas, #tfoot, #tfoots, #th, #thead, #theads, #ths, #time, #times, #titles, #tr, #track, #tracks, #trs, #tspan, #tspans, #u, #ul, #uls, #us, #use, #uses, #var, #vars, #video, #videos, #view, #views, #wbr, #wbrs

Methods included from CommonMethods

#document, #process_browser, #update_object

Constructor Details

#initialize(query_scope, *args) ⇒ Component

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.



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
# 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

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(mth, *args, &block) ⇒ Object

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 

# Nav to linked page only.
s..

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

# Link with modal (if the modal requires args they should be passed as hash keys):
# s.hosted_pages.refresh_urls


281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
# 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

Class Attribute Details

.component_elementsObject (readonly)

Returns the value of attribute component_elements.



22
23
24
# File 'lib/insite/component/component.rb', line 22

def component_elements
  @component_elements
end

Instance Attribute Details

#argsObject (readonly)

Returns the value of attribute args.



10
11
12
# File 'lib/insite/component/component.rb', line 10

def args
  @args
end

#browserObject (readonly)

Returns the value of attribute browser.



10
11
12
# File 'lib/insite/component/component.rb', line 10

def browser
  @browser
end

#non_relativeObject (readonly)

Returns the value of attribute non_relative.



10
11
12
# File 'lib/insite/component/component.rb', line 10

def non_relative
  @non_relative
end

#query_scopeObject (readonly)

Returns the value of attribute query_scope.



10
11
12
# File 'lib/insite/component/component.rb', line 10

def query_scope
  @query_scope
end

#selectorObject (readonly)

Returns the value of attribute selector.



10
11
12
# File 'lib/insite/component/component.rb', line 10

def selector
  @selector
end

#siteObject (readonly)

Returns the value of attribute site.



10
11
12
# File 'lib/insite/component/component.rb', line 10

def site
  @site
end

#targetObject (readonly)

Returns the value of attribute target.



10
11
12
# File 'lib/insite/component/component.rb', line 10

def target
  @target
end

#typeObject (readonly)

Returns the value of attribute type.



10
11
12
# File 'lib/insite/component/component.rb', line 10

def type
  @type
end

Class Method Details

.collection?Boolean

Returns:

  • (Boolean)


39
40
41
# File 'lib/insite/component/component.rb', line 39

def self.collection?
  false
end

.inherited(subclass) ⇒ Object



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# 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

.select_by(hsh = {}) ⇒ Object

self.inherited



130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# 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 = {}) ⇒ Object



172
173
174
# File 'lib/insite/component/component.rb', line 172

def self.select_by!(hsh = {})
  self.selector = hsh
end

Instance Method Details

#attributesObject



176
177
178
179
180
# 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

#classesObject



182
183
184
# File 'lib/insite/component/component.rb', line 182

def classes
  attribute('class').split
end

#collection?Boolean

Returns:

  • (Boolean)


186
187
188
# File 'lib/insite/component/component.rb', line 186

def collection?
  false
end

#inspectObject



253
254
255
256
257
258
259
260
# 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

#present?Boolean

Returns:

  • (Boolean)


393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
# 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