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