insite 0.0.2 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/lib/insite.rb +13 -7
  3. data/lib/insite/component/component.rb +454 -0
  4. data/lib/insite/component/component_collection.rb +112 -0
  5. data/lib/insite/component/component_instance_methods.rb +4 -0
  6. data/lib/insite/component/component_methods.rb +4 -0
  7. data/lib/insite/constants.rb +323 -31
  8. data/lib/insite/element/element.rb +147 -0
  9. data/lib/insite/element/element_collection.rb +102 -0
  10. data/lib/insite/element/generated/class_map.rb +244 -0
  11. data/lib/insite/element/generated/element_classes.rb +721 -0
  12. data/lib/insite/element/generated/element_instance_methods.rb +1594 -0
  13. data/lib/insite/errors.rb +2 -0
  14. data/lib/insite/examples/material_angular_io/components/angular_material_component.rb +13 -0
  15. data/lib/insite/examples/material_angular_io/components/example_viewer.rb +5 -0
  16. data/lib/insite/examples/material_angular_io/components/mat_chip.rb +29 -0
  17. data/lib/insite/examples/material_angular_io/components/mat_chip_list.rb +21 -0
  18. data/lib/insite/examples/material_angular_io/components/mat_form_field.rb +5 -0
  19. data/lib/insite/examples/material_angular_io/components/mat_icon.rb +5 -0
  20. data/lib/insite/examples/material_angular_io/components/mat_input.rb +5 -0
  21. data/lib/insite/examples/material_angular_io/components/mat_option.rb +3 -0
  22. data/lib/insite/examples/material_angular_io/components/mat_select.rb +15 -0
  23. data/lib/insite/examples/material_angular_io/components/mat_select_content.rb +13 -0
  24. data/lib/insite/examples/material_angular_io/components/no_selector.rb +3 -0
  25. data/lib/insite/examples/material_angular_io/pages.rb +20 -0
  26. data/lib/insite/examples/material_angular_io/site.rb +5 -0
  27. data/lib/insite/examples/material_angular_io/utils.rb +6 -0
  28. data/lib/insite/examples/material_angular_io/watir_mods.rb +54 -0
  29. data/lib/insite/examples/material_angular_io_site.rb +54 -0
  30. data/lib/insite/insite.rb +96 -11
  31. data/lib/insite/methods/common_methods.rb +26 -37
  32. data/lib/insite/methods/dom_methods.rb +73 -46
  33. data/lib/insite/page/defined_page.rb +37 -50
  34. data/lib/insite/page/undefined_page.rb +12 -1
  35. data/lib/insite/version.rb +1 -1
  36. metadata +69 -29
  37. data/lib/insite/element_container/element_container.rb +0 -42
  38. data/lib/insite/feature/feature.rb +0 -30
  39. data/lib/insite/widget/widget.rb +0 -346
  40. data/lib/insite/widget/widget_methods.rb +0 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 50349c8eb34288db09aeb89ea128765a6fe0db4b
4
- data.tar.gz: 322d58aadfca0d5108eb35624d92c363fe07608f
3
+ metadata.gz: db6875ab42b0dc5e67d04b58ed8e2784688c3638
4
+ data.tar.gz: a634d3df6da0647fda5665f615a1dec6282f027f
5
5
  SHA512:
6
- metadata.gz: 8d5314d34235fbfb176c028ce011ced5a2a7a609c5fae7da8c5da7c52f669f74f6935c5766d5c91d83ec85a5a62970f25336f2dfff962141461806d19c6fd0b0
7
- data.tar.gz: 01fac2213e909e42c9f61420579677a10a10980858185a93d4b11b15431111bea3e082f77c981ffeb0f35eaa13a18712313171265cc463f07eb9e50777104de6
6
+ metadata.gz: b17d8b1d92af6f10c38b17999cc69a1c9895d408a3a571a34c634bd91ffbd80eda9ab08e662b61c5bcb918feb9adb810a3e06910fc5a1d97bdca0eb5076e191c
7
+ data.tar.gz: 4f4b65feeaa634894a8a237d0863921524ad5ffed17d8439aa4fdb934d95aff16dea9b6faf66c0b3a5a4da6c6b717ac6cd69d446d56951fa0fafcd1f90a5eb93
data/lib/insite.rb CHANGED
@@ -14,15 +14,21 @@ require "insite/version"
14
14
  require "insite/methods/dom_methods"
15
15
  require "insite/methods/common_methods"
16
16
 
17
- # Files for Insite::Widget.
18
- require "insite/widget/widget"
19
- require "insite/widget/widget_methods"
17
+ # Files for Insite element wrapper classes.
18
+ require "insite/element/generated/element_instance_methods"
19
+ require "insite/element/element"
20
+ require "insite/element/element_collection"
21
+ require "insite/element/generated/element_classes"
22
+ require "insite/element/generated/class_map"
20
23
 
21
- # Files for Insite::Feature.
22
- require "insite/feature/feature"
24
+ # Files for Insite::Component.
25
+ require "insite/component/component_instance_methods"
26
+ require "insite/component/component"
27
+ require "insite/component/component_collection"
28
+ require "insite/component/component_methods"
23
29
 
24
- # Files for ElementContainer.
25
- require "insite/element_container/element_container"
30
+ # Files for Insite::Feature.
31
+ # require "insite/feature/feature"
26
32
 
27
33
  # Files for pages (defined/undefined.)
28
34
  require "insite/page/defined_page"
@@ -0,0 +1,454 @@
1
+ require_relative 'component_methods'
2
+ require_relative 'component_collection'
3
+ require_relative 'component_instance_methods'
4
+ require_relative '../methods/common_methods'
5
+ require 'pry'
6
+ # Allows the page object developer to encapsulate common web application features
7
+ # into components that can be reused across multiple pages.
8
+ module Insite
9
+ class Component
10
+ attr_reader :args, :browser, :non_relative, :selector, :site, :type, :target
11
+ class_attribute :selector, default: {}
12
+ self.selector = self.selector.clone
13
+
14
+ include Insite::CommonMethods
15
+ extend Insite::DOMMethods
16
+ include Insite::ElementInstanceMethods
17
+ extend Insite::ComponentMethods
18
+ include Insite::ComponentInstanceMethods
19
+ alias_method :update_component, :update_object
20
+
21
+ class << self
22
+ attr_reader :component_elements
23
+
24
+ # - Don't allow the user to create a component with a name that matches a DOM
25
+ # element.
26
+ #
27
+ # - Don't allow the user to create a component method that references a
28
+ # collection (because this will be done automatically.)
29
+ tmp = name.to_s.underscore.to_sym
30
+ if DOM_METHODS.include?(name.to_s.underscore.to_sym)
31
+ raise "#{name} cannot be used as a component name, as the methodized version of the class name (#{name.to_s.underscore} conflicts with an Insite DOM method.)"
32
+ elsif Watir::Browser.methods.include?(name.to_s.underscore.to_sym)
33
+ raise "#{name} cannot be used as a component name, as the methodized version of the class name (#{name.to_s.underscore} conflicts with a Insite::Browser method.)"
34
+ end
35
+
36
+ if tmp =~ /.*s+/
37
+ raise "Invalid component type :#{tmp}. You can create a component for the DOM object but it must be for :#{tmp.singularize} (:#{tmp} will be created automatically.)"
38
+ end
39
+
40
+ end # Self.
41
+
42
+ extend Forwardable
43
+
44
+ def self.collection?
45
+ false
46
+ end
47
+
48
+ def self.inherited(subclass)
49
+ name_string = subclass.name.demodulize.underscore
50
+ collection_name = name_string + '_collection'
51
+
52
+ if name_string == name_string.pluralize
53
+ collection_method_name = name_string + 'es'
54
+ else
55
+ collection_method_name = name_string.pluralize
56
+ end
57
+
58
+ # Create a collection class for a component when a component is defined.
59
+ collection_class = Class.new(Insite::ComponentCollection) do
60
+ attr_reader :collection_member_type
61
+ @collection_member_type = subclass
62
+ end
63
+ Insite.const_set(collection_name.camelize, collection_class)
64
+
65
+ # Defines class-level methods for defining component accessor methods.
66
+ # Does this for both the individual instance of the component AND the
67
+ # collection. When these methods are call within page objects, they define
68
+ # accessor methods for components and component collections when an
69
+ # INSTANCE of the page object is being used.
70
+ #
71
+ # If a block is provided when using a method to provide access to a
72
+ # component a MODIFIED version of the component class is created within
73
+ # the page object where the method is invoked.
74
+ #
75
+ # If no block is provided, the base component class is used without
76
+ # modifications.
77
+ {
78
+ name_string => subclass,
79
+ collection_method_name => collection_class
80
+ }.each do |nstring, klass|
81
+ ComponentMethods.send(:define_method, nstring) do |mname, *a, &block|
82
+ unless nstring == 'Component'
83
+ @component_elements ||= []
84
+ unless @component_elements.include?(mname.to_sym)
85
+ @component_elements << mname.to_sym
86
+ end
87
+
88
+ # One way or another there must be some arguments to identify the
89
+ # component.
90
+ if klass.selector
91
+ hsh = parse_args(a.to_a).merge(klass.selector)
92
+ elsif a.present?
93
+ hsh = parse_args(a)
94
+ else
95
+ raise(
96
+ Insite::Errors::ComponentReferenceError,
97
+ "Unable to initialize #{nstring}. Base selector options were " \
98
+ "not defined in the component's class definition and no " \
99
+ "selector options were defined in the class or class instance " \
100
+ "method call."
101
+ )
102
+ end
103
+
104
+ # Accessor instance method gets defined here.
105
+ define_method(mname) do
106
+ # If a block is provided then we need to create a modified version
107
+ # of the component or component collection to contain the added
108
+ # functionality. This new class gets created within the page object
109
+ # class and its name is different from the base class.
110
+ if block
111
+ # Create the modified class UNLESS it's already there.
112
+ new_class_name = "#{c}For#{name.to_s.camelcase}"
113
+ unless self.class.const_defined? new_class_name
114
+ target_class = Class.new(klass) do
115
+ class_eval(&block) if block
116
+ end
117
+ const_set(new_class_name, new_klass)
118
+ end
119
+ else
120
+ target_class = klass
121
+ end
122
+
123
+ target_class.new(self, hsh)
124
+ end
125
+ end
126
+ end
127
+
128
+ ComponentInstanceMethods.send(:define_method, nstring) do |*a|
129
+ hsh = parse_args(a).merge(subclass.selector)
130
+ klass.new(self, hsh)
131
+ end
132
+ end
133
+ end # self.inherited
134
+
135
+ def self.select_by(hsh = {})
136
+ tmp = selector.clone
137
+ hsh.each do |k, v|
138
+ if %i(css, xpath).include? k
139
+ raise ArgumentError, "The :#{k} selector argument is not currently allowed for component definitions."
140
+ elsif k == :tag_name && tmp[k] && v && tmp[k] != v
141
+ raise(
142
+ ArgumentError,
143
+ "\n\nInvalid use of the :tag_name selector in the #{self} component class. This component inherits " \
144
+ "from the #{superclass} component, which already defines #{superclass.selector[:tag_name]} as " \
145
+ "the tag name. If you are intentionally trying to overwrite the tag name in the inherited class, " \
146
+ "use #{self}.select_by! in the page definition in place of #{self}.select_by. Warning: The " \
147
+ "select_by! method arguments overwrite the selector that were inherited from #{superclass}. " \
148
+ "So if you DO use it you'll need to specify ALL of the selector needed to properly identify the " \
149
+ "#{self} component.\n\n",
150
+ caller
151
+ )
152
+ elsif tmp[k].is_a?(Array)
153
+ tmp[k] = ([tmp[k]].flatten + [v].flatten).uniq
154
+ else
155
+ tmp[k] = v
156
+ end
157
+ end
158
+ self.selector = tmp
159
+ end
160
+
161
+ def self.select_by!(hsh = {})
162
+ self.selector = hsh
163
+ end
164
+
165
+ def attributes
166
+ nokogiri.xpath("//#{selector[:tag_name]}")[0].attributes.values.map do |x|
167
+ [x.name, x.value]
168
+ end.to_h
169
+ end
170
+
171
+ def classes
172
+ attribute('class').split
173
+ end
174
+
175
+ def collection?
176
+ false
177
+ end
178
+
179
+ # This method gets used 2 different ways. Most of the time, dom_type and args
180
+ # will be a symbol and a set of hash arguments that will be used to select an
181
+ # element.
182
+ #
183
+ # In some cases, dom_type can also be a Watir DOM object, and in this case, the
184
+ # args are ignored and the component is initialized using the DOM object.
185
+ #
186
+ # TODO: Needs a rewrite, lines between individual and collection are blurred
187
+ # here and that makes the code more confusing. And there should be a proper
188
+ # collection class for element collections, with possibly some AR-like accessors.
189
+ def initialize(parent, *args)
190
+ # Figure out the correct query scope.
191
+ parent.respond_to?(:target) ? obj = parent : obj = parent.site
192
+ @parent = obj
193
+
194
+ # @parent = parent
195
+ @site = parent.class.ancestors.include?(Insite) ? parent : parent.site
196
+ @browser = @site.browser
197
+ @component_elements = self.class.component_elements
198
+
199
+ if args[0].is_a?(Insite::Element) || args[0].is_a?(Insite::ElementCollection)
200
+ @dom_type = nil
201
+ @args = nil
202
+ @target = args[0].target
203
+ elsif args[0].is_a?(Watir::Element) || args[0].is_a?(Watir::ElementCollection)
204
+ @dom_type = nil
205
+ @args = nil
206
+ @target = args[0]
207
+ else
208
+ unless self.class.selector.present? || parse_args(args).present?
209
+ raise(
210
+ Insite::Errors::ComponentSelectorError,
211
+ "Unable to initialize a #{self.class} Component for #{parent.class}. " \
212
+ "A Component selector wasn't defined in this Component's class " \
213
+ "definition and the method call did not include selector arguments.",
214
+ caller
215
+ )
216
+ end
217
+
218
+ @selector = self.class.selector.merge(parse_args(args))
219
+ @args = @selector
220
+ @non_relative = @args.delete(:non_relative) || false
221
+
222
+ if @non_relative
223
+ # @args = parse_args(args)
224
+ # @selector = @args
225
+ @target = @browser.send(@args)
226
+ else
227
+ # @args = parse_args(args)
228
+
229
+ # # Figure out the correct query scope.
230
+ # @parent.respond_to?(:target) ? obj = @parent.target : obj = @browser
231
+
232
+ # See if there's a Watir DOM method for the class. If not, then
233
+ # initialize using the default collection.
234
+ if watir_class = Insite::CLASS_MAP.key(self.class)
235
+ @target = watir_class.new(@parent.target, @args)
236
+ # @target = watir_class.new(obj, @args)
237
+ else
238
+ @target = Watir::HTMLElement.new(@parent.target, @args)
239
+ end
240
+ end
241
+
242
+ # New webdriver approach.
243
+ # begin
244
+ # @target.scroll.to
245
+ # sleep 0.1
246
+ # rescue => e
247
+ # t = ::Time.now + 2
248
+ # while ::Time.now <= t do
249
+ # break if @target.present?
250
+ # sleep 0.1
251
+ # end
252
+ # end
253
+ end
254
+ end
255
+
256
+ def inspect
257
+ if @target.selector.present?
258
+ s = @selector.to_s
259
+ else
260
+ s = '{element: (selenium element)}'
261
+ end
262
+ "#<#{self.class}: located: #{!!@target.element}; @selector=#{s}>"
263
+ end
264
+
265
+ # def inspect
266
+ # @selector.empty? ? s = '{element: (selenium element)}' : s = @selector.to_s
267
+ # "#<#{self.class}: located: #{!!@target.element}; @selector=#{s}>"
268
+ # end
269
+
270
+ # Delegates method calls down to the component's wrapped element if the
271
+ # element supports the method being called.
272
+ #
273
+ # Supports dynamic link methods. Examples:
274
+ # s.accounts_page account
275
+ #
276
+ # # Nav to linked page only.
277
+ # s.account_actions.edit_account_info
278
+ #
279
+ # # Update linked page after nav:
280
+ # s.account_actions.edit_account_info username: 'foo'
281
+ #
282
+ # # Link with modal (if the modal requires args they should be passed as hash keys):
283
+ # # s.hosted_pages.refresh_urls
284
+ def method_missing(mth, *args, &block)
285
+ if @target.respond_to? mth
286
+ out = @target.send(mth, *args, &block)
287
+
288
+ if out == @target
289
+ self
290
+ elsif out.is_a?(Watir::Element) || out.is_a?(Watir::ElementCollection)
291
+ Insite::CLASS_MAP[out.class].new(@parent, out)
292
+ else
293
+ out
294
+ end
295
+ elsif @target.respond_to?(:to_subtype) &&
296
+ @target.class.descendants.any? do |klass|
297
+ klass.instance_methods.include?(mth)
298
+ end
299
+ out = @target.to_subtype.send(mth, *args, &block)
300
+ if klass = Insite::CLASS_MAP[out.class]
301
+ klass.new(@site, out)
302
+ else
303
+ out
304
+ end
305
+ else
306
+ if args[0].is_a? Hash
307
+ page_arguments = args[0]
308
+ elsif args.empty?
309
+ raise NoMethodError, "undefined method `#{mth}' for #{self}: #{self.class}."
310
+ elsif args[0].nil?
311
+ raise ArgumentError, "Optional argument for :#{mth} must be a hash. Got NilClass."
312
+ else
313
+ raise ArgumentError, "Optional argument must be a hash (got #{args[0].class}.)"
314
+ end
315
+
316
+ if present?
317
+ # # TODO: Lame and overly specific.
318
+ # # If it's a component we want to hover over it to ensure links are visible
319
+ # # before trying to find them.
320
+ # if self.is_a?(Component)
321
+ # t = ::Time.now
322
+ # puts t
323
+ # loop do
324
+ # begin
325
+ # scroll.to
326
+ # hover
327
+ # sleep 0.2
328
+ # break
329
+ # rescue => e
330
+ # break if ::Time.now > t + 10
331
+ # sleep 0.2
332
+ # end
333
+ #
334
+ # break if present?
335
+ # break if ::Time.now > t + 10
336
+ # end
337
+ # end
338
+
339
+ # Dynamic helper method, returns DOM object for link (no validation).
340
+ if mth.to_s =~ /_link$/
341
+ return a(text: /^#{mth.to_s.sub(/_link$/, '').gsub('_', '.*')}/i)
342
+ # Dynamic helper method, returns DOM object for button (no validation).
343
+ elsif mth.to_s =~ /_button$/
344
+ return button(value: /^#{mth.to_s.sub(/_button$/, '').gsub('_', '.*')}/i)
345
+ # Dynamic helper method for links. If a match is found, clicks on the
346
+ # link and performs follow up actions. Start by seeing if there's a
347
+ # matching button and treat it as a method call if so.
348
+ elsif !collection? && elem = as.to_a.find { |x| x.text =~ /^#{mth.to_s.gsub('_', '.*')}/i }
349
+ elem.click
350
+ sleep 1
351
+
352
+ current_page = @site.page
353
+
354
+ if page_arguments.present?
355
+
356
+ if current_page.respond_to?(:submit)
357
+ current_page.submit page_arguments
358
+ elsif @browser.input(xpath: "//div[starts-with(@class,'Row') and last()]//input[@type='submit' and last()]").present?
359
+ current_page.update_page page_arguments
360
+ @browser.input(xpath: "//div[starts-with(@class,'Row') and last()]//input[@type='submit' and last()]").click
361
+ end
362
+ current_page = @site.page
363
+ end
364
+ # Dynamic helper method for buttons. If a match is found, clicks on the link and performs follow up actions.
365
+ 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.
366
+ elem.click
367
+ sleep 1
368
+
369
+ # TODO: Legacy support. Revisit.
370
+ if @site.respond_to?(:modal) && @site.modal.present?
371
+ @site.modal.continue(page_arguments)
372
+ else
373
+ current_page = @site.page
374
+
375
+ if page_arguments.present?
376
+ if current_page.respond_to?(:submit)
377
+ current_page.submit page_arguments
378
+ elsif @browser.input(xpath: "//div[starts-with(@class,'Row') and last()]//input[@type='submit' and last()]").present?
379
+ current_page.update_page page_arguments
380
+ @browser.input(xpath: "//div[starts-with(@class,'Row') and last()]//input[@type='submit' and last()]").click
381
+ end
382
+ current_page = @site.page
383
+ end
384
+ end
385
+ else
386
+ raise NoMethodError, "undefined method `#{mth}' for #{self.class}.", caller
387
+ end
388
+ else
389
+ 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
390
+ end
391
+
392
+ page_arguments.present? ? page_arguments : current_page
393
+ end
394
+ end
395
+
396
+ def present?
397
+ sleep 0.1
398
+ begin
399
+ if @parent
400
+ if @parent.present? && @target.present?
401
+ true
402
+ else
403
+ false
404
+ end
405
+ else
406
+ if @target.present?
407
+ true
408
+ else
409
+ false
410
+ end
411
+ end
412
+ rescue => e
413
+ false
414
+ end
415
+ end
416
+
417
+ private
418
+ def merge_selector_args(other = {})
419
+ tmp = self.class.selector.clone
420
+
421
+ if tmp.empty? && other.empty?
422
+ raise ArgumentError, "No selector values have been specified for the " \
423
+ "#{self.class} component. And no selector arguments were specified " \
424
+ "when calling the instance component's accessor method. "
425
+ end
426
+
427
+ other.each do |k, v|
428
+ if k == :tag_name && tmp[k] != v
429
+ raise(
430
+ ArgumentError,
431
+ "\n\nInvalid use of the :tag_name selector in the #{self} component class. This component inherits " \
432
+ "from the #{superclass} component, which already defines #{superclass.selector[:tag_name]} as " \
433
+ "the tag name. If you are intentionally trying to overwrite the tag name in the inherited class, " \
434
+ "use #{self}.select_by! in the page definition in place of #{self}.select_by. Warning: The " \
435
+ "select_by! method arguments overwrite the selector arguments that were inherited from #{superclass}. " \
436
+ "So if you DO use it you'll need to specify ALL of the selector needed to properly identify the " \
437
+ "#{self} component (because nothing will be inherited.)\n\n",
438
+ caller
439
+ )
440
+ elsif tmp[k].is_a?(Array) && v.is_a?(Array) # TODO: class check here?
441
+ tmp[k] = (tmp[k].flatten + [v].flatten).uniq
442
+ else
443
+ tmp[k] = v
444
+ end
445
+ end
446
+
447
+ tmp
448
+ end
449
+
450
+ def process_value(value)
451
+ value.is_a?(Regexp) ? value : /^#{value}/i
452
+ end
453
+ end
454
+ end