insite 0.0.2 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
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