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
data/lib/insite/errors.rb CHANGED
@@ -6,10 +6,12 @@ module Insite
6
6
  class Insite::Errors::BrowserNotOpenError < StandardError; end
7
7
  class Insite::Errors::BrowserNotValidError < StandardError; end
8
8
  class Insite::Errors::BrowserResponseError < StandardError; end
9
+ class Insite::Errors::ComponentReferenceError < StandardError; end
9
10
  class Insite::Errors::PageConfigError < StandardError; end
10
11
  class Insite::Errors::PageInitError < StandardError; end
11
12
  class Insite::Errors::PageNavigationError < StandardError; end
12
13
  class Insite::Errors::PageNavigationNotAllowedError < StandardError; end
14
+ class Insite::Errors::ComponentSelectorError < StandardError; end
13
15
  class Insite::Errors::SiteInitError < StandardError; end
14
16
  class Insite::Errors::WrongPageError < StandardError; end
15
17
  end
@@ -0,0 +1,13 @@
1
+ class AngularMaterialComponent < MaterialAngularIO::Component
2
+ attr_reader :ngcontent, :nghost
3
+
4
+ def ngcontent
5
+ @ngcontent ||=
6
+ attributes.keys.find { |k| k.match(/^_ngcontent-.*/) }
7
+ end
8
+
9
+ def nghost
10
+ @nghost ||=
11
+ attributes.keys.find { |k| k.match(/^_nghost-.*/) }
12
+ end
13
+ end
@@ -0,0 +1,5 @@
1
+ require_relative 'angular_material_component'
2
+
3
+ class ExampleViewer < MaterialAngularIO::Component
4
+ select_by tag_name: 'example-viewer'
5
+ end
@@ -0,0 +1,29 @@
1
+ require_relative 'angular_material_component'
2
+
3
+ class MatChip < MaterialAngularIO::Component
4
+ select_by tag_name: 'mat-chip'
5
+
6
+ def disabled?
7
+ element(class: 'mat-chip-disabled').exist?
8
+ end
9
+
10
+ def label
11
+ nokogiri.xpath('//text()')[0]
12
+ end
13
+
14
+ def remove
15
+ element(class: 'mat-chip-remove').click
16
+ end
17
+
18
+ def removable?
19
+ element(class: 'mat-chip-remove').exist?
20
+ end
21
+
22
+ def selected?
23
+ aria_selected == 'true'
24
+ end
25
+
26
+ def selectable?
27
+ element(class: 'mat-chip-select').exist?
28
+ end
29
+ end
@@ -0,0 +1,21 @@
1
+ require_relative 'angular_material_component'
2
+ require 'pry'
3
+ class MatChipList < AngularMaterialComponent
4
+ select_by tag_name: 'mat-chip-list'
5
+
6
+ def add(value)
7
+ mat_input(ngcontent => true).set(value + "\n")
8
+ end
9
+
10
+ def clear_input(value)
11
+ mat_input(ngcontent => true).clear
12
+ end
13
+
14
+ def remove(value)
15
+ mat_input(ngcontent => true).mat_icon.click
16
+ end
17
+
18
+ def set_input(value)
19
+ mat_input(ngcontent => true).set(value)
20
+ end
21
+ end
@@ -0,0 +1,5 @@
1
+ require_relative 'angular_material_component'
2
+
3
+ class MatFormField < MaterialAngularIO::Component
4
+ select_by tag_name: 'mat-form-field'
5
+ end
@@ -0,0 +1,5 @@
1
+ require_relative 'angular_material_component'
2
+
3
+ class MatIcon < MaterialAngularIO::Component
4
+ select_by tag_name: 'mat-icon'
5
+ end
@@ -0,0 +1,5 @@
1
+ require_relative 'angular_material_component'
2
+
3
+ class MatInput < MaterialAngularIO::Component
4
+ select_by tag_name: 'input', class: 'mat-input-element'
5
+ end
@@ -0,0 +1,3 @@
1
+ class MatOption < MaterialAngularIO::Component
2
+ select_by tag_name: 'mat-option'
3
+ end
@@ -0,0 +1,15 @@
1
+ class MatSelect < MaterialAngularIO::Component
2
+ select_by tag_name: 'mat-select'
3
+
4
+ def options(*args)
5
+ mat_options(parse_args(args).merge!(css: "[#{ngcontent}='']"))
6
+ end
7
+
8
+ def option(*args)
9
+ mat_option(parse_args(args).merge!(css: "[#{ngcontent}='']"))
10
+ end
11
+
12
+ def select(*values)
13
+ values.each { |val| option(text: process_value(val)) }.click
14
+ end
15
+ end
@@ -0,0 +1,13 @@
1
+ class MatSelectContent < MaterialAngularIO::Component
2
+ select_by class: 'mat-select-content'
3
+
4
+ # def select(**values)
5
+ # values.each do |v|
6
+ # if tmp = options.find { |opt| opt.text =~ /#{v}/i}
7
+ # tmp.click
8
+ # else
9
+ # raise ArgumentError, ""
10
+ # end
11
+ # end
12
+ # end
13
+ end
@@ -0,0 +1,3 @@
1
+ # For testing behavior when no selector is defined.
2
+ class NoSelector < MaterialAngularIO::Component
3
+ end
@@ -0,0 +1,20 @@
1
+ require_relative 'site'
2
+
3
+ # Auto-generates some of material.angular.io pages
4
+ %w(autocomplete checkbox datepicker form-field input radio
5
+ select slider slide-toggle menu sidenav toolbar card divider
6
+ expansion grid-list list stepper tabs tree button button-toggle
7
+ badge chips icon progress-spinner progress-bar bottom-sheet
8
+ dialog snackbar tooltip paginator sort table
9
+ ).each do |component|
10
+ %w(overview api examples).each do |category|
11
+ klass = Class.new(MaterialAngularIO::Page) do
12
+ set_url "/components/#{component}/#{category}"
13
+ end
14
+
15
+ Object.const_set(
16
+ "#{component.gsub('-', '_')}_#{category}_page".camelcase,
17
+ klass
18
+ )
19
+ end
20
+ end
@@ -0,0 +1,5 @@
1
+ class MaterialAngularIO
2
+ include Insite
3
+
4
+ set_custom_tags "app-component-nav", "app-component-sidenav"
5
+ end
@@ -0,0 +1,6 @@
1
+ def new_angular_material_session
2
+ s = MaterialAngularIO.new 'https://material.angular.io'
3
+ s.open
4
+ s.chips_overview_page
5
+ s
6
+ end
@@ -0,0 +1,54 @@
1
+ # https://github.com/watir/watir/pull/737
2
+ # TODO: Already merged to master, can remove after next release.
3
+ module Watir
4
+ module Locators
5
+ class Element
6
+ class SelectorBuilder
7
+ class XPath
8
+ # @todo Get rid of building
9
+ def lhs_for(_building, key)
10
+ case key
11
+ when :text, 'text'
12
+ 'normalize-space()'
13
+ when String
14
+ "@#{key}"
15
+ when :href
16
+ # TODO: change this behaviour?
17
+ 'normalize-space(@href)'
18
+ when :type
19
+ # type attributes can be upper case - downcase them
20
+ # https://github.com/watir/watir/issues/72
21
+ XpathSupport.downcase('@type')
22
+ else
23
+ "@#{key.to_s.tr("_", "-")}"
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ # https://github.com/watir/watir/blob/0943e7e6a88f58ad6bea0e33a9853e6f5676f31c/lib/watir/element_collection.rb
33
+ # TODO: Already merged to master, can remove after next release.
34
+ module Watir
35
+ class ElementCollection
36
+ def to_a
37
+ hash = {}
38
+ @to_a ||=
39
+ elements.map.with_index do |e, idx|
40
+ element = element_class.new(@query_scope, @selector.merge(element: e, index: idx))
41
+ if [Watir::HTMLElement, Watir::Input].include? element.class
42
+ tag_name = element.tag_name.to_sym
43
+ hash[tag_name] ||= 0
44
+ hash[tag_name] += 1
45
+ Watir.element_class_for(tag_name).new(@query_scope, @selector.merge(element: e,
46
+ tag_name: tag_name,
47
+ index: hash[tag_name] - 1))
48
+ else
49
+ element
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,54 @@
1
+ # Require support files.
2
+ %w(watir_mods site pages utils).each do |f|
3
+ require "insite/examples/material_angular_io/#{f}"
4
+ end
5
+
6
+ # Require hand-crafted artisanal components.
7
+ %w(example_viewer mat_chip_list mat_chip mat_icon mat_form_field
8
+ mat_input mat_option mat_select_content mat_select no_selector).each do |c|
9
+ require "insite/examples/material_angular_io/components/#{c}"
10
+ end
11
+
12
+ # Most of the pages for material.angular.io are auto-generated because
13
+ # their URLs match a pattern. But some custom page definitions are included
14
+ # below. This first overwrites the auto-generated page definition for
15
+ # the /chips/overview page, adding some additional stuff that is used
16
+ # in some of the automated tests.
17
+ class ChipsOverviewPage < MaterialAngularIO::Page
18
+ set_url "/components/chips/overview"
19
+
20
+ no_selector :test_no_selector
21
+ no_selector :test_no_selector_with_args, tag_name: :div, index: 3
22
+
23
+ elements :mat_examples_collection, tag_name: 'example-viewer'
24
+ elements :modified_mat_examples_collection, tag_name: 'example-viewer' do
25
+ def test_method
26
+ 'output'
27
+ end
28
+ end
29
+
30
+ elements :examples_collection, tag_name: 'example-viewer'
31
+ elements :modified_examples_collection, tag_name: 'example-viewer' do
32
+ def test_method
33
+ 'output'
34
+ end
35
+ end
36
+
37
+ mat_chip :chip, tag_name: 'mat-chip'
38
+ element :modified_chip, tag_name: 'mat-chip' do
39
+ def test_method
40
+ text
41
+ end
42
+ end
43
+
44
+ element :chip_element, tag_name: 'mat-chip'
45
+ element :modified_chip_element, tag_name: 'mat-chip' do
46
+ def test_method
47
+ text
48
+ end
49
+ end
50
+
51
+ mat_chip_list :basic_chips, index: 0
52
+ mat_chip_list :chips_with_input, index: 1
53
+ mat_chip_list :chips_autocomplete, index: 2
54
+ end
data/lib/insite/insite.rb CHANGED
@@ -10,19 +10,44 @@ module Insite
10
10
  attr_reader :base_url, :unique_methods, :browser_type
11
11
  attr_accessor :pages, :browser, :arguments, :most_recent_page
12
12
 
13
+ def self.class_to_tag(klass)
14
+ if klass.respond_to?(:collection) && klass.collection?
15
+ Watir.tag_to_class.key(klass) ||
16
+ Watir.tag_to_class.key(CLASS_MAP.key(klass)) ||
17
+ Watir.tag_to_class.key(CLASS_MAP.key(klass.collection_member_class))
18
+ else
19
+ Watir.tag_to_class.key(klass) ||
20
+ Watir.tag_to_class.key(CLASS_MAP.key(klass))
21
+ end
22
+ end
23
+
24
+ def self.tag_to_class(tag)
25
+ CLASS_MAP[Watir.tag_to_class[tag]] || Insite::HTMLElement
26
+ end
27
+
13
28
  # Automatically sets up a Page class when Insite is included. Probably overkill
14
29
  # but it protects against the case where two different sites are used at the
15
30
  # same time: Each site will use its own page objects only.
16
31
  def self.included(base)
17
32
  mod = Module.new
18
- base.const_set('WidgetMethods', mod)
33
+ base.const_set('ComponentMethods', mod)
19
34
 
20
35
  klass = Class.new(DefinedPage)
21
36
  base.const_set('Page', klass)
22
- base::send(:extend, WidgetMethods)
37
+ base::send(:extend, ComponentMethods)
23
38
 
24
39
  klass = Class.new(UndefinedPage)
25
40
  base.const_set('UndefinedPage', klass)
41
+
42
+ class << base
43
+ attr_reader :custom_tags
44
+ @custom_tags = []
45
+ end
46
+
47
+ base.define_singleton_method(:set_custom_tags) do |*tags|
48
+ @custom_tags ||= []
49
+ tags.sort.each { |t| @custom_tags << t.to_s.downcase.dasherize }
50
+ end
26
51
  end
27
52
 
28
53
  # Returns true if there's an open browser (that's also responding.) False if not.
@@ -36,7 +61,7 @@ module Insite
36
61
 
37
62
  # Closes the site object's browser/driver.
38
63
  def close
39
- @browser.close
64
+ @browser.close if browser?
40
65
  end
41
66
 
42
67
  def describe
@@ -74,6 +99,23 @@ EOF
74
99
  browser?
75
100
  end
76
101
 
102
+ def generate_tag_classes
103
+ tags = []
104
+ cli = Highline.new
105
+
106
+ loop do
107
+ tags = (tags + find_non_standard_tags).uniq.sort
108
+ cli.choose do |menu|
109
+ menu.prompt "Found #{tags.length} non-standard tags. Choose one of the following options:"
110
+ menu.choice(:list_tags) { puts tags.join(",\n") + "\n" }
111
+ menu.choice(:continue) {}
112
+ menu.choice(:write_to_console) do
113
+ end
114
+ menu.choice(:exist_without_writing) { break }
115
+
116
+ end
117
+ end
118
+ end
77
119
  # Creates a site object, which will have accessor methods for all pages that
78
120
  # you have defined for the site. This object takes a hash argument. There is
79
121
  # only one required value (the base_url for the site.) Example:
@@ -94,12 +136,25 @@ EOF
94
136
  # site.bar
95
137
  # => 1
96
138
  # TODO: Sort args.
97
- def initialize(base_url, **hsh)
139
+ def initialize(base_url = nil, hsh={})
98
140
  @arguments = hsh.with_indifferent_access
99
141
  @base_url = base_url
100
142
  @browser_type = (@arguments[:browser] ? @arguments[:browser].to_sym : nil)
101
143
  @pages = self.class::DefinedPage.descendants.reject { |p| p.page_template? }
102
144
 
145
+ # Build generic components for custom tags, which are defined
146
+ # using Site.custom_tags.
147
+ if self.class.custom_tags
148
+ self.class.custom_tags.each do |tag|
149
+ # TODO: Ditch string interpolation.
150
+ self.class.class_eval %Q{
151
+ class #{tag.underscore.camelize} < Component
152
+ select_by tag_name: "#{tag}"
153
+ end
154
+ }
155
+ end
156
+ end
157
+
103
158
  # Set up accessor methods for each page and page checking methods..
104
159
  @pages.each do |current_page|
105
160
  unless current_page.page_template?
@@ -144,6 +199,23 @@ EOF
144
199
  "@most_recent_page=#{@most_recent_page}>"
145
200
  end
146
201
 
202
+ def find_non_standard_tags
203
+ @browser.elements(xpath: non_standard_tag_xpath).map do |e|
204
+ e.html.match(/<(\S+?(?=[\s,>]))/)[1]
205
+ end.uniq.sort
206
+ end
207
+
208
+ def html_tags
209
+ %i(html title head body) + Insite::METHOD_MAP.values.flatten.each do |mth|
210
+ elem = @browser.send(mth)
211
+ elem.respond_to?(:selector) ? elem.selector.values.first.to_s : nil
212
+ end.sort
213
+ end
214
+
215
+ def non_standard_tag_xpath
216
+ "//*[#{html_tags.map { |sym| "not(local-name(.) = '#{sym}')" }.join(" and ") }]"
217
+ end
218
+
147
219
  # In cases where Insite doesn't recognize a method call it will try to do the following:
148
220
  # - Delegate the method call to the most recently accessed page, which is stored in
149
221
  # Insite#most_recent_page.
@@ -153,19 +225,23 @@ EOF
153
225
  #
154
226
  # If delegation doesn't work then a NoMethodError will be raised with some details about
155
227
  # what was attempted.
156
- def method_missing(sym, *args, &block)
228
+ def method_missing(mth, *args, &block)
157
229
  original_page = @most_recent_page
158
- if original_page.respond_to?(sym)
159
- original_page.public_send(sym, *args, &block)
230
+ if original_page.respond_to?(mth)
231
+ original_page.public_send(mth, *args, &block)
160
232
  else
161
233
  new_page = page
162
- if new_page.respond_to?(sym)
163
- page.public_send(sym, *args, &block)
234
+ if new_page.respond_to?(mth)
235
+ page.public_send(mth, *args, &block)
236
+ elsif !new_page.defined?
237
+ raise NoMethodError, "Could not apply #{mth}. The current page could not be " \
238
+ "recognized. Current URL #{@browser.url}"
164
239
  else
240
+ # TODO: Make it clearer where the method got called.
165
241
  raise(
166
242
  NoMethodError,
167
- "Unable to apply #{sym}. The site object doesn't support it and the" \
168
- "currently displayed page doesn't support it either.\n" \
243
+ "Unable to apply method call :#{mth}. The site object doesn't support it. " \
244
+ "The currently displayed page (#{new_page}) doesn't support it either.\n" \
169
245
  "Page:\t\t#{new_page.class}\n" \
170
246
  "Current URL:\t#{@browser.url}\n\n",
171
247
  caller
@@ -276,6 +352,15 @@ EOF
276
352
  end
277
353
  end
278
354
 
355
+ def respond_to_missing?(mth, include_priv = false)
356
+ # TODO: Page context changes.
357
+ @most_recent_page.respond_to?(mth, include_priv) || super
358
+ end
359
+
360
+ def target
361
+ @browser
362
+ end
363
+
279
364
  def text
280
365
  @browser.text
281
366
  end